feat(iam): implement group inline policy actions (#8992)

* feat(iam): implement group inline policy actions

Add PutGroupPolicy, GetGroupPolicy, DeleteGroupPolicy, and
ListGroupPolicies to both embedded and standalone IAM servers.

The standalone IAM stores group inline policies in a new
GroupInlinePolicies field in the Policies JSON, mirroring the
existing user inline policy pattern. DeleteGroup now also checks
for inline policies before allowing deletion.

* fix: address review feedback for group inline policies

- Embedded IAM: return NotImplemented for group inline policies
  instead of silently succeeding as no-ops (Gemini + CodeRabbit)
- Standalone IAM: recompute member actions after PutGroupPolicy
  and DeleteGroupPolicy (Gemini)
- Add parameter validation for GroupName/PolicyName/PolicyDocument
  on PutGroupPolicy, DeleteGroupPolicy, ListGroupPolicies (Gemini)
- Add UserName validation for ListUserPolicies in standalone IAM
- Call cleanupGroupInlinePolicies from DeleteGroup (Gemini)
- Migrate GroupInlinePolicies on group rename in UpdateGroup (CodeRabbit)
- Fix integration test cleanup order (CodeRabbit)

* fix: persist recomputed actions and improve error handling

- Set changed=true for PutGroupPolicy/DeleteGroupPolicy in standalone
  IAM DoActions so recomputed member actions are persisted (Gemini critical)
- Make cleanupGroupInlinePolicies accept policies parameter to avoid
  redundant I/O, return error (Gemini)
- Make migrateGroupInlinePolicies return error, handle in caller (Gemini)

* fix: include group policies in action recomputation

Extend computeAllActionsForUser to also aggregate group inline
policies and group managed policies when s3cfg is provided.
Previously, group inline policies were stored but never reflected
in member Identity.Actions. (CodeRabbit critical)

* perf: use identity index in recomputeActionsForGroupMembers for O(N+M)

* fix: skip group inline policy integration test on embedded IAM

The embedded IAM returns NotImplemented for group inline policies.
Skip TestIAMGroupInlinePolicy when running against embedded mode
to avoid CI failures in the group integration test matrix.
This commit is contained in:
Chris Lu
2026-04-08 15:57:04 -07:00
committed by GitHub
parent 3af571a5f3
commit e21d7602c3
7 changed files with 573 additions and 10 deletions
+99
View File
@@ -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{
+34
View File
@@ -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"`
+242
View File
@@ -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
}
}
}
}
+103 -9
View File
@@ -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)
+4
View File
@@ -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
)
+58 -1
View File
@@ -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)
+33
View File
@@ -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()