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