From 204512151bad5c34986f611d4051fb39169e4f46 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sun, 8 Mar 2026 14:29:58 -0700 Subject: [PATCH] iam: add group management handlers to standalone IAM API Add group handlers (CreateGroup, DeleteGroup, GetGroup, ListGroups, AddUserToGroup, RemoveUserFromGroup, AttachGroupPolicy, DetachGroupPolicy, ListAttachedGroupPolicies, ListGroupsForUser) and wire into DoActions dispatch. Also add helper functions for user/policy side effects. --- weed/iamapi/iamapi_group_handlers.go | 282 ++++++++++++++++++++++ weed/iamapi/iamapi_management_handlers.go | 70 ++++++ weed/iamapi/iamapi_response.go | 11 + 3 files changed, 363 insertions(+) create mode 100644 weed/iamapi/iamapi_group_handlers.go diff --git a/weed/iamapi/iamapi_group_handlers.go b/weed/iamapi/iamapi_group_handlers.go new file mode 100644 index 000000000..0af42880b --- /dev/null +++ b/weed/iamapi/iamapi_group_handlers.go @@ -0,0 +1,282 @@ +package iamapi + +import ( + "fmt" + "net/url" + "strings" + + "github.com/aws/aws-sdk-go/service/iam" + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" +) + +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))} + } + s3cfg.Groups = append(s3cfg.Groups[:i], s3cfg.Groups[i+1:]...) + 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 { + m := member + resp.GetGroupResult.Users = append(resp.GetGroupResult.Users, &iam.User{UserName: &m}) + } + 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 + policyFound := false + for _, p := range s3cfg.Policies { + if p.Name == policyName { + policyFound = true + break + } + } + if !policyFound { + 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)} + } + for _, g := range s3cfg.Groups { + for _, m := range g.Members { + if m == userName { + name := g.Name + resp.ListGroupsForUserResult.Groups = append(resp.ListGroupsForUserResult.Groups, &iam.Group{GroupName: &name}) + break + } + } + } + return resp, nil +} + +// removeUserFromAllGroups removes a user from all groups they belong to. +func removeUserFromAllGroups(s3cfg *iam_pb.S3ApiConfiguration, userName string) { + for _, g := range s3cfg.Groups { + 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 +} + +// policyNameFromArn extracts policy name from ARN for standalone handlers. +func policyNameFromArn(policyArn string) string { + return strings.TrimPrefix(policyArn, policyArnPrefix) +} diff --git a/weed/iamapi/iamapi_management_handlers.go b/weed/iamapi/iamapi_management_handlers.go index c2141e6cd..78c580821 100644 --- a/weed/iamapi/iamapi_management_handlers.go +++ b/weed/iamapi/iamapi_management_handlers.go @@ -1055,6 +1055,76 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) { 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 "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 "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) diff --git a/weed/iamapi/iamapi_response.go b/weed/iamapi/iamapi_response.go index ea596655d..a8dafb50c 100644 --- a/weed/iamapi/iamapi_response.go +++ b/weed/iamapi/iamapi_response.go @@ -36,4 +36,15 @@ type ( ListServiceAccountsResponse = iamlib.ListServiceAccountsResponse GetServiceAccountResponse = iamlib.GetServiceAccountResponse UpdateServiceAccountResponse = iamlib.UpdateServiceAccountResponse + // Group response types + CreateGroupResponse = iamlib.CreateGroupResponse + DeleteGroupResponse = iamlib.DeleteGroupResponse + GetGroupResponse = iamlib.GetGroupResponse + ListGroupsResponse = iamlib.ListGroupsResponse + AddUserToGroupResponse = iamlib.AddUserToGroupResponse + RemoveUserFromGroupResponse = iamlib.RemoveUserFromGroupResponse + AttachGroupPolicyResponse = iamlib.AttachGroupPolicyResponse + DetachGroupPolicyResponse = iamlib.DetachGroupPolicyResponse + ListAttachedGroupPoliciesResponse = iamlib.ListAttachedGroupPoliciesResponse + ListGroupsForUserResponse = iamlib.ListGroupsForUserResponse )