diff options
| -rw-r--r-- | weed/s3api/auth_credentials.go | 4 | ||||
| -rw-r--r-- | weed/s3api/policy_engine/README_POLICY_ENGINE.md | 119 | ||||
| -rw-r--r-- | weed/s3api/policy_engine/conditions.go | 10 | ||||
| -rw-r--r-- | weed/s3api/policy_engine/engine_test.go | 7 | ||||
| -rw-r--r-- | weed/s3api/policy_engine/types.go | 2 | ||||
| -rw-r--r-- | weed/s3api/s3api_bucket_handlers.go | 3 |
6 files changed, 123 insertions, 22 deletions
diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index ec9edb6a0..378788084 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -582,9 +582,7 @@ func (iam *IdentityAccessManagement) authRequest(r *http.Request, action Action) // - No policy or indeterminate → fall through to IAM checks if iam.policyEngine != nil && bucket != "" { principal := buildPrincipalARN(identity) - // Evaluate bucket policy with request context for accurate action resolution - // Note: objectEntry is nil here as we don't have the entry at auth time - // For tag-based conditions to work, the caller should re-evaluate with entry after fetching it + // Evaluate bucket policy (objectEntry nil - not yet fetched at auth time) allowed, evaluated, err := iam.policyEngine.EvaluatePolicy(bucket, object, string(action), principal, r, nil) if err != nil { diff --git a/weed/s3api/policy_engine/README_POLICY_ENGINE.md b/weed/s3api/policy_engine/README_POLICY_ENGINE.md index 70dbf37f1..efb19f68d 100644 --- a/weed/s3api/policy_engine/README_POLICY_ENGINE.md +++ b/weed/s3api/policy_engine/README_POLICY_ENGINE.md @@ -135,8 +135,34 @@ Standard AWS condition keys are supported: - `aws:UserAgent` - Client user agent - `s3:x-amz-acl` - Requested ACL - `s3:VersionId` - Object version ID +- `s3:ExistingObjectTag/<tag-key>` - Value of an existing object tag (see example below) - And many more... +### 5. Object Tag-Based Access Control + +You can control access based on object tags using `s3:ExistingObjectTag/<tag-key>`: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::my-bucket/*", + "Condition": { + "StringEquals": { + "s3:ExistingObjectTag/status": ["public"] + } + } + } + ] +} +``` + +This allows anonymous access only to objects that have a tag `status=public`. + ## Policy Evaluation ### Evaluation Order (AWS-Compatible) @@ -212,6 +238,56 @@ Standard AWS condition keys are supported: } ``` +### Tag-Based Access Control + +Allow public read only for objects tagged as public: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::my-bucket/*", + "Condition": { + "StringEquals": { + "s3:ExistingObjectTag/visibility": ["public"] + } + } + } + ] +} +``` + +Deny access to confidential objects: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::my-bucket/*" + }, + { + "Effect": "Deny", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::my-bucket/*", + "Condition": { + "StringEquals": { + "s3:ExistingObjectTag/classification": ["confidential", "secret"] + } + } + } + ] +} +``` + ## Integration ### For Existing SeaweedFS Users @@ -270,10 +346,39 @@ go test -v -run TestPolicyValidation ## Compatibility -- ✅ **Full backward compatibility** with existing `identities.json` -- ✅ **AWS S3 API compatibility** for bucket policies -- ✅ **Standard condition operators** and keys -- ✅ **Proper evaluation precedence** (Deny > Allow > Default Deny) -- ✅ **Performance optimized** with caching and compiled patterns - -The policy engine provides a seamless upgrade path from SeaweedFS's existing simple IAM system to full AWS S3-compatible policies, giving you the best of both worlds: simplicity for basic use cases and power for complex enterprise scenarios.
\ No newline at end of file +- Full backward compatibility with existing `identities.json` +- AWS S3 API compatibility for bucket policies +- Standard condition operators and keys +- Proper evaluation precedence (Deny > Allow > Default Deny) +- Performance optimized with caching and compiled patterns + +The policy engine provides a seamless upgrade path from SeaweedFS's existing simple IAM system to full AWS S3-compatible policies, giving you the best of both worlds: simplicity for basic use cases and power for complex enterprise scenarios. + +## Feature Status + +### Implemented + +| Feature | Description | +|---------|-------------| +| Bucket Policies | Full AWS S3-compatible bucket policies | +| Condition Operators | StringEquals, IpAddress, Bool, DateGreaterThan, etc. | +| `aws:SourceIp` | IP-based access control with CIDR support | +| `aws:SecureTransport` | Require HTTPS | +| `aws:CurrentTime` | Time-based access control | +| `s3:ExistingObjectTag/<key>` | Tag-based access control for existing objects | +| Wildcard Patterns | Support for `*` and `?` in actions and resources | +| Principal Matching | `*`, account IDs, and user ARNs | + +### Planned + +| Feature | GitHub Issue | +|---------|--------------| +| `s3:RequestObjectTag/<key>` | For tag conditions on PUT requests | +| `s3:RequestObjectTagKeys` | Check which tag keys are in request | +| `s3:x-amz-content-sha256` | Content hash condition | +| `s3:x-amz-server-side-encryption` | SSE condition | +| `s3:x-amz-storage-class` | Storage class condition | +| Cross-account access | Access across different accounts | +| VPC Endpoint policies | Network-level policies | + +For feature requests or to track progress, see the [GitHub Issues](https://github.com/seaweedfs/seaweedfs/issues).
\ No newline at end of file diff --git a/weed/s3api/policy_engine/conditions.go b/weed/s3api/policy_engine/conditions.go index feb582e89..ffbae51e6 100644 --- a/weed/s3api/policy_engine/conditions.go +++ b/weed/s3api/policy_engine/conditions.go @@ -10,6 +10,7 @@ import ( "time" "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" ) // LRUNode represents a node in the doubly-linked list for efficient LRU operations @@ -705,12 +706,9 @@ func GetConditionEvaluator(operator string) (ConditionEvaluator, error) { } } -// ExistingObjectTagPrefix is the prefix for object tag condition keys +// ExistingObjectTagPrefix is the prefix for S3 policy condition keys const ExistingObjectTagPrefix = "s3:ExistingObjectTag/" -// ObjectTagMetadataPrefix is the prefix used to store tags in entry.Extended -const ObjectTagMetadataPrefix = "X-Amz-Tagging-" - // EvaluateConditions evaluates all conditions in a policy statement // objectEntry is the object's metadata from entry.Extended (can be nil) func EvaluateConditions(conditions PolicyConditions, contextValues map[string][]string, objectEntry map[string][]byte) bool { @@ -733,7 +731,7 @@ func EvaluateConditions(conditions PolicyConditions, contextValues map[string][] if strings.HasPrefix(key, ExistingObjectTagPrefix) { // Extract tag value from entry.Extended using the tag prefix tagKey := key[len(ExistingObjectTagPrefix):] - metadataKey := ObjectTagMetadataPrefix + tagKey + metadataKey := s3_constants.AmzObjectTaggingPrefix + tagKey if objectEntry != nil { if tagValue, exists := objectEntry[metadataKey]; exists { contextVals = []string{string(tagValue)} @@ -784,7 +782,7 @@ func EvaluateConditionsLegacy(conditions map[string]interface{}, contextValues m // Handle s3:ExistingObjectTag/<tag-key> condition keys if strings.HasPrefix(key, ExistingObjectTagPrefix) { tagKey := key[len(ExistingObjectTagPrefix):] - metadataKey := ObjectTagMetadataPrefix + tagKey + metadataKey := s3_constants.AmzObjectTaggingPrefix + tagKey if objectEntry != nil { if tagValue, exists := objectEntry[metadataKey]; exists { contextVals = []string{string(tagValue)} diff --git a/weed/s3api/policy_engine/engine_test.go b/weed/s3api/policy_engine/engine_test.go index 2b9565738..7ad2ca35b 100644 --- a/weed/s3api/policy_engine/engine_test.go +++ b/weed/s3api/policy_engine/engine_test.go @@ -5,6 +5,7 @@ import ( "net/url" "testing" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) @@ -749,7 +750,7 @@ func TestExistingObjectTagCondition(t *testing.T) { } entry := make(map[string][]byte) for k, v := range tags { - entry["X-Amz-Tagging-"+k] = []byte(v) + entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v) } return entry } @@ -840,7 +841,7 @@ func TestExistingObjectTagConditionMultipleTags(t *testing.T) { tagsToEntry := func(tags map[string]string) map[string][]byte { entry := make(map[string][]byte) for k, v := range tags { - entry["X-Amz-Tagging-"+k] = []byte(v) + entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v) } return entry } @@ -934,7 +935,7 @@ func TestExistingObjectTagDenyPolicy(t *testing.T) { } entry := make(map[string][]byte) for k, v := range tags { - entry["X-Amz-Tagging-"+k] = []byte(v) + entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v) } return entry } diff --git a/weed/s3api/policy_engine/types.go b/weed/s3api/policy_engine/types.go index a8f822fb8..c6c76b55f 100644 --- a/weed/s3api/policy_engine/types.go +++ b/weed/s3api/policy_engine/types.go @@ -108,7 +108,7 @@ type PolicyEvaluationArgs struct { Conditions map[string][]string // ObjectEntry is the object's metadata from entry.Extended. // Used for evaluating conditions like s3:ExistingObjectTag/<tag-key>. - // Tags are stored as "X-Amz-Tagging-<key>" -> value. + // Tags are stored with s3_constants.AmzObjectTaggingPrefix (X-Amz-Tagging-) prefix. // Can be nil for bucket-level operations or when object doesn't exist. ObjectEntry map[string][]byte } diff --git a/weed/s3api/s3api_bucket_handlers.go b/weed/s3api/s3api_bucket_handlers.go index 928d500b0..2d67aa551 100644 --- a/weed/s3api/s3api_bucket_handlers.go +++ b/weed/s3api/s3api_bucket_handlers.go @@ -765,8 +765,7 @@ func (s3a *S3ApiServer) AuthWithPublicRead(handler http.HandlerFunc, action Acti // Check bucket policy for anonymous access using the policy engine principal := "*" // Anonymous principal - // Evaluate bucket policy with request context for accurate action resolution - // Note: objectEntry is nil here - for tag-based conditions, re-evaluate after fetching entry + // Evaluate bucket policy (objectEntry nil - not yet fetched) allowed, evaluated, err := s3a.policyEngine.EvaluatePolicy(bucket, object, string(action), principal, r, nil) if err != nil { // SECURITY: Fail-close on policy evaluation errors |
