Files
seaweedfs/weed/s3api/policy_engine/conditions.go
T
15ff5c113a fix(s3tables): validate policy conditions and fail safe on unusable ones (#12389)
* s3tables: validate policy conditions and fail safe on unusable ones

* s3vectors: reject a condition operator with no keys

The shared evaluator now treats a keyless operator as unusable, so a
stored keyless Allow would never grant while the write succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3vectors: reject an Allow whose Condition is null

A null Condition decodes as no Condition, so the Allow granted
unconditionally. A null Condition on a Deny only widens the veto and
stays accepted, as in the S3 Tables validator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: treat a NaN numeric condition value as unreadable

strconv.ParseFloat accepts "NaN", but no comparison with it holds, so a
Deny on NumericLessThan never applied and an Allow on NumericNotEquals
matched every request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: reject condition keys the evaluator never supplies

A condition on a key such as aws:SourceIp reads no request value, so a
Deny on it never applied and an Allow never granted. Accept only the
keys getConditionContextValues resolves, as the vector validator does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3vectors: require Statement and Effect spelled as written

The evaluator decodes field names regardless of case, so an "effect" or
"statement" key slipped past the null Condition check and the other
raw-key checks while the stored Allow still granted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: treat an infinite numeric condition value as unreadable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: a stored Allow with a null Condition never grants

The typed decode reads "Condition": null as no Condition, so an Allow
stored before validation refused it still granted unconditionally, for
table and vector policies alike. Record the literal null while decoding
a Statement and skip such an Allow during evaluation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: validate a null Condition from the decoded Statement

validatePolicyDocument re-parsed the document to find a literal null
Condition; the Statement decode now records it, so the validator reads
the same flag the evaluator does and nullConditionStatements goes away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: keep Condition objects merged across key spellings

Decoding Condition through a json.RawMessage kept only the last of
"Condition" and "condition", so an empty object under one spelling
erased the other's restriction. Decode the statement as before and
probe for the literal null separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: refuse a Condition split across key spellings

Condition objects under differently cased keys decode into one map, and an
operator they share keeps only the last spelling's keys, so a restriction
under the other spelling was dropped. Validation now refuses a statement
whose Condition is under more than one key, and one stored before that
never grants on an Allow and vetoes on a Deny.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: refuse a Condition key repeated under the same spelling

PolicyDocument re-marshaled each statement through interface{}, and
Statement.UnmarshalJSON counted keys from a map, so both kept only the
last of a repeated Condition key. "Condition":null,"Condition":{} or two
Condition objects sharing an operator passed validation with a restriction
dropped. Statements now decode from their original bytes and the keys are
counted while walking the object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3vectors: refuse a repeated Condition key in a vector bucket policy

The validator read Condition from a map, which keeps only the last of a
repeated key, so it accepted a statement the evaluator never grants on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: keep the re-marshaled statement decode, count Condition keys apart

Decoding statements from their original bytes let document order pick
between spellings of every field, where the re-marshal sorted the keys
first, so a stored "effect":"Deny","Effect":"Allow" became an Allow.
Statements decode as before again; only the Condition keys are counted
from the original bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3tables: test that validation refuses a split Condition on a Deny

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3vectors: test that validation refuses a repeated Condition on a Deny

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:13:05 -08:00

864 lines
25 KiB
Go

package policy_engine
import (
"fmt"
"net"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
)
// LRUNode represents a node in the doubly-linked list for efficient LRU operations
type LRUNode struct {
key string
value []string
prev *LRUNode
next *LRUNode
}
// NormalizedValueCache provides size-limited caching for normalized values with efficient LRU eviction
type NormalizedValueCache struct {
mu sync.RWMutex
cache map[string]*LRUNode
maxSize int
head *LRUNode // Most recently used
tail *LRUNode // Least recently used
}
// NewNormalizedValueCache creates a new normalized value cache with configurable size
func NewNormalizedValueCache(maxSize int) *NormalizedValueCache {
if maxSize <= 0 {
maxSize = 1000 // Default size
}
// Create dummy head and tail nodes for easier list manipulation
head := &LRUNode{}
tail := &LRUNode{}
head.next = tail
tail.prev = head
return &NormalizedValueCache{
cache: make(map[string]*LRUNode),
maxSize: maxSize,
head: head,
tail: tail,
}
}
// Get retrieves a cached value and updates access order in O(1) time
func (c *NormalizedValueCache) Get(key string) ([]string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if node, exists := c.cache[key]; exists {
// Move to head (most recently used) - O(1) operation
c.moveToHead(node)
return node.value, true
}
return nil, false
}
// Set stores a value in the cache with size limit enforcement in O(1) time
func (c *NormalizedValueCache) Set(key string, value []string) {
c.mu.Lock()
defer c.mu.Unlock()
if node, exists := c.cache[key]; exists {
// Update existing node and move to head
node.value = value
c.moveToHead(node)
return
}
// Create new node
newNode := &LRUNode{
key: key,
value: value,
}
// If at max size, evict least recently used
if len(c.cache) >= c.maxSize {
c.evictLeastRecentlyUsed()
}
// Add to cache and move to head
c.cache[key] = newNode
c.addToHead(newNode)
}
// moveToHead moves a node to the head of the list (most recently used) - O(1)
func (c *NormalizedValueCache) moveToHead(node *LRUNode) {
c.removeNode(node)
c.addToHead(node)
}
// addToHead adds a node right after the head - O(1)
func (c *NormalizedValueCache) addToHead(node *LRUNode) {
node.prev = c.head
node.next = c.head.next
c.head.next.prev = node
c.head.next = node
}
// removeNode removes a node from the list - O(1)
func (c *NormalizedValueCache) removeNode(node *LRUNode) {
node.prev.next = node.next
node.next.prev = node.prev
}
// removeTail removes the last node before tail (least recently used) - O(1)
func (c *NormalizedValueCache) removeTail() *LRUNode {
lastNode := c.tail.prev
c.removeNode(lastNode)
return lastNode
}
// evictLeastRecentlyUsed removes the least recently used item in O(1) time
func (c *NormalizedValueCache) evictLeastRecentlyUsed() {
tail := c.removeTail()
delete(c.cache, tail.key)
}
// Global cache instance with size limit
var normalizedValueCache = NewNormalizedValueCache(1000)
// getCachedNormalizedValues returns cached normalized values or caches new ones
func getCachedNormalizedValues(value interface{}) []string {
if value == nil {
return nil // no expected values; never reach reflect.TypeOf(nil)
}
// Create a string key for caching - more efficient than fmt.Sprintf
typeStr := reflect.TypeOf(value).String()
cacheKey := typeStr + ":" + fmt.Sprint(value)
// Try to get from cache
if cached, exists := normalizedValueCache.Get(cacheKey); exists {
return cached
}
// Not in cache, normalize and store
// Use the error-handling version for better error reporting
normalized, err := normalizeToStringSliceWithError(value)
if err != nil {
glog.Warningf("Failed to normalize policy value %v: %v", value, err)
// Fallback to string conversion for backward compatibility
normalized = []string{fmt.Sprintf("%v", value)}
}
normalizedValueCache.Set(cacheKey, normalized)
return normalized
}
// ConditionEvaluator evaluates policy conditions
type ConditionEvaluator interface {
Evaluate(conditionValue interface{}, contextValues []string) bool
}
// StringEqualsEvaluator evaluates StringEquals conditions
type StringEqualsEvaluator struct{}
func (e *StringEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
for _, contextValue := range contextValues {
if expected == contextValue {
return true
}
}
}
return false
}
// StringNotEqualsEvaluator evaluates StringNotEquals conditions
type StringNotEqualsEvaluator struct{}
func (e *StringNotEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
for _, contextValue := range contextValues {
if expected == contextValue {
return false
}
}
}
return true
}
// StringLikeEvaluator evaluates StringLike conditions (supports wildcards)
type StringLikeEvaluator struct{}
func (e *StringLikeEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
patterns := getCachedNormalizedValues(conditionValue)
for _, pattern := range patterns {
for _, contextValue := range contextValues {
if wildcard.MatchesWildcard(pattern, contextValue) {
return true
}
}
}
return false
}
// StringNotLikeEvaluator evaluates StringNotLike conditions
type StringNotLikeEvaluator struct{}
func (e *StringNotLikeEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
patterns := getCachedNormalizedValues(conditionValue)
for _, pattern := range patterns {
for _, contextValue := range contextValues {
if wildcard.MatchesWildcard(pattern, contextValue) {
return false
}
}
}
return true
}
// StringEqualsIgnoreCaseEvaluator evaluates StringEqualsIgnoreCase conditions
type StringEqualsIgnoreCaseEvaluator struct{}
func (e *StringEqualsIgnoreCaseEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
for _, contextValue := range contextValues {
if strings.EqualFold(expected, contextValue) {
return true
}
}
}
return false
}
// StringNotEqualsIgnoreCaseEvaluator evaluates StringNotEqualsIgnoreCase conditions
type StringNotEqualsIgnoreCaseEvaluator struct{}
func (e *StringNotEqualsIgnoreCaseEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
for _, contextValue := range contextValues {
if strings.EqualFold(expected, contextValue) {
return false
}
}
}
return true
}
// NumericEqualsEvaluator evaluates NumericEquals conditions
type NumericEqualsEvaluator struct{}
func (e *NumericEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedFloat, err := strconv.ParseFloat(expected, 64)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextFloat, err := strconv.ParseFloat(contextValue, 64)
if err != nil {
continue
}
if expectedFloat == contextFloat {
return true
}
}
}
return false
}
// NumericNotEqualsEvaluator evaluates NumericNotEquals conditions
type NumericNotEqualsEvaluator struct{}
func (e *NumericNotEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedFloat, err := strconv.ParseFloat(expected, 64)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextFloat, err := strconv.ParseFloat(contextValue, 64)
if err != nil {
continue
}
if expectedFloat == contextFloat {
return false
}
}
}
return true
}
// NumericLessThanEvaluator evaluates NumericLessThan conditions
type NumericLessThanEvaluator struct{}
func (e *NumericLessThanEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedFloat, err := strconv.ParseFloat(expected, 64)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextFloat, err := strconv.ParseFloat(contextValue, 64)
if err != nil {
continue
}
if contextFloat < expectedFloat {
return true
}
}
}
return false
}
// NumericLessThanEqualsEvaluator evaluates NumericLessThanEquals conditions
type NumericLessThanEqualsEvaluator struct{}
func (e *NumericLessThanEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedFloat, err := strconv.ParseFloat(expected, 64)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextFloat, err := strconv.ParseFloat(contextValue, 64)
if err != nil {
continue
}
if contextFloat <= expectedFloat {
return true
}
}
}
return false
}
// NumericGreaterThanEvaluator evaluates NumericGreaterThan conditions
type NumericGreaterThanEvaluator struct{}
func (e *NumericGreaterThanEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedFloat, err := strconv.ParseFloat(expected, 64)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextFloat, err := strconv.ParseFloat(contextValue, 64)
if err != nil {
continue
}
if contextFloat > expectedFloat {
return true
}
}
}
return false
}
// NumericGreaterThanEqualsEvaluator evaluates NumericGreaterThanEquals conditions
type NumericGreaterThanEqualsEvaluator struct{}
func (e *NumericGreaterThanEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedFloat, err := strconv.ParseFloat(expected, 64)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextFloat, err := strconv.ParseFloat(contextValue, 64)
if err != nil {
continue
}
if contextFloat >= expectedFloat {
return true
}
}
}
return false
}
// DateEqualsEvaluator evaluates DateEquals conditions
type DateEqualsEvaluator struct{}
func (e *DateEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedTime, err := time.Parse(time.RFC3339, expected)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextTime, err := time.Parse(time.RFC3339, contextValue)
if err != nil {
continue
}
if expectedTime.Equal(contextTime) {
return true
}
}
}
return false
}
// DateNotEqualsEvaluator evaluates DateNotEquals conditions
type DateNotEqualsEvaluator struct{}
func (e *DateNotEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedTime, err := time.Parse(time.RFC3339, expected)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextTime, err := time.Parse(time.RFC3339, contextValue)
if err != nil {
continue
}
if expectedTime.Equal(contextTime) {
return false
}
}
}
return true
}
// DateLessThanEvaluator evaluates DateLessThan conditions
type DateLessThanEvaluator struct{}
func (e *DateLessThanEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedTime, err := time.Parse(time.RFC3339, expected)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextTime, err := time.Parse(time.RFC3339, contextValue)
if err != nil {
continue
}
if contextTime.Before(expectedTime) {
return true
}
}
}
return false
}
// DateLessThanEqualsEvaluator evaluates DateLessThanEquals conditions
type DateLessThanEqualsEvaluator struct{}
func (e *DateLessThanEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedTime, err := time.Parse(time.RFC3339, expected)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextTime, err := time.Parse(time.RFC3339, contextValue)
if err != nil {
continue
}
if contextTime.Before(expectedTime) || contextTime.Equal(expectedTime) {
return true
}
}
}
return false
}
// DateGreaterThanEvaluator evaluates DateGreaterThan conditions
type DateGreaterThanEvaluator struct{}
func (e *DateGreaterThanEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedTime, err := time.Parse(time.RFC3339, expected)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextTime, err := time.Parse(time.RFC3339, contextValue)
if err != nil {
continue
}
if contextTime.After(expectedTime) {
return true
}
}
}
return false
}
// DateGreaterThanEqualsEvaluator evaluates DateGreaterThanEquals conditions
type DateGreaterThanEqualsEvaluator struct{}
func (e *DateGreaterThanEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedTime, err := time.Parse(time.RFC3339, expected)
if err != nil {
continue
}
for _, contextValue := range contextValues {
contextTime, err := time.Parse(time.RFC3339, contextValue)
if err != nil {
continue
}
if contextTime.After(expectedTime) || contextTime.Equal(expectedTime) {
return true
}
}
}
return false
}
// BoolEvaluator evaluates Bool conditions
type BoolEvaluator struct{}
func (e *BoolEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
for _, contextValue := range contextValues {
if strings.ToLower(expected) == strings.ToLower(contextValue) {
return true
}
}
}
return false
}
// IpAddressEvaluator evaluates IpAddress conditions
type IpAddressEvaluator struct{}
func (e *IpAddressEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
_, expectedNet, err := net.ParseCIDR(expected)
if err != nil {
// Try parsing as single IP
expectedIP := net.ParseIP(expected)
if expectedIP == nil {
glog.V(3).Infof("Failed to parse expected IP address: %s", expected)
continue
}
for _, contextValue := range contextValues {
contextIP := net.ParseIP(contextValue)
if contextIP == nil {
glog.V(3).Infof("Failed to parse IP address: %s", contextValue)
continue
}
if contextIP.Equal(expectedIP) {
return true
}
}
} else {
// CIDR network
for _, contextValue := range contextValues {
contextIP := net.ParseIP(contextValue)
if contextIP == nil {
glog.V(3).Infof("Failed to parse IP address: %s", contextValue)
continue
}
if expectedNet.Contains(contextIP) {
return true
}
}
}
}
return false
}
// NotIpAddressEvaluator evaluates NotIpAddress conditions
type NotIpAddressEvaluator struct{}
func (e *NotIpAddressEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
_, expectedNet, err := net.ParseCIDR(expected)
if err != nil {
// Try parsing as single IP
expectedIP := net.ParseIP(expected)
if expectedIP == nil {
glog.V(3).Infof("Failed to parse expected IP address: %s", expected)
continue
}
for _, contextValue := range contextValues {
contextIP := net.ParseIP(contextValue)
if contextIP == nil {
glog.V(3).Infof("Failed to parse IP address: %s", contextValue)
continue
}
if contextIP.Equal(expectedIP) {
return false
}
}
} else {
// CIDR network
for _, contextValue := range contextValues {
contextIP := net.ParseIP(contextValue)
if contextIP == nil {
glog.V(3).Infof("Failed to parse IP address: %s", contextValue)
continue
}
if expectedNet.Contains(contextIP) {
return false
}
}
}
}
return true
}
// ArnEqualsEvaluator evaluates ArnEquals conditions
type ArnEqualsEvaluator struct{}
func (e *ArnEqualsEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
for _, contextValue := range contextValues {
if expected == contextValue {
return true
}
}
}
return false
}
// ArnLikeEvaluator evaluates ArnLike conditions
type ArnLikeEvaluator struct{}
func (e *ArnLikeEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
patterns := getCachedNormalizedValues(conditionValue)
for _, pattern := range patterns {
for _, contextValue := range contextValues {
if wildcard.MatchesWildcard(pattern, contextValue) {
return true
}
}
}
return false
}
// NullEvaluator evaluates Null conditions
type NullEvaluator struct{}
func (e *NullEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
expectedValues := getCachedNormalizedValues(conditionValue)
for _, expected := range expectedValues {
expectedBool := strings.ToLower(expected) == "true"
contextExists := len(contextValues) > 0
if expectedBool && !contextExists {
return true // Key should be null and it is
}
if !expectedBool && contextExists {
return true // Key should not be null and it isn't
}
}
return false
}
// forAllValuesEvaluator matches when every request value satisfies the base
// operator on its own, and so also when the request carries no value.
type forAllValuesEvaluator struct {
base ConditionEvaluator
}
func (e *forAllValuesEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
for _, v := range contextValues {
if !e.base.Evaluate(conditionValue, []string{v}) {
return false
}
}
return true
}
// forAnyValueEvaluator matches when at least one request value satisfies the
// base operator on its own. Checking values one at a time keeps a negated
// base such as StringNotEquals from meaning "none of them".
type forAnyValueEvaluator struct {
base ConditionEvaluator
}
func (e *forAnyValueEvaluator) Evaluate(conditionValue interface{}, contextValues []string) bool {
for _, v := range contextValues {
if e.base.Evaluate(conditionValue, []string{v}) {
return true
}
}
return false
}
// GetConditionEvaluator returns the appropriate evaluator for a condition
// operator, including the ForAllValues: and ForAnyValue: set qualifiers.
func GetConditionEvaluator(operator string) (ConditionEvaluator, error) {
if base, ok := strings.CutPrefix(operator, "ForAllValues:"); ok {
evaluator, err := baseConditionEvaluator(base)
if err != nil {
return nil, err
}
return &forAllValuesEvaluator{base: evaluator}, nil
}
if base, ok := strings.CutPrefix(operator, "ForAnyValue:"); ok {
evaluator, err := baseConditionEvaluator(base)
if err != nil {
return nil, err
}
return &forAnyValueEvaluator{base: evaluator}, nil
}
return baseConditionEvaluator(operator)
}
func baseConditionEvaluator(operator string) (ConditionEvaluator, error) {
switch operator {
case "StringEquals":
return &StringEqualsEvaluator{}, nil
case "StringNotEquals":
return &StringNotEqualsEvaluator{}, nil
case "StringLike":
return &StringLikeEvaluator{}, nil
case "StringNotLike":
return &StringNotLikeEvaluator{}, nil
case "StringEqualsIgnoreCase":
return &StringEqualsIgnoreCaseEvaluator{}, nil
case "StringNotEqualsIgnoreCase":
return &StringNotEqualsIgnoreCaseEvaluator{}, nil
case "NumericEquals":
return &NumericEqualsEvaluator{}, nil
case "NumericNotEquals":
return &NumericNotEqualsEvaluator{}, nil
case "NumericLessThan":
return &NumericLessThanEvaluator{}, nil
case "NumericLessThanEquals":
return &NumericLessThanEqualsEvaluator{}, nil
case "NumericGreaterThan":
return &NumericGreaterThanEvaluator{}, nil
case "NumericGreaterThanEquals":
return &NumericGreaterThanEqualsEvaluator{}, nil
case "DateEquals":
return &DateEqualsEvaluator{}, nil
case "DateNotEquals":
return &DateNotEqualsEvaluator{}, nil
case "DateLessThan":
return &DateLessThanEvaluator{}, nil
case "DateLessThanEquals":
return &DateLessThanEqualsEvaluator{}, nil
case "DateGreaterThan":
return &DateGreaterThanEvaluator{}, nil
case "DateGreaterThanEquals":
return &DateGreaterThanEqualsEvaluator{}, nil
case "Bool":
return &BoolEvaluator{}, nil
case "IpAddress":
return &IpAddressEvaluator{}, nil
case "NotIpAddress":
return &NotIpAddressEvaluator{}, nil
case "ArnEquals":
return &ArnEqualsEvaluator{}, nil
case "ArnLike":
return &ArnLikeEvaluator{}, nil
case "Null":
return &NullEvaluator{}, nil
default:
return nil, fmt.Errorf("unsupported condition operator: %s", operator)
}
}
// ExistingObjectTagPrefix is the prefix for S3 policy condition keys
const ExistingObjectTagPrefix = "s3:ExistingObjectTag/"
// RequestObjectTagPrefix is the prefix for condition keys on the tags a
// request sets through the X-Amz-Tagging header.
const RequestObjectTagPrefix = "s3:RequestObjectTag/"
// RequestObjectTagKeys is the multi-valued condition key listing the keys of
// the tags a request sets through the X-Amz-Tagging header.
const RequestObjectTagKeys = "s3:RequestObjectTagKeys"
// getConditionContextValue resolves the value(s) for a condition key.
// For s3:ExistingObjectTag/<key> conditions, it looks up the tag in objectEntry.
// For other condition keys, it looks up the value in contextValues.
func getConditionContextValue(key string, contextValues map[string][]string, objectEntry map[string][]byte) []string {
if strings.HasPrefix(key, ExistingObjectTagPrefix) {
tagKey := key[len(ExistingObjectTagPrefix):]
if tagKey == "" {
return []string{} // Invalid: empty tag key
}
metadataKey := s3_constants.AmzObjectTaggingPrefix + tagKey
if objectEntry != nil {
if tagValue, exists := objectEntry[metadataKey]; exists {
return []string{string(tagValue)}
}
}
return []string{}
}
if vals, exists := contextValues[key]; exists {
return vals
}
return []string{}
}
// EvaluateConditions evaluates all conditions in a policy statement
// objectEntry is the object's metadata from entry.Extended (can be nil)
// claims are JWT claims for jwt:* policy variables (can be nil)
func EvaluateConditions(conditions PolicyConditions, contextValues map[string][]string, objectEntry map[string][]byte, claims map[string]interface{}) bool {
return evaluateConditions(conditions, contextValues, objectEntry, claims, nil)
}
// evaluateConditions supplements positive string grant conditions in explicit denies with the original complete list.
// Select values per operator so whitespace or escapes cannot change negative conditions on the same key.
func evaluateConditions(conditions PolicyConditions, contextValues map[string][]string, objectEntry map[string][]byte, claims map[string]interface{}, originalGrants map[string][]string) bool {
if len(conditions) == 0 {
return true // No conditions means always true
}
for operator, conditionMap := range conditions {
conditionEvaluator, err := GetConditionEvaluator(operator)
if err != nil {
glog.Warningf("Unsupported condition operator: %s", operator)
return false
}
for key, value := range conditionMap {
contextVals := getConditionContextValue(key, contextValues, objectEntry)
if original := originalGrants[key]; len(original) != 0 && isGrantConditionKey(key) {
// Extra values can only make ForAllValues fail, so it keeps the canonical list.
switch strings.TrimPrefix(operator, "ForAnyValue:") {
case "StringEquals", "StringEqualsIgnoreCase", "StringLike", "ArnEquals", "ArnLike":
contextVals = append(append([]string(nil), contextVals...), original...)
}
}
// Substitute variables in expected values
expectedValues := value.Strings()
substitutedValues := make([]string, len(expectedValues))
for i, v := range expectedValues {
substitutedValues[i] = SubstituteVariables(v, contextValues, claims)
}
// Pass substituted values (casted to interface{} to match signature if needed, or update evaluators to accept []string)
// The evaluators take interface{}, but getCachedNormalizedValues handles []string.
if !conditionEvaluator.Evaluate(substitutedValues, contextVals) {
return false // If any condition fails, the whole condition block fails
}
}
}
return true
}