diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-07-13 16:21:36 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-13 16:21:36 -0700 |
| commit | 7cb1ca13082568bfdcdab974d8cefddf650443c5 (patch) | |
| tree | 573b5e15d080d37b9312cade4151da9e3fb7ddee /weed/s3api/policy_engine/engine.go | |
| parent | 1549ee2e154ab040e211ac7b3bc361272069abef (diff) | |
| download | seaweedfs-7cb1ca13082568bfdcdab974d8cefddf650443c5.tar.xz seaweedfs-7cb1ca13082568bfdcdab974d8cefddf650443c5.zip | |
Add policy engine (#6970)
Diffstat (limited to 'weed/s3api/policy_engine/engine.go')
| -rw-r--r-- | weed/s3api/policy_engine/engine.go | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/weed/s3api/policy_engine/engine.go b/weed/s3api/policy_engine/engine.go new file mode 100644 index 000000000..1e0126eb6 --- /dev/null +++ b/weed/s3api/policy_engine/engine.go @@ -0,0 +1,432 @@ +package policy_engine + +import ( + "fmt" + "net" + "net/http" + "regexp" + "strings" + "sync" + "time" + + "github.com/seaweedfs/seaweedfs/weed/glog" +) + +// PolicyEvaluationResult represents the result of policy evaluation +type PolicyEvaluationResult int + +const ( + PolicyResultDeny PolicyEvaluationResult = iota + PolicyResultAllow + PolicyResultIndeterminate +) + +// PolicyEvaluationContext manages policy evaluation for a bucket +type PolicyEvaluationContext struct { + bucketName string + policy *CompiledPolicy + cache *PolicyCache + mutex sync.RWMutex +} + +// PolicyEngine is the main policy evaluation engine +type PolicyEngine struct { + contexts map[string]*PolicyEvaluationContext + mutex sync.RWMutex +} + +// NewPolicyEngine creates a new policy evaluation engine +func NewPolicyEngine() *PolicyEngine { + return &PolicyEngine{ + contexts: make(map[string]*PolicyEvaluationContext), + } +} + +// SetBucketPolicy sets the policy for a bucket +func (engine *PolicyEngine) SetBucketPolicy(bucketName string, policyJSON string) error { + policy, err := ParsePolicy(policyJSON) + if err != nil { + return fmt.Errorf("invalid policy: %v", err) + } + + compiled, err := CompilePolicy(policy) + if err != nil { + return fmt.Errorf("failed to compile policy: %v", err) + } + + engine.mutex.Lock() + defer engine.mutex.Unlock() + + context := &PolicyEvaluationContext{ + bucketName: bucketName, + policy: compiled, + cache: NewPolicyCache(), + } + + engine.contexts[bucketName] = context + glog.V(2).Infof("Set bucket policy for %s", bucketName) + return nil +} + +// GetBucketPolicy gets the policy for a bucket +func (engine *PolicyEngine) GetBucketPolicy(bucketName string) (*PolicyDocument, error) { + engine.mutex.RLock() + defer engine.mutex.RUnlock() + + context, exists := engine.contexts[bucketName] + if !exists { + return nil, fmt.Errorf("no policy found for bucket %s", bucketName) + } + + return context.policy.Document, nil +} + +// DeleteBucketPolicy deletes the policy for a bucket +func (engine *PolicyEngine) DeleteBucketPolicy(bucketName string) error { + engine.mutex.Lock() + defer engine.mutex.Unlock() + + delete(engine.contexts, bucketName) + glog.V(2).Infof("Deleted bucket policy for %s", bucketName) + return nil +} + +// EvaluatePolicy evaluates a policy for the given arguments +func (engine *PolicyEngine) EvaluatePolicy(bucketName string, args *PolicyEvaluationArgs) PolicyEvaluationResult { + engine.mutex.RLock() + context, exists := engine.contexts[bucketName] + engine.mutex.RUnlock() + + if !exists { + return PolicyResultIndeterminate + } + + return engine.evaluateCompiledPolicy(context.policy, args) +} + +// evaluateCompiledPolicy evaluates a compiled policy +func (engine *PolicyEngine) evaluateCompiledPolicy(policy *CompiledPolicy, args *PolicyEvaluationArgs) PolicyEvaluationResult { + // AWS Policy evaluation logic: + // 1. Check for explicit Deny - if found, return Deny + // 2. Check for explicit Allow - if found, return Allow + // 3. If no explicit Allow is found, return Deny (default deny) + + hasExplicitAllow := false + + for _, stmt := range policy.Statements { + if engine.evaluateStatement(&stmt, args) { + if stmt.Statement.Effect == PolicyEffectDeny { + return PolicyResultDeny // Explicit deny trumps everything + } + if stmt.Statement.Effect == PolicyEffectAllow { + hasExplicitAllow = true + } + } + } + + if hasExplicitAllow { + return PolicyResultAllow + } + + return PolicyResultDeny // Default deny +} + +// evaluateStatement evaluates a single policy statement +func (engine *PolicyEngine) evaluateStatement(stmt *CompiledStatement, args *PolicyEvaluationArgs) bool { + // Check if action matches + if !engine.matchesPatterns(stmt.ActionPatterns, args.Action) { + return false + } + + // Check if resource matches + if !engine.matchesPatterns(stmt.ResourcePatterns, args.Resource) { + return false + } + + // Check if principal matches (if specified) + if len(stmt.PrincipalPatterns) > 0 { + if !engine.matchesPatterns(stmt.PrincipalPatterns, args.Principal) { + return false + } + } + + // Check conditions + if len(stmt.Statement.Condition) > 0 { + if !EvaluateConditions(stmt.Statement.Condition, args.Conditions) { + return false + } + } + + return true +} + +// matchesPatterns checks if a value matches any of the compiled patterns +func (engine *PolicyEngine) matchesPatterns(patterns []*regexp.Regexp, value string) bool { + for _, pattern := range patterns { + if pattern.MatchString(value) { + return true + } + } + return false +} + +// ExtractConditionValuesFromRequest extracts condition values from HTTP request +func ExtractConditionValuesFromRequest(r *http.Request) map[string][]string { + values := make(map[string][]string) + + // AWS condition keys + // Extract IP address without port for proper IP matching + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + // Log a warning if splitting fails + glog.Warningf("Failed to parse IP address from RemoteAddr %q: %v", r.RemoteAddr, err) + // If splitting fails, use the original RemoteAddr (might be just IP without port) + host = r.RemoteAddr + } + values["aws:SourceIp"] = []string{host} + values["aws:SecureTransport"] = []string{fmt.Sprintf("%t", r.TLS != nil)} + // Use AWS standard condition key for current time + values["aws:CurrentTime"] = []string{time.Now().Format(time.RFC3339)} + // Keep RequestTime for backward compatibility + values["aws:RequestTime"] = []string{time.Now().Format(time.RFC3339)} + + // S3 specific condition keys + if userAgent := r.Header.Get("User-Agent"); userAgent != "" { + values["aws:UserAgent"] = []string{userAgent} + } + + if referer := r.Header.Get("Referer"); referer != "" { + values["aws:Referer"] = []string{referer} + } + + // S3 object-level conditions + if r.Method == "GET" || r.Method == "HEAD" { + values["s3:ExistingObjectTag"] = extractObjectTags(r) + } + + // S3 bucket-level conditions + if delimiter := r.URL.Query().Get("delimiter"); delimiter != "" { + values["s3:delimiter"] = []string{delimiter} + } + + if prefix := r.URL.Query().Get("prefix"); prefix != "" { + values["s3:prefix"] = []string{prefix} + } + + if maxKeys := r.URL.Query().Get("max-keys"); maxKeys != "" { + values["s3:max-keys"] = []string{maxKeys} + } + + // Authentication method + if authHeader := r.Header.Get("Authorization"); authHeader != "" { + if strings.HasPrefix(authHeader, "AWS4-HMAC-SHA256") { + values["s3:authType"] = []string{"REST-HEADER"} + } else if strings.HasPrefix(authHeader, "AWS ") { + values["s3:authType"] = []string{"REST-HEADER"} + } + } else if r.URL.Query().Get("AWSAccessKeyId") != "" { + values["s3:authType"] = []string{"REST-QUERY-STRING"} + } + + // HTTP method + values["s3:RequestMethod"] = []string{r.Method} + + // Extract custom headers + for key, headerValues := range r.Header { + if strings.HasPrefix(strings.ToLower(key), "x-amz-") { + values[strings.ToLower(key)] = headerValues + } + } + + return values +} + +// extractObjectTags extracts object tags from request (placeholder implementation) +func extractObjectTags(r *http.Request) []string { + // This would need to be implemented based on how object tags are stored + // For now, return empty slice + return []string{} +} + +// BuildResourceArn builds an ARN for the given bucket and object +func BuildResourceArn(bucketName, objectName string) string { + if objectName == "" { + return fmt.Sprintf("arn:aws:s3:::%s", bucketName) + } + return fmt.Sprintf("arn:aws:s3:::%s/%s", bucketName, objectName) +} + +// BuildActionName builds a standardized action name +func BuildActionName(action string) string { + if strings.HasPrefix(action, "s3:") { + return action + } + return fmt.Sprintf("s3:%s", action) +} + +// IsReadAction checks if an action is a read action +func IsReadAction(action string) bool { + readActions := []string{ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetObjectAcl", + "s3:GetObjectVersionAcl", + "s3:GetObjectTagging", + "s3:GetObjectVersionTagging", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:GetBucketLocation", + "s3:GetBucketVersioning", + "s3:GetBucketAcl", + "s3:GetBucketCors", + "s3:GetBucketPolicy", + "s3:GetBucketTagging", + "s3:GetBucketNotification", + "s3:GetBucketObjectLockConfiguration", + "s3:GetObjectRetention", + "s3:GetObjectLegalHold", + } + + for _, readAction := range readActions { + if action == readAction { + return true + } + } + return false +} + +// IsWriteAction checks if an action is a write action +func IsWriteAction(action string) bool { + writeActions := []string{ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectTagging", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:DeleteObjectTagging", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploads", + "s3:ListParts", + "s3:PutBucketAcl", + "s3:PutBucketCors", + "s3:PutBucketPolicy", + "s3:PutBucketTagging", + "s3:PutBucketNotification", + "s3:PutBucketVersioning", + "s3:DeleteBucketPolicy", + "s3:DeleteBucketTagging", + "s3:DeleteBucketCors", + "s3:PutBucketObjectLockConfiguration", + "s3:PutObjectRetention", + "s3:PutObjectLegalHold", + "s3:BypassGovernanceRetention", + } + + for _, writeAction := range writeActions { + if action == writeAction { + return true + } + } + return false +} + +// GetBucketNameFromArn extracts bucket name from ARN +func GetBucketNameFromArn(arn string) string { + if strings.HasPrefix(arn, "arn:aws:s3:::") { + parts := strings.SplitN(arn[13:], "/", 2) + return parts[0] + } + return "" +} + +// GetObjectNameFromArn extracts object name from ARN +func GetObjectNameFromArn(arn string) string { + if strings.HasPrefix(arn, "arn:aws:s3:::") { + parts := strings.SplitN(arn[13:], "/", 2) + if len(parts) > 1 { + return parts[1] + } + } + return "" +} + +// HasPolicyForBucket checks if a bucket has a policy +func (engine *PolicyEngine) HasPolicyForBucket(bucketName string) bool { + engine.mutex.RLock() + defer engine.mutex.RUnlock() + + _, exists := engine.contexts[bucketName] + return exists +} + +// GetPolicyStatements returns all policy statements for a bucket +func (engine *PolicyEngine) GetPolicyStatements(bucketName string) []PolicyStatement { + engine.mutex.RLock() + defer engine.mutex.RUnlock() + + context, exists := engine.contexts[bucketName] + if !exists { + return nil + } + + return context.policy.Document.Statement +} + +// ValidatePolicyForBucket validates if a policy is valid for a bucket +func (engine *PolicyEngine) ValidatePolicyForBucket(bucketName string, policyJSON string) error { + policy, err := ParsePolicy(policyJSON) + if err != nil { + return err + } + + // Additional validation specific to the bucket + for _, stmt := range policy.Statement { + resources := normalizeToStringSlice(stmt.Resource) + for _, resource := range resources { + if resourceBucket := GetBucketFromResource(resource); resourceBucket != "" { + if resourceBucket != bucketName { + return fmt.Errorf("policy resource %s does not match bucket %s", resource, bucketName) + } + } + } + } + + return nil +} + +// ClearAllPolicies clears all bucket policies +func (engine *PolicyEngine) ClearAllPolicies() { + engine.mutex.Lock() + defer engine.mutex.Unlock() + + engine.contexts = make(map[string]*PolicyEvaluationContext) + glog.V(2).Info("Cleared all bucket policies") +} + +// GetAllBucketsWithPolicies returns all buckets that have policies +func (engine *PolicyEngine) GetAllBucketsWithPolicies() []string { + engine.mutex.RLock() + defer engine.mutex.RUnlock() + + buckets := make([]string, 0, len(engine.contexts)) + for bucketName := range engine.contexts { + buckets = append(buckets, bucketName) + } + return buckets +} + +// EvaluatePolicyForRequest evaluates policy for an HTTP request +func (engine *PolicyEngine) EvaluatePolicyForRequest(bucketName, objectName, action, principal string, r *http.Request) PolicyEvaluationResult { + resource := BuildResourceArn(bucketName, objectName) + actionName := BuildActionName(action) + conditions := ExtractConditionValuesFromRequest(r) + + args := &PolicyEvaluationArgs{ + Action: actionName, + Resource: resource, + Principal: principal, + Conditions: conditions, + } + + return engine.EvaluatePolicy(bucketName, args) +} |
