package policy_engine import ( "fmt" "strings" ) // MaxBucketPolicySize mirrors AWS S3's 20 KB bucket-policy limit, enforced // by both writers (the S3 gateway's PutBucketPolicy and the admin UI) so // neither surface can store a document the other refuses to manage. const MaxBucketPolicySize = 20 * 1024 // ValidateBucketPolicy performs bucket-specific policy validation, on top of // the generic structural checks in ValidatePolicy - callers run that first, // so the version and non-empty-statement rules are not re-checked here. It // enforces the rules that make a policy document valid as an S3 *bucket* // policy specifically: every statement must name a Principal, and every // Resource/NotResource/Action must scope to the given bucket. // // This is shared between the S3 gateway's PutBucketPolicy handler // (weed/s3api/s3api_bucket_policy_handlers.go) and the admin UI // (weed/admin/dash) so both enforce identical rules. func ValidateBucketPolicy(policyDoc *PolicyDocument, bucket string) error { for i, statement := range policyDoc.Statement { // Bucket policies must have Principal if statement.Principal == nil { return fmt.Errorf("statement %d: bucket policies must specify a Principal", i) } // Validate resources refer to this bucket for _, resource := range statement.Resource.Strings() { if !ResourceMatchesBucket(resource, bucket) { return fmt.Errorf("statement %d: resource %s does not match bucket %s", i, resource, bucket) } } // Validate NotResources refer to this bucket if statement.NotResource != nil { for _, notResource := range statement.NotResource.Strings() { if !ResourceMatchesBucket(notResource, bucket) { return fmt.Errorf("statement %d: NotResource %s does not match bucket %s", i, notResource, bucket) } } } // Validate actions are S3 actions for _, action := range statement.Action.Strings() { if !strings.HasPrefix(action, "s3:") { return fmt.Errorf("statement %d: bucket policies only support S3 actions, got %s", i, action) } } } return nil } // ResourceMatchesBucket checks if a resource ARN is valid for the given bucket. func ResourceMatchesBucket(resource, bucket string) bool { // Accepted formats for S3 bucket policies: // AWS-style ARNs (standard): // arn:aws:s3:::bucket-name // arn:aws:s3:::bucket-name/* // arn:aws:s3:::bucket-name/path/to/object // Simplified formats (for convenience): // bucket-name // bucket-name/* // bucket-name/path/to/object var resourcePath string const awsPrefix = "arn:aws:s3:::" // Strip the optional ARN prefix to get the resource path if path, ok := strings.CutPrefix(resource, awsPrefix); ok { resourcePath = path } else { resourcePath = resource } // After stripping the optional ARN prefix, the resource path must // either match the bucket name exactly, or be a path within the bucket. return resourcePath == bucket || resourcePath == bucket+"/*" || strings.HasPrefix(resourcePath, bucket+"/") }