package iamapi import ( "encoding/json" "errors" "fmt" "net/url" "sort" "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/policy_engine" ) func (iama *IamApiServer) CreateGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*CreateGroupResponse, *IamError) { resp := &CreateGroupResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } for _, g := range s3cfg.Groups { if g.Name == groupName { return resp, &IamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("group %s already exists", groupName)} } } s3cfg.Groups = append(s3cfg.Groups, &iam_pb.Group{Name: groupName}) resp.CreateGroupResult.Group.GroupName = &groupName return resp, nil } func (iama *IamApiServer) DeleteGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*DeleteGroupResponse, *IamError) { resp := &DeleteGroupResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } for i, g := range s3cfg.Groups { if g.Name == groupName { if len(g.Members) > 0 { return resp, &IamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete group %s: group has %d member(s)", groupName, len(g.Members))} } if len(g.PolicyNames) > 0 { return resp, &IamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete group %s: group has %d attached policy(ies)", groupName, len(g.PolicyNames))} } // Check for inline policies 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 gp := policies.GroupInlinePolicies[groupName]; len(gp) > 0 { return resp, &IamError{Code: iam.ErrCodeDeleteConflictException, Error: fmt.Errorf("cannot delete group %s: group has %d inline policy(ies)", groupName, len(gp))} } s3cfg.Groups = append(s3cfg.Groups[:i], s3cfg.Groups[i+1:]...) // Clean up any empty inline policy entries, reuse already-fetched policies if err := cleanupGroupInlinePolicies(iama, groupName, &policies); err != nil { glog.Warningf("Failed to cleanup inline policies for group %s: %v", groupName, err) } return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) UpdateGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*UpdateGroupResponse, *IamError) { resp := &UpdateGroupResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } for _, g := range s3cfg.Groups { if g.Name == groupName { if disabled := values.Get("Disabled"); disabled != "" { if disabled != "true" && disabled != "false" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("Disabled must be 'true' or 'false'")} } g.Disabled = disabled == "true" } if newName := values.Get("NewGroupName"); newName != "" && newName != g.Name { for _, other := range s3cfg.Groups { if other.Name == newName { return resp, &IamError{Code: iam.ErrCodeEntityAlreadyExistsException, Error: fmt.Errorf("group %s already exists", newName)} } } oldName := g.Name g.Name = newName if err := migrateGroupInlinePolicies(iama, oldName, newName); err != nil { glog.Warningf("Failed to migrate inline policies for group rename %s -> %s: %v", oldName, newName, err) } } return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) GetGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*GetGroupResponse, *IamError) { resp := &GetGroupResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } for _, g := range s3cfg.Groups { if g.Name == groupName { resp.GetGroupResult.Group.GroupName = &g.Name for _, member := range g.Members { user := iamlib.NewUser(member) resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &user) } return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) ListGroups(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) *ListGroupsResponse { resp := &ListGroupsResponse{} for _, g := range s3cfg.Groups { name := g.Name resp.ListGroupsResult.Groups = append(resp.ListGroupsResult.Groups, &iam.Group{GroupName: &name}) } return resp } func (iama *IamApiServer) AddUserToGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*AddUserToGroupResponse, *IamError) { resp := &AddUserToGroupResponse{} groupName := values.Get("GroupName") userName := values.Get("UserName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } if userName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")} } userFound := false for _, ident := range s3cfg.Identities { if ident.Name == userName { userFound = true break } } if !userFound { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("user %s does not exist", userName)} } for _, g := range s3cfg.Groups { if g.Name == groupName { for _, m := range g.Members { if m == userName { return resp, nil } } g.Members = append(g.Members, userName) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) RemoveUserFromGroup(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*RemoveUserFromGroupResponse, *IamError) { resp := &RemoveUserFromGroupResponse{} groupName := values.Get("GroupName") userName := values.Get("UserName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } if userName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")} } for _, g := range s3cfg.Groups { if g.Name == groupName { for i, m := range g.Members { if m == userName { g.Members = append(g.Members[:i], g.Members[i+1:]...) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("user %s is not a member of group %s", userName, groupName)} } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) AttachGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*AttachGroupPolicyResponse, *IamError) { resp := &AttachGroupPolicyResponse{} groupName := values.Get("GroupName") policyArn := values.Get("PolicyArn") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } policyName, iamErr := parsePolicyArn(policyArn) if iamErr != nil { return resp, iamErr } // Verify policy exists in the persisted policies store 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 _, exists := policies.Policies[policyName]; !exists { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found", policyName)} } for _, g := range s3cfg.Groups { if g.Name == groupName { for _, p := range g.PolicyNames { if p == policyName { return resp, nil } } g.PolicyNames = append(g.PolicyNames, policyName) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) DetachGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*DetachGroupPolicyResponse, *IamError) { resp := &DetachGroupPolicyResponse{} groupName := values.Get("GroupName") policyArn := values.Get("PolicyArn") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } policyName, iamErr := parsePolicyArn(policyArn) if iamErr != nil { return resp, iamErr } for _, g := range s3cfg.Groups { if g.Name == groupName { for i, p := range g.PolicyNames { if p == policyName { g.PolicyNames = append(g.PolicyNames[:i], g.PolicyNames[i+1:]...) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s is not attached to group %s", policyName, groupName)} } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) ListAttachedGroupPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*ListAttachedGroupPoliciesResponse, *IamError) { resp := &ListAttachedGroupPoliciesResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } for _, g := range s3cfg.Groups { if g.Name == groupName { for _, policyName := range g.PolicyNames { pn := policyName policyArn := policyArnPrefix + pn resp.ListAttachedGroupPoliciesResult.AttachedPolicies = append(resp.ListAttachedGroupPoliciesResult.AttachedPolicies, &iam.AttachedPolicy{ PolicyName: &pn, PolicyArn: &policyArn, }) } return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } func (iama *IamApiServer) ListGroupsForUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*ListGroupsForUserResponse, *IamError) { resp := &ListGroupsForUserResponse{} userName := values.Get("UserName") if userName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("UserName is required")} } userFound := false for _, ident := range s3cfg.Identities { if ident.Name == userName { userFound = true break } } if !userFound { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("user %s does not exist", userName)} } // Build reverse index for efficient lookup userGroupsIndex := buildUserGroupsIndex(s3cfg) for _, gName := range userGroupsIndex[userName] { name := gName resp.ListGroupsForUserResult.Groups = append(resp.ListGroupsForUserResult.Groups, &iam.Group{GroupName: &name}) } return resp, nil } // removeUserFromAllGroups removes a user from all groups they belong to. // Uses a reverse index for efficient lookup of which groups to modify. func removeUserFromAllGroups(s3cfg *iam_pb.S3ApiConfiguration, userName string) { userGroupsIndex := buildUserGroupsIndex(s3cfg) groupNames, found := userGroupsIndex[userName] if !found { return } // Build a set for fast group name lookup targetGroups := make(map[string]bool, len(groupNames)) for _, gn := range groupNames { targetGroups[gn] = true } for _, g := range s3cfg.Groups { if !targetGroups[g.Name] { continue } for i, m := range g.Members { if m == userName { g.Members = append(g.Members[:i], g.Members[i+1:]...) break } } } } // updateUserInGroups updates group membership references when a user is renamed. func updateUserInGroups(s3cfg *iam_pb.S3ApiConfiguration, oldUserName, newUserName string) { for _, g := range s3cfg.Groups { for i, m := range g.Members { if m == oldUserName { g.Members[i] = newUserName break } } } } // isPolicyAttachedToAnyGroup checks if a policy is attached to any group. func isPolicyAttachedToAnyGroup(s3cfg *iam_pb.S3ApiConfiguration, policyName string) (string, bool) { for _, g := range s3cfg.Groups { for _, p := range g.PolicyNames { if p == policyName { return g.Name, true } } } return "", false } // buildUserGroupsIndex builds a reverse index mapping usernames to group names. func buildUserGroupsIndex(s3cfg *iam_pb.S3ApiConfiguration) map[string][]string { index := make(map[string][]string) for _, g := range s3cfg.Groups { for _, m := range g.Members { index[m] = append(index[m], g.Name) } } return index } // PutGroupPolicy attaches an inline policy to a group. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_PutGroupPolicy.html func (iama *IamApiServer) PutGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*PutGroupPolicyResponse, *IamError) { resp := &PutGroupPolicyResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } policyName := values.Get("PolicyName") if policyName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")} } policyDocumentString := values.Get("PolicyDocument") if policyDocumentString == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyDocument is required")} } 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} } // Find group and get its members for action recomputation var targetGroup *iam_pb.Group for _, g := range s3cfg.Groups { if g.Name == groupName { targetGroup = g break } } if targetGroup == nil { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } // Persist inline policy policies := Policies{} if pErr := iama.s3ApiConfig.GetPolicies(&policies); pErr != nil && !errors.Is(pErr, filer_pb.ErrNotFound) { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } groupPolicies := policies.getOrCreateGroupPolicies(groupName) groupPolicies[policyName] = policyDocument if pErr := iama.s3ApiConfig.PutPolicies(&policies); pErr != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } // Recompute actions for all group members recomputeActionsForGroupMembers(iama, s3cfg, targetGroup, &policies) return resp, nil } // GetGroupPolicy gets an inline policy attached to a group. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetGroupPolicy.html func (iama *IamApiServer) GetGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*GetGroupPolicyResponse, *IamError) { resp := &GetGroupPolicyResponse{} groupName := values.Get("GroupName") policyName := values.Get("PolicyName") // Verify group exists found := false for _, g := range s3cfg.Groups { if g.Name == groupName { found = true break } } if !found { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } 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 groupPolicies := policies.GroupInlinePolicies[groupName]; groupPolicies != nil { if policyDocument, exists := groupPolicies[policyName]; exists { policyDocumentJSON, err := json.Marshal(policyDocument) if err != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } resp.GetGroupPolicyResult.GroupName = groupName resp.GetGroupPolicyResult.PolicyName = policyName resp.GetGroupPolicyResult.PolicyDocument = string(policyDocumentJSON) return resp, nil } } return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("policy %s not found on group %s", policyName, groupName)} } // DeleteGroupPolicy removes an inline policy from a group. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteGroupPolicy.html func (iama *IamApiServer) DeleteGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*DeleteGroupPolicyResponse, *IamError) { resp := &DeleteGroupPolicyResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } policyName := values.Get("PolicyName") if policyName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("PolicyName is required")} } // Find group for member action recomputation var targetGroup *iam_pb.Group for _, g := range s3cfg.Groups { if g.Name == groupName { targetGroup = g break } } if targetGroup == nil { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } 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 groupPolicies := policies.GroupInlinePolicies[groupName]; groupPolicies != nil { delete(groupPolicies, policyName) if pErr := iama.s3ApiConfig.PutPolicies(&policies); pErr != nil { return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: pErr} } } // Recompute actions for all group members recomputeActionsForGroupMembers(iama, s3cfg, targetGroup, &policies) return resp, nil } // ListGroupPolicies lists the names of inline policies attached to a group. // https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListGroupPolicies.html func (iama *IamApiServer) ListGroupPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*ListGroupPoliciesResponse, *IamError) { resp := &ListGroupPoliciesResponse{} groupName := values.Get("GroupName") if groupName == "" { return resp, &IamError{Code: iam.ErrCodeInvalidInputException, Error: fmt.Errorf("GroupName is required")} } // Verify group exists found := false for _, g := range s3cfg.Groups { if g.Name == groupName { found = true break } } if !found { return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf("group %s does not exist", groupName)} } 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 groupPolicies := policies.GroupInlinePolicies[groupName]; groupPolicies != nil { for policyName := range groupPolicies { resp.ListGroupPoliciesResult.PolicyNames = append(resp.ListGroupPoliciesResult.PolicyNames, policyName) } sort.Strings(resp.ListGroupPoliciesResult.PolicyNames) } resp.ListGroupPoliciesResult.IsTruncated = false return resp, nil } // cleanupGroupInlinePolicies removes all inline policies for a group from persistent storage. // If policies is provided, it uses that to avoid redundant I/O; otherwise fetches from storage. func cleanupGroupInlinePolicies(iama *IamApiServer, groupName string, policies *Policies) error { if policies == nil { policies = &Policies{} if err := iama.s3ApiConfig.GetPolicies(policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return err } } if _, exists := policies.GroupInlinePolicies[groupName]; exists { delete(policies.GroupInlinePolicies, groupName) return iama.s3ApiConfig.PutPolicies(policies) } return nil } // migrateGroupInlinePolicies renames the inline policies key when a group is renamed. func migrateGroupInlinePolicies(iama *IamApiServer, oldName, newName string) error { policies := Policies{} if err := iama.s3ApiConfig.GetPolicies(&policies); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { return err } if oldPolicies, exists := policies.GroupInlinePolicies[oldName]; exists { if policies.GroupInlinePolicies == nil { policies.GroupInlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument) } policies.GroupInlinePolicies[newName] = oldPolicies delete(policies.GroupInlinePolicies, oldName) return iama.s3ApiConfig.PutPolicies(&policies) } return nil } // recomputeActionsForGroupMembers recomputes the aggregated actions for all members of a group. // Uses an identity index for O(N+M) complexity instead of O(N*M). func recomputeActionsForGroupMembers(iama *IamApiServer, s3cfg *iam_pb.S3ApiConfiguration, group *iam_pb.Group, policies *Policies) { // Build name -> identity index for O(1) lookup identIndex := make(map[string]*iam_pb.Identity, len(s3cfg.Identities)) for _, ident := range s3cfg.Identities { identIndex[ident.Name] = ident } for _, memberName := range group.Members { if ident, ok := identIndex[memberName]; ok { aggregatedActions, err := computeAllActionsForUser(iama, memberName, policies, ident, s3cfg) if err != nil { glog.Warningf("Failed to recompute actions for user %s after group policy change: %v", memberName, err) } else { ident.Actions = aggregatedActions } } } }