aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/s3api_bucket_policy_engine.go
diff options
context:
space:
mode:
authorChris Lu <chrislusf@users.noreply.github.com>2025-12-09 09:48:13 -0800
committerGitHub <noreply@github.com>2025-12-09 09:48:13 -0800
commitd6d893c8c374ff78bb7704b4da6666d36939bf1b (patch)
tree5174da93832bcf87e5003591bafee4e584321347 /weed/s3api/s3api_bucket_policy_engine.go
parentd5f21fd8ba6ee20c2504455093a9ceeaa178b826 (diff)
downloadseaweedfs-d6d893c8c374ff78bb7704b4da6666d36939bf1b.tar.xz
seaweedfs-d6d893c8c374ff78bb7704b4da6666d36939bf1b.zip
s3: add s3:ExistingObjectTag condition support for bucket policies (#7677)
* s3: add s3:ExistingObjectTag condition support in policy engine Add support for s3:ExistingObjectTag/<tag-key> condition keys in bucket policies, allowing access control based on object tags. Changes: - Add ObjectEntry field to PolicyEvaluationArgs (entry.Extended metadata) - Update EvaluateConditions to handle s3:ExistingObjectTag/<key> format - Extract tag value from entry metadata using X-Amz-Tagging-<key> prefix This enables policies like: { "Condition": { "StringEquals": { "s3:ExistingObjectTag/status": ["public"] } } } Fixes: https://github.com/seaweedfs/seaweedfs/issues/7447 * s3: update EvaluatePolicy to accept object entry for tag conditions Update BucketPolicyEngine.EvaluatePolicy to accept objectEntry parameter (entry.Extended metadata) for evaluating tag-based policy conditions. Changes: - Add objectEntry parameter to EvaluatePolicy method - Update callers in auth_credentials.go and s3api_bucket_handlers.go - Pass nil for objectEntry in auth layer (entry fetched later in handlers) For tag-based conditions to work, handlers should call EvaluatePolicy with the object's entry.Extended after fetching the entry from filer. * s3: add tests for s3:ExistingObjectTag policy conditions Add comprehensive tests for object tag-based policy conditions: - TestExistingObjectTagCondition: Basic tag matching scenarios - Matching/non-matching tag values - Missing tags, no tags, empty tags - Multiple tags with one matching - TestExistingObjectTagConditionMultipleTags: Multiple tag conditions - Both tags match - Only one tag matches - TestExistingObjectTagDenyPolicy: Deny policies with tag conditions - Default allow without tag - Deny when specific tag present * 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 * Implement tag-based policy re-check in handlers - Add checkPolicyWithEntry helper to S3ApiServer for handlers to re-check policy after fetching object entry (for s3:ExistingObjectTag conditions) - Add HasPolicyForBucket method to policy engine for efficient check - Integrate policy re-check in GetObjectHandler after entry is fetched - Integrate policy re-check in HeadObjectHandler after entry is fetched - Update auth_credentials.go comments to explain two-phase evaluation - Update documentation with supported operations for tag-based conditions This implements 'Approach 1' where handlers re-check the policy with the object entry after fetching it, allowing tag-based conditions to be properly evaluated. * Add integration tests for s3:ExistingObjectTag conditions - Add TestCheckPolicyWithEntry: tests checkPolicyWithEntry helper with various tag scenarios (matching tags, non-matching tags, empty entry, nil entry) - Add TestCheckPolicyWithEntryNoPolicyForBucket: tests early return when no policy - Add TestCheckPolicyWithEntryNilPolicyEngine: tests nil engine handling - Add TestCheckPolicyWithEntryDenyPolicy: tests deny policies with tag conditions - Add TestHasPolicyForBucket: tests HasPolicyForBucket method These tests cover the Phase 2 policy evaluation with object entry metadata, ensuring tag-based conditions are properly evaluated. * Address code review nitpicks - Remove unused extractObjectTags placeholder function (engine.go) - Add clarifying comment about s3:ExistingObjectTag/<key> evaluation - Consolidate duplicate tag-based examples in README - Factor out tagsToEntry helper to package level in tests * Address code review feedback - Fix unsafe type assertions in GetObjectHandler and HeadObjectHandler when getting identity from context (properly handle type assertion failure) - Extract getConditionContextValue helper to eliminate duplicated logic between EvaluateConditions and EvaluateConditionsLegacy - Ensure consistent handling of missing condition keys (always return empty slice) * Fix GetObjectHandler to match HeadObjectHandler pattern Add safety check for nil objectEntryForSSE before tag-based policy evaluation, ensuring tag-based conditions are always evaluated rather than silently skipped if entry is unexpectedly nil. Addresses review comment from Copilot. * Fix HeadObject action name in docs for consistency Change 'HeadObject' to 's3:HeadObject' to match other action names. * Extract recheckPolicyWithObjectEntry helper to reduce duplication Move the repeated identity extraction and policy re-check logic from GetObjectHandler and HeadObjectHandler into a shared helper method. * Add validation for empty tag key in s3:ExistingObjectTag condition Prevent potential issues with malformed policies containing s3:ExistingObjectTag/ (empty tag key after slash).
Diffstat (limited to 'weed/s3api/s3api_bucket_policy_engine.go')
-rw-r--r--weed/s3api/s3api_bucket_policy_engine.go90
1 files changed, 29 insertions, 61 deletions
diff --git a/weed/s3api/s3api_bucket_policy_engine.go b/weed/s3api/s3api_bucket_policy_engine.go
index fc674e12f..c8cd05344 100644
--- a/weed/s3api/s3api_bucket_policy_engine.go
+++ b/weed/s3api/s3api_bucket_policy_engine.go
@@ -87,56 +87,27 @@ func (bpe *BucketPolicyEngine) DeleteBucketPolicy(bucket string) error {
return bpe.engine.DeleteBucketPolicy(bucket)
}
-// EvaluatePolicy evaluates whether an action is allowed by bucket policy
-// Returns: (allowed bool, evaluated bool, error)
-// - allowed: whether the policy allows the action
-// - evaluated: whether a policy was found and evaluated (false = no policy exists)
-// - error: any error during evaluation
-func (bpe *BucketPolicyEngine) EvaluatePolicy(bucket, object, action, principal string) (allowed bool, evaluated bool, err error) {
- // Validate required parameters
- if bucket == "" {
- return false, false, fmt.Errorf("bucket cannot be empty")
- }
- if action == "" {
- return false, false, fmt.Errorf("action cannot be empty")
- }
-
- // Convert action to S3 action format using base mapping (no HTTP context available)
- s3Action := mapBaseActionToS3Format(action)
-
- // Build resource ARN
- resource := buildResourceARN(bucket, object)
-
- glog.V(4).Infof("EvaluatePolicy: bucket=%s, resource=%s, action=%s, principal=%s", bucket, resource, s3Action, principal)
-
- // Evaluate using the policy engine
- args := &policy_engine.PolicyEvaluationArgs{
- Action: s3Action,
- Resource: resource,
- Principal: principal,
- }
-
- result := bpe.engine.EvaluatePolicy(bucket, args)
-
- switch result {
- case policy_engine.PolicyResultAllow:
- glog.V(3).Infof("EvaluatePolicy: ALLOW - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
- return true, true, nil
- case policy_engine.PolicyResultDeny:
- glog.V(3).Infof("EvaluatePolicy: DENY - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
- return false, true, nil
- case policy_engine.PolicyResultIndeterminate:
- // No policy exists for this bucket
- glog.V(4).Infof("EvaluatePolicy: INDETERMINATE (no policy) - bucket=%s", bucket)
- return false, false, nil
- default:
- return false, false, fmt.Errorf("unknown policy result: %v", result)
- }
+// HasPolicyForBucket checks if a bucket has a policy configured
+func (bpe *BucketPolicyEngine) HasPolicyForBucket(bucket string) bool {
+ return bpe.engine.HasPolicyForBucket(bucket)
}
-// EvaluatePolicyWithContext evaluates whether an action is allowed by bucket policy using HTTP request context
-// This version uses the HTTP request to determine the actual S3 action more accurately
-func (bpe *BucketPolicyEngine) EvaluatePolicyWithContext(bucket, object, action, principal string, r *http.Request) (allowed bool, evaluated bool, err error) {
+// EvaluatePolicy evaluates whether an action is allowed by bucket policy
+//
+// Parameters:
+// - bucket: the bucket name
+// - object: the object key (can be empty for bucket-level operations)
+// - action: the action being performed (e.g., "Read", "Write")
+// - principal: the principal ARN or identifier
+// - r: the HTTP request (optional, used for condition evaluation and action resolution)
+// - objectEntry: the object's metadata from entry.Extended (can be nil at auth time,
+// should be passed when available for tag-based conditions like s3:ExistingObjectTag)
+//
+// Returns:
+// - allowed: whether the policy allows the action
+// - evaluated: whether a policy was found and evaluated (false = no policy exists)
+// - error: any error during evaluation
+func (bpe *BucketPolicyEngine) EvaluatePolicy(bucket, object, action, principal string, r *http.Request, objectEntry map[string][]byte) (allowed bool, evaluated bool, err error) {
// Validate required parameters
if bucket == "" {
return false, false, fmt.Errorf("bucket cannot be empty")
@@ -145,41 +116,38 @@ func (bpe *BucketPolicyEngine) EvaluatePolicyWithContext(bucket, object, action,
return false, false, fmt.Errorf("action cannot be empty")
}
- // Convert action to S3 action format using request context
+ // Convert action to S3 action format
// ResolveS3Action handles nil request internally (falls back to mapBaseActionToS3Format)
s3Action := ResolveS3Action(r, action, bucket, object)
// Build resource ARN
resource := buildResourceARN(bucket, object)
- glog.V(4).Infof("EvaluatePolicyWithContext: bucket=%s, resource=%s, action=%s (from %s), principal=%s",
- bucket, resource, s3Action, action, principal)
+ glog.V(4).Infof("EvaluatePolicy: bucket=%s, resource=%s, action=%s, principal=%s",
+ bucket, resource, s3Action, principal)
// Evaluate using the policy engine
args := &policy_engine.PolicyEvaluationArgs{
- Action: s3Action,
- Resource: resource,
- Principal: principal,
+ Action: s3Action,
+ Resource: resource,
+ Principal: principal,
+ ObjectEntry: objectEntry,
}
result := bpe.engine.EvaluatePolicy(bucket, args)
switch result {
case policy_engine.PolicyResultAllow:
- glog.V(3).Infof("EvaluatePolicyWithContext: ALLOW - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
+ glog.V(3).Infof("EvaluatePolicy: ALLOW - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
return true, true, nil
case policy_engine.PolicyResultDeny:
- glog.V(3).Infof("EvaluatePolicyWithContext: DENY - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
+ glog.V(3).Infof("EvaluatePolicy: DENY - bucket=%s, action=%s, principal=%s", bucket, s3Action, principal)
return false, true, nil
case policy_engine.PolicyResultIndeterminate:
// No policy exists for this bucket
- glog.V(4).Infof("EvaluatePolicyWithContext: INDETERMINATE (no policy) - bucket=%s", bucket)
+ glog.V(4).Infof("EvaluatePolicy: INDETERMINATE (no policy) - bucket=%s", bucket)
return false, false, nil
default:
return false, false, fmt.Errorf("unknown policy result: %v", result)
}
}
-
-// NOTE: The convertActionToS3Format wrapper has been removed for simplicity.
-// EvaluatePolicy and EvaluatePolicyWithContext now call ResolveS3Action or
-// mapBaseActionToS3Format directly, making the control flow more explicit.