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:
@@ -619,7 +619,7 @@ func TestLoadS3ApiConfigurationFromCredentialManagerHydratesInlinePolicies(t *te
|
||||
{
|
||||
Effect: policy_engine.PolicyEffectAllow,
|
||||
Action: policy_engine.NewStringOrStringSlice("s3:PutObject"),
|
||||
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::test-bucket/*"),
|
||||
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::test-bucket/*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -673,7 +673,7 @@ func TestLoadS3ApiConfigurationFromCredentialManagerHydratesInlinePoliciesThroug
|
||||
{
|
||||
Effect: policy_engine.PolicyEffectAllow,
|
||||
Action: policy_engine.NewStringOrStringSlice("s3:PutObject"),
|
||||
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::test-bucket/*"),
|
||||
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::test-bucket/*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ func convertSingleAction(action string) (*PolicyStatement, error) {
|
||||
return &PolicyStatement{
|
||||
Effect: PolicyEffectAllow,
|
||||
Action: NewStringOrStringSlice(s3Actions...),
|
||||
Resource: NewStringOrStringSlice(resources...),
|
||||
Resource: NewStringOrStringSlicePtr(resources...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ func CreatePolicyFromLegacyIdentity(identityName string, actions []string) (*Pol
|
||||
Sid: fmt.Sprintf("%s-%s", identityName, strings.ReplaceAll(resourcePattern, "/", "-")),
|
||||
Effect: PolicyEffectAllow,
|
||||
Action: NewStringOrStringSlice(s3Actions...),
|
||||
Resource: NewStringOrStringSlice(resources...),
|
||||
Resource: NewStringOrStringSlicePtr(resources...),
|
||||
}
|
||||
|
||||
statements = append(statements, statement)
|
||||
|
||||
@@ -82,8 +82,11 @@ func (s StringOrStringSlice) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.values)
|
||||
}
|
||||
|
||||
// Strings returns the slice of strings
|
||||
func (s StringOrStringSlice) Strings() []string {
|
||||
// Strings returns the slice of strings. Nil-safe for pointer receivers.
|
||||
func (s *StringOrStringSlice) Strings() []string {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.values
|
||||
}
|
||||
|
||||
@@ -92,6 +95,11 @@ func NewStringOrStringSlice(values ...string) StringOrStringSlice {
|
||||
return StringOrStringSlice{values: values}
|
||||
}
|
||||
|
||||
// NewStringOrStringSlicePtr creates a new *StringOrStringSlice from strings
|
||||
func NewStringOrStringSlicePtr(values ...string) *StringOrStringSlice {
|
||||
return &StringOrStringSlice{values: values}
|
||||
}
|
||||
|
||||
// PolicyConditions represents policy conditions with proper typing
|
||||
type PolicyConditions map[string]map[string]StringOrStringSlice
|
||||
|
||||
@@ -138,8 +146,8 @@ type PolicyStatement struct {
|
||||
Effect PolicyEffect `json:"Effect"`
|
||||
Principal *StringOrStringSlice `json:"Principal,omitempty"`
|
||||
Action StringOrStringSlice `json:"Action"`
|
||||
Resource StringOrStringSlice `json:"Resource,omitempty"`
|
||||
NotResource StringOrStringSlice `json:"NotResource,omitempty"`
|
||||
Resource *StringOrStringSlice `json:"Resource,omitempty"`
|
||||
NotResource *StringOrStringSlice `json:"NotResource,omitempty"`
|
||||
Condition PolicyConditions `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
@@ -296,8 +304,16 @@ func compileStatement(stmt *PolicyStatement) (*CompiledStatement, error) {
|
||||
}
|
||||
|
||||
// Deep clone Resource/NotResource into the internal statement as well for completeness
|
||||
compiled.Statement.Resource.values = slices.Clone(stmt.Resource.values)
|
||||
compiled.Statement.NotResource.values = slices.Clone(stmt.NotResource.values)
|
||||
if stmt.Resource != nil {
|
||||
resourceClone := *stmt.Resource
|
||||
resourceClone.values = slices.Clone(stmt.Resource.values)
|
||||
compiled.Statement.Resource = &resourceClone
|
||||
}
|
||||
if stmt.NotResource != nil {
|
||||
notResourceClone := *stmt.NotResource
|
||||
notResourceClone.values = slices.Clone(stmt.NotResource.values)
|
||||
compiled.Statement.NotResource = ¬ResourceClone
|
||||
}
|
||||
compiled.Statement.Action.values = slices.Clone(stmt.Action.values)
|
||||
|
||||
// Deep clone Condition map
|
||||
@@ -448,6 +464,8 @@ func normalizeToStringSliceWithError(value interface{}) ([]string, error) {
|
||||
return result, nil
|
||||
case StringOrStringSlice:
|
||||
return v.Strings(), nil
|
||||
case *StringOrStringSlice:
|
||||
return v.Strings(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type for policy value: %T", v)
|
||||
}
|
||||
|
||||
@@ -317,9 +317,11 @@ func (s3a *S3ApiServer) validateBucketPolicy(policyDoc *policy_engine.PolicyDocu
|
||||
}
|
||||
|
||||
// Validate NotResources refer to this bucket
|
||||
for _, notResource := range statement.NotResource.Strings() {
|
||||
if !s3a.validateResourceForBucket(notResource, bucket) {
|
||||
return fmt.Errorf("statement %d: NotResource %s does not match bucket %s", i, notResource, bucket)
|
||||
if statement.NotResource != nil {
|
||||
for _, notResource := range statement.NotResource.Strings() {
|
||||
if !s3a.validateResourceForBucket(notResource, bucket) {
|
||||
return fmt.Errorf("statement %d: NotResource %s does not match bucket %s", i, notResource, bucket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -898,7 +898,7 @@ func (e *EmbeddedIamApi) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values
|
||||
for i, statement := range policyDocument.Statement {
|
||||
// Use order-independent comparison to avoid duplicates from different action orderings
|
||||
if iamStringSlicesEqual(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
|
||||
@@ -910,7 +910,7 @@ func (e *EmbeddedIamApi) 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)
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@ func TestEmbeddedIamAttachUserPolicyRefreshesIAM(t *testing.T) {
|
||||
{
|
||||
Effect: policy_engine.PolicyEffectAllow,
|
||||
Action: policy_engine.NewStringOrStringSlice("s3:GetObject"),
|
||||
Resource: policy_engine.NewStringOrStringSlice("arn:aws:s3:::bucket/*"),
|
||||
Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::bucket/*"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user