diff --git a/test/s3/iam/s3_iam_group_test.go b/test/s3/iam/s3_iam_group_test.go index 1043e7c95..bcb9d895e 100644 --- a/test/s3/iam/s3_iam_group_test.go +++ b/test/s3/iam/s3_iam_group_test.go @@ -778,6 +778,105 @@ func TestIAMGroupRawAPI(t *testing.T) { }) } +// TestIAMGroupInlinePolicy tests group inline policy operations: +// PutGroupPolicy, GetGroupPolicy, ListGroupPolicies, DeleteGroupPolicy +func TestIAMGroupInlinePolicy(t *testing.T) { + framework := NewS3IAMTestFramework(t) + defer framework.Cleanup() + + iamClient, err := framework.CreateIAMClientWithJWT("admin-user", "TestAdminRole") + require.NoError(t, err) + + // Skip if running against embedded IAM (which returns NotImplemented for group inline policies) + _, probeErr := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{GroupName: aws.String("probe-group-inline-support")}) + if probeErr != nil { + if awsErr, ok := probeErr.(awserr.Error); ok && awsErr.Code() == "NotImplemented" { + t.Skip("Skipping: group inline policies not supported in embedded IAM mode") + } + } + require.NoError(t, err) + + groupName := "test-group-inline-policy" + policyName := "TestInlinePolicy" + _, err = iamClient.CreateGroup(&iam.CreateGroupInput{ + GroupName: aws.String(groupName), + }) + require.NoError(t, err) + defer func() { + // Clean up inline policies before deleting the group + iamClient.DeleteGroupPolicy(&iam.DeleteGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyName: aws.String(policyName), + }) + iamClient.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)}) + }() + + t.Run("list_empty", func(t *testing.T) { + resp, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{ + GroupName: aws.String(groupName), + }) + require.NoError(t, err) + assert.Empty(t, resp.PolicyNames) + assert.False(t, *resp.IsTruncated) + }) + + policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}` + + t.Run("put_group_policy", func(t *testing.T) { + _, err := iamClient.PutGroupPolicy(&iam.PutGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyName: aws.String(policyName), + PolicyDocument: aws.String(policyDoc), + }) + require.NoError(t, err) + }) + + t.Run("list_after_put", func(t *testing.T) { + resp, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{ + GroupName: aws.String(groupName), + }) + require.NoError(t, err) + assert.NotEmpty(t, resp.PolicyNames) + }) + + t.Run("get_group_policy", func(t *testing.T) { + resp, err := iamClient.GetGroupPolicy(&iam.GetGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyName: aws.String(policyName), + }) + require.NoError(t, err) + assert.Equal(t, groupName, *resp.GroupName) + assert.Equal(t, policyName, *resp.PolicyName) + assert.Contains(t, *resp.PolicyDocument, "s3:GetObject") + }) + + t.Run("delete_group_policy", func(t *testing.T) { + _, err := iamClient.DeleteGroupPolicy(&iam.DeleteGroupPolicyInput{ + GroupName: aws.String(groupName), + PolicyName: aws.String(policyName), + }) + require.NoError(t, err) + }) + + t.Run("list_after_delete", func(t *testing.T) { + resp, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{ + GroupName: aws.String(groupName), + }) + require.NoError(t, err) + assert.Empty(t, resp.PolicyNames) + }) + + t.Run("nonexistent_group", func(t *testing.T) { + _, err := iamClient.ListGroupPolicies(&iam.ListGroupPoliciesInput{ + GroupName: aws.String("nonexistent-group-for-policies"), + }) + require.Error(t, err) + awsErr, ok := err.(awserr.Error) + require.True(t, ok, "Expected AWS error type") + assert.Equal(t, iam.ErrCodeNoSuchEntityException, awsErr.Code()) + }) +} + // createS3Client creates an S3 client with static credentials func createS3Client(t *testing.T, accessKey, secretKey string) *s3.S3 { sess, err := session.NewSession(&aws.Config{ diff --git a/weed/iam/responses.go b/weed/iam/responses.go index 67e5b71e9..786a3aa20 100644 --- a/weed/iam/responses.go +++ b/weed/iam/responses.go @@ -371,6 +371,40 @@ type ListAttachedGroupPoliciesResponse struct { CommonResponse } +// PutGroupPolicyResponse is the response for PutGroupPolicy action. +type PutGroupPolicyResponse struct { + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutGroupPolicyResponse"` + CommonResponse +} + +// GetGroupPolicyResponse is the response for GetGroupPolicy action. +type GetGroupPolicyResponse struct { + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetGroupPolicyResponse"` + GetGroupPolicyResult struct { + GroupName string `xml:"GroupName"` + PolicyName string `xml:"PolicyName"` + PolicyDocument string `xml:"PolicyDocument"` + } `xml:"GetGroupPolicyResult"` + CommonResponse +} + +// DeleteGroupPolicyResponse is the response for DeleteGroupPolicy action. +type DeleteGroupPolicyResponse struct { + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteGroupPolicyResponse"` + CommonResponse +} + +// ListGroupPoliciesResponse is the response for ListGroupPolicies action. +type ListGroupPoliciesResponse struct { + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListGroupPoliciesResponse"` + ListGroupPoliciesResult struct { + PolicyNames []string `xml:"PolicyNames>member"` + IsTruncated bool `xml:"IsTruncated"` + Marker string `xml:"Marker,omitempty"` + } `xml:"ListGroupPoliciesResult"` + CommonResponse +} + // ListGroupsForUserResponse is the response for ListGroupsForUser action. type ListGroupsForUserResponse struct { XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListGroupsForUserResponse"` diff --git a/weed/iamapi/iamapi_group_handlers.go b/weed/iamapi/iamapi_group_handlers.go index 620fc1f62..3e44294d2 100644 --- a/weed/iamapi/iamapi_group_handlers.go +++ b/weed/iamapi/iamapi_group_handlers.go @@ -1,13 +1,17 @@ package iamapi import ( + "encoding/json" "errors" "fmt" "net/url" + "sort" "github.com/aws/aws-sdk-go/service/iam" + "github.com/seaweedfs/seaweedfs/weed/glog" "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) { @@ -40,7 +44,19 @@ func (iama *IamApiServer) DeleteGroup(s3cfg *iam_pb.S3ApiConfiguration, values u 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 } } @@ -67,7 +83,11 @@ func (iama *IamApiServer) UpdateGroup(s3cfg *iam_pb.S3ApiConfiguration, values u 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 } @@ -326,3 +346,225 @@ func buildUserGroupsIndex(s3cfg *iam_pb.S3ApiConfiguration) map[string][]string } 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 + } + } + } +} diff --git a/weed/iamapi/iamapi_management_handlers.go b/weed/iamapi/iamapi_management_handlers.go index cfcaac098..1e4b9c404 100644 --- a/weed/iamapi/iamapi_management_handlers.go +++ b/weed/iamapi/iamapi_management_handlers.go @@ -70,6 +70,17 @@ func (p *Policies) getOrCreateUserPolicies(userName string) map[string]policy_en 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). @@ -139,6 +150,10 @@ type Policies struct { // 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 { @@ -394,7 +409,7 @@ func (iama *IamApiServer) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values } // Recompute aggregated actions (inline + managed) - aggregatedActions, computeErr := computeAllActionsForUser(iama, userName, &policies, targetIdent) + 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 { @@ -521,7 +536,7 @@ func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, val } // Recompute aggregated actions from remaining inline + managed policies - aggregatedActions, computeErr := computeAllActionsForUser(iama, userName, &policies, targetIdent) + 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 { @@ -535,6 +550,9 @@ func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, val 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 @@ -690,8 +708,8 @@ func (iama *IamApiServer) AttachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, val prevPolicyNames := ident.PolicyNames ident.PolicyNames = append(ident.PolicyNames, policyName) - // Recompute aggregated actions (inline + managed) - aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident) + // 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 @@ -740,7 +758,7 @@ func (iama *IamApiServer) DetachUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, val ident.PolicyNames = prevPolicyNames return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err} } - aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident) + aggregatedActions, err := computeAllActionsForUser(iama, userName, &policies, ident, s3cfg) if err != nil { // Roll back PolicyNames to keep identity consistent ident.PolicyNames = prevPolicyNames @@ -773,8 +791,10 @@ func (iama *IamApiServer) ListAttachedUserPolicies(s3cfg *iam_pb.S3ApiConfigurat return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)} } -// computeAllActionsForUser computes the union of actions from both inline and managed policies. -func computeAllActionsForUser(iama *IamApiServer, userName string, policies *Policies, ident *iam_pb.Identity) ([]string, error) { +// 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 @@ -787,14 +807,14 @@ func computeAllActionsForUser(iama *IamApiServer, userName string, policies *Pol } } - // Include inline policy actions + // Include user inline policy actions inlineActions, err := computeAggregatedActionsForUser(iama, userName, policies) if err != nil { return nil, err } addUniqueActions(inlineActions) - // Include managed policy actions + // Include user managed policy actions for _, policyName := range ident.PolicyNames { if policyDoc, exists := policies.Policies[policyName]; exists { actions, err := GetActions(&policyDoc) @@ -806,6 +826,48 @@ func computeAllActionsForUser(iama *IamApiServer, userName string, policies *Pol } } + // 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 } @@ -1221,6 +1283,38 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) { 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) diff --git a/weed/iamapi/iamapi_response.go b/weed/iamapi/iamapi_response.go index 2f28e0e23..00f66c725 100644 --- a/weed/iamapi/iamapi_response.go +++ b/weed/iamapi/iamapi_response.go @@ -48,5 +48,9 @@ type ( AttachGroupPolicyResponse = iamlib.AttachGroupPolicyResponse DetachGroupPolicyResponse = iamlib.DetachGroupPolicyResponse ListAttachedGroupPoliciesResponse = iamlib.ListAttachedGroupPoliciesResponse + PutGroupPolicyResponse = iamlib.PutGroupPolicyResponse + GetGroupPolicyResponse = iamlib.GetGroupPolicyResponse + DeleteGroupPolicyResponse = iamlib.DeleteGroupPolicyResponse + ListGroupPoliciesResponse = iamlib.ListGroupPoliciesResponse ListGroupsForUserResponse = iamlib.ListGroupsForUserResponse ) diff --git a/weed/s3api/s3api_embedded_iam.go b/weed/s3api/s3api_embedded_iam.go index 06971e684..d30b41abf 100644 --- a/weed/s3api/s3api_embedded_iam.go +++ b/weed/s3api/s3api_embedded_iam.go @@ -125,6 +125,10 @@ type ( iamAttachGroupPolicyResponse = iamlib.AttachGroupPolicyResponse iamDetachGroupPolicyResponse = iamlib.DetachGroupPolicyResponse iamListAttachedGroupPoliciesResponse = iamlib.ListAttachedGroupPoliciesResponse + iamPutGroupPolicyResponse = iamlib.PutGroupPolicyResponse + iamGetGroupPolicyResponse = iamlib.GetGroupPolicyResponse + iamDeleteGroupPolicyResponse = iamlib.DeleteGroupPolicyResponse + iamListGroupPoliciesResponse = iamlib.ListGroupPoliciesResponse iamListGroupsForUserResponse = iamlib.ListGroupsForUserResponse ) @@ -1771,6 +1775,31 @@ func (e *EmbeddedIamApi) ListGroupsForUser(s3cfg *iam_pb.S3ApiConfiguration, val return resp, nil } +// notImplementedError returns a NotImplemented IAM error for the embedded server. +func notImplementedGroupInlineError() *iamError { + return &iamError{Code: s3err.GetAPIError(s3err.ErrNotImplemented).Code, Error: fmt.Errorf("group inline policies are not supported in embedded IAM mode; use the standalone IAM server or managed policies (AttachGroupPolicy)")} +} + +// PutGroupPolicy is not supported in embedded IAM mode. +func (e *EmbeddedIamApi) PutGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamPutGroupPolicyResponse, *iamError) { + return &iamPutGroupPolicyResponse{}, notImplementedGroupInlineError() +} + +// GetGroupPolicy is not supported in embedded IAM mode. +func (e *EmbeddedIamApi) GetGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamGetGroupPolicyResponse, *iamError) { + return &iamGetGroupPolicyResponse{}, notImplementedGroupInlineError() +} + +// DeleteGroupPolicy is not supported in embedded IAM mode. +func (e *EmbeddedIamApi) DeleteGroupPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamDeleteGroupPolicyResponse, *iamError) { + return &iamDeleteGroupPolicyResponse{}, notImplementedGroupInlineError() +} + +// ListGroupPolicies is not supported in embedded IAM mode. +func (e *EmbeddedIamApi) ListGroupPolicies(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (*iamListGroupPoliciesResponse, *iamError) { + return &iamListGroupPoliciesResponse{}, notImplementedGroupInlineError() +} + // 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." @@ -1925,7 +1954,7 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s if e.readOnly { switch action { case "ListUsers", "ListAccessKeys", "GetUser", "GetUserPolicy", "ListUserPolicies", "ListAttachedUserPolicies", "ListPolicies", "GetPolicy", "ListPolicyVersions", "GetPolicyVersion", "ListServiceAccounts", "GetServiceAccount", - "GetGroup", "ListGroups", "ListAttachedGroupPolicies", "ListGroupsForUser": + "GetGroup", "ListGroups", "ListAttachedGroupPolicies", "GetGroupPolicy", "ListGroupPolicies", "ListGroupsForUser": // Allowed read-only actions default: return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code, Error: fmt.Errorf("IAM write operations are disabled on this server")} @@ -2179,6 +2208,34 @@ func (e *EmbeddedIamApi) ExecuteAction(ctx context.Context, values url.Values, s return nil, iamErr } changed = false + case "PutGroupPolicy": + var iamErr *iamError + response, iamErr = e.PutGroupPolicy(s3cfg, values) + if iamErr != nil { + return nil, iamErr + } + changed = false + case "GetGroupPolicy": + var iamErr *iamError + response, iamErr = e.GetGroupPolicy(s3cfg, values) + if iamErr != nil { + return nil, iamErr + } + changed = false + case "DeleteGroupPolicy": + var iamErr *iamError + response, iamErr = e.DeleteGroupPolicy(s3cfg, values) + if iamErr != nil { + return nil, iamErr + } + changed = false + case "ListGroupPolicies": + var iamErr *iamError + response, iamErr = e.ListGroupPolicies(s3cfg, values) + if iamErr != nil { + return nil, iamErr + } + changed = false case "ListGroupsForUser": var iamErr *iamError response, iamErr = e.ListGroupsForUser(s3cfg, values) diff --git a/weed/s3api/s3api_embedded_iam_test.go b/weed/s3api/s3api_embedded_iam_test.go index b5ff318b1..a035af9e0 100644 --- a/weed/s3api/s3api_embedded_iam_test.go +++ b/weed/s3api/s3api_embedded_iam_test.go @@ -534,6 +534,39 @@ func TestEmbeddedIamListUserPolicies(t *testing.T) { assert.Equal(t, http.StatusNotFound, rr3.Code) } +// TestEmbeddedIamGroupInlinePoliciesNotImplemented tests that group inline policies +// return NotImplemented in embedded IAM mode. +func TestEmbeddedIamGroupInlinePoliciesNotImplemented(t *testing.T) { + api := NewEmbeddedIamApiForTest() + s3cfg := &iam_pb.S3ApiConfiguration{ + Groups: []*iam_pb.Group{ + {Name: "developers", Members: []string{"alice"}}, + }, + } + + notImpl := s3err.GetAPIError(s3err.ErrNotImplemented).Code + + _, iamErr := api.PutGroupPolicy(s3cfg, url.Values{ + "GroupName": {"developers"}, + "PolicyName": {"DevPolicy"}, + "PolicyDocument": {`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::*"}]}`}, + }) + assert.NotNil(t, iamErr) + assert.Equal(t, notImpl, iamErr.Code) + + _, iamErr = api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}}) + assert.NotNil(t, iamErr) + assert.Equal(t, notImpl, iamErr.Code) + + _, iamErr = api.DeleteGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}}) + assert.NotNil(t, iamErr) + assert.Equal(t, notImpl, iamErr.Code) + + _, iamErr = api.ListGroupPolicies(s3cfg, url.Values{"GroupName": {"developers"}}) + assert.NotNil(t, iamErr) + assert.Equal(t, notImpl, iamErr.Code) +} + // TestEmbeddedIamAttachUserPolicy tests attaching a managed policy to a user. func TestEmbeddedIamAttachUserPolicy(t *testing.T) { api := NewEmbeddedIamApiForTest()