Skip to content

Policy Evaluation

Policies are the core abstraction for defining authorization rules in CoSec. A policy contains a collection of statements, each with an action matcher and optional condition. The evaluation follows a strict order: condition check first, then deny-first across all matching statements.

Policy Structure

PolicyData

PolicyData is the concrete implementation of the Policy interface:

kotlin
class PolicyData(
    override val id: String,
    override val category: String,
    override val name: String,
    override val description: String,
    override val type: PolicyType,       // GLOBAL, SYSTEM, or CUSTOM
    override val tenantId: String,
    override val condition: ConditionMatcher = AllConditionMatcher.INSTANCE,
    override val statements: List<Statement> = listOf()
) : Policy

Key properties:

  • id: Unique policy identifier
  • type: GLOBAL, SYSTEM, or CUSTOM -- global policies, platform-managed system policies, or user-defined policies
  • tenantId: Tenant scope -- policies are tenant-scoped
  • condition: A top-level condition that must match before any statement is evaluated
  • statements: Ordered list of permission rules

StatementData

StatementData represents a single rule:

kotlin
data class StatementData(
    override val name: String = "",
    override val effect: Effect = Effect.ALLOW,
    override val action: ActionMatcher,
    override val condition: ConditionMatcher = AllConditionMatcher.INSTANCE
) : Statement
  • effect: ALLOW or DENY -- determines the outcome when this statement matches
  • action: Matches the request (e.g., path pattern). See Action Matchers
  • condition: Additional condition that must be met. Defaults to AllConditionMatcher (always matches)

Policy Verification Algorithm

Policy-Level Verification

When a policy is evaluated against a request, the process follows this sequence:

  1. Policy condition check: policy.condition.match(request, securityContext) -- if false, the entire policy is skipped
  2. Deny-first statement scan: All DENY statements are checked first; if any matches, the result is EXPLICIT_DENY
  3. Allow statement scan: ALLOW statements are checked next; if any matches, the result is ALLOW
  4. No match: Returns null (falls through to the next authorization source)

Statement-Level Verification

Each statement is verified in two steps:

  1. Action match: statement.action.match(request, securityContext) -- does the request match the action pattern?
  2. Condition match: statement.condition.match(request, securityContext) -- do additional conditions hold?

Both must return true for the statement to match.

Load-Time Validation

DefaultPolicyEvaluator

DefaultPolicyEvaluator validates policies at load time using a mock request and context:

kotlin
object DefaultPolicyEvaluator : PolicyEvaluator {
    override fun evaluate(policy: Policy) {
        val evaluateRequest = EvaluateRequest()
        val mockContext = SimpleSecurityContext(SimpleTenantPrincipal.ANONYMOUS)
        // Verify policy condition
        safeEvaluate { policy.condition.match(evaluateRequest, mockContext) }
        // Verify each statement
        policy.statements.forEach { statement ->
            safeEvaluate { statement.condition.match(evaluateRequest, mockContext) }
            statement.action.match(evaluateRequest, mockContext)
            safeEvaluate { statement.verify(evaluateRequest, mockContext) }
        }
        // Verify full policy
        safeEvaluate { policy.verify(evaluateRequest, mockContext) }
    }
}

The safeEvaluate wrapper catches TooManyRequestsException (from rate limiter conditions) and RegexTimeoutException (from regex conditions exceeding their time budget) and continues, since neither can be meaningfully evaluated with mock data.

EvaluateRequest

EvaluateRequest provides sensible defaults for validation:

kotlin
data class EvaluateRequest(
    override val path: String = "/policies/test",
    override val method: String = "POST",
    override val remoteIp: String = "127.0.0.1",
    override val origin: URI = URI.create("http://mockOrigin"),
    override val referer: URI = URI.create("http://mockReferer"),
    override val attributes: Map<String, String> = mapOf(),
    private val headers: Map<String, String> = mapOf(),
    private val queries: Map<String, String> = mapOf(),
    private val cookies: Map<String, String> = mapOf(),
) : Request

