From e660180438a516b0145737092f647a9c8b3660cc Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sun, 8 Mar 2026 14:31:40 -0700 Subject: [PATCH] iam: integrate group policies into authorization Add groups and userGroups reverse index to IdentityAccessManagement. Populate both maps during ReplaceS3ApiConfiguration and MergeS3ApiConfiguration. Modify evaluateIAMPolicies to evaluate policies from user's enabled groups in addition to user policies. Update VerifyActionPermission to consider group policies when checking hasAttachedPolicies. --- weed/s3api/auth_credentials.go | 80 ++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index ae7b48be3..e412008f0 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -49,6 +49,8 @@ type IdentityAccessManagement struct { accessKeyIdent map[string]*Identity nameToIdentity map[string]*Identity // O(1) lookup by identity name policies map[string]*iam_pb.Policy + groups map[string]*iam_pb.Group // group name -> group + userGroups map[string][]string // user name -> group names (reverse index) accounts map[string]*Account emailAccount map[string]*Account hashes map[string]*sync.Pool @@ -563,6 +565,16 @@ func (iam *IdentityAccessManagement) ReplaceS3ApiConfiguration(config *iam_pb.S3 for _, policy := range config.Policies { policies[policy.Name] = policy } + groups := make(map[string]*iam_pb.Group) + userGroupsMap := make(map[string][]string) + for _, g := range config.Groups { + groups[g.Name] = g + if !g.Disabled { + for _, member := range g.Members { + userGroupsMap[member] = append(userGroupsMap[member], g.Name) + } + } + } for _, ident := range config.Identities { glog.V(3).Infof("loading identity %s (disabled=%v)", ident.Name, ident.Disabled) t := &Identity{ @@ -663,6 +675,8 @@ func (iam *IdentityAccessManagement) ReplaceS3ApiConfiguration(config *iam_pb.S3 iam.nameToIdentity = nameToIdentity iam.accessKeyIdent = accessKeyIdent iam.policies = policies + iam.groups = groups + iam.userGroups = userGroupsMap iam.rebuildIAMPolicyEngineLocked() // Re-add environment-based identities that were preserved @@ -911,6 +925,18 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap policies[policy.Name] = policy } + // Process groups from dynamic config + mergedGroups := make(map[string]*iam_pb.Group) + mergedUserGroups := make(map[string][]string) + for _, g := range config.Groups { + mergedGroups[g.Name] = g + if !g.Disabled { + for _, member := range g.Members { + mergedUserGroups[member] = append(mergedUserGroups[member], g.Name) + } + } + } + iam.m.Lock() // atomically switch iam.identities = identities @@ -920,6 +946,8 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap iam.nameToIdentity = nameToIdentity iam.accessKeyIdent = accessKeyIdent iam.policies = policies + iam.groups = mergedGroups + iam.userGroups = mergedUserGroups iam.rebuildIAMPolicyEngineLocked() // Update authentication state based on whether identities exist // Once enabled, keep it enabled (one-way toggle) @@ -1837,14 +1865,18 @@ func determineIAMAuthPath(sessionToken, principal, principalArn string) iamAuthP // Returns true if any matching statement explicitly allows the action. // Uses the cached iamPolicyEngine to avoid re-parsing policy JSON on every request. func (iam *IdentityAccessManagement) evaluateIAMPolicies(r *http.Request, identity *Identity, action Action, bucket, object string) bool { - if identity == nil || len(identity.PolicyNames) == 0 { - return false - } - iam.m.RLock() engine := iam.iamPolicyEngine + groupNames := iam.userGroups[identity.Name] + groupMap := iam.groups iam.m.RUnlock() + // Collect all policy names: user policies + group policies + hasPolicies := len(identity.PolicyNames) > 0 || len(groupNames) > 0 + if identity == nil || !hasPolicies { + return false + } + if engine == nil { return false } @@ -1858,15 +1890,17 @@ func (iam *IdentityAccessManagement) evaluateIAMPolicies(r *http.Request, identi conditions[k] = v } - for _, policyName := range identity.PolicyNames { - result := engine.EvaluatePolicy(policyName, &policy_engine.PolicyEvaluationArgs{ - Action: s3Action, - Resource: resource, - Principal: principal, - Conditions: conditions, - Claims: identity.Claims, - }) + evalArgs := &policy_engine.PolicyEvaluationArgs{ + Action: s3Action, + Resource: resource, + Principal: principal, + Conditions: conditions, + Claims: identity.Claims, + } + // Evaluate user's own policies + for _, policyName := range identity.PolicyNames { + result := engine.EvaluatePolicy(policyName, evalArgs) if result == policy_engine.PolicyResultDeny { return false } @@ -1875,6 +1909,23 @@ func (iam *IdentityAccessManagement) evaluateIAMPolicies(r *http.Request, identi } } + // Evaluate policies from user's groups (skip disabled groups) + for _, gName := range groupNames { + g, ok := groupMap[gName] + if !ok || g.Disabled { + continue + } + for _, policyName := range g.PolicyNames { + result := engine.EvaluatePolicy(policyName, evalArgs) + if result == policy_engine.PolicyResultDeny { + return false + } + if result == policy_engine.PolicyResultAllow { + explicitAllow = true + } + } + } + return explicitAllow } @@ -1894,7 +1945,10 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide hasSessionToken := r.Header.Get("X-SeaweedFS-Session-Token") != "" || r.Header.Get("X-Amz-Security-Token") != "" || r.URL.Query().Get("X-Amz-Security-Token") != "" - hasAttachedPolicies := len(identity.PolicyNames) > 0 + iam.m.RLock() + userGroupNames := iam.userGroups[identity.Name] + iam.m.RUnlock() + hasAttachedPolicies := len(identity.PolicyNames) > 0 || len(userGroupNames) > 0 if (len(identity.Actions) == 0 || hasSessionToken || hasAttachedPolicies) && iam.iamIntegration != nil { return iam.authorizeWithIAM(r, identity, action, bucket, object)