s3api: cache parsed IAM policy engines for fallback auth

Previously, evaluateIAMPolicies created a new PolicyEngine and re-parsed
the JSON policy document for every policy on every request. This adds a
shared iamPolicyEngine field that caches compiled policies, kept in sync
by PutPolicy, DeletePolicy, and bulk config reload paths.

- PutPolicy deletes the old cache entry before setting the new one, so a
  parse failure on update does not leave a stale allow.
- Log warnings when policy compilation fails instead of silently
  discarding errors.
- Add test for valid-to-invalid policy update regression.
This commit is contained in:
Chris Lu
2026-03-05 14:27:48 -08:00
parent 4eb45ecc5e
commit 1b6e96614d
2 changed files with 70 additions and 10 deletions

View File

@@ -68,6 +68,10 @@ type IdentityAccessManagement struct {
// Bucket policy engine for evaluating bucket policies
policyEngine *BucketPolicyEngine
// Cached policy engine for IAM policy fallback evaluation.
// Keyed by policy name, kept in sync by PutPolicy/DeletePolicy.
iamPolicyEngine *policy_engine.PolicyEngine
// background polling
stopChan chan struct{}
shutdownOnce sync.Once
@@ -659,6 +663,7 @@ func (iam *IdentityAccessManagement) ReplaceS3ApiConfiguration(config *iam_pb.S3
iam.nameToIdentity = nameToIdentity
iam.accessKeyIdent = accessKeyIdent
iam.policies = policies
iam.rebuildIAMPolicyEngineLocked()
// Re-add environment-based identities that were preserved
for _, envIdent := range envIdentities {
@@ -915,6 +920,7 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap
iam.nameToIdentity = nameToIdentity
iam.accessKeyIdent = accessKeyIdent
iam.policies = policies
iam.rebuildIAMPolicyEngineLocked()
// Update authentication state based on whether identities exist
// Once enabled, keep it enabled (one-way toggle)
authJustEnabled := iam.updateAuthenticationState(len(identities))
@@ -1661,11 +1667,20 @@ func determineIAMAuthPath(sessionToken, principal, principalArn string) iamAuthP
// evaluateIAMPolicies evaluates attached IAM policies for a user identity.
// 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
iam.m.RUnlock()
if engine == nil {
return false
}
resource := buildResourceARN(bucket, object)
principal := buildPrincipalARN(identity, r)
s3Action := ResolveS3Action(r, string(action), bucket, object)
@@ -1676,16 +1691,6 @@ func (iam *IdentityAccessManagement) evaluateIAMPolicies(r *http.Request, identi
}
for _, policyName := range identity.PolicyNames {
policy, err := iam.GetPolicy(policyName)
if err != nil {
continue
}
engine := policy_engine.NewPolicyEngine()
if err := engine.SetBucketPolicy(policyName, policy.Content); err != nil {
continue
}
result := engine.EvaluatePolicy(policyName, &policy_engine.PolicyEvaluationArgs{
Action: s3Action,
Resource: resource,
@@ -1808,6 +1813,12 @@ func (iam *IdentityAccessManagement) PutPolicy(name string, content string) erro
iam.policies = make(map[string]*iam_pb.Policy)
}
iam.policies[name] = &iam_pb.Policy{Name: name, Content: content}
iam.ensureIAMPolicyEngine()
// Remove old entry first so that a parse failure doesn't leave a stale allow.
_ = iam.iamPolicyEngine.DeleteBucketPolicy(name)
if err := iam.iamPolicyEngine.SetBucketPolicy(name, content); err != nil {
glog.Warningf("IAM policy %q is stored but could not be compiled for cache: %v", name, err)
}
return nil
}
@@ -1826,9 +1837,36 @@ func (iam *IdentityAccessManagement) DeletePolicy(name string) error {
iam.m.Lock()
defer iam.m.Unlock()
delete(iam.policies, name)
if iam.iamPolicyEngine != nil {
_ = iam.iamPolicyEngine.DeleteBucketPolicy(name)
}
return nil
}
// ensureIAMPolicyEngine lazily initializes the shared IAM policy engine.
// Must be called with iam.m held.
func (iam *IdentityAccessManagement) ensureIAMPolicyEngine() {
if iam.iamPolicyEngine == nil {
iam.iamPolicyEngine = policy_engine.NewPolicyEngine()
}
}
// rebuildIAMPolicyEngineLocked rebuilds the entire IAM policy engine cache
// from the current policies map. Must be called with iam.m held.
func (iam *IdentityAccessManagement) rebuildIAMPolicyEngineLocked() {
if len(iam.policies) == 0 {
iam.iamPolicyEngine = nil
return
}
engine := policy_engine.NewPolicyEngine()
for name, p := range iam.policies {
if err := engine.SetBucketPolicy(name, p.Content); err != nil {
glog.Warningf("IAM policy cache rebuild: skipping invalid policy %q: %v", name, err)
}
}
iam.iamPolicyEngine = engine
}
// ListPolicies lists all policies
func (iam *IdentityAccessManagement) ListPolicies() []*iam_pb.Policy {
iam.m.RLock()