aboutsummaryrefslogtreecommitdiff
path: root/weed/iam/policy/policy_engine.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/iam/policy/policy_engine.go')
-rw-r--r--weed/iam/policy/policy_engine.go1142
1 files changed, 1142 insertions, 0 deletions
diff --git a/weed/iam/policy/policy_engine.go b/weed/iam/policy/policy_engine.go
new file mode 100644
index 000000000..5af1d7e1a
--- /dev/null
+++ b/weed/iam/policy/policy_engine.go
@@ -0,0 +1,1142 @@
+package policy
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+// Effect represents the policy evaluation result
+type Effect string
+
+const (
+ EffectAllow Effect = "Allow"
+ EffectDeny Effect = "Deny"
+)
+
+// Package-level regex cache for performance optimization
+var (
+ regexCache = make(map[string]*regexp.Regexp)
+ regexCacheMu sync.RWMutex
+)
+
+// PolicyEngine evaluates policies against requests
+type PolicyEngine struct {
+ config *PolicyEngineConfig
+ initialized bool
+ store PolicyStore
+}
+
+// PolicyEngineConfig holds policy engine configuration
+type PolicyEngineConfig struct {
+ // DefaultEffect when no policies match (Allow or Deny)
+ DefaultEffect string `json:"defaultEffect"`
+
+ // StoreType specifies the policy store backend (memory, filer, etc.)
+ StoreType string `json:"storeType"`
+
+ // StoreConfig contains store-specific configuration
+ StoreConfig map[string]interface{} `json:"storeConfig,omitempty"`
+}
+
+// PolicyDocument represents an IAM policy document
+type PolicyDocument struct {
+ // Version of the policy language (e.g., "2012-10-17")
+ Version string `json:"Version"`
+
+ // Id is an optional policy identifier
+ Id string `json:"Id,omitempty"`
+
+ // Statement contains the policy statements
+ Statement []Statement `json:"Statement"`
+}
+
+// Statement represents a single policy statement
+type Statement struct {
+ // Sid is an optional statement identifier
+ Sid string `json:"Sid,omitempty"`
+
+ // Effect specifies whether to Allow or Deny
+ Effect string `json:"Effect"`
+
+ // Principal specifies who the statement applies to (optional in role policies)
+ Principal interface{} `json:"Principal,omitempty"`
+
+ // NotPrincipal specifies who the statement does NOT apply to
+ NotPrincipal interface{} `json:"NotPrincipal,omitempty"`
+
+ // Action specifies the actions this statement applies to
+ Action []string `json:"Action"`
+
+ // NotAction specifies actions this statement does NOT apply to
+ NotAction []string `json:"NotAction,omitempty"`
+
+ // Resource specifies the resources this statement applies to
+ Resource []string `json:"Resource"`
+
+ // NotResource specifies resources this statement does NOT apply to
+ NotResource []string `json:"NotResource,omitempty"`
+
+ // Condition specifies conditions for when this statement applies
+ Condition map[string]map[string]interface{} `json:"Condition,omitempty"`
+}
+
+// EvaluationContext provides context for policy evaluation
+type EvaluationContext struct {
+ // Principal making the request (e.g., "user:alice", "role:admin")
+ Principal string `json:"principal"`
+
+ // Action being requested (e.g., "s3:GetObject")
+ Action string `json:"action"`
+
+ // Resource being accessed (e.g., "arn:seaweed:s3:::bucket/key")
+ Resource string `json:"resource"`
+
+ // RequestContext contains additional request information
+ RequestContext map[string]interface{} `json:"requestContext,omitempty"`
+}
+
+// EvaluationResult contains the result of policy evaluation
+type EvaluationResult struct {
+ // Effect is the final decision (Allow or Deny)
+ Effect Effect `json:"effect"`
+
+ // MatchingStatements contains statements that matched the request
+ MatchingStatements []StatementMatch `json:"matchingStatements,omitempty"`
+
+ // EvaluationDetails provides detailed evaluation information
+ EvaluationDetails *EvaluationDetails `json:"evaluationDetails,omitempty"`
+}
+
+// StatementMatch represents a statement that matched during evaluation
+type StatementMatch struct {
+ // PolicyName is the name of the policy containing this statement
+ PolicyName string `json:"policyName"`
+
+ // StatementSid is the statement identifier
+ StatementSid string `json:"statementSid,omitempty"`
+
+ // Effect is the effect of this statement
+ Effect Effect `json:"effect"`
+
+ // Reason explains why this statement matched
+ Reason string `json:"reason,omitempty"`
+}
+
+// EvaluationDetails provides detailed information about policy evaluation
+type EvaluationDetails struct {
+ // Principal that was evaluated
+ Principal string `json:"principal"`
+
+ // Action that was evaluated
+ Action string `json:"action"`
+
+ // Resource that was evaluated
+ Resource string `json:"resource"`
+
+ // PoliciesEvaluated lists all policies that were evaluated
+ PoliciesEvaluated []string `json:"policiesEvaluated"`
+
+ // ConditionsEvaluated lists all conditions that were evaluated
+ ConditionsEvaluated []string `json:"conditionsEvaluated,omitempty"`
+}
+
+// PolicyStore defines the interface for storing and retrieving policies
+type PolicyStore interface {
+ // StorePolicy stores a policy document (filerAddress ignored for memory stores)
+ StorePolicy(ctx context.Context, filerAddress string, name string, policy *PolicyDocument) error
+
+ // GetPolicy retrieves a policy document (filerAddress ignored for memory stores)
+ GetPolicy(ctx context.Context, filerAddress string, name string) (*PolicyDocument, error)
+
+ // DeletePolicy deletes a policy document (filerAddress ignored for memory stores)
+ DeletePolicy(ctx context.Context, filerAddress string, name string) error
+
+ // ListPolicies lists all policy names (filerAddress ignored for memory stores)
+ ListPolicies(ctx context.Context, filerAddress string) ([]string, error)
+}
+
+// NewPolicyEngine creates a new policy engine
+func NewPolicyEngine() *PolicyEngine {
+ return &PolicyEngine{}
+}
+
+// Initialize initializes the policy engine with configuration
+func (e *PolicyEngine) Initialize(config *PolicyEngineConfig) error {
+ if config == nil {
+ return fmt.Errorf("config cannot be nil")
+ }
+
+ if err := e.validateConfig(config); err != nil {
+ return fmt.Errorf("invalid configuration: %w", err)
+ }
+
+ e.config = config
+
+ // Initialize policy store
+ store, err := e.createPolicyStore(config)
+ if err != nil {
+ return fmt.Errorf("failed to create policy store: %w", err)
+ }
+ e.store = store
+
+ e.initialized = true
+ return nil
+}
+
+// InitializeWithProvider initializes the policy engine with configuration and a filer address provider
+func (e *PolicyEngine) InitializeWithProvider(config *PolicyEngineConfig, filerAddressProvider func() string) error {
+ if config == nil {
+ return fmt.Errorf("config cannot be nil")
+ }
+
+ if err := e.validateConfig(config); err != nil {
+ return fmt.Errorf("invalid configuration: %w", err)
+ }
+
+ e.config = config
+
+ // Initialize policy store with provider
+ store, err := e.createPolicyStoreWithProvider(config, filerAddressProvider)
+ if err != nil {
+ return fmt.Errorf("failed to create policy store: %w", err)
+ }
+ e.store = store
+
+ e.initialized = true
+ return nil
+}
+
+// validateConfig validates the policy engine configuration
+func (e *PolicyEngine) validateConfig(config *PolicyEngineConfig) error {
+ if config.DefaultEffect != "Allow" && config.DefaultEffect != "Deny" {
+ return fmt.Errorf("invalid default effect: %s", config.DefaultEffect)
+ }
+
+ if config.StoreType == "" {
+ config.StoreType = "filer" // Default to filer store for persistence
+ }
+
+ return nil
+}
+
+// createPolicyStore creates a policy store based on configuration
+func (e *PolicyEngine) createPolicyStore(config *PolicyEngineConfig) (PolicyStore, error) {
+ switch config.StoreType {
+ case "memory":
+ return NewMemoryPolicyStore(), nil
+ case "", "filer":
+ // Check if caching is explicitly disabled
+ if config.StoreConfig != nil {
+ if noCache, ok := config.StoreConfig["noCache"].(bool); ok && noCache {
+ return NewFilerPolicyStore(config.StoreConfig, nil)
+ }
+ }
+ // Default to generic cached filer store for better performance
+ return NewGenericCachedPolicyStore(config.StoreConfig, nil)
+ case "cached-filer", "generic-cached":
+ return NewGenericCachedPolicyStore(config.StoreConfig, nil)
+ default:
+ return nil, fmt.Errorf("unsupported store type: %s", config.StoreType)
+ }
+}
+
+// createPolicyStoreWithProvider creates a policy store with a filer address provider function
+func (e *PolicyEngine) createPolicyStoreWithProvider(config *PolicyEngineConfig, filerAddressProvider func() string) (PolicyStore, error) {
+ switch config.StoreType {
+ case "memory":
+ return NewMemoryPolicyStore(), nil
+ case "", "filer":
+ // Check if caching is explicitly disabled
+ if config.StoreConfig != nil {
+ if noCache, ok := config.StoreConfig["noCache"].(bool); ok && noCache {
+ return NewFilerPolicyStore(config.StoreConfig, filerAddressProvider)
+ }
+ }
+ // Default to generic cached filer store for better performance
+ return NewGenericCachedPolicyStore(config.StoreConfig, filerAddressProvider)
+ case "cached-filer", "generic-cached":
+ return NewGenericCachedPolicyStore(config.StoreConfig, filerAddressProvider)
+ default:
+ return nil, fmt.Errorf("unsupported store type: %s", config.StoreType)
+ }
+}
+
+// IsInitialized returns whether the engine is initialized
+func (e *PolicyEngine) IsInitialized() bool {
+ return e.initialized
+}
+
+// AddPolicy adds a policy to the engine (filerAddress ignored for memory stores)
+func (e *PolicyEngine) AddPolicy(filerAddress string, name string, policy *PolicyDocument) error {
+ if !e.initialized {
+ return fmt.Errorf("policy engine not initialized")
+ }
+
+ if name == "" {
+ return fmt.Errorf("policy name cannot be empty")
+ }
+
+ if policy == nil {
+ return fmt.Errorf("policy cannot be nil")
+ }
+
+ if err := ValidatePolicyDocument(policy); err != nil {
+ return fmt.Errorf("invalid policy document: %w", err)
+ }
+
+ return e.store.StorePolicy(context.Background(), filerAddress, name, policy)
+}
+
+// Evaluate evaluates policies against a request context (filerAddress ignored for memory stores)
+func (e *PolicyEngine) Evaluate(ctx context.Context, filerAddress string, evalCtx *EvaluationContext, policyNames []string) (*EvaluationResult, error) {
+ if !e.initialized {
+ return nil, fmt.Errorf("policy engine not initialized")
+ }
+
+ if evalCtx == nil {
+ return nil, fmt.Errorf("evaluation context cannot be nil")
+ }
+
+ result := &EvaluationResult{
+ Effect: Effect(e.config.DefaultEffect),
+ EvaluationDetails: &EvaluationDetails{
+ Principal: evalCtx.Principal,
+ Action: evalCtx.Action,
+ Resource: evalCtx.Resource,
+ PoliciesEvaluated: policyNames,
+ },
+ }
+
+ var matchingStatements []StatementMatch
+ explicitDeny := false
+ hasAllow := false
+
+ // Evaluate each policy
+ for _, policyName := range policyNames {
+ policy, err := e.store.GetPolicy(ctx, filerAddress, policyName)
+ if err != nil {
+ continue // Skip policies that can't be loaded
+ }
+
+ // Evaluate each statement in the policy
+ for _, statement := range policy.Statement {
+ if e.statementMatches(&statement, evalCtx) {
+ match := StatementMatch{
+ PolicyName: policyName,
+ StatementSid: statement.Sid,
+ Effect: Effect(statement.Effect),
+ Reason: "Action, Resource, and Condition matched",
+ }
+ matchingStatements = append(matchingStatements, match)
+
+ if statement.Effect == "Deny" {
+ explicitDeny = true
+ } else if statement.Effect == "Allow" {
+ hasAllow = true
+ }
+ }
+ }
+ }
+
+ result.MatchingStatements = matchingStatements
+
+ // AWS IAM evaluation logic:
+ // 1. If there's an explicit Deny, the result is Deny
+ // 2. If there's an Allow and no Deny, the result is Allow
+ // 3. Otherwise, use the default effect
+ if explicitDeny {
+ result.Effect = EffectDeny
+ } else if hasAllow {
+ result.Effect = EffectAllow
+ }
+
+ return result, nil
+}
+
+// statementMatches checks if a statement matches the evaluation context
+func (e *PolicyEngine) statementMatches(statement *Statement, evalCtx *EvaluationContext) bool {
+ // Check action match
+ if !e.matchesActions(statement.Action, evalCtx.Action, evalCtx) {
+ return false
+ }
+
+ // Check resource match
+ if !e.matchesResources(statement.Resource, evalCtx.Resource, evalCtx) {
+ return false
+ }
+
+ // Check conditions
+ if !e.matchesConditions(statement.Condition, evalCtx) {
+ return false
+ }
+
+ return true
+}
+
+// matchesActions checks if any action in the list matches the requested action
+func (e *PolicyEngine) matchesActions(actions []string, requestedAction string, evalCtx *EvaluationContext) bool {
+ for _, action := range actions {
+ if awsIAMMatch(action, requestedAction, evalCtx) {
+ return true
+ }
+ }
+ return false
+}
+
+// matchesResources checks if any resource in the list matches the requested resource
+func (e *PolicyEngine) matchesResources(resources []string, requestedResource string, evalCtx *EvaluationContext) bool {
+ for _, resource := range resources {
+ if awsIAMMatch(resource, requestedResource, evalCtx) {
+ return true
+ }
+ }
+ return false
+}
+
+// matchesConditions checks if all conditions are satisfied
+func (e *PolicyEngine) matchesConditions(conditions map[string]map[string]interface{}, evalCtx *EvaluationContext) bool {
+ if len(conditions) == 0 {
+ return true // No conditions means always match
+ }
+
+ for conditionType, conditionBlock := range conditions {
+ if !e.evaluateConditionBlock(conditionType, conditionBlock, evalCtx) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// evaluateConditionBlock evaluates a single condition block
+func (e *PolicyEngine) evaluateConditionBlock(conditionType string, block map[string]interface{}, evalCtx *EvaluationContext) bool {
+ switch conditionType {
+ // IP Address conditions
+ case "IpAddress":
+ return e.evaluateIPCondition(block, evalCtx, true)
+ case "NotIpAddress":
+ return e.evaluateIPCondition(block, evalCtx, false)
+
+ // String conditions
+ case "StringEquals":
+ return e.EvaluateStringCondition(block, evalCtx, true, false)
+ case "StringNotEquals":
+ return e.EvaluateStringCondition(block, evalCtx, false, false)
+ case "StringLike":
+ return e.EvaluateStringCondition(block, evalCtx, true, true)
+ case "StringEqualsIgnoreCase":
+ return e.evaluateStringConditionIgnoreCase(block, evalCtx, true, false)
+ case "StringNotEqualsIgnoreCase":
+ return e.evaluateStringConditionIgnoreCase(block, evalCtx, false, false)
+ case "StringLikeIgnoreCase":
+ return e.evaluateStringConditionIgnoreCase(block, evalCtx, true, true)
+
+ // Numeric conditions
+ case "NumericEquals":
+ return e.evaluateNumericCondition(block, evalCtx, "==")
+ case "NumericNotEquals":
+ return e.evaluateNumericCondition(block, evalCtx, "!=")
+ case "NumericLessThan":
+ return e.evaluateNumericCondition(block, evalCtx, "<")
+ case "NumericLessThanEquals":
+ return e.evaluateNumericCondition(block, evalCtx, "<=")
+ case "NumericGreaterThan":
+ return e.evaluateNumericCondition(block, evalCtx, ">")
+ case "NumericGreaterThanEquals":
+ return e.evaluateNumericCondition(block, evalCtx, ">=")
+
+ // Date conditions
+ case "DateEquals":
+ return e.evaluateDateCondition(block, evalCtx, "==")
+ case "DateNotEquals":
+ return e.evaluateDateCondition(block, evalCtx, "!=")
+ case "DateLessThan":
+ return e.evaluateDateCondition(block, evalCtx, "<")
+ case "DateLessThanEquals":
+ return e.evaluateDateCondition(block, evalCtx, "<=")
+ case "DateGreaterThan":
+ return e.evaluateDateCondition(block, evalCtx, ">")
+ case "DateGreaterThanEquals":
+ return e.evaluateDateCondition(block, evalCtx, ">=")
+
+ // Boolean conditions
+ case "Bool":
+ return e.evaluateBoolCondition(block, evalCtx)
+
+ // Null conditions
+ case "Null":
+ return e.evaluateNullCondition(block, evalCtx)
+
+ default:
+ // Unknown condition types default to false (more secure)
+ return false
+ }
+}
+
+// evaluateIPCondition evaluates IP address conditions
+func (e *PolicyEngine) evaluateIPCondition(block map[string]interface{}, evalCtx *EvaluationContext, shouldMatch bool) bool {
+ sourceIP, exists := evalCtx.RequestContext["sourceIP"]
+ if !exists {
+ return !shouldMatch // If no IP in context, condition fails for positive match
+ }
+
+ sourceIPStr, ok := sourceIP.(string)
+ if !ok {
+ return !shouldMatch
+ }
+
+ sourceIPAddr := net.ParseIP(sourceIPStr)
+ if sourceIPAddr == nil {
+ return !shouldMatch
+ }
+
+ for key, value := range block {
+ if key == "seaweed:SourceIP" {
+ ranges, ok := value.([]string)
+ if !ok {
+ continue
+ }
+
+ for _, ipRange := range ranges {
+ if strings.Contains(ipRange, "/") {
+ // CIDR range
+ _, cidr, err := net.ParseCIDR(ipRange)
+ if err != nil {
+ continue
+ }
+ if cidr.Contains(sourceIPAddr) {
+ return shouldMatch
+ }
+ } else {
+ // Single IP
+ if sourceIPStr == ipRange {
+ return shouldMatch
+ }
+ }
+ }
+ }
+ }
+
+ return !shouldMatch
+}
+
+// EvaluateStringCondition evaluates string-based conditions
+func (e *PolicyEngine) EvaluateStringCondition(block map[string]interface{}, evalCtx *EvaluationContext, shouldMatch bool, useWildcard bool) bool {
+ // Iterate through all condition keys in the block
+ for conditionKey, conditionValue := range block {
+ // Get the context values for this condition key
+ contextValues, exists := evalCtx.RequestContext[conditionKey]
+ if !exists {
+ // If the context key doesn't exist, condition fails for positive match
+ if shouldMatch {
+ return false
+ }
+ continue
+ }
+
+ // Convert context value to string slice
+ var contextStrings []string
+ switch v := contextValues.(type) {
+ case string:
+ contextStrings = []string{v}
+ case []string:
+ contextStrings = v
+ case []interface{}:
+ for _, item := range v {
+ if str, ok := item.(string); ok {
+ contextStrings = append(contextStrings, str)
+ }
+ }
+ default:
+ // Convert to string as fallback
+ contextStrings = []string{fmt.Sprintf("%v", v)}
+ }
+
+ // Convert condition value to string slice
+ var expectedStrings []string
+ switch v := conditionValue.(type) {
+ case string:
+ expectedStrings = []string{v}
+ case []string:
+ expectedStrings = v
+ case []interface{}:
+ for _, item := range v {
+ if str, ok := item.(string); ok {
+ expectedStrings = append(expectedStrings, str)
+ } else {
+ expectedStrings = append(expectedStrings, fmt.Sprintf("%v", item))
+ }
+ }
+ default:
+ expectedStrings = []string{fmt.Sprintf("%v", v)}
+ }
+
+ // Evaluate the condition using AWS IAM-compliant matching
+ conditionMet := false
+ for _, expected := range expectedStrings {
+ for _, contextValue := range contextStrings {
+ if useWildcard {
+ // Use AWS IAM-compliant wildcard matching for StringLike conditions
+ // This handles case-insensitivity and policy variables
+ if awsIAMMatch(expected, contextValue, evalCtx) {
+ conditionMet = true
+ break
+ }
+ } else {
+ // For StringEquals/StringNotEquals, also support policy variables but be case-sensitive
+ expandedExpected := expandPolicyVariables(expected, evalCtx)
+ if expandedExpected == contextValue {
+ conditionMet = true
+ break
+ }
+ }
+ }
+ if conditionMet {
+ break
+ }
+ }
+
+ // For shouldMatch=true (StringEquals, StringLike): condition must be met
+ // For shouldMatch=false (StringNotEquals): condition must NOT be met
+ if shouldMatch && !conditionMet {
+ return false
+ }
+ if !shouldMatch && conditionMet {
+ return false
+ }
+ }
+
+ return true
+}
+
+// ValidatePolicyDocument validates a policy document structure
+func ValidatePolicyDocument(policy *PolicyDocument) error {
+ return ValidatePolicyDocumentWithType(policy, "resource")
+}
+
+// ValidateTrustPolicyDocument validates a trust policy document structure
+func ValidateTrustPolicyDocument(policy *PolicyDocument) error {
+ return ValidatePolicyDocumentWithType(policy, "trust")
+}
+
+// ValidatePolicyDocumentWithType validates a policy document for specific type
+func ValidatePolicyDocumentWithType(policy *PolicyDocument, policyType string) error {
+ if policy == nil {
+ return fmt.Errorf("policy document cannot be nil")
+ }
+
+ if policy.Version == "" {
+ return fmt.Errorf("version is required")
+ }
+
+ if len(policy.Statement) == 0 {
+ return fmt.Errorf("at least one statement is required")
+ }
+
+ for i, statement := range policy.Statement {
+ if err := validateStatementWithType(&statement, policyType); err != nil {
+ return fmt.Errorf("statement %d is invalid: %w", i, err)
+ }
+ }
+
+ return nil
+}
+
+// validateStatement validates a single statement (for backward compatibility)
+func validateStatement(statement *Statement) error {
+ return validateStatementWithType(statement, "resource")
+}
+
+// validateStatementWithType validates a single statement based on policy type
+func validateStatementWithType(statement *Statement, policyType string) error {
+ if statement.Effect != "Allow" && statement.Effect != "Deny" {
+ return fmt.Errorf("invalid effect: %s (must be Allow or Deny)", statement.Effect)
+ }
+
+ if len(statement.Action) == 0 {
+ return fmt.Errorf("at least one action is required")
+ }
+
+ // Trust policies don't require Resource field, but resource policies do
+ if policyType == "resource" {
+ if len(statement.Resource) == 0 {
+ return fmt.Errorf("at least one resource is required")
+ }
+ } else if policyType == "trust" {
+ // Trust policies should have Principal field
+ if statement.Principal == nil {
+ return fmt.Errorf("trust policy statement must have Principal field")
+ }
+
+ // Trust policies typically have specific actions
+ validTrustActions := map[string]bool{
+ "sts:AssumeRole": true,
+ "sts:AssumeRoleWithWebIdentity": true,
+ "sts:AssumeRoleWithCredentials": true,
+ }
+
+ for _, action := range statement.Action {
+ if !validTrustActions[action] {
+ return fmt.Errorf("invalid action for trust policy: %s", action)
+ }
+ }
+ }
+
+ return nil
+}
+
+// matchResource checks if a resource pattern matches a requested resource
+// Uses hybrid approach: simple suffix wildcards for compatibility, filepath.Match for complex patterns
+func matchResource(pattern, resource string) bool {
+ if pattern == resource {
+ return true
+ }
+
+ // Handle simple suffix wildcard (backward compatibility)
+ if strings.HasSuffix(pattern, "*") {
+ prefix := pattern[:len(pattern)-1]
+ return strings.HasPrefix(resource, prefix)
+ }
+
+ // For complex patterns, use filepath.Match for advanced wildcard support (*, ?, [])
+ matched, err := filepath.Match(pattern, resource)
+ if err != nil {
+ // Fallback to exact match if pattern is malformed
+ return pattern == resource
+ }
+
+ return matched
+}
+
+// awsIAMMatch performs AWS IAM-compliant pattern matching with case-insensitivity and policy variable support
+func awsIAMMatch(pattern, value string, evalCtx *EvaluationContext) bool {
+ // Step 1: Substitute policy variables (e.g., ${aws:username}, ${saml:username})
+ expandedPattern := expandPolicyVariables(pattern, evalCtx)
+
+ // Step 2: Handle special patterns
+ if expandedPattern == "*" {
+ return true // Universal wildcard
+ }
+
+ // Step 3: Case-insensitive exact match
+ if strings.EqualFold(expandedPattern, value) {
+ return true
+ }
+
+ // Step 4: Handle AWS-style wildcards (case-insensitive)
+ if strings.Contains(expandedPattern, "*") || strings.Contains(expandedPattern, "?") {
+ return AwsWildcardMatch(expandedPattern, value)
+ }
+
+ return false
+}
+
+// expandPolicyVariables substitutes AWS policy variables in the pattern
+func expandPolicyVariables(pattern string, evalCtx *EvaluationContext) string {
+ if evalCtx == nil || evalCtx.RequestContext == nil {
+ return pattern
+ }
+
+ expanded := pattern
+
+ // Common AWS policy variables that might be used in SeaweedFS
+ variableMap := map[string]string{
+ "${aws:username}": getContextValue(evalCtx, "aws:username", ""),
+ "${saml:username}": getContextValue(evalCtx, "saml:username", ""),
+ "${oidc:sub}": getContextValue(evalCtx, "oidc:sub", ""),
+ "${aws:userid}": getContextValue(evalCtx, "aws:userid", ""),
+ "${aws:principaltype}": getContextValue(evalCtx, "aws:principaltype", ""),
+ }
+
+ for variable, value := range variableMap {
+ if value != "" {
+ expanded = strings.ReplaceAll(expanded, variable, value)
+ }
+ }
+
+ return expanded
+}
+
+// getContextValue safely gets a value from the evaluation context
+func getContextValue(evalCtx *EvaluationContext, key, defaultValue string) string {
+ if value, exists := evalCtx.RequestContext[key]; exists {
+ if str, ok := value.(string); ok {
+ return str
+ }
+ }
+ return defaultValue
+}
+
+// AwsWildcardMatch performs case-insensitive wildcard matching like AWS IAM
+func AwsWildcardMatch(pattern, value string) bool {
+ // Create regex pattern key for caching
+ // First escape all regex metacharacters, then replace wildcards
+ regexPattern := regexp.QuoteMeta(pattern)
+ regexPattern = strings.ReplaceAll(regexPattern, "\\*", ".*")
+ regexPattern = strings.ReplaceAll(regexPattern, "\\?", ".")
+ regexPattern = "^" + regexPattern + "$"
+ regexKey := "(?i)" + regexPattern
+
+ // Try to get compiled regex from cache
+ regexCacheMu.RLock()
+ regex, found := regexCache[regexKey]
+ regexCacheMu.RUnlock()
+
+ if !found {
+ // Compile and cache the regex
+ compiledRegex, err := regexp.Compile(regexKey)
+ if err != nil {
+ // Fallback to simple case-insensitive comparison if regex fails
+ return strings.EqualFold(pattern, value)
+ }
+
+ // Store in cache with write lock
+ regexCacheMu.Lock()
+ // Double-check in case another goroutine added it
+ if existingRegex, exists := regexCache[regexKey]; exists {
+ regex = existingRegex
+ } else {
+ regexCache[regexKey] = compiledRegex
+ regex = compiledRegex
+ }
+ regexCacheMu.Unlock()
+ }
+
+ return regex.MatchString(value)
+}
+
+// matchAction checks if an action pattern matches a requested action
+// Uses hybrid approach: simple suffix wildcards for compatibility, filepath.Match for complex patterns
+func matchAction(pattern, action string) bool {
+ if pattern == action {
+ return true
+ }
+
+ // Handle simple suffix wildcard (backward compatibility)
+ if strings.HasSuffix(pattern, "*") {
+ prefix := pattern[:len(pattern)-1]
+ return strings.HasPrefix(action, prefix)
+ }
+
+ // For complex patterns, use filepath.Match for advanced wildcard support (*, ?, [])
+ matched, err := filepath.Match(pattern, action)
+ if err != nil {
+ // Fallback to exact match if pattern is malformed
+ return pattern == action
+ }
+
+ return matched
+}
+
+// evaluateStringConditionIgnoreCase evaluates string conditions with case insensitivity
+func (e *PolicyEngine) evaluateStringConditionIgnoreCase(block map[string]interface{}, evalCtx *EvaluationContext, shouldMatch bool, useWildcard bool) bool {
+ for key, expectedValues := range block {
+ contextValue, exists := evalCtx.RequestContext[key]
+ if !exists {
+ if !shouldMatch {
+ continue // For NotEquals, missing key is OK
+ }
+ return false
+ }
+
+ contextStr, ok := contextValue.(string)
+ if !ok {
+ return false
+ }
+
+ contextStr = strings.ToLower(contextStr)
+ matched := false
+
+ // Handle different value types
+ switch v := expectedValues.(type) {
+ case string:
+ expectedStr := strings.ToLower(v)
+ if useWildcard {
+ matched, _ = filepath.Match(expectedStr, contextStr)
+ } else {
+ matched = expectedStr == contextStr
+ }
+ case []interface{}:
+ for _, val := range v {
+ if valStr, ok := val.(string); ok {
+ expectedStr := strings.ToLower(valStr)
+ if useWildcard {
+ if m, _ := filepath.Match(expectedStr, contextStr); m {
+ matched = true
+ break
+ }
+ } else {
+ if expectedStr == contextStr {
+ matched = true
+ break
+ }
+ }
+ }
+ }
+ }
+
+ if shouldMatch && !matched {
+ return false
+ }
+ if !shouldMatch && matched {
+ return false
+ }
+ }
+ return true
+}
+
+// evaluateNumericCondition evaluates numeric conditions
+func (e *PolicyEngine) evaluateNumericCondition(block map[string]interface{}, evalCtx *EvaluationContext, operator string) bool {
+ for key, expectedValues := range block {
+ contextValue, exists := evalCtx.RequestContext[key]
+ if !exists {
+ return false
+ }
+
+ contextNum, err := parseNumeric(contextValue)
+ if err != nil {
+ return false
+ }
+
+ matched := false
+
+ // Handle different value types
+ switch v := expectedValues.(type) {
+ case string:
+ expectedNum, err := parseNumeric(v)
+ if err != nil {
+ return false
+ }
+ matched = compareNumbers(contextNum, expectedNum, operator)
+ case []interface{}:
+ for _, val := range v {
+ expectedNum, err := parseNumeric(val)
+ if err != nil {
+ continue
+ }
+ if compareNumbers(contextNum, expectedNum, operator) {
+ matched = true
+ break
+ }
+ }
+ }
+
+ if !matched {
+ return false
+ }
+ }
+ return true
+}
+
+// evaluateDateCondition evaluates date conditions
+func (e *PolicyEngine) evaluateDateCondition(block map[string]interface{}, evalCtx *EvaluationContext, operator string) bool {
+ for key, expectedValues := range block {
+ contextValue, exists := evalCtx.RequestContext[key]
+ if !exists {
+ return false
+ }
+
+ contextTime, err := parseDateTime(contextValue)
+ if err != nil {
+ return false
+ }
+
+ matched := false
+
+ // Handle different value types
+ switch v := expectedValues.(type) {
+ case string:
+ expectedTime, err := parseDateTime(v)
+ if err != nil {
+ return false
+ }
+ matched = compareDates(contextTime, expectedTime, operator)
+ case []interface{}:
+ for _, val := range v {
+ expectedTime, err := parseDateTime(val)
+ if err != nil {
+ continue
+ }
+ if compareDates(contextTime, expectedTime, operator) {
+ matched = true
+ break
+ }
+ }
+ }
+
+ if !matched {
+ return false
+ }
+ }
+ return true
+}
+
+// evaluateBoolCondition evaluates boolean conditions
+func (e *PolicyEngine) evaluateBoolCondition(block map[string]interface{}, evalCtx *EvaluationContext) bool {
+ for key, expectedValues := range block {
+ contextValue, exists := evalCtx.RequestContext[key]
+ if !exists {
+ return false
+ }
+
+ contextBool, err := parseBool(contextValue)
+ if err != nil {
+ return false
+ }
+
+ matched := false
+
+ // Handle different value types
+ switch v := expectedValues.(type) {
+ case string:
+ expectedBool, err := parseBool(v)
+ if err != nil {
+ return false
+ }
+ matched = contextBool == expectedBool
+ case bool:
+ matched = contextBool == v
+ case []interface{}:
+ for _, val := range v {
+ expectedBool, err := parseBool(val)
+ if err != nil {
+ continue
+ }
+ if contextBool == expectedBool {
+ matched = true
+ break
+ }
+ }
+ }
+
+ if !matched {
+ return false
+ }
+ }
+ return true
+}
+
+// evaluateNullCondition evaluates null conditions
+func (e *PolicyEngine) evaluateNullCondition(block map[string]interface{}, evalCtx *EvaluationContext) bool {
+ for key, expectedValues := range block {
+ _, exists := evalCtx.RequestContext[key]
+
+ expectedNull := false
+ switch v := expectedValues.(type) {
+ case string:
+ expectedNull = v == "true"
+ case bool:
+ expectedNull = v
+ }
+
+ // If we expect null (true) and key exists, or expect non-null (false) and key doesn't exist
+ if expectedNull == exists {
+ return false
+ }
+ }
+ return true
+}
+
+// Helper functions for parsing and comparing values
+
+// parseNumeric parses a value as a float64
+func parseNumeric(value interface{}) (float64, error) {
+ switch v := value.(type) {
+ case float64:
+ return v, nil
+ case float32:
+ return float64(v), nil
+ case int:
+ return float64(v), nil
+ case int64:
+ return float64(v), nil
+ case string:
+ return strconv.ParseFloat(v, 64)
+ default:
+ return 0, fmt.Errorf("cannot parse %T as numeric", value)
+ }
+}
+
+// compareNumbers compares two numbers using the given operator
+func compareNumbers(a, b float64, operator string) bool {
+ switch operator {
+ case "==":
+ return a == b
+ case "!=":
+ return a != b
+ case "<":
+ return a < b
+ case "<=":
+ return a <= b
+ case ">":
+ return a > b
+ case ">=":
+ return a >= b
+ default:
+ return false
+ }
+}
+
+// parseDateTime parses a value as a time.Time
+func parseDateTime(value interface{}) (time.Time, error) {
+ switch v := value.(type) {
+ case string:
+ // Try common date formats
+ formats := []string{
+ time.RFC3339,
+ "2006-01-02T15:04:05Z",
+ "2006-01-02T15:04:05",
+ "2006-01-02 15:04:05",
+ "2006-01-02",
+ }
+ for _, format := range formats {
+ if t, err := time.Parse(format, v); err == nil {
+ return t, nil
+ }
+ }
+ return time.Time{}, fmt.Errorf("cannot parse date: %s", v)
+ case time.Time:
+ return v, nil
+ default:
+ return time.Time{}, fmt.Errorf("cannot parse %T as date", value)
+ }
+}
+
+// compareDates compares two dates using the given operator
+func compareDates(a, b time.Time, operator string) bool {
+ switch operator {
+ case "==":
+ return a.Equal(b)
+ case "!=":
+ return !a.Equal(b)
+ case "<":
+ return a.Before(b)
+ case "<=":
+ return a.Before(b) || a.Equal(b)
+ case ">":
+ return a.After(b)
+ case ">=":
+ return a.After(b) || a.Equal(b)
+ default:
+ return false
+ }
+}
+
+// parseBool parses a value as a boolean
+func parseBool(value interface{}) (bool, error) {
+ switch v := value.(type) {
+ case bool:
+ return v, nil
+ case string:
+ return strconv.ParseBool(v)
+ default:
+ return false, fmt.Errorf("cannot parse %T as boolean", value)
+ }
+}