mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-06-13 23:36:45 +03:00
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:
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user