Files
seaweedfs/weed/s3api/policy_engine/engine.go
T
Chris Lu 02749c1192 s3api: configurable trusted-proxy allowlist for aws:SourceIp (#11302) (#11315)
* s3api: add TrustedProxies allowlist helper for aws:SourceIp extraction

Introduces a policy_engine.TrustedProxies type that parses a
comma-separated list of bare IPs and CIDRs (mirroring Guard.UpdateWhiteList)
and extracts the client IP for aws:SourceIp condition evaluation.

When the direct TCP peer is in the allowlist, X-Forwarded-For is walked
right-to-left skipping trusted hops (then X-Real-Ip); otherwise the direct
peer address is returned. This is the building block for restoring
configurable forwarded-header trust removed in b88156f (#11231), as
proposed in #11302.

* s3api: honor trusted-proxy allowlist in bucket/IAM policy engine

Make ExtractConditionValuesFromRequest a method on *PolicyEngine so it
can use the engine TrustedProxies when resolving aws:SourceIp. With no
allowlist configured the behavior is unchanged from b88156f: the direct
TCP peer is used and forwarded headers are ignored. When an allowlist is
configured via SetTrustedProxies, requests from a trusted peer honor
X-Forwarded-For (right-to-left) then X-Real-Ip.

Update the two call sites (auth_credentials.go, s3api_bucket_policy_engine.go)
and the engine tests to the method form, and add a regression test for the
trusted-proxy path.

* s3api: honor trusted-proxy allowlist in IAM role/session policies

Make extractRequestContext and extractSourceIP methods on
*S3IAMIntegration so they can use the integration TrustedProxies when
resolving aws:SourceIp. With no allowlist configured the behavior is
unchanged from b88156f: the direct TCP peer is used and forwarded
headers are ignored. When an allowlist is configured via
SetTrustedProxies, requests from a trusted peer honor X-Forwarded-For
(right-to-left) then X-Real-Ip.

Update the call site in isActionExplicitlyDeniedByIAM to type-assert
the integration and use the method, and add a regression test for the
trusted-proxy path.

* s3api: load [s3.trusted_proxies] from security.toml and wire to engines

Read s3.trusted_proxies.white_list (comma-separated IPs/CIDRs) from
security.toml and propagate the allowlist to the bucket policy engine,
the IAM policy engine (persisted across rebuilds via
IdentityAccessManagement.SetTrustedProxies), and the IAM integration.
Reloaded on SIGHUP alongside the JWT signing keys. Document the new
section in the scaffold security.toml.

Closes #11302.

* s3api: harden TrustedProxies parsing and X-Forwarded-For traversal

Canonicalize bare IP entries (via net.ParseIP + String) so non-canonical
IPv6 allowlist entries such as 2001:0db8::1 match peers rendered as
2001:db8::1, and log+skip unparseable bare entries instead of storing
them inertly.

When walking X-Forwarded-For right-to-left, stop at the first malformed
(non-empty, unparseable) entry instead of skipping it, and only fall
back to the leftmost valid IP when the chain was well-formed. This
prevents a malformed hop from masking a forged IP to its left.

Addresses review feedback on #11315.

* s3api: make TrustedProxies reload race-free via atomic.Pointer

Store the trusted-proxy allowlist behind sync/atomic.Pointer in
PolicyEngine and S3IAMIntegration so SIGHUP reloads (which swap the
allowlist) cannot race with concurrent request handlers reading it.
This mirrors the existing Guard guardState pattern. The
IdentityAccessManagement copy is already protected by iam.m.

Addresses review feedback on #11315.
2026-09-14 13:54:26 -07:00

669 lines
21 KiB
Go

package policy_engine
import (
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
// PolicyEvaluationResult represents the result of policy evaluation
type PolicyEvaluationResult int
const (
PolicyResultDeny PolicyEvaluationResult = iota
PolicyResultAllow
PolicyResultIndeterminate
)
// PolicyEvaluationContext manages policy evaluation for a bucket
type PolicyEvaluationContext struct {
bucketName string
policy *CompiledPolicy
cache *PolicyCache
}
// PolicyEngine is the main policy evaluation engine
type PolicyEngine struct {
contexts map[string]*PolicyEvaluationContext
mutex sync.RWMutex
trustedProxies atomic.Pointer[TrustedProxies]
}
// NewPolicyEngine creates a new policy evaluation engine
func NewPolicyEngine() *PolicyEngine {
return &PolicyEngine{
contexts: make(map[string]*PolicyEvaluationContext),
}
}
// SetTrustedProxies configures the allowlist used to decide whether
// forwarded headers are honored when extracting aws:SourceIp.
func (engine *PolicyEngine) SetTrustedProxies(tp *TrustedProxies) {
engine.trustedProxies.Store(tp)
}
// SetBucketPolicy sets the policy for a bucket
func (engine *PolicyEngine) SetBucketPolicy(bucketName string, policyJSON string) error {
policy, err := ParsePolicy(policyJSON)
if err != nil {
return fmt.Errorf("invalid policy: %w", err)
}
compiled, err := CompilePolicy(policy)
if err != nil {
return fmt.Errorf("failed to compile policy: %w", err)
}
engine.mutex.Lock()
defer engine.mutex.Unlock()
context := &PolicyEvaluationContext{
bucketName: bucketName,
policy: compiled,
cache: NewPolicyCache(),
}
engine.contexts[bucketName] = context
glog.V(4).Infof("SetBucketPolicy: Successfully cached policy for bucket=%s, statements=%d", bucketName, len(compiled.Statements))
return nil
}
// GetBucketPolicy gets the policy for a bucket
func (engine *PolicyEngine) GetBucketPolicy(bucketName string) (*PolicyDocument, error) {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
context, exists := engine.contexts[bucketName]
if !exists {
return nil, fmt.Errorf("no policy found for bucket %s", bucketName)
}
return context.policy.Document, nil
}
// DeleteBucketPolicy deletes the policy for a bucket
func (engine *PolicyEngine) DeleteBucketPolicy(bucketName string) error {
engine.mutex.Lock()
defer engine.mutex.Unlock()
delete(engine.contexts, bucketName)
glog.V(2).Infof("Deleted bucket policy for %s", bucketName)
return nil
}
// HasPolicyForBucket checks if a bucket has a policy configured
func (engine *PolicyEngine) HasPolicyForBucket(bucketName string) bool {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
_, exists := engine.contexts[bucketName]
return exists
}
// EvaluatePolicy evaluates a policy for the given arguments
func (engine *PolicyEngine) EvaluatePolicy(bucketName string, args *PolicyEvaluationArgs) PolicyEvaluationResult {
engine.mutex.RLock()
context, exists := engine.contexts[bucketName]
engine.mutex.RUnlock()
if !exists {
glog.V(4).Infof("EvaluatePolicy: No policy found for bucket=%s (PolicyResultIndeterminate)", bucketName)
return PolicyResultIndeterminate
}
glog.V(4).Infof("EvaluatePolicy: Found policy for bucket=%s, evaluating with action=%s resource=%s principal=%s",
bucketName, args.Action, args.Resource, args.Principal)
return engine.evaluateCompiledPolicy(context.policy, args)
}
// evaluateCompiledPolicy evaluates a compiled policy
func (engine *PolicyEngine) evaluateCompiledPolicy(policy *CompiledPolicy, args *PolicyEvaluationArgs) PolicyEvaluationResult {
// AWS Policy evaluation logic:
// 1. Check for explicit Deny - if found, return Deny
// 2. Check for explicit Allow - if found, return Allow
// 3. If no matching statements, return Indeterminate (fall through to IAM)
hasExplicitAllow := false
for _, stmt := range policy.Statements {
if engine.evaluateStatement(stmt, args) {
if stmt.Statement.Effect == PolicyEffectDeny {
return PolicyResultDeny // Explicit deny trumps everything
}
if stmt.Statement.Effect == PolicyEffectAllow {
hasExplicitAllow = true
}
}
}
if hasExplicitAllow {
return PolicyResultAllow
}
// No matching statements - return Indeterminate to fall through to IAM
// This allows IAM policies to grant access even when bucket policy doesn't mention the action
return PolicyResultIndeterminate
}
// matchesDynamicPatterns checks if a value matches any of the dynamic patterns after variable substitution
func (engine *PolicyEngine) matchesDynamicPatterns(patterns []string, value string, args *PolicyEvaluationArgs) bool {
for _, pattern := range patterns {
substituted := SubstituteVariables(pattern, args.Conditions, args.Claims)
if FastMatchesWildcard(substituted, value) {
return true
}
}
return false
}
// evaluateStatement evaluates a single policy statement
func (engine *PolicyEngine) evaluateStatement(stmt *CompiledStatement, args *PolicyEvaluationArgs) bool {
// Check if action matches
matchedAction := engine.matchesPatterns(stmt.ActionPatterns, args.Action)
if !matchedAction {
matchedAction = engine.matchesDynamicPatterns(stmt.DynamicActionPatterns, args.Action, args)
}
// Multipart upload actions (CreateMultipartUpload, UploadPart, CompleteMultipartUpload, etc.)
// are implicitly allowed by s3:PutObject, since multipart upload is an implementation
// detail of putting objects. Check if this is a multipart action and the statement
// grants s3:PutObject.
if !matchedAction && multipartActionSet[args.Action] {
matchedAction = engine.matchesPatterns(stmt.ActionPatterns, "s3:PutObject")
if !matchedAction {
matchedAction = engine.matchesDynamicPatterns(stmt.DynamicActionPatterns, "s3:PutObject", args)
}
}
if !matchedAction {
return false
}
// Check if resource matches
hasResource := len(stmt.ResourcePatterns) > 0 || len(stmt.DynamicResourcePatterns) > 0
hasNotResource := len(stmt.NotResourcePatterns) > 0 || len(stmt.DynamicNotResourcePatterns) > 0
if hasResource {
matchedResource := engine.matchesPatterns(stmt.ResourcePatterns, args.Resource)
if !matchedResource {
matchedResource = engine.matchesDynamicPatterns(stmt.DynamicResourcePatterns, args.Resource, args)
}
if !matchedResource {
return false
}
}
if hasNotResource {
matchedNotResource := false
for _, matcher := range stmt.NotResourceMatchers {
if matcher.Match(args.Resource) {
matchedNotResource = true
break
}
}
if !matchedNotResource {
matchedNotResource = engine.matchesDynamicPatterns(stmt.DynamicNotResourcePatterns, args.Resource, args)
}
if matchedNotResource {
return false
}
}
// Check if principal matches
if len(stmt.PrincipalPatterns) > 0 || len(stmt.DynamicPrincipalPatterns) > 0 {
matchedPrincipal := engine.matchesPatterns(stmt.PrincipalPatterns, args.Principal)
if !matchedPrincipal {
matchedPrincipal = engine.matchesDynamicPatterns(stmt.DynamicPrincipalPatterns, args.Principal, args)
}
if !matchedPrincipal {
return false
}
}
// Check NotPrincipal (statement applies to everyone EXCEPT these principals)
if len(stmt.NotPrincipalPatterns) > 0 || len(stmt.DynamicNotPrincipalPatterns) > 0 {
matchedNotPrincipal := engine.matchesPatterns(stmt.NotPrincipalPatterns, args.Principal)
if !matchedNotPrincipal {
matchedNotPrincipal = engine.matchesDynamicPatterns(stmt.DynamicNotPrincipalPatterns, args.Principal, args)
}
if matchedNotPrincipal {
return false
}
}
// Check conditions
if len(stmt.Statement.Condition) > 0 {
condCtx := args.Conditions
// Multipart continuation actions (UploadPart, UploadPartCopy) inherit SSE
// from CreateMultipartUpload and do not carry their own SSE header.
// Inject the real inherited algorithm so Null/StringEquals conditions
// evaluate against the value that was set at upload initiation.
if IsMultipartContinuationAction(args.Action) {
condCtx = injectSSEForMultipart(args.Conditions, args.InheritedSSEAlgorithm)
}
match := EvaluateConditions(stmt.Statement.Condition, condCtx, args.ObjectEntry, args.Claims)
if !match {
return false
}
}
return true
}
// matchesPatterns checks if a value matches any of the compiled patterns
func (engine *PolicyEngine) matchesPatterns(patterns []*regexp.Regexp, value string) bool {
for _, pattern := range patterns {
if pattern.MatchString(value) {
return true
}
}
return false
}
// SubstituteVariables replaces ${variable} in a pattern with values from context and claims
// Supports:
// - Standard context variables (aws:SourceIp, s3:prefix, etc.)
// - JWT claims (jwt:preferred_username, jwt:sub, jwt:*)
// - LDAP claims (ldap:username, ldap:dn, ldap:*)
func SubstituteVariables(pattern string, context map[string][]string, claims map[string]interface{}) string {
result := PolicyVariableRegex.ReplaceAllStringFunc(pattern, func(match string) string {
// match is like "${aws:username}"
// extract variable name "aws:username"
variable := match[2 : len(match)-1]
// Check standard context first
if values, ok := context[variable]; ok && len(values) > 0 {
return values[0]
}
// Check JWT claims for jwt:* variables
if strings.HasPrefix(variable, "jwt:") {
claimName := variable[4:] // Remove "jwt:" prefix
if claimValue, ok := claims[claimName]; ok {
switch v := claimValue.(type) {
case string:
return v
case float64:
// JWT numbers are often float64
if v == float64(int64(v)) {
return fmt.Sprintf("%d", int64(v))
}
return fmt.Sprintf("%g", v)
case bool:
return fmt.Sprintf("%t", v)
case int:
return fmt.Sprintf("%d", v)
case int32:
return fmt.Sprintf("%d", v)
case int64:
return fmt.Sprintf("%d", v)
default:
return fmt.Sprintf("%v", v)
}
}
}
// Check LDAP claims for ldap:* variables
// FALLBACK MECHANISM: Try both prefixed and unprefixed keys
// Some LDAP providers store claims with the "ldap:" prefix (e.g., "ldap:username")
// while others store them without the prefix (e.g., "username").
// We check the prefixed key first for consistency, then fall back to unprefixed.
if strings.HasPrefix(variable, "ldap:") {
claimName := variable[5:] // Remove "ldap:" prefix
// Try prefixed key first (e.g., "ldap:username"), then unprefixed
var claimValue interface{}
var ok bool
if claimValue, ok = claims[variable]; !ok {
claimValue, ok = claims[claimName]
}
if ok {
switch v := claimValue.(type) {
case string:
return v
case float64:
if v == float64(int64(v)) {
return fmt.Sprintf("%d", int64(v))
}
return fmt.Sprintf("%g", v)
case bool:
return fmt.Sprintf("%t", v)
case int:
return fmt.Sprintf("%d", v)
case int32:
return fmt.Sprintf("%d", v)
case int64:
return fmt.Sprintf("%d", v)
default:
return fmt.Sprintf("%v", v)
}
}
}
// Variable not found, leave as-is to avoid unexpected matching
return match
})
return result
}
// ExtractPrincipalVariables extracts policy variables from a principal ARN
func ExtractPrincipalVariables(principal string) map[string][]string {
vars := make(map[string][]string)
// Handle non-ARN principals (e.g., "*" or simple usernames)
if !strings.HasPrefix(principal, "arn:aws:") {
return vars
}
// Parse ARN: arn:aws:service::account:resource
parts := strings.Split(principal, ":")
if len(parts) < 6 {
return vars
}
account := parts[4] // account ID
resourcePart := parts[5] // user/username or assumed-role/role/session
// Set aws:PrincipalAccount if account is present
if account != "" {
vars["aws:PrincipalAccount"] = []string{account}
}
resourceParts := strings.Split(resourcePart, "/")
if len(resourceParts) < 2 {
return vars
}
resourceType := resourceParts[0] // "user", "role", "assumed-role"
// Set aws:principaltype and extract username/userid based on resource type
switch resourceType {
case "user":
vars["aws:principaltype"] = []string{"IAMUser"}
// For users with paths like "user/path/to/username", use the last segment
username := resourceParts[len(resourceParts)-1]
vars["aws:username"] = []string{username}
vars["aws:userid"] = []string{username} // In SeaweedFS, userid is same as username
case "role":
vars["aws:principaltype"] = []string{"IAMRole"}
// For roles with paths like "role/path/to/rolename", use the last segment
// Note: IAM Roles do NOT have aws:userid, but aws:PrincipalAccount is kept for condition evaluations
if len(resourceParts) >= 2 {
roleName := resourceParts[len(resourceParts)-1]
vars["aws:username"] = []string{roleName}
}
case "assumed-role":
vars["aws:principaltype"] = []string{"AssumedRole"}
// For assumed roles: assumed-role/RoleName/SessionName or assumed-role/path/to/RoleName/SessionName
// The session name is always the last segment
if len(resourceParts) >= 3 {
sessionName := resourceParts[len(resourceParts)-1]
vars["aws:username"] = []string{sessionName}
vars["aws:userid"] = []string{sessionName}
}
}
// Note: principaltype is already set correctly in the switch above based on resource type
return vars
}
// ExtractConditionValuesFromRequest extracts condition values from HTTP request
func (engine *PolicyEngine) ExtractConditionValuesFromRequest(r *http.Request) map[string][]string {
values := make(map[string][]string)
// AWS condition keys
values["aws:SourceIp"] = []string{engine.extractSourceIP(r)}
values["aws:SecureTransport"] = []string{fmt.Sprintf("%t", r.TLS != nil)}
// Use AWS standard condition key for current time
values["aws:CurrentTime"] = []string{time.Now().Format(time.RFC3339)}
// Keep RequestTime for backward compatibility
values["aws:RequestTime"] = []string{time.Now().Format(time.RFC3339)}
// S3 specific condition keys
if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
values["aws:UserAgent"] = []string{userAgent}
}
if referer := r.Header.Get("Referer"); referer != "" {
values["aws:Referer"] = []string{referer}
}
// Note: s3:ExistingObjectTag/<key> conditions are evaluated using objectEntry
// passed to EvaluatePolicy, not extracted from the request.
// S3 bucket-level conditions
if delimiter := r.URL.Query().Get("delimiter"); delimiter != "" {
values["s3:delimiter"] = []string{delimiter}
}
if prefix := r.URL.Query().Get("prefix"); prefix != "" {
values["s3:prefix"] = []string{prefix}
}
if maxKeys := r.URL.Query().Get("max-keys"); maxKeys != "" {
values["s3:max-keys"] = []string{maxKeys}
}
// Authentication method
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
if strings.HasPrefix(authHeader, "AWS4-HMAC-SHA256") {
values["s3:authType"] = []string{"REST-HEADER"}
} else if strings.HasPrefix(authHeader, "AWS ") {
values["s3:authType"] = []string{"REST-HEADER"}
}
} else if r.URL.Query().Get("AWSAccessKeyId") != "" {
values["s3:authType"] = []string{"REST-QUERY-STRING"}
}
// HTTP method
values["s3:RequestMethod"] = []string{r.Method}
// Extract custom headers with s3: prefix for AWS-compatible condition keys
for key, headerValues := range r.Header {
lowerKey := strings.ToLower(key)
if strings.HasPrefix(lowerKey, "x-amz-") {
values["s3:"+lowerKey] = headerValues
}
}
// Normalize s3:x-amz-server-side-encryption value to canonical form.
// AWS accepts "AES256" case-insensitively; normalise so that policy
// StringEquals conditions work regardless of client capitalisation.
const sseKey = "s3:x-amz-server-side-encryption"
if sseVals, ok := values[sseKey]; ok {
normalized := make([]string, len(sseVals))
for i, v := range sseVals {
switch strings.ToUpper(v) {
case "AES256":
normalized[i] = "AES256"
case "AWS:KMS":
normalized[i] = "aws:kms"
default:
normalized[i] = v
}
}
values[sseKey] = normalized
}
return values
}
// IsMultipartContinuationAction returns true for actions that do not carry
// their own SSE header because SSE is inherited from CreateMultipartUpload.
func IsMultipartContinuationAction(action string) bool {
return action == "s3:UploadPart" || action == "s3:UploadPartCopy"
}
// injectSSEForMultipart returns a condition context augmented with the
// inherited SSE algorithm for multipart continuation actions.
//
// UploadPart and UploadPartCopy do not re-send the SSE header because
// encryption is set once at CreateMultipartUpload. The caller supplies
// inheritedSSE (the canonical algorithm, e.g. "AES256" or "aws:kms") so
// that Null/StringEquals conditions on s3:x-amz-server-side-encryption
// evaluate against the real value.
//
// If inheritedSSE is empty (no SSE was requested at initiation), the
// conditions map is returned unchanged so Null("true") will correctly
// match and deny the request.
func injectSSEForMultipart(conditions map[string][]string, inheritedSSE string) map[string][]string {
const sseKey = "s3:x-amz-server-side-encryption"
if inheritedSSE == "" {
return conditions // no SSE at upload initiation; let Null("true") fire
}
if _, exists := conditions[sseKey]; exists {
return conditions // SSE header was actually sent on this request
}
modified := make(map[string][]string, len(conditions)+1)
for k, v := range conditions {
modified[k] = v
}
modified[sseKey] = []string{inheritedSSE}
return modified
}
// extractSourceIP returns the client IP for aws:SourceIp condition
// evaluation, honoring forwarded headers only when the direct TCP peer is in
// the configured trusted-proxy allowlist (see SetTrustedProxies).
func (engine *PolicyEngine) extractSourceIP(r *http.Request) string {
return engine.trustedProxies.Load().ExtractSourceIP(r)
}
// BuildResourceArn builds an ARN for the given bucket and object
func BuildResourceArn(bucketName, objectName string) string {
if objectName == "" {
return fmt.Sprintf("arn:aws:s3:::%s", bucketName)
}
return fmt.Sprintf("arn:aws:s3:::%s/%s", bucketName, objectName)
}
// BuildActionName builds a standardized action name
func BuildActionName(action string) string {
if strings.HasPrefix(action, "s3:") {
return action
}
return fmt.Sprintf("s3:%s", action)
}
// GetPolicyStatements returns all policy statements for a bucket
func (engine *PolicyEngine) GetPolicyStatements(bucketName string) []PolicyStatement {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
context, exists := engine.contexts[bucketName]
if !exists {
return nil
}
return context.policy.Document.Statement
}
// BucketsAllowedForAction returns the buckets a policy names in the Allow
// statements that can match the action, and whether those names cover every
// bucket the policy can allow it on. A statement reaching buckets it does not
// name -- a wildcard resource, a policy variable, a NotResource -- leaves the
// set incomplete, as does an unknown policy name.
func (engine *PolicyEngine) BucketsAllowedForAction(policyName string, action string) (buckets []string, complete bool) {
engine.mutex.RLock()
context, exists := engine.contexts[policyName]
engine.mutex.RUnlock()
if !exists {
return nil, false
}
for _, statement := range context.policy.Document.Statement {
// A Deny only narrows what an Allow named.
if statement.Effect != PolicyEffectAllow || !statementMayAllowAction(statement.Action.Strings(), action) {
continue
}
if statement.Resource == nil || statement.NotResource != nil {
return nil, false
}
resources := statement.Resource.Strings()
if len(resources) == 0 {
return nil, false
}
for _, resource := range resources {
if PolicyVariableRegex.MatchString(resource) {
return nil, false
}
bucket := GetBucketFromResource(resource)
if bucket == "" || strings.ContainsAny(bucket, "*?") {
return nil, false
}
buckets = append(buckets, bucket)
}
}
return buckets, true
}
// ValidatePolicyForBucket validates if a policy is valid for a bucket
func (engine *PolicyEngine) ValidatePolicyForBucket(bucketName string, policyJSON string) error {
policy, err := ParsePolicy(policyJSON)
if err != nil {
return err
}
// Additional validation specific to the bucket
for _, stmt := range policy.Statement {
resources := normalizeToStringSlice(stmt.Resource)
for _, resource := range resources {
if resourceBucket := GetBucketFromResource(resource); resourceBucket != "" {
if resourceBucket != bucketName {
return fmt.Errorf("policy resource %s does not match bucket %s", resource, bucketName)
}
}
}
}
return nil
}
// ClearAllPolicies clears all bucket policies
func (engine *PolicyEngine) ClearAllPolicies() {
engine.mutex.Lock()
defer engine.mutex.Unlock()
engine.contexts = make(map[string]*PolicyEvaluationContext)
glog.V(2).Info("Cleared all bucket policies")
}
// GetAllBucketsWithPolicies returns all buckets that have policies
func (engine *PolicyEngine) GetAllBucketsWithPolicies() []string {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
buckets := make([]string, 0, len(engine.contexts))
for bucketName := range engine.contexts {
buckets = append(buckets, bucketName)
}
return buckets
}
// EvaluatePolicyForRequest evaluates policy for an HTTP request
func (engine *PolicyEngine) EvaluatePolicyForRequest(bucketName, objectName, action, principal string, r *http.Request) PolicyEvaluationResult {
resource := BuildResourceArn(bucketName, objectName)
actionName := BuildActionName(action)
conditions := engine.ExtractConditionValuesFromRequest(r)
// Extract principal information for variables
principalVars := ExtractPrincipalVariables(principal)
for k, v := range principalVars {
conditions[k] = v
}
args := &PolicyEvaluationArgs{
Action: actionName,
Resource: resource,
Principal: principal,
Conditions: conditions,
}
return engine.EvaluatePolicy(bucketName, args)
}