Browse Source

s3: document s3:ExistingObjectTag support and feature status

Update policy engine documentation:

- Add s3:ExistingObjectTag/<tag-key> to supported condition keys
- Add 'Object Tag-Based Access Control' section with examples
- Add 'Feature Status' section with implemented and planned features

Planned features for future implementation:
- s3:RequestObjectTag/<key>
- s3:RequestObjectTagKeys
- s3:x-amz-server-side-encryption
- Cross-account access
copilot/sub-pr-7677
chrislu 1 month ago
parent
commit
50eba1ecf8
  1. 4
      weed/s3api/auth_credentials.go
  2. 119
      weed/s3api/policy_engine/README_POLICY_ENGINE.md
  3. 10
      weed/s3api/policy_engine/conditions.go
  4. 7
      weed/s3api/policy_engine/engine_test.go
  5. 2
      weed/s3api/policy_engine/types.go
  6. 3
      weed/s3api/s3api_bucket_handlers.go

4
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 // - No policy or indeterminate → fall through to IAM checks
if iam.policyEngine != nil && bucket != "" { if iam.policyEngine != nil && bucket != "" {
principal := buildPrincipalARN(identity) 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) allowed, evaluated, err := iam.policyEngine.EvaluatePolicy(bucket, object, string(action), principal, r, nil)
if err != nil { if err != nil {

119
weed/s3api/policy_engine/README_POLICY_ENGINE.md

@ -135,8 +135,34 @@ Standard AWS condition keys are supported:
- `aws:UserAgent` - Client user agent - `aws:UserAgent` - Client user agent
- `s3:x-amz-acl` - Requested ACL - `s3:x-amz-acl` - Requested ACL
- `s3:VersionId` - Object version ID - `s3:VersionId` - Object version ID
- `s3:ExistingObjectTag/<tag-key>` - Value of an existing object tag (see example below)
- And many more... - 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 ## Policy Evaluation
### Evaluation Order (AWS-Compatible) ### 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 ## Integration
### For Existing SeaweedFS Users ### For Existing SeaweedFS Users
@ -270,10 +346,39 @@ go test -v -run TestPolicyValidation
## Compatibility ## 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.
- 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).

10
weed/s3api/policy_engine/conditions.go

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/seaweedfs/seaweedfs/weed/glog" "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 // 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/" 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 // EvaluateConditions evaluates all conditions in a policy statement
// objectEntry is the object's metadata from entry.Extended (can be nil) // 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 { 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) { if strings.HasPrefix(key, ExistingObjectTagPrefix) {
// Extract tag value from entry.Extended using the tag prefix // Extract tag value from entry.Extended using the tag prefix
tagKey := key[len(ExistingObjectTagPrefix):] tagKey := key[len(ExistingObjectTagPrefix):]
metadataKey := ObjectTagMetadataPrefix + tagKey
metadataKey := s3_constants.AmzObjectTaggingPrefix + tagKey
if objectEntry != nil { if objectEntry != nil {
if tagValue, exists := objectEntry[metadataKey]; exists { if tagValue, exists := objectEntry[metadataKey]; exists {
contextVals = []string{string(tagValue)} contextVals = []string{string(tagValue)}
@ -784,7 +782,7 @@ func EvaluateConditionsLegacy(conditions map[string]interface{}, contextValues m
// Handle s3:ExistingObjectTag/<tag-key> condition keys // Handle s3:ExistingObjectTag/<tag-key> condition keys
if strings.HasPrefix(key, ExistingObjectTagPrefix) { if strings.HasPrefix(key, ExistingObjectTagPrefix) {
tagKey := key[len(ExistingObjectTagPrefix):] tagKey := key[len(ExistingObjectTagPrefix):]
metadataKey := ObjectTagMetadataPrefix + tagKey
metadataKey := s3_constants.AmzObjectTaggingPrefix + tagKey
if objectEntry != nil { if objectEntry != nil {
if tagValue, exists := objectEntry[metadataKey]; exists { if tagValue, exists := objectEntry[metadataKey]; exists {
contextVals = []string{string(tagValue)} contextVals = []string{string(tagValue)}

7
weed/s3api/policy_engine/engine_test.go

@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"testing" "testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
) )
@ -749,7 +750,7 @@ func TestExistingObjectTagCondition(t *testing.T) {
} }
entry := make(map[string][]byte) entry := make(map[string][]byte)
for k, v := range tags { for k, v := range tags {
entry["X-Amz-Tagging-"+k] = []byte(v)
entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v)
} }
return entry return entry
} }
@ -840,7 +841,7 @@ func TestExistingObjectTagConditionMultipleTags(t *testing.T) {
tagsToEntry := func(tags map[string]string) map[string][]byte { tagsToEntry := func(tags map[string]string) map[string][]byte {
entry := make(map[string][]byte) entry := make(map[string][]byte)
for k, v := range tags { for k, v := range tags {
entry["X-Amz-Tagging-"+k] = []byte(v)
entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v)
} }
return entry return entry
} }
@ -934,7 +935,7 @@ func TestExistingObjectTagDenyPolicy(t *testing.T) {
} }
entry := make(map[string][]byte) entry := make(map[string][]byte)
for k, v := range tags { for k, v := range tags {
entry["X-Amz-Tagging-"+k] = []byte(v)
entry[s3_constants.AmzObjectTaggingPrefix+k] = []byte(v)
} }
return entry return entry
} }

2
weed/s3api/policy_engine/types.go

@ -108,7 +108,7 @@ type PolicyEvaluationArgs struct {
Conditions map[string][]string Conditions map[string][]string
// ObjectEntry is the object's metadata from entry.Extended. // ObjectEntry is the object's metadata from entry.Extended.
// Used for evaluating conditions like s3:ExistingObjectTag/<tag-key>. // 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. // Can be nil for bucket-level operations or when object doesn't exist.
ObjectEntry map[string][]byte ObjectEntry map[string][]byte
} }

3
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 // Check bucket policy for anonymous access using the policy engine
principal := "*" // Anonymous principal 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) allowed, evaluated, err := s3a.policyEngine.EvaluatePolicy(bucket, object, string(action), principal, r, nil)
if err != nil { if err != nil {
// SECURITY: Fail-close on policy evaluation errors // SECURITY: Fail-close on policy evaluation errors

Loading…
Cancel
Save