fix(s3): omit NotResource:null from bucket policy JSON response (#8658)

* fix(s3): omit NotResource:null from bucket policy JSON response (#8657)

Change NotResource from value type to pointer (*StringOrStringSlice) so
that omitempty properly omits it when unset, matching the existing
Principal field pattern. This prevents IaC tools (Terraform, Ansible)
from detecting false configuration drift.

Add bucket policy round-trip idempotency integration tests.

* simplify JSON comparison in bucket policy idempotency test

Use require.JSONEq directly on the raw JSON strings instead of
round-tripping through unmarshal/marshal, since JSONEq already
handles normalization internally.

* fix bucket policy test cases that locked out the admin user

The Deny+NotResource test cases used Action:"s3:*" which denied the
admin's own GetBucketPolicy call. Scope deny to s3:GetObject only,
and add an Allow+NotResource variant instead.

* fix(s3): also make Resource a pointer to fix empty string in JSON

Apply the same omitempty pointer fix to the Resource field, which was
emitting "Resource":"" when only NotResource was set. Add
NewStringOrStringSlicePtr helper, make Strings() nil-safe, and handle
*StringOrStringSlice in normalizeToStringSliceWithError.

* improve bucket policy integration tests per review feedback

- Replace time.Sleep with waitForClusterReady using ListBuckets
- Use structural hasKey check instead of brittle substring NotContains
- Assert specific NoSuchBucketPolicy error code after delete
- Handle single-statement policies in hasKey helper
This commit is contained in:
Chris Lu
2026-03-16 12:58:26 -07:00
committed by GitHub
parent acea36a181
commit 9984ce7dcb
11 changed files with 370 additions and 30 deletions

View File

@@ -459,7 +459,7 @@ func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values
for i, statement := range policyDocument.Statement {
// Use order-independent comparison to avoid duplicates from different action orderings
if stringSlicesEqual(statement.Action.Strings(), actions) {
policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlice(append(
policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlicePtr(append(
policyDocument.Statement[i].Resource.Strings(), resource)...)
isEqAction = true
break
@@ -471,7 +471,7 @@ func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values
policyDocumentStatement := policy_engine.PolicyStatement{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice(actions...),
Resource: policy_engine.NewStringOrStringSlice(resource),
Resource: policy_engine.NewStringOrStringSlicePtr(resource),
}
policyDocument.Statement = append(policyDocument.Statement, policyDocumentStatement)
}

View File

@@ -46,7 +46,7 @@ func TestGetActionsUserPath(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:Put*", "s3:PutBucketAcl", "s3:Get*", "s3:GetBucketAcl", "s3:List*", "s3:Tagging*", "s3:DeleteBucket*"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::shared/user-Alice/*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::shared/user-Alice/*"),
},
},
}
@@ -73,7 +73,7 @@ func TestGetActionsWildcardPath(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:Get*", "s3:PutBucketAcl"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}
@@ -94,7 +94,7 @@ func TestGetActionsInvalidAction(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:InvalidAction"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::shared/user-Alice/*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::shared/user-Alice/*"),
},
},
}
@@ -278,7 +278,7 @@ func TestGetPolicy(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}
@@ -323,7 +323,7 @@ func TestDeletePolicy(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}
@@ -371,7 +371,7 @@ func TestListPolicies(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}
@@ -399,7 +399,7 @@ func TestAttachUserPolicy(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}
@@ -459,7 +459,7 @@ func TestManagedPolicyActionsPreservedAcrossInlineMutations(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}
@@ -514,7 +514,7 @@ func TestDetachUserPolicy(t *testing.T) {
{
Effect: policy_engine.PolicyEffectAllow,
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::*"),
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::*"),
},
},
}