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) | |
| }
 |