diff options
Diffstat (limited to 'weed/s3api/s3api_bucket_policy_engine.go')
| -rw-r--r-- | weed/s3api/s3api_bucket_policy_engine.go | 203 |
1 files changed, 203 insertions, 0 deletions
diff --git a/weed/s3api/s3api_bucket_policy_engine.go b/weed/s3api/s3api_bucket_policy_engine.go new file mode 100644 index 000000000..9e77f407c --- /dev/null +++ b/weed/s3api/s3api_bucket_policy_engine.go @@ -0,0 +1,203 @@ +package s3api + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/iam/policy" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" +) + +// BucketPolicyEngine wraps the policy_engine to provide bucket policy evaluation +type BucketPolicyEngine struct { + engine *policy_engine.PolicyEngine +} + +// NewBucketPolicyEngine creates a new bucket policy engine +func NewBucketPolicyEngine() *BucketPolicyEngine { + return &BucketPolicyEngine{ + engine: policy_engine.NewPolicyEngine(), + } +} + +// LoadBucketPolicy loads a bucket policy into the engine from the filer entry +func (bpe *BucketPolicyEngine) LoadBucketPolicy(bucket string, entry *filer_pb.Entry) error { + if entry == nil || entry.Extended == nil { + return nil + } + + policyJSON, exists := entry.Extended[BUCKET_POLICY_METADATA_KEY] + if !exists || len(policyJSON) == 0 { + // No policy for this bucket - remove it if it exists + bpe.engine.DeleteBucketPolicy(bucket) + return nil + } + + // Set the policy in the engine + if err := bpe.engine.SetBucketPolicy(bucket, string(policyJSON)); err != nil { + glog.Errorf("Failed to load bucket policy for %s: %v", bucket, err) + return err + } + + glog.V(3).Infof("Loaded bucket policy for %s into policy engine", bucket) + return nil +} + +// LoadBucketPolicyFromCache loads a bucket policy from a cached BucketConfig +// +// NOTE: This function uses JSON marshaling/unmarshaling to convert between +// policy.PolicyDocument and policy_engine.PolicyDocument. This is inefficient +// but necessary because the two types are defined in different packages and +// have subtle differences. A future improvement would be to unify these types +// or create a direct conversion function for better performance and type safety. +func (bpe *BucketPolicyEngine) LoadBucketPolicyFromCache(bucket string, policyDoc *policy.PolicyDocument) error { + if policyDoc == nil { + // No policy for this bucket - remove it if it exists + bpe.engine.DeleteBucketPolicy(bucket) + return nil + } + + // Convert policy.PolicyDocument to policy_engine.PolicyDocument + // We use JSON marshaling as an intermediate format since both types + // follow the same AWS S3 policy structure + policyJSON, err := json.Marshal(policyDoc) + if err != nil { + glog.Errorf("Failed to marshal bucket policy for %s: %v", bucket, err) + return err + } + + // Set the policy in the engine + if err := bpe.engine.SetBucketPolicy(bucket, string(policyJSON)); err != nil { + glog.Errorf("Failed to load bucket policy for %s: %v", bucket, err) + return err + } + + glog.V(4).Infof("Loaded bucket policy for %s into policy engine from cache", bucket) + return nil +} + +// DeleteBucketPolicy removes a bucket policy from the engine +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 + s3Action := convertActionToS3Format(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) + } +} + +// convertActionToS3Format converts internal action strings to S3 action format +// +// KNOWN LIMITATION: The current Action type uses coarse-grained constants +// (ACTION_READ, ACTION_WRITE, etc.) that map to specific S3 actions, but these +// are used for multiple operations. For example, ACTION_WRITE is used for both +// PutObject and DeleteObject, but this function maps it to only s3:PutObject. +// This means bucket policies requiring fine-grained permissions (e.g., allowing +// s3:DeleteObject but not s3:PutObject) will not work correctly. +// +// TODO: Refactor to use specific S3 action strings throughout the S3 API handlers +// instead of coarse-grained Action constants. This is a major architectural change +// that should be done in a separate PR. +// +// This function explicitly maps all known actions to prevent security issues from +// overly permissive default behavior. +func convertActionToS3Format(action string) string { + // Handle multipart actions that already have s3: prefix + if strings.HasPrefix(action, "s3:") { + return action + } + + // Explicit mapping for all known actions + switch action { + // Basic operations + case s3_constants.ACTION_READ: + return "s3:GetObject" + case s3_constants.ACTION_WRITE: + return "s3:PutObject" + case s3_constants.ACTION_LIST: + return "s3:ListBucket" + case s3_constants.ACTION_TAGGING: + return "s3:PutObjectTagging" + case s3_constants.ACTION_ADMIN: + return "s3:*" + + // ACL operations + case s3_constants.ACTION_READ_ACP: + return "s3:GetObjectAcl" + case s3_constants.ACTION_WRITE_ACP: + return "s3:PutObjectAcl" + + // Bucket operations + case s3_constants.ACTION_DELETE_BUCKET: + return "s3:DeleteBucket" + + // Object Lock operations + case s3_constants.ACTION_BYPASS_GOVERNANCE_RETENTION: + return "s3:BypassGovernanceRetention" + case s3_constants.ACTION_GET_OBJECT_RETENTION: + return "s3:GetObjectRetention" + case s3_constants.ACTION_PUT_OBJECT_RETENTION: + return "s3:PutObjectRetention" + case s3_constants.ACTION_GET_OBJECT_LEGAL_HOLD: + return "s3:GetObjectLegalHold" + case s3_constants.ACTION_PUT_OBJECT_LEGAL_HOLD: + return "s3:PutObjectLegalHold" + case s3_constants.ACTION_GET_BUCKET_OBJECT_LOCK_CONFIG: + return "s3:GetBucketObjectLockConfiguration" + case s3_constants.ACTION_PUT_BUCKET_OBJECT_LOCK_CONFIG: + return "s3:PutBucketObjectLockConfiguration" + + default: + // Log warning for unmapped actions to help catch issues + glog.Warningf("convertActionToS3Format: unmapped action '%s', prefixing with 's3:'", action) + // For unknown actions, prefix with s3: to maintain format consistency + // This maintains backward compatibility while alerting developers + return "s3:" + action + } +} |
