s3api: add AttachUserPolicy/DetachUserPolicy/ListAttachedUserPolicies (#8379)
* iam: add XML responses for managed user policy APIs * s3api: implement attach/detach/list attached user policies * s3api: add embedded IAM tests for managed user policies * iam: update CredentialStore interface and Manager for managed policies Updated the `CredentialStore` interface to include `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies` methods. The `CredentialManager` was updated to delegate these calls to the store. Added common error variables for policy management. * iam: implement managed policy methods in MemoryStore Implemented `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies` in the MemoryStore. Also ensured deep copying of identities includes PolicyNames. * iam: implement managed policy methods in PostgresStore Modified Postgres schema to include `policy_names` JSONB column in `users`. Implemented `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies`. Updated user CRUD operations to handle policy names persistence. * iam: implement managed policy methods in remaining stores Implemented user policy management in: - `FilerEtcStore` (partial implementation) - `IamGrpcStore` (delegated via GetUser/UpdateUser) - `PropagatingCredentialStore` (to broadcast updates) Ensures cluster-wide consistency for policy attachments. * s3api: refactor EmbeddedIamApi to use managed policy APIs - Refactored `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies` to use `e.credentialManager` directly. - Fixed a critical error suppression bug in `ExecuteAction` that always returned success even on failure. - Implemented robust error matching using string comparison fallbacks. - Improved consistency by reloading configuration after policy changes. * s3api: update and refine IAM integration tests - Updated tests to use a real `MemoryStore`-backed `CredentialManager`. - Refined test configuration synchronization using `sync.Once` and manual deep-copying to prevent state corruption. - Improved `extractEmbeddedIamErrorCodeAndMessage` to handle more XML formats robustly. - Adjusted test expectations to match current AWS IAM behavior. * fix compilation * visibility * ensure 10 policies * reload * add integration tests * Guard raft command registration * Allow IAM actions in policy tests * Validate gRPC policy attachments * Revert Validate gRPC policy attachments * Tighten gRPC policy attach/detach * Improve IAM managed policy handling * Improve managed policy filters
This commit is contained in:
@@ -62,26 +62,30 @@ const (
|
||||
// Operational limits (AWS IAM compatible)
|
||||
MaxServiceAccountsPerUser = 100 // Maximum service accounts per user
|
||||
MaxDescriptionLength = 1000 // Maximum description length in characters
|
||||
MaxManagedPoliciesPerUser = 10 // Maximum managed policies attached to a user
|
||||
)
|
||||
|
||||
// Type aliases for IAM response types from shared package
|
||||
type (
|
||||
iamListUsersResponse = iamlib.ListUsersResponse
|
||||
iamListAccessKeysResponse = iamlib.ListAccessKeysResponse
|
||||
iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse
|
||||
iamCreatePolicyResponse = iamlib.CreatePolicyResponse
|
||||
iamCreateUserResponse = iamlib.CreateUserResponse
|
||||
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
||||
iamGetUserResponse = iamlib.GetUserResponse
|
||||
iamUpdateUserResponse = iamlib.UpdateUserResponse
|
||||
iamCreateAccessKeyResponse = iamlib.CreateAccessKeyResponse
|
||||
iamPutUserPolicyResponse = iamlib.PutUserPolicyResponse
|
||||
iamDeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse
|
||||
iamGetUserPolicyResponse = iamlib.GetUserPolicyResponse
|
||||
iamSetUserStatusResponse = iamlib.SetUserStatusResponse
|
||||
iamUpdateAccessKeyResponse = iamlib.UpdateAccessKeyResponse
|
||||
iamErrorResponse = iamlib.ErrorResponse
|
||||
iamError = iamlib.Error
|
||||
iamListUsersResponse = iamlib.ListUsersResponse
|
||||
iamListAccessKeysResponse = iamlib.ListAccessKeysResponse
|
||||
iamDeleteAccessKeyResponse = iamlib.DeleteAccessKeyResponse
|
||||
iamCreatePolicyResponse = iamlib.CreatePolicyResponse
|
||||
iamCreateUserResponse = iamlib.CreateUserResponse
|
||||
iamDeleteUserResponse = iamlib.DeleteUserResponse
|
||||
iamGetUserResponse = iamlib.GetUserResponse
|
||||
iamUpdateUserResponse = iamlib.UpdateUserResponse
|
||||
iamCreateAccessKeyResponse = iamlib.CreateAccessKeyResponse
|
||||
iamPutUserPolicyResponse = iamlib.PutUserPolicyResponse
|
||||
iamDeleteUserPolicyResponse = iamlib.DeleteUserPolicyResponse
|
||||
iamGetUserPolicyResponse = iamlib.GetUserPolicyResponse
|
||||
iamAttachUserPolicyResponse = iamlib.AttachUserPolicyResponse
|
||||
iamDetachUserPolicyResponse = iamlib.DetachUserPolicyResponse
|
||||
iamListAttachedUserPoliciesResponse = iamlib.ListAttachedUserPoliciesResponse
|
||||
iamSetUserStatusResponse = iamlib.SetUserStatusResponse
|
||||
iamUpdateAccessKeyResponse = iamlib.UpdateAccessKeyResponse
|
||||
iamErrorResponse = iamlib.ErrorResponse
|
||||
iamError = iamlib.Error
|
||||
// Service account response types
|
||||
iamServiceAccountInfo = iamlib.ServiceAccountInfo
|
||||
iamCreateServiceAccountResponse = iamlib.CreateServiceAccountResponse
|
||||
@@ -166,6 +170,8 @@ func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Re
|
||||
s3err.WriteXMLResponse(w, r, http.StatusForbidden, errorResp)
|
||||
case iam.ErrCodeServiceFailureException:
|
||||
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
||||
case "NotImplemented":
|
||||
s3err.WriteXMLResponse(w, r, http.StatusNotImplemented, errorResp)
|
||||
default:
|
||||
s3err.WriteXMLResponse(w, r, http.StatusInternalServerError, internalErrorResponse)
|
||||
}
|
||||
@@ -375,7 +381,7 @@ func (e *EmbeddedIamApi) GetPolicyDocument(policy *string) (policy_engine.Policy
|
||||
// NOTE: Currently this only validates the policy document and returns policy metadata.
|
||||
// The policy is not persisted to a managed policy store. To apply permissions to a user,
|
||||
// use PutUserPolicy which stores the policy inline on the user's identity.
|
||||
// TODO: Implement managed policy storage for full AWS IAM compatibility (ListPolicies, GetPolicy, AttachUserPolicy).
|
||||
// TODO: Implement managed policy storage for full AWS IAM compatibility (ListPolicies, GetPolicy).
|
||||
func (e *EmbeddedIamApi) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (iamCreatePolicyResponse, *iamError) {
|
||||
var resp iamCreatePolicyResponse
|
||||
policyName := values.Get("PolicyName")
|
||||
@@ -392,6 +398,31 @@ func (e *EmbeddedIamApi) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values u
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func iamPolicyNameFromArn(policyArn string) (string, error) {
|
||||
const policyPathDelimiter = ":policy/"
|
||||
idx := strings.Index(policyArn, policyPathDelimiter)
|
||||
if idx < 0 {
|
||||
return "", fmt.Errorf("invalid policy arn: %s", policyArn)
|
||||
}
|
||||
|
||||
policyPath := strings.Trim(policyArn[idx+len(policyPathDelimiter):], "/")
|
||||
if policyPath == "" {
|
||||
return "", fmt.Errorf("invalid policy arn: %s", policyArn)
|
||||
}
|
||||
|
||||
parts := strings.Split(policyPath, "/")
|
||||
policyName := parts[len(parts)-1]
|
||||
if policyName == "" {
|
||||
return "", fmt.Errorf("invalid policy arn: %s", policyArn)
|
||||
}
|
||||
|
||||
return policyName, nil
|
||||
}
|
||||
|
||||
func iamPolicyArn(policyName string) string {
|
||||
return fmt.Sprintf("arn:aws:iam:::policy/%s", policyName)
|
||||
}
|
||||
|
||||
// getActions extracts actions from a policy document.
|
||||
// S3 ARN format: arn:aws:s3:::bucket or arn:aws:s3:::bucket/path/*
|
||||
// res[5] contains the bucket and optional path after :::
|
||||
@@ -559,6 +590,192 @@ func (e *EmbeddedIamApi) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, valu
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user.
|
||||
func (e *EmbeddedIamApi) AttachUserPolicy(ctx context.Context, values url.Values) (iamAttachUserPolicyResponse, *iamError) {
|
||||
var resp iamAttachUserPolicyResponse
|
||||
|
||||
userName := values.Get("UserName")
|
||||
if userName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
|
||||
}
|
||||
|
||||
policyArn := values.Get("PolicyArn")
|
||||
policyName, err := iamPolicyNameFromArn(policyArn)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
|
||||
}
|
||||
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
|
||||
policy, err := e.credentialManager.GetPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
if policy == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
|
||||
}
|
||||
|
||||
attachedPolicies, err := e.credentialManager.ListAttachedUserPolicies(ctx, userName)
|
||||
if err != nil {
|
||||
if errors.Is(err, credential.ErrUserNotFound) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
for _, attached := range attachedPolicies {
|
||||
if attached == policyName {
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
if len(attachedPolicies) >= MaxManagedPoliciesPerUser {
|
||||
return resp, &iamError{
|
||||
Code: iam.ErrCodeLimitExceededException,
|
||||
Error: fmt.Errorf("cannot attach more than %d managed policies to user %s", MaxManagedPoliciesPerUser, userName),
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.credentialManager.AttachUserPolicy(ctx, userName, policyName); err != nil {
|
||||
if errors.Is(err, credential.ErrUserNotFound) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
if errors.Is(err, credential.ErrPolicyNotFound) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
|
||||
}
|
||||
if errors.Is(err, credential.ErrPolicyAlreadyAttached) {
|
||||
// AWS IAM is idempotent for AttachUserPolicy
|
||||
return resp, nil
|
||||
}
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user.
|
||||
func (e *EmbeddedIamApi) DetachUserPolicy(ctx context.Context, values url.Values) (iamDetachUserPolicyResponse, *iamError) {
|
||||
var resp iamDetachUserPolicyResponse
|
||||
|
||||
userName := values.Get("UserName")
|
||||
if userName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
|
||||
}
|
||||
|
||||
policyArn := values.Get("PolicyArn")
|
||||
policyName, err := iamPolicyNameFromArn(policyArn)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: err}
|
||||
}
|
||||
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
|
||||
policy, err := e.credentialManager.GetPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
if policy == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)}
|
||||
}
|
||||
|
||||
if err := e.credentialManager.DetachUserPolicy(ctx, userName, policyName); err != nil {
|
||||
if errors.Is(err, credential.ErrUserNotFound) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
if errors.Is(err, credential.ErrPolicyNotAttached) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not attached to user %s", policyName, userName)}
|
||||
}
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies lists managed policies attached to a user.
|
||||
func (e *EmbeddedIamApi) ListAttachedUserPolicies(ctx context.Context, values url.Values) (iamListAttachedUserPoliciesResponse, *iamError) {
|
||||
var resp iamListAttachedUserPoliciesResponse
|
||||
|
||||
userName := values.Get("UserName")
|
||||
if userName == "" {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")}
|
||||
}
|
||||
|
||||
pathPrefix := values.Get("PathPrefix")
|
||||
if pathPrefix == "" {
|
||||
pathPrefix = "/"
|
||||
}
|
||||
|
||||
maxItems := 0
|
||||
if maxItemsStr := values.Get("MaxItems"); maxItemsStr != "" {
|
||||
parsedMaxItems, err := strconv.Atoi(maxItemsStr)
|
||||
if err != nil || parsedMaxItems <= 0 {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("MaxItems must be a positive integer")}
|
||||
}
|
||||
maxItems = parsedMaxItems
|
||||
}
|
||||
marker := values.Get("Marker")
|
||||
|
||||
if e.credentialManager == nil {
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("credential manager not configured")}
|
||||
}
|
||||
|
||||
policyNames, err := e.credentialManager.ListAttachedUserPolicies(ctx, userName)
|
||||
if err != nil {
|
||||
if errors.Is(err, credential.ErrUserNotFound) {
|
||||
return resp, &iamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(iamUserDoesNotExist, userName)}
|
||||
}
|
||||
return resp, &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
|
||||
}
|
||||
|
||||
var attachedPolicies []*iam.AttachedPolicy
|
||||
for _, attachedPolicyName := range policyNames {
|
||||
// Policy paths are not tracked in the current configuration, so PathPrefix
|
||||
// filtering is not supported yet. Always return the policy for now.
|
||||
policyNameCopy := attachedPolicyName
|
||||
policyArn := iamPolicyArn(attachedPolicyName)
|
||||
policyArnCopy := policyArn
|
||||
attachedPolicies = append(attachedPolicies, &iam.AttachedPolicy{
|
||||
PolicyName: &policyNameCopy,
|
||||
PolicyArn: &policyArnCopy,
|
||||
})
|
||||
}
|
||||
|
||||
start := 0
|
||||
markerFound := false
|
||||
if marker != "" {
|
||||
for i, p := range attachedPolicies {
|
||||
if p.PolicyName != nil && *p.PolicyName == marker {
|
||||
start = i + 1
|
||||
markerFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !markerFound && len(attachedPolicies) > 0 {
|
||||
return resp, &iamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("marker %s not found", marker)}
|
||||
}
|
||||
}
|
||||
if start > 0 && start < len(attachedPolicies) {
|
||||
attachedPolicies = attachedPolicies[start:]
|
||||
} else if start >= len(attachedPolicies) {
|
||||
attachedPolicies = nil
|
||||
}
|
||||
|
||||
if maxItems > 0 && len(attachedPolicies) > maxItems {
|
||||
resp.ListAttachedUserPoliciesResult.AttachedPolicies = attachedPolicies[:maxItems]
|
||||
resp.ListAttachedUserPoliciesResult.IsTruncated = true
|
||||
if name := resp.ListAttachedUserPoliciesResult.AttachedPolicies[maxItems-1].PolicyName; name != nil {
|
||||
resp.ListAttachedUserPoliciesResult.Marker = *name
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.ListAttachedUserPoliciesResult.AttachedPolicies = attachedPolicies
|
||||
resp.ListAttachedUserPoliciesResult.IsTruncated = false
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SetUserStatus enables or disables a user without deleting them.
|
||||
// This is a SeaweedFS extension for temporary user suspension, offboarding, etc.
|
||||
// When a user is disabled, all API requests using their credentials will return AccessDenied.
|
||||
@@ -1049,7 +1266,7 @@ func (e *EmbeddedIamApi) AuthIam(f http.HandlerFunc, _ Action) http.HandlerFunc
|
||||
|
||||
// ExecuteAction executes an IAM action with the given values.
|
||||
// If skipPersist is true, the changed configuration is not saved to the persistent store.
|
||||
func (e *EmbeddedIamApi) ExecuteAction(values url.Values, skipPersist bool) (interface{}, *iamError) {
|
||||
func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, skipPersist bool) (interface{}, *iamError) {
|
||||
// Lock to prevent concurrent read-modify-write race conditions
|
||||
e.policyLock.Lock()
|
||||
defer e.policyLock.Unlock()
|
||||
@@ -1057,7 +1274,7 @@ func (e *EmbeddedIamApi) ExecuteAction(values url.Values, skipPersist bool) (int
|
||||
action := values.Get("Action")
|
||||
if e.readOnly {
|
||||
switch action {
|
||||
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListServiceAccounts", "GetServiceAccount":
|
||||
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListAttachedUserPolicies", "ListServiceAccounts", "GetServiceAccount":
|
||||
// Allowed read-only actions
|
||||
default:
|
||||
return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code, Error: fmt.Errorf("IAM write operations are disabled on this server")}
|
||||
@@ -1141,6 +1358,24 @@ func (e *EmbeddedIamApi) ExecuteAction(values url.Values, skipPersist bool) (int
|
||||
if iamErr != nil {
|
||||
return nil, iamErr
|
||||
}
|
||||
case "AttachUserPolicy":
|
||||
response, iamErr = e.AttachUserPolicy(ctx, values)
|
||||
if iamErr != nil {
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "DetachUserPolicy":
|
||||
response, iamErr = e.DetachUserPolicy(ctx, values)
|
||||
if iamErr != nil {
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "ListAttachedUserPolicies":
|
||||
response, iamErr = e.ListAttachedUserPolicies(ctx, values)
|
||||
if iamErr != nil {
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "SetUserStatus":
|
||||
response, iamErr = e.SetUserStatus(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
@@ -1193,8 +1428,14 @@ func (e *EmbeddedIamApi) ExecuteAction(values url.Values, skipPersist bool) (int
|
||||
glog.Errorf("Failed to reload IAM configuration after mutation: %v", err)
|
||||
// Don't fail the request since the persistent save succeeded
|
||||
}
|
||||
} else if iamErr == nil && (action == "AttachUserPolicy" || action == "DetachUserPolicy") {
|
||||
// Even if changed=false (persisted via credentialManager), we should still reload
|
||||
// if we are utilizing the local in-memory cache for speed
|
||||
if err := e.ReloadConfiguration(); err != nil {
|
||||
glog.Errorf("Failed to reload IAM configuration after managed policy mutation: %v", err)
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
return response, iamErr
|
||||
}
|
||||
|
||||
// DoActions handles IAM API actions.
|
||||
@@ -1214,7 +1455,7 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
values.Set("CreatedBy", createdBy)
|
||||
}
|
||||
|
||||
response, iamErr := e.ExecuteAction(values, false)
|
||||
response, iamErr := e.ExecuteAction(r.Context(), values, false)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential/memory"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
@@ -30,179 +33,63 @@ type EmbeddedIamApiForTest struct {
|
||||
}
|
||||
|
||||
func NewEmbeddedIamApiForTest() *EmbeddedIamApiForTest {
|
||||
store := &memory.MemoryStore{}
|
||||
store.Initialize(nil, "")
|
||||
cm := &credential.CredentialManager{Store: store}
|
||||
e := &EmbeddedIamApiForTest{
|
||||
EmbeddedIamApi: &EmbeddedIamApi{
|
||||
iam: &IdentityAccessManagement{},
|
||||
iam: &IdentityAccessManagement{credentialManager: cm},
|
||||
credentialManager: cm,
|
||||
},
|
||||
mockConfig: &iam_pb.S3ApiConfiguration{},
|
||||
}
|
||||
var syncOnce sync.Once
|
||||
e.getS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
if e.mockConfig != nil {
|
||||
cloned := proto.Clone(e.mockConfig).(*iam_pb.S3ApiConfiguration)
|
||||
proto.Merge(s3cfg, cloned)
|
||||
// If mockConfig was set directly in test, sync it to store first (only once)
|
||||
var syncErr error
|
||||
syncOnce.Do(func() {
|
||||
if e.mockConfig != nil {
|
||||
syncErr = cm.SaveConfiguration(context.Background(), e.mockConfig)
|
||||
}
|
||||
})
|
||||
if syncErr != nil {
|
||||
return syncErr
|
||||
}
|
||||
return nil
|
||||
config, err := cm.LoadConfiguration(context.Background())
|
||||
if err == nil {
|
||||
e.mockConfig = config
|
||||
proto.Reset(s3cfg)
|
||||
// Manually copy identities and other fields to avoid Merge issues with slices
|
||||
s3cfg.Identities = make([]*iam_pb.Identity, len(config.Identities))
|
||||
for i, ident := range config.Identities {
|
||||
s3cfg.Identities[i] = proto.Clone(ident).(*iam_pb.Identity)
|
||||
}
|
||||
s3cfg.Policies = make([]*iam_pb.Policy, len(config.Policies))
|
||||
for i, p := range config.Policies {
|
||||
s3cfg.Policies[i] = proto.Clone(p).(*iam_pb.Policy)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
e.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
|
||||
return nil
|
||||
return cm.SaveConfiguration(context.Background(), s3cfg)
|
||||
}
|
||||
e.reloadConfigurationFunc = func() error {
|
||||
config, err := cm.LoadConfiguration(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.mockConfig = config
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Override GetS3ApiConfiguration for testing
|
||||
func (e *EmbeddedIamApiForTest) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
// Use proto.Clone for proper deep copy semantics
|
||||
if e.mockConfig != nil {
|
||||
cloned := proto.Clone(e.mockConfig).(*iam_pb.S3ApiConfiguration)
|
||||
proto.Merge(s3cfg, cloned)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Override PutS3ApiConfiguration for testing
|
||||
func (e *EmbeddedIamApiForTest) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
// Use proto.Clone for proper deep copy semantics
|
||||
e.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoActions handles IAM API actions for testing
|
||||
func (e *EmbeddedIamApiForTest) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
values := r.PostForm
|
||||
s3cfg := &iam_pb.S3ApiConfiguration{}
|
||||
if err := e.GetS3ApiConfiguration(s3cfg); err != nil {
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var response interface{}
|
||||
var iamErr *iamError
|
||||
changed := true
|
||||
|
||||
action := r.Form.Get("Action")
|
||||
|
||||
if e.readOnly {
|
||||
switch action {
|
||||
case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListServiceAccounts", "GetServiceAccount":
|
||||
// Allowed read-only actions
|
||||
default:
|
||||
e.writeIamErrorResponse(w, r, &iamError{Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code, Error: fmt.Errorf("IAM write operations are disabled on this server")})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "ListUsers":
|
||||
response = e.ListUsers(s3cfg, values)
|
||||
changed = false
|
||||
case "ListAccessKeys":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response = e.ListAccessKeys(s3cfg, values)
|
||||
changed = false
|
||||
case "CreateUser":
|
||||
response, iamErr = e.CreateUser(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
case "GetUser":
|
||||
userName := values.Get("UserName")
|
||||
response, iamErr = e.GetUser(s3cfg, userName)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
changed = false
|
||||
case "UpdateUser":
|
||||
response, iamErr = e.UpdateUser(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case "DeleteUser":
|
||||
userName := values.Get("UserName")
|
||||
response, iamErr = e.DeleteUser(s3cfg, userName)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
case "CreateAccessKey":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response, iamErr = e.CreateAccessKey(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case "DeleteAccessKey":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response = e.DeleteAccessKey(s3cfg, values)
|
||||
case "CreatePolicy":
|
||||
response, iamErr = e.CreatePolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case "PutUserPolicy":
|
||||
response, iamErr = e.PutUserPolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
case "GetUserPolicy":
|
||||
response, iamErr = e.GetUserPolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
changed = false
|
||||
case "DeleteUserPolicy":
|
||||
response, iamErr = e.DeleteUserPolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
case "SetUserStatus":
|
||||
response, iamErr = e.SetUserStatus(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
case "UpdateAccessKey":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response, iamErr = e.UpdateAccessKey(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Not implemented", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := e.PutS3ApiConfiguration(s3cfg); err != nil {
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
xmlBytes, err := xml.Marshal(response)
|
||||
if err != nil {
|
||||
// This should not happen in tests, but log it for debugging
|
||||
http.Error(w, "Internal error: failed to marshal response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(xmlBytes)
|
||||
// Call the real DoActions
|
||||
e.EmbeddedIamApi.DoActions(w, r)
|
||||
}
|
||||
|
||||
// executeEmbeddedIamRequest executes an IAM request against the given API instance.
|
||||
@@ -229,11 +116,38 @@ type embeddedIamErrorResponseForTest struct {
|
||||
}
|
||||
|
||||
func extractEmbeddedIamErrorCodeAndMessage(response *httptest.ResponseRecorder) (string, string) {
|
||||
var er embeddedIamErrorResponseForTest
|
||||
if err := xml.Unmarshal(response.Body.Bytes(), &er); err != nil {
|
||||
return "", ""
|
||||
body := response.Body.Bytes()
|
||||
// Try parsing with ErrorResponse root
|
||||
type localError struct {
|
||||
Code string `xml:"Code"`
|
||||
Message string `xml:"Message"`
|
||||
}
|
||||
return er.Error.Code, er.Error.Message
|
||||
type localResponse struct {
|
||||
XMLName xml.Name `xml:"ErrorResponse"`
|
||||
Error localError `xml:"Error"`
|
||||
}
|
||||
var lr localResponse
|
||||
if err := xml.Unmarshal(body, &lr); err == nil && lr.Error.Code != "" {
|
||||
return lr.Error.Code, lr.Error.Message
|
||||
}
|
||||
|
||||
// Try parsing with Error root
|
||||
type simpleError struct {
|
||||
XMLName xml.Name `xml:"Error"`
|
||||
Code string `xml:"Code"`
|
||||
Message string `xml:"Message"`
|
||||
}
|
||||
var se simpleError
|
||||
if err := xml.Unmarshal(body, &se); err == nil && se.Code != "" {
|
||||
return se.Code, se.Message
|
||||
}
|
||||
|
||||
var er embeddedIamErrorResponseForTest
|
||||
if err := xml.Unmarshal(body, &er); err == nil {
|
||||
return er.Error.Code, er.Error.Message
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// TestEmbeddedIamCreateUser tests creating a user via the embedded IAM API
|
||||
@@ -528,6 +442,210 @@ func TestEmbeddedIamDeleteUserPolicyUserNotFound(t *testing.T) {
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamAttachUserPolicy tests attaching a managed policy to a user.
|
||||
func TestEmbeddedIamAttachUserPolicy(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser"},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
},
|
||||
}
|
||||
|
||||
params := &iam.AttachUserPolicyInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
out := iamAttachUserPolicyResponse{}
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, response.Code)
|
||||
assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamAttachUserPolicyNoSuchPolicy tests attach failure when managed policy does not exist.
|
||||
func TestEmbeddedIamAttachUserPolicyNoSuchPolicy(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser"},
|
||||
},
|
||||
}
|
||||
|
||||
params := &iam.AttachUserPolicyInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
PolicyArn: aws.String("arn:aws:iam:::policy/DoesNotExist"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
||||
assert.Equal(t, "NoSuchEntity", code)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamDetachUserPolicy tests detaching a managed policy from a user.
|
||||
func TestEmbeddedIamDetachUserPolicy(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser", PolicyNames: []string{"TestManagedPolicy", "KeepPolicy"}},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
{Name: "KeepPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
},
|
||||
}
|
||||
|
||||
params := &iam.DetachUserPolicyInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).DetachUserPolicyRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
out := iamDetachUserPolicyResponse{}
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, response.Code)
|
||||
assert.Equal(t, []string{"KeepPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamAttachAlreadyAttachedPolicy ensures attaching a policy already
|
||||
// present on the user is idempotent.
|
||||
func TestEmbeddedIamAttachAlreadyAttachedPolicy(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser", PolicyNames: []string{"TestManagedPolicy"}},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
},
|
||||
}
|
||||
|
||||
params := &iam.AttachUserPolicyInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
out := iamAttachUserPolicyResponse{}
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, response.Code)
|
||||
assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamDetachNotAttachedPolicy verifies detaching a policy that's not
|
||||
// attached returns NoSuchEntity.
|
||||
func TestEmbeddedIamDetachNotAttachedPolicy(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser"},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{Name: "MissingPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
},
|
||||
}
|
||||
|
||||
params := &iam.DetachUserPolicyInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
PolicyArn: aws.String("arn:aws:iam:::policy/MissingPolicy"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).DetachUserPolicyRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
||||
assert.Equal(t, "NoSuchEntity", code)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamAttachPolicyLimitExceeded ensures we honor the managed policy limit.
|
||||
func TestEmbeddedIamAttachPolicyLimitExceeded(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
existingPolicies := make([]string, 0, MaxManagedPoliciesPerUser)
|
||||
configPolicies := make([]*iam_pb.Policy, 0, MaxManagedPoliciesPerUser+1)
|
||||
for i := 0; i < MaxManagedPoliciesPerUser; i++ {
|
||||
name := fmt.Sprintf("ManagedPolicy%d", i)
|
||||
existingPolicies = append(existingPolicies, name)
|
||||
configPolicies = append(configPolicies, &iam_pb.Policy{
|
||||
Name: name,
|
||||
Content: `{"Version":"2012-10-17","Statement":[]}`,
|
||||
})
|
||||
}
|
||||
configPolicies = append(configPolicies, &iam_pb.Policy{
|
||||
Name: "NewPolicy",
|
||||
Content: `{"Version":"2012-10-17","Statement":[]}`,
|
||||
})
|
||||
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser", PolicyNames: existingPolicies},
|
||||
},
|
||||
Policies: configPolicies,
|
||||
}
|
||||
|
||||
params := &iam.AttachUserPolicyInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
PolicyArn: aws.String("arn:aws:iam:::policy/NewPolicy"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, response.Code)
|
||||
code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
|
||||
assert.Equal(t, iam.ErrCodeLimitExceededException, code)
|
||||
assert.Len(t, api.mockConfig.Identities[0].PolicyNames, MaxManagedPoliciesPerUser)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamListAttachedUserPolicies tests listing managed policies attached to a user.
|
||||
func TestEmbeddedIamListAttachedUserPolicies(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
api.mockConfig = &iam_pb.S3ApiConfiguration{
|
||||
Identities: []*iam_pb.Identity{
|
||||
{Name: "TestUser", PolicyNames: []string{"PolicyA", "PolicyB"}},
|
||||
},
|
||||
Policies: []*iam_pb.Policy{
|
||||
{Name: "PolicyA", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
{Name: "PolicyB", Content: `{"Version":"2012-10-17","Statement":[]}`},
|
||||
},
|
||||
}
|
||||
|
||||
params := &iam.ListAttachedUserPoliciesInput{
|
||||
UserName: aws.String("TestUser"),
|
||||
}
|
||||
req, _ := iam.New(session.New()).ListAttachedUserPoliciesRequest(params)
|
||||
_ = req.Build()
|
||||
|
||||
out := iamListAttachedUserPoliciesResponse{}
|
||||
response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, response.Code)
|
||||
assert.False(t, out.ListAttachedUserPoliciesResult.IsTruncated)
|
||||
assert.Len(t, out.ListAttachedUserPoliciesResult.AttachedPolicies, 2)
|
||||
|
||||
got := map[string]string{}
|
||||
for _, attached := range out.ListAttachedUserPoliciesResult.AttachedPolicies {
|
||||
got[aws.StringValue(attached.PolicyName)] = aws.StringValue(attached.PolicyArn)
|
||||
}
|
||||
assert.Equal(t, "arn:aws:iam:::policy/PolicyA", got["PolicyA"])
|
||||
assert.Equal(t, "arn:aws:iam:::policy/PolicyB", got["PolicyB"])
|
||||
}
|
||||
|
||||
// TestEmbeddedIamUpdateUser tests updating a user
|
||||
func TestEmbeddedIamUpdateUser(t *testing.T) {
|
||||
api := NewEmbeddedIamApiForTest()
|
||||
@@ -913,7 +1031,7 @@ func TestEmbeddedIamUpdateUserNotFound(t *testing.T) {
|
||||
req, _ := iam.New(session.New()).UpdateUserRequest(params)
|
||||
_ = req.Build()
|
||||
response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, response.Code)
|
||||
assert.Equal(t, http.StatusNotFound, response.Code)
|
||||
}
|
||||
|
||||
// TestEmbeddedIamCreateAccessKeyForExistingUser tests CreateAccessKey creates credentials for existing user
|
||||
@@ -1703,7 +1821,7 @@ func TestEmbeddedIamExecuteAction(t *testing.T) {
|
||||
vals.Set("Action", "CreateUser")
|
||||
vals.Set("UserName", "ExecuteActionUser")
|
||||
|
||||
resp, iamErr := api.ExecuteAction(vals, false)
|
||||
resp, iamErr := api.ExecuteAction(context.Background(), vals, false)
|
||||
assert.Nil(t, iamErr)
|
||||
|
||||
// Verify response type
|
||||
|
||||
Reference in New Issue
Block a user