You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							432 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							432 lines
						
					
					
						
							12 KiB
						
					
					
				
								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: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									compiled, err := CompilePolicy(policy)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to compile policy: %w", 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)
							 | 
						|
								}
							 |