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