Documentation
¶
Overview ¶
Package policy provides the policy evaluation engine for S3 authorization. It evaluates bucket policies and IAM policies to determine if a request should be allowed or denied.
Example (BeforeAndAfter) ¶
Example showing the difference between interface{} usage before and after improvements
package main
import (
"fmt"
"github.com/DirIO-S3/dirio/internal/policy"
"github.com/DirIO-S3/dirio/sdk/iam"
)
func main() {
stmt := iam.Statement{
Action: []any{"s3:GetObject", "s3:PutObject"}, // From JSON unmarshal
}
// ❌ BEFORE: Manual type assertion (error-prone)
fmt.Println("=== Before (manual type assertions) ===")
var actionsBefore []string
switch v := stmt.Action.(type) {
case string:
actionsBefore = []string{v}
case []string:
actionsBefore = v
case []any:
actionsBefore = make([]string, len(v))
for i, item := range v {
if s, ok := item.(string); ok {
actionsBefore[i] = s
}
}
}
fmt.Printf("Actions: %v\n", actionsBefore)
// ✅ AFTER: Use normalization function (clean, tested)
fmt.Println("\n=== After (using NormalizeAction) ===")
actionsAfter, err := policy.NormalizeAction(stmt.Action)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Actions: %v\n", actionsAfter)
}
Output: === Before (manual type assertions) === Actions: [s3:GetObject s3:PutObject] === After (using NormalizeAction) === Actions: [s3:GetObject s3:PutObject]
Index ¶
- func AuthorizationMiddleware(config *AuthorizationConfig) func(http.Handler) http.Handler
- func NormalizeAction(v any) ([]string, error)
- func NormalizeResource(v any) ([]string, error)
- func ValidateAction(v any) error
- func ValidateCondition(conditions ConditionMap) error
- func ValidatePrincipal(v any) error
- func ValidateResource(v any) error
- type ActionMapper
- type AdminKeyChecker
- type AuthorizationConfig
- type Cache
- func (c *Cache) BucketPolicyCount() int
- func (c *Cache) GetAllBucketPolicies() map[string]*iam.PolicyDocument
- func (c *Cache) GetBucketPolicy(bucket string) *iam.PolicyDocument
- func (c *Cache) GetUserPolicies(username string) []*iam.PolicyDocument
- func (c *Cache) HasBucketPolicy(bucket string) bool
- func (c *Cache) LoadBucketPolicies(policies map[string]*iam.PolicyDocument)
- func (c *Cache) LoadUserPolicies(policies map[string][]*iam.PolicyDocument)
- func (c *Cache) SetBucketPolicy(bucket string, policy *iam.PolicyDocument)
- func (c *Cache) SetUserPolicies(username string, policies []*iam.PolicyDocument)
- type ConditionContext
- type ConditionMap
- type Decision
- type Engine
- func (e *Engine) DeleteBucketPolicy(bucket string)
- func (e *Engine) Evaluate(ctx context.Context, req *RequestContext) Decision
- func (e *Engine) GetActionMapper() *ActionMapper
- func (e *Engine) GetCache() *Cache
- func (e *Engine) HasBucketPolicy(bucket string) bool
- func (e *Engine) LoadBucketPolicies(ctx context.Context, policies map[string]*iam.PolicyDocument)
- func (e *Engine) UpdateBucketPolicy(bucket string, policy *iam.PolicyDocument)
- type MetadataResolver
- func (r *MetadataResolver) GetGroupPoliciesForUser(ctx context.Context, userUUID uuid.UUID) ([]string, error)
- func (r *MetadataResolver) GetPolicyDocument(ctx context.Context, name string) (*iam.PolicyDocument, error)
- func (r *MetadataResolver) GetUserPolicyNamesByUUID(ctx context.Context, userUUID uuid.UUID) ([]string, error)
- type Principal
- type RequestContext
- type Resolver
- type Resource
- type Value
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AuthorizationMiddleware ¶
func AuthorizationMiddleware(config *AuthorizationConfig) func(http.Handler) http.Handler
AuthorizationMiddleware creates middleware that enforces policy-based authorization.
This middleware: 1. Extracts the S3 action from route context (set by teapot-router) 2. Translates the action to required IAM permission(s) using ActionMapper 3. Builds a RequestContext with principal, action, and resource 4. Evaluates the request against bucket policies 5. Returns 403 AccessDenied if the request is denied
For multi-resource operations (CopyObject), it checks permissions on both the source and destination resources.
func NormalizeAction ¶
NormalizeAction converts an action value to a consistent []string format. This is useful for code that wants to iterate over actions uniformly.
Example ¶
Example showing how to normalize actions to a consistent []string format
package main
import (
"fmt"
"github.com/DirIO-S3/dirio/internal/policy"
)
func main() {
// Actions can come in different formats from JSON
examples := []any{
"s3:GetObject", // Single string
[]string{"s3:GetObject", "s3:Put*"}, // Array of strings
[]any{"s3:ListBucket"}, // Array from JSON unmarshal
}
for i, action := range examples {
normalized, err := policy.NormalizeAction(action)
if err != nil {
fmt.Printf("Error normalizing action %d: %v\n", i, err)
continue
}
fmt.Printf("Example %d: %v -> %v\n", i+1, action, normalized)
}
}
Output: Example 1: s3:GetObject -> [s3:GetObject] Example 2: [s3:GetObject s3:Put*] -> [s3:GetObject s3:Put*] Example 3: [s3:ListBucket] -> [s3:ListBucket]
func NormalizeResource ¶
NormalizeResource converts a resource value to a consistent []string format. This is useful for code that wants to iterate over resources uniformly.
func ValidateAction ¶
ValidateAction checks if an action value has a valid structure. Valid structures:
- string: single action (e.g., "s3:GetObject")
- []string: array of actions (e.g., ["s3:GetObject", "s3:PutObject"])
- []any: array from JSON (will be validated to contain only strings)
Example ¶
Example showing how to validate a policy document after unmarshaling
package main
import (
"encoding/json"
"fmt"
"github.com/DirIO-S3/dirio/internal/policy"
"github.com/DirIO-S3/dirio/sdk/iam"
)
func main() {
// Simulate unmarshaling a policy from JSON
var stmt iam.Statement
policyJSON := `{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}`
json.Unmarshal([]byte(policyJSON), &stmt)
// Validate the Action field
if err := policy.ValidateAction(stmt.Action); err != nil {
fmt.Printf("Invalid action: %v\n", err)
return
}
fmt.Println("Action is valid")
}
Output: Action is valid
func ValidateCondition ¶
func ValidateCondition(conditions ConditionMap) error
ValidateCondition checks if a condition map has a valid structure. Returns an error if the structure is invalid (helps catch malformed policies early).
func ValidatePrincipal ¶
ValidatePrincipal checks if a principal value has a valid structure. Valid structures:
- string: "*" (public access)
- map[string]any: {"AWS": "*"} or {"AWS": "arn:..."} or {"AWS": ["arn:...", ...]}
func ValidateResource ¶
ValidateResource checks if a resource value has a valid structure. Valid structures:
- string: single resource ARN or "*" (e.g., "arn:aws:s3:::bucket/*")
- []string: array of resource ARNs
- []any: array from JSON (will be validated to contain only strings)
Types ¶
type ActionMapper ¶
type ActionMapper struct {
// contains filtered or unexported fields
}
ActionMapper translates S3 API action names (from routes) to the actual IAM permissions required to perform those operations.
This is critical because S3 action names do NOT always match 1:1 with the IAM permissions required:
- HeadObject requires s3:GetObject (not s3:HeadObject)
- CopyObject requires BOTH s3:GetObject AND s3:PutObject
- All multipart operations (except Abort) require s3:PutObject
See docs/action-permission-mapping.md for complete specification.
func NewActionMapper ¶
func NewActionMapper() *ActionMapper
NewActionMapper creates a new action mapper with static mappings
func (*ActionMapper) GetRequiredPermissions ¶
func (m *ActionMapper) GetRequiredPermissions(action string) []string
GetRequiredPermissions returns the IAM permission(s) needed for an S3 action.
Examples:
- "s3:HeadObject" → ["s3:GetObject"]
- "s3:CopyObject" → ["s3:GetObject", "s3:PutObject"]
- "s3:GetObject" → ["s3:GetObject"] (1:1 mapping, falls through)
For multi-resource operations (CopyObject, UploadPartCopy), the returned permissions should be checked against different resources:
- permissions[0] → source resource
- permissions[1] → destination resource
func (*ActionMapper) IsMultiResourceAction ¶
func (m *ActionMapper) IsMultiResourceAction(action string) bool
IsMultiResourceAction returns true if the action requires checking permissions on multiple resources (e.g., CopyObject needs source and dest).
type AdminKeyChecker ¶
AdminKeyChecker provides the current admin access keys for authorization bypass decisions. auth.Authenticator implements this interface, allowing live credential reloads to propagate without restarting the server.
type AuthorizationConfig ¶
type AuthorizationConfig struct {
// Engine is the policy evaluation engine
Engine *Engine
// Metadata is the metadata manager for fetching ownership information
Metadata *metadata.Manager
// AdminKeys provides the current admin access keys for bypass checks.
// Using an interface (rather than captured strings) lets the authenticator
// rotate credentials at runtime and have them reflected immediately.
AdminKeys AdminKeyChecker
}
AuthorizationConfig holds configuration for the authorization middleware
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache holds all policies in memory for fast evaluation. It provides thread-safe access to bucket policies using sync.RWMutex.
Design principles: - Optimized for fast reads (common case) - Write operations are rare (policy changes) - Nothing in cache that's not on disk (defensive)
func (*Cache) BucketPolicyCount ¶
BucketPolicyCount returns the number of bucket policies in cache. Thread-safe for reads.
func (*Cache) GetAllBucketPolicies ¶
func (c *Cache) GetAllBucketPolicies() map[string]*iam.PolicyDocument
GetAllBucketPolicies returns a copy of all bucket policies. Used for debugging and testing. Thread-safe for reads.
func (*Cache) GetBucketPolicy ¶
func (c *Cache) GetBucketPolicy(bucket string) *iam.PolicyDocument
GetBucketPolicy retrieves the bucket policy for a specific bucket. Returns nil if no policy is set for the bucket. Thread-safe for concurrent reads.
func (*Cache) GetUserPolicies ¶
func (c *Cache) GetUserPolicies(username string) []*iam.PolicyDocument
GetUserPolicies retrieves all policies attached to a user. Returns nil if user has no policies attached. Thread-safe for concurrent reads.
func (*Cache) HasBucketPolicy ¶
HasBucketPolicy checks if a bucket has a policy set. Thread-safe for concurrent reads.
func (*Cache) LoadBucketPolicies ¶
func (c *Cache) LoadBucketPolicies(policies map[string]*iam.PolicyDocument)
LoadBucketPolicies replaces all bucket policies at once. Used at server startup to bulk-load policies from storage. Thread-safe for writes.
func (*Cache) LoadUserPolicies ¶
func (c *Cache) LoadUserPolicies(policies map[string][]*iam.PolicyDocument)
LoadUserPolicies replaces all user policies at once. Used at server startup to bulk-load policies from storage. Thread-safe for writes.
func (*Cache) SetBucketPolicy ¶
func (c *Cache) SetBucketPolicy(bucket string, policy *iam.PolicyDocument)
SetBucketPolicy updates or removes a bucket policy. Pass nil to remove the policy for a bucket. Thread-safe for writes.
func (*Cache) SetUserPolicies ¶
func (c *Cache) SetUserPolicies(username string, policies []*iam.PolicyDocument)
SetUserPolicies sets all policies for a user. Pass nil or empty slice to remove all policies for a user. Thread-safe for writes.
type ConditionContext ¶
type ConditionContext struct {
SourceIP string
UserAgent string
SecureTransport bool
CurrentTime time.Time
ContentLength int64 // Used by s3:content-length / s3:RequestObjectSize conditions
}
ConditionContext contains request metadata for condition evaluation. All HTTP-specific values (ContentLength, SourceIP, etc.) are extracted by the HTTP layer and stored here so the policy engine has no dependency on *http.Request.
type ConditionMap ¶
ConditionMap represents the Condition block in a policy statement. Structure: map[operator]map[key]value
Example:
{
"StringEquals": {"aws:username": "alice"},
"IpAddress": {"aws:SourceIp": ["192.168.1.0/24", "10.0.0.0/8"]}
}
Operators can be: StringEquals, NumericLessThan, DateGreaterThan, IpAddress, Bool, etc. Values can be: string, []string, []any, number, bool (depending on operator)
type Decision ¶
type Decision int
Decision is the result of policy evaluation
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the core policy evaluation engine. It maintains an in-memory cache of policies for fast evaluation and provides the main Evaluate() method for authorization decisions.
Design principles: - Pure in-memory evaluation (no direct FS access) - Thread-safe concurrent access via Cache - Service layer notifies Engine of policy changes
func New ¶
New creates a new policy engine with the given resolver. Pass a MetadataResolver for production use; pass nil to skip IAM policy evaluation (useful in tests that only exercise bucket policies or ownership).
func (*Engine) DeleteBucketPolicy ¶
DeleteBucketPolicy removes a bucket policy at runtime. Called by service layer when DeleteBucketPolicy is executed.
func (*Engine) Evaluate ¶
func (e *Engine) Evaluate(ctx context.Context, req *RequestContext) Decision
Evaluate evaluates a request against all applicable policies. This is the main entry point for authorization decisions.
Evaluation order (AWS-like model): 1. Admin bypass - authenticated admin can do everything 2. Explicit deny in bucket policy - immediately denies (irrevocable) 3. IAM Policy Evalutions 3.1. Allow in bucket policy - allows if found 3.2. IAM user policies (Phase 5) - not implemented yet 3.2. IAM group policies (Phase 5) - not implemented yet 4. Ownership check - resource owner has implicit access 5. Default deny - if no explicit allow found
The action in req.Action should be the MAPPED permission (from ActionMapper), not the route action. The authorization middleware handles this translation.
func (*Engine) GetActionMapper ¶
func (e *Engine) GetActionMapper() *ActionMapper
GetActionMapper returns the action mapper for use by authorization middleware
func (*Engine) HasBucketPolicy ¶
HasBucketPolicy checks if a bucket has a policy set
func (*Engine) LoadBucketPolicies ¶
LoadBucketPolicies loads all bucket policies at startup. Called by server during initialization.
func (*Engine) UpdateBucketPolicy ¶
func (e *Engine) UpdateBucketPolicy(bucket string, policy *iam.PolicyDocument)
UpdateBucketPolicy updates a single bucket policy at runtime. Called by service layer when PutBucketPolicy is executed.
type MetadataResolver ¶
type MetadataResolver struct {
// contains filtered or unexported fields
}
MetadataResolver implements Resolver using the metadata.Manager.
func NewMetadataResolver ¶
func NewMetadataResolver(manager *metadata.Manager) *MetadataResolver
NewMetadataResolver creates a new MetadataResolver backed by the given metadata.Manager.
func (*MetadataResolver) GetGroupPoliciesForUser ¶
func (r *MetadataResolver) GetGroupPoliciesForUser(ctx context.Context, userUUID uuid.UUID) ([]string, error)
GetGroupPoliciesForUser returns all policy names from every active group the user belongs to.
func (*MetadataResolver) GetPolicyDocument ¶
func (r *MetadataResolver) GetPolicyDocument(ctx context.Context, name string) (*iam.PolicyDocument, error)
GetPolicyDocument returns the policy document for the named policy.
func (*MetadataResolver) GetUserPolicyNamesByUUID ¶
func (r *MetadataResolver) GetUserPolicyNamesByUUID(ctx context.Context, userUUID uuid.UUID) ([]string, error)
GetUserPolicyNamesByUUID returns the attached policy names for the user with the given UUID.
type Principal ¶
type Principal struct {
User *metadata.User // nil for anonymous requests
IsAnonymous bool // true if no authentication provided
IsAdmin bool // true if root admin (bypass all policies)
// Service account fields (populated by authorization middleware when request is from a SA)
IsServiceAccount bool // true if this principal is a service account
ParentUserUUID *uuid.UUID // parent user UUID (nil if no parent or not a SA)
PolicyMode iam.PolicyMode // "inherit" or "override" (empty string treated as inherit)
EmbeddedPolicyJSON string // raw IAM policy JSON; evaluated directly in override mode
}
Principal represents the requester making the API call
type RequestContext ¶
type RequestContext struct {
Principal *Principal
Action string // The IAM permission to check (e.g., "s3:GetObject")
Resource *Resource // The resource being accessed
Conditions *ConditionContext
VarContext *variables.Context // Variable substitution context (Phase 3.3)
// Ownership information (Phase 3.3) - populated by middleware for ownership-based authorization
BucketOwnerUUID *uuid.UUID // Owner UUID of the bucket (nil if admin-only or unknown)
ObjectOwnerUUID *uuid.UUID // Owner UUID of the object (nil if admin-only, unknown, or bucket operation)
}
RequestContext contains all information needed for policy evaluation
type Resolver ¶
type Resolver interface {
// GetPolicyDocument returns the parsed policy document for the named policy.
GetPolicyDocument(ctx context.Context, name string) (*iam.PolicyDocument, error)
// GetUserPolicyNamesByUUID returns the list of policy names attached to the user with the given UUID.
GetUserPolicyNamesByUUID(ctx context.Context, userUUID uuid.UUID) ([]string, error)
// GetGroupPoliciesForUser returns the union of all policy names attached to every active group
// the user with the given UUID belongs to.
GetGroupPoliciesForUser(ctx context.Context, userUUID uuid.UUID) ([]string, error)
}
Resolver fetches IAM policy documents and user policy lists on demand. It is called by the Engine during Engine.Evaluate to implement IAM policy evaluation.
type Resource ¶
type Resource struct {
Bucket string // Bucket name
Key string // Object key (empty for bucket operations)
}
Resource represents the S3 resource being accessed
type Value ¶
type Value = any
Value represents a field value from an IAM policy statement that can be a string or array of strings. This type is used for Principal, Action, and Resource fields.
Valid underlying types from JSON unmarshaling:
- string: single value (e.g., "s3:GetObject" or "*")
- []string: array of values (e.g., ["s3:GetObject", "s3:PutObject"])
- []any: array from JSON (e.g., ["s3:GetObject", "s3:PutObject"])
- map[string]any: structured value (e.g., {"AWS": "*"} for Principal)
After JSON unmarshaling, use Validate* functions to ensure the value conforms to the expected schema for its field type.