package iamapi // This file provides IAM API handlers for the standalone IAM server. // Common IAM types and helpers are imported from the shared weed/iam package. import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "sort" "strings" "sync" "github.com/aws/aws-sdk-go/service/iam" "github.com/seaweedfs/seaweedfs/weed/glog" iamlib "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" "github.com/seaweedfs/seaweedfs/weed/s3api" "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" "github.com/seaweedfs/seaweedfs/weed/util/request_id" ) // Constants from shared package const ( charsetUpper = iamlib.CharsetUpper charset = iamlib.Charset policyDocumentVersion = iamlib.PolicyDocumentVersion StatementActionAdmin = iamlib.StatementActionAdmin StatementActionWrite = iamlib.StatementActionWrite StatementActionWriteAcp = iamlib.StatementActionWriteAcp StatementActionRead = iamlib.StatementActionRead StatementActionReadAcp = iamlib.StatementActionReadAcp StatementActionList = iamlib.StatementActionList StatementActionTagging = iamlib.StatementActionTagging StatementActionDelete = iamlib.StatementActionDelete USER_DOES_NOT_EXIST = iamlib.UserDoesNotExist accessKeyStatusActive = iamlib.AccessKeyStatusActive accessKeyStatusInactive = iamlib.AccessKeyStatusInactive ) var policyLock = sync.RWMutex{} const policyArnPrefix = "arn:aws:iam:::policy/" // policyDefaultVersionId is the single managed-policy version SeaweedFS exposes. // Managed policies are stored as one current document with no version history, // so the API presents exactly one version ("v1") which is always the default. const policyDefaultVersionId = "v1" // parsePolicyArn validates an IAM policy ARN and extracts the policy name. func parsePolicyArn(policyArn string) (string, *IamError) { if !strings.HasPrefix(policyArn, policyArnPrefix) { return "", &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("invalid policy ARN: %s", policyArn)} } policyName := strings.TrimPrefix(policyArn, policyArnPrefix) if policyName == "" { return "", &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("invalid policy ARN: %s", policyArn)} } return policyName, nil } // userPolicyKey returns a namespaced key for inline user policies to prevent collision with managed policies. // getOrCreateUserPolicies returns the policy map for a user, creating it if needed. // Returns a pointer to the user's policy map from Policies.InlinePolicies. func (p *Policies) getOrCreateUserPolicies(userName string) map[string]policy_engine.PolicyDocument { if p.InlinePolicies == nil { p.InlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument) } if p.InlinePolicies[userName] == nil { p.InlinePolicies[userName] = make(map[string]policy_engine.PolicyDocument) } return p.InlinePolicies[userName] } // getOrCreateGroupPolicies returns the policy map for a group, creating it if needed. func (p *Policies) getOrCreateGroupPolicies(groupName string) map[string]policy_engine.PolicyDocument { if p.GroupInlinePolicies == nil { p.GroupInlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument) } if p.GroupInlinePolicies[groupName] == nil { p.GroupInlinePolicies[groupName] = make(map[string]policy_engine.PolicyDocument) } return p.GroupInlinePolicies[groupName] } // computeAggregatedActionsForUser computes the union of actions across all inline policies for a user. // Directly accesses user's policies from Policies.InlinePolicies[userName] for O(1) lookup. // If policies is non-nil, it uses that instead of fetching from storage (for I/O optimization). // When policies is nil, it fetches from storage using GetPolicies. // // Performance: O(user_policies) instead of O(all_policies) with per-user index. // // Best-effort aggregation: If GetActions fails for a policy document, that policy is logged at Warning level // but is NOT removed from persistent storage. This intentional choice ensures: // - Stored policy documents survive even if they temporarily fail to parse // - The policy data is preserved for potential future fixes or manual inspection // - Only the runtime action set (ident.Actions) is affected when GetActions fails // This keeps persistent state consistent while gracefully handling parsing errors. func computeAggregatedActionsForUser(iama *IamApiServer, userName string, policies *Policies) ([]string, error) { var aggregatedActions []string actionSet := make(map[string]bool) var policiesToUse Policies if policies != nil { // Use provided Policies (caller already fetched, avoids redundant I/O) policiesToUse = *policies } else { // Fetch from storage if err := iama.s3ApiConfig.GetPolicies(&policiesToUse); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return nil, err } } // Direct O(1) access to user's policies using per-user index userPolicies := policiesToUse.InlinePolicies[userName] if len(userPolicies) == 0 { return aggregatedActions, nil } for policyName, policyDocument := range userPolicies { actions, err := GetActions(&policyDocument) if err != nil { // Best-effort: policy stored successfully but failed to parse; log and skip from aggregation glog.Warningf("Failed to get actions from stored policy '%s' for user %s (policy retained in storage): %v", policyName, userName, err) continue } for _, action := range actions { if !actionSet[action] { actionSet[action] = true aggregatedActions = append(aggregatedActions, action) } } } return aggregatedActions, nil } // Helper function wrappers using shared package func MapToStatementAction(action string) string { return iamlib.MapToStatementAction(action) } func MapToIdentitiesAction(action string) string { return iamlib.MapToIdentitiesAction(action) } type Policies struct { // Policies: managed policies (flat map, unchanged for backward compatibility) Policies map[string]policy_engine.PolicyDocument `json:"policies"` // InlinePolicies: user-indexed inline policies for O(1) lookup // Structure: [userName][policyName] -> PolicyDocument // Enables fast access without iterating all policies InlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"inlinePolicies"` // GroupInlinePolicies: group-indexed inline policies for O(1) lookup // Structure: [groupName][policyName] -> PolicyDocument GroupInlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"groupInlinePolicies,omitempty"` } func Hash(s *string) string { return iamlib.Hash(s) } func StringWithCharset(length int, charset string) (string, error) { return iamlib.GenerateRandomString(length, charset) } func stringSlicesEqual(a, b []string) bool { return iamlib.StringSlicesEqual(a, b) } func validateAccessKeyStatus(status string) error { switch status { case accessKeyStatusActive, accessKeyStatusInactive: return nil case "": return fmt.Errorf("Status parameter is required") default: return fmt.Errorf("Status must be '%s' or '%s'", accessKeyStatusActive, accessKeyStatusInactive) } } func (iama *IamApiServer) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListUsersResponse) { resp = &ListUsersResponse{} for _, ident := range s3cfg.Identities { user := iamlib.NewUser(ident.Name) resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &user) } return resp } func (iama *IamApiServer) ListAccessKeys(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListAccessKeysResponse) { resp = &ListAccessKeysResponse{} userName := values.Get("UserName") for _, ident := range s3cfg.Identities { if userName != "" && userName != ident.Name { continue } for _, cred := range ident.Credentials { status := cred.Status if status == "" { status = accessKeyStatusActive } identName := ident.Name accessKey := cred.AccessKey resp.ListAccessKeysResult.AccessKeyMetadata = append(resp.ListAccessKeysResult.AccessKeyMetadata, &iam.AccessKeyMetadata{UserName: &identName, AccessKeyId: &accessKey, Status: &status}, ) } } return resp } func (iama *IamApiServer) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreateUserResponse) { resp = &CreateUserResponse{} userName := values.Get("UserName") resp.CreateUserResult.User = iamlib.NewUser(userName) s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName}) return resp } func (iama *IamApiServer) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (resp *DeleteUserResponse, err *IamError) { resp = &DeleteUserResponse{} for i, ident := range s3cfg.Identities { if userName == ident.Name { // Clean up any inline policies stored for this user policies := Policies{} if pErr := iama.s3ApiConfig.GetPolicies(&policies); pErr != nil && !errors.Is(pErr, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } if policies.InlinePolicies != nil { if _, exists := policies.InlinePolicies[userName]; exists { delete(policies.InlinePolicies, userName) if pErr := iama.s3ApiConfig.PutPolicies(&policies); pErr != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } } } s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...) // Remove user from all groups removeUserFromAllGroups(s3cfg, userName) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } func (iama *IamApiServer) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (resp *GetUserResponse, err *IamError) { resp = &GetUserResponse{} for _, ident := range s3cfg.Identities { if userName == ident.Name { resp.GetUserResult.User = iamlib.NewUser(ident.Name) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } func (iama *IamApiServer) UpdateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *UpdateUserResponse, err *IamError) { resp = &UpdateUserResponse{} userName := values.Get("UserName") newUserName := values.Get("NewUserName") if newUserName == "" { return resp, nil } // Find the source identity first var sourceIdent *iam_pb.Identity for _, ident := range s3cfg.Identities { if ident.Name == userName { sourceIdent = ident break } } if sourceIdent == nil { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // No-op if renaming to the same name if newUserName == userName { return resp, nil } // Check for name collision before renaming for _, ident := range s3cfg.Identities { if ident.Name == newUserName { return resp, &IamError{ Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("user %s already exists", newUserName), } } } // Check for inline policy collision policies := Policies{} if pErr := iama.s3ApiConfig.GetPolicies(&policies); pErr != nil && !errors.Is(pErr, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } if policies.InlinePolicies != nil { if _, exists := policies.InlinePolicies[newUserName]; exists { return resp, &IamError{ Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("inline policies already exist for user %s", newUserName), } } } sourceIdent.Name = newUserName // Move any inline policies from old username to new username if policies.InlinePolicies != nil { if userPolicies, exists := policies.InlinePolicies[userName]; exists { delete(policies.InlinePolicies, userName) policies.InlinePolicies[newUserName] = userPolicies if pErr := iama.s3ApiConfig.PutPolicies(&policies); pErr != nil { // Rollback: restore identity name and inline policies sourceIdent.Name = userName delete(policies.InlinePolicies, newUserName) policies.InlinePolicies[userName] = userPolicies return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } } } // Update group membership references updateUserInGroups(s3cfg, userName, newUserName) // Update service account parent references for _, sa := range s3cfg.ServiceAccounts { if sa.ParentUser == userName { sa.ParentUser = newUserName } } return resp, nil } func GetPolicyDocument(policy *string) (policy_engine.PolicyDocument, error) { var policyDocument policy_engine.PolicyDocument if err := json.Unmarshal([]byte(*policy), &policyDocument); err != nil { return policy_engine.PolicyDocument{}, err } return policyDocument, nil } func (iama *IamApiServer) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreatePolicyResponse, iamError *IamError) { resp = &CreatePolicyResponse{} policyName := values.Get("PolicyName") policyDocumentString := values.Get("PolicyDocument") policyDocument, err := GetPolicyDocument(&policyDocumentString) if err != nil { return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err} } policyId := Hash(&policyName) arn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName) resp.CreatePolicyResult.Policy.PolicyName = &policyName resp.CreatePolicyResult.Policy.Arn = &arn resp.CreatePolicyResult.Policy.PolicyId = &policyId policies := Policies{} // Note: Lock is already held by DoActions, no need to acquire here if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if policies.Policies == nil { policies.Policies = make(map[string]policy_engine.PolicyDocument) } policies.Policies[policyName] = policyDocument if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } return resp, nil } type IamError struct { Code string Error error } // https://docs.aws.amazon.com/IAM/latest/APIReference/API_PutUserPolicy.html func (iama *IamApiServer) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *PutUserPolicyResponse, iamError *IamError) { resp = &PutUserPolicyResponse{} userName := values.Get("UserName") policyName := values.Get("PolicyName") policyDocumentString := values.Get("PolicyDocument") policyDocument, err := GetPolicyDocument(&policyDocumentString) if err != nil { return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err} } if _, err := GetActions(&policyDocument); err != nil { return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err} } // Verify the user exists before persisting the policy var targetIdent *iam_pb.Identity for _, ident := range s3cfg.Identities { if ident.Name == userName { targetIdent = ident break } } if targetIdent == nil { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the user with name %s cannot be found", userName)} } // Persist inline policy to storage using per-user indexed structure policies := Policies{} if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } userPolicies := policies.getOrCreateUserPolicies(userName) userPolicies[policyName] = policyDocument if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } // Recompute aggregated actions (inline + managed) aggregatedActions, computeErr := computeAllActionsForUser(iama, userName, &policies, targetIdent, s3cfg) if computeErr != nil { glog.Warningf("Failed to compute aggregated actions for user %s: %v; keeping existing actions", userName, computeErr) } else { targetIdent.Actions = aggregatedActions } return resp, nil } func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetUserPolicyResponse, err *IamError) { resp = &GetUserPolicyResponse{} userName := values.Get("UserName") policyName := values.Get("PolicyName") for _, ident := range s3cfg.Identities { if userName != ident.Name { continue } resp.GetUserPolicyResult.UserName = userName resp.GetUserPolicyResult.PolicyName = policyName // Try to retrieve stored inline policy from persistent storage using per-user index policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { // Propagate storage errors return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } // Direct O(1) access to user's policy using per-user index if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil { if policyDocument, exists := userPolicies[policyName]; exists { policyDocumentJSON, err := json.Marshal(policyDocument) if err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } resp.GetUserPolicyResult.PolicyDocument = string(policyDocumentJSON) return resp, nil } else { // User's inline policies exist but this specific policy does not return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: errors.New("policy not found")} } } if len(ident.Actions) == 0 { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: errors.New("no actions found")} } policyDocument := policy_engine.PolicyDocument{Version: policyDocumentVersion} statements := make(map[string][]string) seenAction := make(map[string]map[string]bool) for _, action := range ident.Actions { // parse "Read:EXAMPLE-BUCKET" or "Read:EXAMPLE-BUCKET/prefix/*" // Use SplitN so the path component (which may contain ':') is preserved intact. act := strings.SplitN(action, ":", 2) resource := "*" if len(act) == 2 { // Preserve the stored path verbatim so bucket-level and // object-level resources remain distinguishable. GetActions // stores the path exactly as parsed from the original ARN // (e.g. "b-le*" for the bucket, "b-le*/*" for objects), and // reconstruction should not rewrite one into the other. resource = fmt.Sprintf("arn:aws:s3:::%s", act[1]) } s3Action := fmt.Sprintf("s3:%s", MapToIdentitiesAction(act[0])) // Dedupe actions per resource: the Read/Write/List internal verbs map to // coarse wildcards (s3:Get*, s3:Put*, s3:List*), so multiple distinct // original actions can collapse to the same reconstructed verb. if seenAction[resource] == nil { seenAction[resource] = make(map[string]bool) } if seenAction[resource][s3Action] { continue } seenAction[resource][s3Action] = true statements[resource] = append(statements[resource], s3Action) } for resource, actions := range statements { isEqAction := false for i, statement := range policyDocument.Statement { // Use order-independent comparison to avoid duplicates from different action orderings if stringSlicesEqual(statement.Action.Strings(), actions) { policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlicePtr(append( policyDocument.Statement[i].Resource.Strings(), resource)...) isEqAction = true break } } if isEqAction { continue } policyDocumentStatement := policy_engine.PolicyStatement{ Effect: policy_engine.PolicyEffectAllow, Action: policy_engine.NewStringOrStringSlice(actions...), Resource: policy_engine.NewStringOrStringSlicePtr(resource), } policyDocument.Statement = append(policyDocument.Statement, policyDocumentStatement) } policyDocumentJSON, err := json.Marshal(policyDocument) if err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } resp.GetUserPolicyResult.PolicyDocument = string(policyDocumentJSON) return resp, nil } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // DeleteUserPolicy removes the inline policy from a user (clears their actions). func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeleteUserPolicyResponse, err *IamError) { resp = &DeleteUserPolicyResponse{} userName := values.Get("UserName") policyName := values.Get("PolicyName") // First, verify the user exists in identities before modifying storage var targetIdent *iam_pb.Identity for _, ident := range s3cfg.Identities { if ident.Name == userName { targetIdent = ident break } } if targetIdent == nil { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // User exists; now proceed with removing the stored inline policy from persistent storage policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { // Propagate storage errors immediately return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } // Direct O(1) access to user's policy map using per-user index if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil { delete(userPolicies, policyName) // Note: userPolicies is a map, so the delete modifies the map in policies.InlinePolicies[userName] if err := iama.s3ApiConfig.PutPolicies(&policies); err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } } // Recompute aggregated actions from remaining inline + managed policies aggregatedActions, computeErr := computeAllActionsForUser(iama, userName, &policies, targetIdent, s3cfg) if computeErr != nil { glog.Warningf("Failed to recompute aggregated actions for user %s: %v; keeping existing actions", userName, computeErr) } else { targetIdent.Actions = aggregatedActions } return resp, nil } // ListUserPolicies lists the names of inline policies attached to a user. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUserPolicies.html func (iama *IamApiServer) ListUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListUserPoliciesResponse, iamError *IamError) { resp = &ListUserPoliciesResponse{} userName := values.Get("UserName") if userName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")} } // Verify the user exists found := false for _, ident := range s3cfg.Identities { if ident.Name == userName { found = true break } } if !found { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // List inline policy names from persistent storage policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if userPolicies := policies.InlinePolicies[userName]; userPolicies != nil { for policyName := range userPolicies { resp.ListUserPoliciesResult.PolicyNames = append(resp.ListUserPoliciesResult.PolicyNames, policyName) } sort.Strings(resp.ListUserPoliciesResult.PolicyNames) } resp.ListUserPoliciesResult.IsTruncated = false return resp, nil } // GetPolicy retrieves a managed policy by ARN. func (iama *IamApiServer) GetPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetPolicyResponse, iamError *IamError) { resp = &GetPolicyResponse{} policyArn := values.Get("PolicyArn") policyName, iamError := parsePolicyArn(policyArn) if iamError != nil { return resp, iamError } policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } policyId := Hash(&policyName) path := "/" defaultVersionId := policyDefaultVersionId isAttachable := true resp.GetPolicyResult.Policy.PolicyName = &policyName resp.GetPolicyResult.Policy.Arn = &policyArn resp.GetPolicyResult.Policy.PolicyId = &policyId resp.GetPolicyResult.Policy.Path = &path resp.GetPolicyResult.Policy.DefaultVersionId = &defaultVersionId resp.GetPolicyResult.Policy.IsAttachable = &isAttachable return resp, nil } // DeletePolicy removes a managed policy. Rejects deletion if the policy is still attached to any user // (matching AWS IAM behavior: must detach before deleting). func (iama *IamApiServer) DeletePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeletePolicyResponse, iamError *IamError) { resp = &DeletePolicyResponse{} policyArn := values.Get("PolicyArn") policyName, iamError := parsePolicyArn(policyArn) if iamError != nil { return resp, iamError } policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } // Reject deletion if the policy is still attached to any user for _, ident := range s3cfg.Identities { for _, name := range ident.PolicyNames { if name == policyName { return resp, &IamError{ Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("policy %s is still attached to user %s", policyName, ident.Name), } } } } // Reject deletion if the policy is attached to any group if groupName, attached := isPolicyAttachedToAnyGroup(s3cfg, policyName); attached { return resp, &IamError{ Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("policy %s is still attached to group %s", policyName, groupName), } } delete(policies.Policies, policyName) if err := iama.s3ApiConfig.PutPolicies(&policies); err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } return resp, nil } // ListPolicies lists all managed policies. func (iama *IamApiServer) ListPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListPoliciesResponse, iamError *IamError) { resp = &ListPoliciesResponse{} policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } for policyName := range policies.Policies { name := policyName arn := fmt.Sprintf("arn:aws:iam:::policy/%s", name) policyId := Hash(&name) resp.ListPoliciesResult.Policies = append(resp.ListPoliciesResult.Policies, &iam.Policy{ PolicyName: &name, Arn: &arn, PolicyId: &policyId, }) } return resp, nil } // CreatePolicyVersion replaces a managed policy's document with a new version. // SeaweedFS keeps a single current document per managed policy (no version // history), so the new document always becomes version "v1" / the default. This // is what the AWS Terraform provider calls to update an aws_iam_policy in place. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicyVersion.html func (iama *IamApiServer) CreatePolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreatePolicyVersionResponse, iamError *IamError) { resp = &CreatePolicyVersionResponse{} policyArn := values.Get("PolicyArn") policyName, iamError := parsePolicyArn(policyArn) if iamError != nil { return resp, iamError } policyDocumentString := values.Get("PolicyDocument") if policyDocumentString == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")} } // SeaweedFS stores a single, always-default managed policy version. On AWS, // SetAsDefault=false stages a non-default version without activating it; we // can't honor that, so reject it rather than silently changing permissions. if !strings.EqualFold(values.Get("SetAsDefault"), "true") { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("SetAsDefault must be true: SeaweedFS stores a single managed policy version")} } policyDocument, err := GetPolicyDocument(&policyDocumentString) if err != nil { return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err} } if _, err := GetActions(&policyDocument); err != nil { return resp, &IamError{Code: iam.ErrCodeMalformedPolicyDocumentException, Error: err} } policies := Policies{} if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } policies.Policies[policyName] = policyDocument if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } // The denormalized Identity.Actions caches were derived from the old // document; refresh every identity that has this managed policy attached // (directly or via group membership) so the new document takes effect. recomputeActionsForManagedPolicy(iama, s3cfg, &policies, policyName) versionId := policyDefaultVersionId isDefault := true document := policyDocumentString resp.CreatePolicyVersionResult.PolicyVersion = iam.PolicyVersion{ VersionId: &versionId, IsDefaultVersion: &isDefault, Document: &document, } return resp, nil } // ListPolicyVersions lists the versions of a managed policy. SeaweedFS stores a // single current document per policy, so exactly one version ("v1", the // default) is reported. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListPolicyVersions.html func (iama *IamApiServer) ListPolicyVersions(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListPolicyVersionsResponse, iamError *IamError) { resp = &ListPolicyVersionsResponse{} policyName, iamError := parsePolicyArn(values.Get("PolicyArn")) if iamError != nil { return resp, iamError } policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } versionId := policyDefaultVersionId isDefault := true resp.ListPolicyVersionsResult.Versions = []*iam.PolicyVersion{{ VersionId: &versionId, IsDefaultVersion: &isDefault, }} resp.ListPolicyVersionsResult.IsTruncated = false return resp, nil } // GetPolicyVersion returns the document for a managed policy version. Only the // single stored version ("v1") exists. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicyVersion.html func (iama *IamApiServer) GetPolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *GetPolicyVersionResponse, iamError *IamError) { resp = &GetPolicyVersionResponse{} policyName, iamError := parsePolicyArn(values.Get("PolicyArn")) if iamError != nil { return resp, iamError } versionId := values.Get("VersionId") if versionId == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")} } policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } policyDocument, exists := policies.Policies[policyName] if !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } if versionId != policyDefaultVersionId { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionId)} } policyDocumentJSON, err := json.Marshal(policyDocument) if err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } isDefault := true document := string(policyDocumentJSON) resp.GetPolicyVersionResult.PolicyVersion = iam.PolicyVersion{ VersionId: &versionId, IsDefaultVersion: &isDefault, Document: &document, } return resp, nil } // DeletePolicyVersion is accepted for API completeness. With a single stored // version that is always the default, the only valid responses are NoSuchEntity // (unknown version) and the AWS "cannot delete the default version" conflict. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeletePolicyVersion.html func (iama *IamApiServer) DeletePolicyVersion(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeletePolicyVersionResponse, iamError *IamError) { resp = &DeletePolicyVersionResponse{} policyName, iamError := parsePolicyArn(values.Get("PolicyArn")) if iamError != nil { return resp, iamError } versionId := values.Get("VersionId") if versionId == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("VersionId is required")} } policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } if versionId == policyDefaultVersionId { return resp, &IamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete the default version of policy %s", policyName)} } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy version %s not found", versionId)} } // recomputeActionsForManagedPolicy refreshes Identity.Actions for every identity // affected by a change to the managed policy named policyName: users with the // policy attached directly, and members of groups the policy is attached to. func recomputeActionsForManagedPolicy(iama *IamApiServer, s3cfg *iam_pb.S3ApiConfiguration, policies *Policies, policyName string) { affected := make(map[string]*iam_pb.Identity) identIndex := make(map[string]*iam_pb.Identity, len(s3cfg.Identities)) for _, ident := range s3cfg.Identities { identIndex[ident.Name] = ident for _, pn := range ident.PolicyNames { if pn == policyName { affected[ident.Name] = ident break } } } for _, g := range s3cfg.Groups { attached := false for _, pn := range g.PolicyNames { if pn == policyName { attached = true break } } if !attached { continue } for _, member := range g.Members { if ident, ok := identIndex[member]; ok { affected[member] = ident } } } for name, ident := range affected { aggregatedActions, err := computeAllActionsForUser(iama, name, policies, ident, s3cfg) if err != nil { glog.Warningf("Failed to recompute actions for user %s after managed policy %s change: %v", name, policyName, err) continue } ident.Actions = aggregatedActions } } // AttachUserPolicy attaches a managed policy to a user. func (iama *IamApiServer) AttachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *AttachUserPolicyResponse, iamError *IamError) { resp = &AttachUserPolicyResponse{} userName := values.Get("UserName") policyArn := values.Get("PolicyArn") policyName, iamError := parsePolicyArn(policyArn) if iamError != nil { return resp, iamError } // Verify managed policy exists policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } if _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } // Find user and attach policy for _, ident := range s3cfg.Identities { if ident.Name != userName { continue } // Check if already attached for _, name := range ident.PolicyNames { if name == policyName { return resp, nil // Already attached, idempotent } } prevPolicyNames := ident.PolicyNames ident.PolicyNames = append(ident.PolicyNames, policyName) // Recompute aggregated actions (inline + managed + group) aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident, s3cfg) if err != nil { // Roll back PolicyNames to keep identity consistent ident.PolicyNames = prevPolicyNames return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to compute actions after attaching policy: %w", err)} } ident.Actions = aggregatedActions return resp, nil } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // DetachUserPolicy detaches a managed policy from a user. func (iama *IamApiServer) DetachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DetachUserPolicyResponse, iamError *IamError) { resp = &DetachUserPolicyResponse{} userName := values.Get("UserName") policyArn := values.Get("PolicyArn") policyName, iamError := parsePolicyArn(policyArn) if iamError != nil { return resp, iamError } for _, ident := range s3cfg.Identities { if ident.Name != userName { continue } // Find and remove policy name from the list prevPolicyNames := make([]string, len(ident.PolicyNames)) copy(prevPolicyNames, ident.PolicyNames) found := false for i, name := range ident.PolicyNames { if name == policyName { ident.PolicyNames = append(ident.PolicyNames[:i], ident.PolicyNames[i+1:]...) found = true break } } if !found { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s is not attached to user %s", policyName, userName)} } // Recompute aggregated actions (inline + managed) policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { // Roll back PolicyNames on storage error ident.PolicyNames = prevPolicyNames return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident, s3cfg) if err != nil { // Roll back PolicyNames to keep identity consistent ident.PolicyNames = prevPolicyNames return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to compute actions after detaching policy: %w", err)} } ident.Actions = aggregatedActions return resp, nil } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // ListAttachedUserPolicies lists the managed policies attached to a user. func (iama *IamApiServer) ListAttachedUserPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *ListAttachedUserPoliciesResponse, iamError *IamError) { resp = &ListAttachedUserPoliciesResponse{} userName := values.Get("UserName") for _, ident := range s3cfg.Identities { if ident.Name != userName { continue } for _, policyName := range ident.PolicyNames { name := policyName arn := fmt.Sprintf("arn:aws:iam:::policy/%s", name) resp.ListAttachedUserPoliciesResult.AttachedPolicies = append( resp.ListAttachedUserPoliciesResult.AttachedPolicies, &iam.AttachedPolicy{PolicyName: &name, PolicyArn: &arn}, ) } return resp, nil } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } // computeAllActionsForUser computes the union of actions from user inline policies, // user managed policies, group inline policies, and group managed policies. // If s3cfg is provided, group memberships are resolved to include group policies. func computeAllActionsForUser(iama *IamApiServer, userName string, policies *Policies, ident *iam_pb.Identity, s3cfgs ...*iam_pb.S3ApiConfiguration) ([]string, error) { actionSet := make(map[string]bool) var aggregatedActions []string addUniqueActions := func(actions []string) { for _, action := range actions { if !actionSet[action] { actionSet[action] = true aggregatedActions = append(aggregatedActions, action) } } } // Include user inline policy actions inlineActions, err := computeAggregatedActionsForUser(iama, userName, policies) if err != nil { return nil, err } addUniqueActions(inlineActions) // Include user managed policy actions for _, policyName := range ident.PolicyNames { if policyDoc, exists := policies.Policies[policyName]; exists { actions, err := GetActions(&policyDoc) if err != nil { glog.Warningf("Failed to get actions from managed policy '%s' for user %s: %v", policyName, userName, err) continue } addUniqueActions(actions) } } // Include group policies (both inline and managed) if s3cfg is available if len(s3cfgs) > 0 && s3cfgs[0] != nil { s3cfg := s3cfgs[0] for _, g := range s3cfg.Groups { if g.Disabled { continue } isMember := false for _, m := range g.Members { if m == userName { isMember = true break } } if !isMember { continue } // Group managed policies for _, policyName := range g.PolicyNames { if policyDoc, exists := policies.Policies[policyName]; exists { actions, err := GetActions(&policyDoc) if err != nil { glog.Warningf("Failed to get actions from group managed policy '%s' (group %s) for user %s: %v", policyName, g.Name, userName, err) continue } addUniqueActions(actions) } } // Group inline policies if groupPolicies := policies.GroupInlinePolicies[g.Name]; groupPolicies != nil { for policyName, policyDoc := range groupPolicies { actions, err := GetActions(&policyDoc) if err != nil { glog.Warningf("Failed to get actions from group inline policy '%s' (group %s) for user %s: %v", policyName, g.Name, userName, err) continue } addUniqueActions(actions) } } } } return aggregatedActions, nil } func GetActions(policy *policy_engine.PolicyDocument) ([]string, error) { var actions []string for _, statement := range policy.Statement { if statement.Effect != policy_engine.PolicyEffectAllow { return nil, fmt.Errorf("not a valid effect: '%s'. Only 'Allow' is possible", statement.Effect) } for _, resource := range statement.Resource.Strings() { // Parse "arn:aws:s3:::my-bucket/shared/*" res := strings.Split(resource, ":") if len(res) != 6 || res[0] != "arn" || res[1] != "aws" || res[2] != "s3" { continue } for _, action := range statement.Action.Strings() { // Parse "s3:Get*" act := strings.Split(action, ":") if len(act) != 2 || act[0] != "s3" { continue } statementAction := MapToStatementAction(act[1]) if statementAction == "" { return nil, fmt.Errorf("not a valid action: '%s'", act[1]) } path := res[5] if path == "*" { actions = append(actions, statementAction) continue } actions = append(actions, fmt.Sprintf("%s:%s", statementAction, path)) } } } return actions, nil } func (iama *IamApiServer) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *CreateAccessKeyResponse, iamErr *IamError) { resp = &CreateAccessKeyResponse{} userName := values.Get("UserName") status := iam.StatusTypeActive accessKeyId := values.Get("AccessKeyId") secretAccessKey := values.Get("SecretAccessKey") if accessKeyId != "" { if err := iamlib.ValidateCallerSuppliedAccessKeyId(accessKeyId); err != nil { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: err} } } if secretAccessKey != "" { if err := iamlib.ValidateCallerSuppliedSecretAccessKey(secretAccessKey); err != nil { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: err} } } if (accessKeyId != "") != (secretAccessKey != "") { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("AccessKeyId and SecretAccessKey must be supplied together")} } if owner := iamlib.FindAccessKeyOwner(s3cfg, accessKeyId); owner != nil { glog.V(4).Infof("CreateAccessKey: supplied AccessKeyId already in use by %s %s", owner.Type, owner.Name) return resp, &IamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("AccessKeyId is already in use")} } if accessKeyId == "" { var err error accessKeyId, err = StringWithCharset(21, charsetUpper) if err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate access key: %w", err)} } } if secretAccessKey == "" { var err error secretAccessKey, err = StringWithCharset(42, charset) if err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate secret key: %w", err)} } } resp.CreateAccessKeyResult.AccessKey.AccessKeyId = &accessKeyId resp.CreateAccessKeyResult.AccessKey.SecretAccessKey = &secretAccessKey resp.CreateAccessKeyResult.AccessKey.UserName = &userName resp.CreateAccessKeyResult.AccessKey.Status = &status changed := false for _, ident := range s3cfg.Identities { if userName == ident.Name { ident.Credentials = append(ident.Credentials, &iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey, Status: accessKeyStatusActive}) changed = true break } } if !changed { s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{ Name: userName, Credentials: []*iam_pb.Credential{ { AccessKey: accessKeyId, SecretKey: secretAccessKey, Status: accessKeyStatusActive, }, }, }, ) } return resp, nil } // UpdateAccessKey updates the status of an access key (Active or Inactive). func (iama *IamApiServer) UpdateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *UpdateAccessKeyResponse, err *IamError) { resp = &UpdateAccessKeyResponse{} userName := values.Get("UserName") accessKeyId := values.Get("AccessKeyId") status := values.Get("Status") if userName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")} } if accessKeyId == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("AccessKeyId is required")} } if err := validateAccessKeyStatus(status); err != nil { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: err} } for _, ident := range s3cfg.Identities { if ident.Name != userName { continue } for _, cred := range ident.Credentials { if cred.AccessKey == accessKeyId { cred.Status = status return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("the access key with id %s for user %s cannot be found", accessKeyId, userName)} } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } func (iama *IamApiServer) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp *DeleteAccessKeyResponse) { resp = &DeleteAccessKeyResponse{} userName := values.Get("UserName") accessKeyId := values.Get("AccessKeyId") for _, ident := range s3cfg.Identities { if userName == ident.Name { for i, cred := range ident.Credentials { if cred.AccessKey == accessKeyId { ident.Credentials = append(ident.Credentials[:i], ident.Credentials[i+1:]...) break } } break } } return resp } // handleImplicitUsername adds username who signs the request to values if 'username' is not specified. // According to AWS documentation: "If you do not specify a user name, IAM determines the user name // implicitly based on the Amazon Web Services access key ID signing the request." // This function extracts the AccessKeyId from the SigV4 credential and looks up the corresponding // identity name in the credential store. func (iama *IamApiServer) handleImplicitUsername(r *http.Request, values url.Values) { if len(r.Header["Authorization"]) == 0 || values.Get("UserName") != "" { return } // Log presence of auth header without exposing sensitive signature material glog.V(4).Infof("Authorization header present, extracting access key") // Parse AWS SigV4 Authorization header format: // "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/iam/aws4_request, ..." s := strings.Split(r.Header["Authorization"][0], "Credential=") if len(s) < 2 { return } s = strings.Split(s[1], ",") if len(s) < 1 { return } s = strings.Split(s[0], "/") if len(s) < 1 { return } // s[0] is the AccessKeyId accessKeyId := s[0] if accessKeyId == "" { return } // Nil-guard: ensure iam is initialized before lookup if iama.iam == nil { glog.V(4).Infof("IAM not initialized, cannot look up access key") return } // Look up the identity by access key to get the username identity, _, found := iama.iam.LookupByAccessKey(accessKeyId) if !found { // Mask access key in logs - show only first 4 chars maskedKey := accessKeyId if len(accessKeyId) > 4 { maskedKey = accessKeyId[:4] + "***" } glog.V(4).Infof("Access key %s not found in credential store", maskedKey) return } values.Set("UserName", identity.Name) } func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) { // Lock to prevent concurrent read-modify-write race conditions policyLock.Lock() defer policyLock.Unlock() r, reqID := request_id.Ensure(r) if err := r.ParseForm(); err != nil { s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) return } values := r.PostForm s3cfg := &iam_pb.S3ApiConfiguration{} if err := iama.s3ApiConfig.GetS3ApiConfiguration(s3cfg); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) return } glog.V(4).Infof("DoActions: %+v", iamlib.RedactSensitiveFormValues(values)) var response iamlib.RequestIDSetter changed := true switch r.Form.Get("Action") { case "ListUsers": response = iama.ListUsers(s3cfg, values) changed = false case "ListAccessKeys": iama.handleImplicitUsername(r, values) response = iama.ListAccessKeys(s3cfg, values) changed = false case "CreateUser": response = iama.CreateUser(s3cfg, values) // Use targeted create to avoid rewriting all existing user files via SaveConfiguration. // credentialManager.CreateUser writes only the new user's file. if iama.iam != nil { if cm := iama.iam.GetCredentialManager(); cm != nil { userName := values.Get("UserName") if err := cm.CreateUser(r.Context(), &iam_pb.Identity{Name: userName}); err != nil { writeIamErrorResponse(w, r, reqID, &IamError{Code: s3api.CredentialErrToIamErrCode(err), Error: err}) return } changed = false } } case "GetUser": userName := values.Get("UserName") var err *IamError response, err = iama.GetUser(s3cfg, userName) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "UpdateUser": var err *IamError response, err = iama.UpdateUser(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "DeleteUser": userName := values.Get("UserName") var err *IamError response, err = iama.DeleteUser(s3cfg, userName) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "CreateAccessKey": iama.handleImplicitUsername(r, values) var err *IamError response, err = iama.CreateAccessKey(s3cfg, values) if err != nil { glog.Errorf("CreateAccessKey: %+v", err.Error) writeIamErrorResponse(w, r, reqID, err) return } case "DeleteAccessKey": iama.handleImplicitUsername(r, values) response = iama.DeleteAccessKey(s3cfg, values) case "UpdateAccessKey": iama.handleImplicitUsername(r, values) var err *IamError response, err = iama.UpdateAccessKey(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "CreatePolicy": var err *IamError response, err = iama.CreatePolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } // CreatePolicy persists the policy document via iama.s3ApiConfig.PutPolicies(). // The `changed` flag is false because this does not modify the main s3cfg.Identities configuration. changed = false case "PutUserPolicy": var err *IamError response, err = iama.PutUserPolicy(s3cfg, values) if err != nil { glog.Errorf("PutUserPolicy: %+v", err.Error) writeIamErrorResponse(w, r, reqID, err) return } case "GetUserPolicy": var err *IamError response, err = iama.GetUserPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "DeleteUserPolicy": var err *IamError response, err = iama.DeleteUserPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "ListUserPolicies": iama.handleImplicitUsername(r, values) var err *IamError response, err = iama.ListUserPolicies(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "GetPolicy": var err *IamError response, err = iama.GetPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "DeletePolicy": var err *IamError response, err = iama.DeletePolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "ListPolicies": var err *IamError response, err = iama.ListPolicies(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "CreatePolicyVersion": var err *IamError response, err = iama.CreatePolicyVersion(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } // CreatePolicyVersion persists the new document via PutPolicies and // recomputes affected Identity.Actions on s3cfg; keep changed=true so the // updated identities are saved. case "ListPolicyVersions": var err *IamError response, err = iama.ListPolicyVersions(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "GetPolicyVersion": var err *IamError response, err = iama.GetPolicyVersion(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "DeletePolicyVersion": var err *IamError response, err = iama.DeletePolicyVersion(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "AttachUserPolicy": var err *IamError response, err = iama.AttachUserPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "DetachUserPolicy": var err *IamError response, err = iama.DetachUserPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "ListAttachedUserPolicies": var err *IamError response, err = iama.ListAttachedUserPolicies(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false // Group actions case "CreateGroup": var err *IamError response, err = iama.CreateGroup(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "DeleteGroup": var err *IamError response, err = iama.DeleteGroup(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "UpdateGroup": var err *IamError response, err = iama.UpdateGroup(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "GetGroup": var err *IamError response, err = iama.GetGroup(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "ListGroups": response = iama.ListGroups(s3cfg, values) changed = false case "AddUserToGroup": var err *IamError response, err = iama.AddUserToGroup(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "RemoveUserFromGroup": var err *IamError response, err = iama.RemoveUserFromGroup(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "AttachGroupPolicy": var err *IamError response, err = iama.AttachGroupPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "DetachGroupPolicy": var err *IamError response, err = iama.DetachGroupPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } case "ListAttachedGroupPolicies": var err *IamError response, err = iama.ListAttachedGroupPolicies(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "PutGroupPolicy": var err *IamError response, err = iama.PutGroupPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } // changed = true: PutGroupPolicy recomputes member Identity.Actions case "GetGroupPolicy": var err *IamError response, err = iama.GetGroupPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "DeleteGroupPolicy": var err *IamError response, err = iama.DeleteGroupPolicy(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } // changed = true: DeleteGroupPolicy recomputes member Identity.Actions case "ListGroupPolicies": var err *IamError response, err = iama.ListGroupPolicies(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false case "ListGroupsForUser": var err *IamError response, err = iama.ListGroupsForUser(s3cfg, values) if err != nil { writeIamErrorResponse(w, r, reqID, err) return } changed = false default: errNotImplemented := s3err.GetAPIError(s3err.ErrNotImplemented) errorResponse := newErrorResponse(errNotImplemented.Code, errNotImplemented.Description, reqID) s3err.WriteXMLResponse(w, r, errNotImplemented.HTTPStatusCode, errorResponse) return } if changed { err := iama.s3ApiConfig.PutS3ApiConfiguration(s3cfg) if err != nil { var iamError = IamError{Code: iam.ErrCodeServiceFailureException, Error: err} writeIamErrorResponse(w, r, reqID, &iamError) return } // Reload in-memory identity maps so subsequent LookupByAccessKey calls // can see newly created or deleted keys immediately if iama.iam != nil { if err := iama.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil { glog.Warningf("Failed to reload IAM configuration after mutation: %v", err) // Don't fail the request since the persistent save succeeded } } } response.SetRequestId(reqID) s3err.WriteXMLResponse(w, r, http.StatusOK, response) }