Architecture Diagrams

Policy Verification Flow

mermaid
flowchart TD
    A["Policy.verify(request, context)"] --> B{"policy.condition.match?"}
    B -->|"false"| C["Skip policy"]
    B -->|"true"| D["Phase 1: Scan DENY statements"]
    D --> E{"DENY statement.action.match?"}
    E -->|"false"| F["Next DENY statement"]
    E -->|"true"| G{"statement.condition.match?"}
    G -->|"false"| F
    G -->|"true"| H["return EXPLICIT_DENY"]
    F --> I{"More DENY statements?"}
    I -->|"yes"| E
    I -->|"no"| J["Phase 2: Scan ALLOW statements"]
    J --> K{"ALLOW statement.action.match?"}
    K -->|"false"| L["Next ALLOW statement"]
    K -->|"true"| M{"statement.condition.match?"}
    M -->|"false"| L
    M -->|"true"| N["return ALLOW"]
    L --> O{"More ALLOW statements?"}
    O -->|"yes"| K
    O -->|"no"| P["return null (no match)"]

    style A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style D fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style E fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style F fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style G fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style H fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style I fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style J fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style K fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style M fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style N fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style O fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style P fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Statement Verification Sequence

mermaid
sequenceDiagram
    autonumber
    participant Policy as Policy
    participant Stmt as Statement
    participant Action as ActionMatcher
    participant Cond as ConditionMatcher

    Policy->>Stmt: verify(request, context)
    Stmt->>Action: match(request, context)
    alt Action does not match
        Action-->>Stmt: false
        Stmt-->>Policy: IMPLICIT_DENY
    else Action matches
        Action-->>Stmt: true
        Stmt->>Cond: match(request, context)
        alt Condition does not match
            Cond-->>Stmt: false
            Stmt-->>Policy: IMPLICIT_DENY
        else Condition matches
            Cond-->>Stmt: true
            Stmt-->>Policy: effect (ALLOW or DENY)
        end
    end

Load-Time Validation Sequence

mermaid
sequenceDiagram
    autonumber
    participant Loader as PolicyLoader
    participant Evaluator as DefaultPolicyEvaluator
    participant Policy as Policy
    participant MockCtx as MockContext

    Loader->>Evaluator: evaluate(policy)
    Evaluator->>MockCtx: create EvaluateRequest + Anonymous context
    Evaluator->>Policy: condition.match(mockRequest, mockCtx)
    loop each statement
        Evaluator->>Policy: statement.condition.match(mockRequest, mockCtx)
        Evaluator->>Policy: statement.action.match(mockRequest, mockCtx)
        Evaluator->>Policy: statement.verify(mockRequest, mockCtx)
    end
    Evaluator->>Policy: policy.verify(mockRequest, mockCtx)
    Evaluator-->>Loader: validation passed / exception on error

PolicyVerifyContext

When a policy or statement matches during authorization, a PolicyVerifyContext is created and attached to the SecurityContext:

kotlin
data class PolicyVerifyContext(
    val source: PolicyVerifySource,
    val policy: Policy,
    val statementIndex: Int,
    val statement: Statement,
    override val result: VerifyResult
) : VerifyContext

This enables downstream code (audit logging, debugging, OpenTelemetry tracing) to know exactly which policy and statement produced the authorization decision. The source property records which policy group produced the match — PolicyVerifySource.GLOBAL for global policies or PolicyVerifySource.PRINCIPAL for principal-assigned policies.

Performance: Sequence-Based Evaluation

The evaluateDenyFirst function takes a Kotlin Sequence<T> and materializes it exactly once via items.partition { ... }, which builds two intermediate lists (deny items and allow items). This means:

  • Every upstream condition (e.g. a policy-level rate limiter) is evaluated exactly once per authorization, including conditions positioned after a matching early DENY
  • Within each pass, verification short-circuits on the first decisive result: the DENY pass stops at the first EXPLICIT_DENY, the ALLOW pass at the first ALLOW

References

Licensed under the Apache License, Version 2.0.