policy

package
v0.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 23 Imported by: 0

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

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

func NormalizeAction(v any) ([]string, error)

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

func NormalizeResource(v any) ([]string, error)

NormalizeResource converts a resource value to a consistent []string format. This is useful for code that wants to iterate over resources uniformly.

func ValidateAction

func ValidateAction(v any) error

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

func ValidatePrincipal(v any) error

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

func ValidateResource(v any) error

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

type AdminKeyChecker interface {
	PrimaryRootAccessKey() string
	AltRootAccessKey() string
}

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 NewCache

func NewCache() *Cache

NewCache creates a new empty policy cache

func (*Cache) BucketPolicyCount

func (c *Cache) BucketPolicyCount() int

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

func (c *Cache) HasBucketPolicy(bucket string) bool

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

type ConditionMap = map[string]any

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

const (
	DecisionDeny         Decision = iota // Default - no explicit allow found
	DecisionAllow                        // Explicit allow found
	DecisionExplicitDeny                 // Explicit deny (highest precedence, always wins)
)

func (Decision) IsAllowed

func (d Decision) IsAllowed() bool

IsAllowed returns true if the decision permits the operation

func (Decision) String

func (d Decision) String() string

String returns a human-readable decision name

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

func New(resolver Resolver) *Engine

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

func (e *Engine) DeleteBucketPolicy(bucket string)

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) GetCache

func (e *Engine) GetCache() *Cache

GetCache returns the cache for testing/debugging purposes

func (*Engine) HasBucketPolicy

func (e *Engine) HasBucketPolicy(bucket string) bool

HasBucketPolicy checks if a bucket has a policy set

func (*Engine) LoadBucketPolicies

func (e *Engine) LoadBucketPolicies(ctx context.Context, policies map[string]*iam.PolicyDocument)

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

func (*Resource) ARN

func (r *Resource) ARN() string

ARN returns AWS ARN format for this resource

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL