mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-26 01:44:48 +00:00
* s3: support StringEqualsIgnoreCase and related condition operators The S3 bucket-policy condition engine rejected StringEqualsIgnoreCase (and StringNotEqualsIgnoreCase, StringLikeIgnoreCase, StringNotLikeIgnoreCase), which AWS and the IAM policy engine both accept. Add evaluators and register them in GetConditionEvaluator so valid policies using these operators evaluate correctly instead of being skipped. * s3: reject bucket policies with unsupported condition operators validateStatement did not check Condition operators, so a policy with an unknown operator (e.g. a typo or unsupported key) was accepted at upload time and only surfaced at evaluation, where it was silently skipped. Reuse GetConditionEvaluator to reject unknown operators when a policy is parsed or stored, failing closed at the entry point instead of relying on evaluation-time handling. * s3: fail closed on unsupported condition operators at evaluation EvaluateConditions skipped statements whose condition operator was unsupported, logging a warning and continuing. With no remaining conditions to fail, the function returned true, so an Allow statement conditioned on an unrecognized operator became unconditional and granted access to private objects. Return false instead so an unrecognized operator fails the condition block and the statement does not match, matching the fail-closed behavior of the IAM policy engine. * s3: validate condition operators at upload time only, not load time Validating condition operators in validateStatement rejected the whole policy document from ParsePolicy, which SetBucketPolicy uses when loading stored bucket policies. A legacy policy saved before this change could contain an unsupported operator, and rejecting it at load time dropped the entire policy - including unrelated explicit Deny statements - so the bucket lost its protections. Move the operator check into ValidateBucketPolicy, which only the PutBucketPolicy handler and admin UI run at upload time, so legacy policies still load and EvaluateConditions fails the unsupported statement closed instead. * s3: drop non-AWS StringLikeIgnoreCase and StringNotLikeIgnoreCase operators AWS defines StringEqualsIgnoreCase and StringNotEqualsIgnoreCase but not StringLikeIgnoreCase or StringNotLikeIgnoreCase (StringLike and StringNotLike are case-sensitive only). Registering the wildcard IgnoreCase variants made the engine accept operators AWS rejects. Keep only the two AWS-defined IgnoreCase operators and add a test asserting the wildcard IgnoreCase names are unsupported.
91 lines
3.1 KiB
Go
91 lines
3.1 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
for operator := range statement.Condition {
|
|
if _, err := GetConditionEvaluator(operator); err != nil {
|
|
return fmt.Errorf("statement %d: unsupported condition operator %q: %v", i, operator, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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+"/")
|
|
}
|