Skip to content

Condition Matchers ​

Condition matchers add fine-grained control to policy and statement evaluation. While action matchers determine what request is being made, condition matchers determine when a rule should apply based on request properties, security context, rate limits, or custom expressions.

ConditionMatcher Interface ​

ConditionMatcher extends RequestMatcher:

kotlin
interface ConditionMatcher : RequestMatcher

AbstractConditionMatcher ​

AbstractConditionMatcher provides a negate option and template method pattern:

kotlin
abstract class AbstractConditionMatcher(
    final override val type: String,
    final override val configuration: Configuration
) : ConditionMatcher {
    private val negate: Boolean = configuration.get("negate")?.asBoolean() ?: false

    override fun match(request, securityContext): Boolean {
        val match = internalMatch(request, securityContext)
        return if (negate) !match else match
    }
}

Any condition can be inverted by setting "negate": true in its configuration.

PartExtractor Framework ​

Many condition matchers share a common pattern: extract a value from the request or security context, then compare it. The PartExtractor framework formalizes this:

kotlin
fun interface PartExtractor {
    fun extract(request: Request, securityContext: SecurityContext): String
}

Part Identifiers ​

DefaultPartExtractor supports these part identifiers:

Request Parts (prefix request.):

PartDescription
request.pathRequest URL path
request.methodHTTP method
request.appIdApplication ID
request.spaceIdSpace ID
request.deviceIdDevice ID
request.remoteIpClient IP address
request.originOrigin header URI
request.origin.hostOrigin host
request.refererReferer header URI
request.referer.hostReferer host
request.header.{key}Request header value
request.attributes.{key}Request attribute
request.path.var.{key}Path variable (set by PathActionMatcher)

Security Context Parts (prefix context.):

PartDescription
context.tenantIdCurrent tenant ID
context.principal.idPrincipal user ID
context.principal.attributes.{key}Principal attribute value

Condition Matcher Types ​

Path-Based Matchers ​

All path-based matchers extend PartConditionMatcher and operate on an extracted string value:

MatcherType KeyDescription
EqConditionMatchereqEquality check (supports template expressions and ignoreCase)
ContainsConditionMatchercontainsSubstring containment check
StartsWithConditionMatcherstartsWithPrefix check
EndsWithConditionMatcherendsWithSuffix check
InConditionMatcherinSet membership check
RegularConditionMatcherregularRegex pattern match
PathConditionMatcherpathSpring PathPattern match

Context-Based Matchers ​

These inspect the security context directly:

MatcherType KeyDescription
AuthenticatedConditionMatcherauthenticatedChecks securityContext.principal.authenticated
InRoleConditionMatcherinRoleChecks if principal has a specific role
InTenantConditionMatcherinTenantChecks tenant type (DEFAULT, USER, PLATFORM)

Rate Limiters ​

MatcherType KeyDescription
RateLimiterConditionMatcherrateLimiterGlobal rate limiting using Guava RateLimiter
GroupedRateLimiterConditionMatchergroupedRateLimiterPer-group rate limiting (e.g., per-user, per-IP)
RedisRateLimiterConditionMatcherredisRateLimiterCluster-wide rate limiting via Redis (sliding window)
RedisGroupedRateLimiterConditionMatcherredisGroupedRateLimiterCluster-wide per-group rate limiting via Redis

RateLimiterConditionMatcher creates a single rate limiter shared across all requests. GroupedRateLimiterConditionMatcher creates a LoadingCache of rate limiters keyed by the extracted part value, with automatic expiration after access.

The rateLimiter and groupedRateLimiter matchers are in-memory only: each instance counts independently, so N instances multiply the effective quota by N. For cluster deployments, use the Redis-backed redisRateLimiter / redisGroupedRateLimiter matchers instead (requires the cacheSupport Gradle feature and Redis). They implement the sliding-window (interpolated counters) algorithm atomically in a single Lua script, so the limit is enforced cluster-wide with O(1) memory per group:

json
{
  "redisRateLimiter": {
    "permitsPerSecond": 10
  }
}
KeyRequiredDefaultDescription
permitsPerSecondyes-Allowed requests per second
windowSecondsno1Sliding window size; larger windows tolerate bursts (quota per window = permitsPerSecond * windowSeconds)
strictFailurenofalsefalse fails open (requests pass) when Redis is unreachable; true fails closed (rejects with TooManyRequestsException)

Redis key prefix is configurable via cosec.limiter.key-prefix (default cosec:rate-limiter). Policies with identical limiter configurations share one cluster-wide quota; different configurations are isolated automatically.

Important: the key prefix must be unique per application. Two applications (or prod and staging) sharing one Redis with the same prefix and identical limiter configs will silently share a single quota. Change cosec.limiter.key-prefix per deployment.

When a rate limit is exceeded, TooManyRequestsException is thrown. CoSec's web filters (ReactiveSecurityFilter for WebFlux, AuthorizationFilter for WebMVC) catch it and convert the response to AuthorizeResult.TOO_MANY_REQUESTS.

Expression-Based Matchers ​

MatcherType KeyDescription
OgnlConditionMatcherognlOGNL expression evaluation
SpelConditionMatcherspelSpring Expression Language evaluation

Both provide access to request and context objects in their expression root, enabling arbitrarily complex conditions.

Boolean Combinators ​

BoolConditionMatcher combines multiple conditions with AND/OR logic:

kotlin
class BoolConditionMatcher(configuration: Configuration) {
    val and: List<ConditionMatcher>
    val or: List<ConditionMatcher>
}
  • AND: All conditions in the and list must match. If any fails, the result is false.
  • OR: At least one condition in the or list must match. If none match, the result is false.
  • When only and is present (no or), returns true after all AND conditions pass.

SPI: ConditionMatcherFactory ​

ConditionMatcherFactory is the SPI interface:

kotlin
interface ConditionMatcherFactory {
    val type: String
    fun create(configuration: Configuration): ConditionMatcher
}

Custom condition matchers are registered via META-INF/services/me.ahoo.cosec.policy.condition.ConditionMatcherFactory.

Architecture Diagrams ​

Condition Matcher Class Hierarchy ​

mermaid
classDiagram
    direction TB
    class ConditionMatcher {
        <<interface>>
        +match(request, context): Boolean
    }
    class AbstractConditionMatcher {
        <<abstract>>
        +type: String
        -negate: Boolean
        +match(request, context): Boolean
        #internalMatch(request, context): Boolean
    }
    class PartConditionMatcher {
        <<abstract>>
        -partExtractor: PartExtractor
        #internalMatch(request, context): Boolean
        #matchPart(partValue, context): Boolean
    }

    ConditionMatcher <|.. AbstractConditionMatcher
    ConditionMatcher <|.. RateLimiterConditionMatcher
    ConditionMatcher <|.. BoolConditionMatcher
    AbstractConditionMatcher <|-- PartConditionMatcher
    AbstractConditionMatcher <|-- AuthenticatedConditionMatcher
    AbstractConditionMatcher <|-- InRoleConditionMatcher
    AbstractConditionMatcher <|-- InTenantConditionMatcher
    AbstractConditionMatcher <|-- OgnlConditionMatcher
    AbstractConditionMatcher <|-- SpelConditionMatcher
    PartConditionMatcher <|-- EqConditionMatcher
    PartConditionMatcher <|-- ContainsConditionMatcher
    PartConditionMatcher <|-- StartsWithConditionMatcher
    PartConditionMatcher <|-- EndsWithConditionMatcher
    PartConditionMatcher <|-- InConditionMatcher
    PartConditionMatcher <|-- RegularConditionMatcher
    PartConditionMatcher <|-- PathConditionMatcher
    PartConditionMatcher <|-- GroupedRateLimiterConditionMatcher

Part-Based Condition Evaluation Flow ​

mermaid
flowchart TD
    A["PartConditionMatcher.match(request, context)"] --> B["negate check"]
    B --> C["partExtractor.extract(request, context)"]
    C --> D["Extract value from request or context"]
    D --> E["matchPart(partValue, context)"]
    E --> F{"Result"}
    F -->|"true"| G["return true (or false if negate)"]
    F -->|"false"| H["return false (or true if negate)"]

    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

Boolean Combinator Evaluation ​

mermaid
sequenceDiagram
    autonumber
    participant Caller
    participant Bool as BoolConditionMatcher
    participant AndC as "AND Conditions"
    participant OrC as "OR Conditions"

    Caller->>Bool: match(request, context)
    loop each AND condition
        Bool->>AndC: condition.match(request, context)
        alt AND fails
            AndC-->>Bool: false
            Bool-->>Caller: false
        end
    end
    Note over Bool: All AND conditions passed
    alt OR list is empty
        Bool-->>Caller: true
    else OR list has items
        loop each OR condition
            Bool->>OrC: condition.match(request, context)
            alt OR matches
                OrC-->>Bool: true
                Bool-->>Caller: true
            end
        end
        Note over Bool: No OR condition matched
        Bool-->>Caller: false
    end

Policy JSON Examples ​

Path-based condition ​

json
{
  "condition": {
    "eq": {
      "part": "request.path",
      "value": "/api/admin"
    }
  }
}

Rate limiter (per-user) ​

json
{
  "condition": {
    "groupedRateLimiter": {
      "part": "context.principal.id",
      "permitsPerSecond": 10.0,
      "expireAfterAccessSecond": 60
    }
  }
}

Boolean combination ​

json
{
  "condition": {
    "bool": {
      "and": [
        { "authenticated": {} },
        { "inRole": { "value": "admin" } }
      ],
      "or": [
        { "eq": { "part": "request.method", "value": "GET" } },
        { "startsWith": { "part": "request.path", "value": "/api/public" } }
      ]
    }
  }
}

References ​

Licensed under the Apache License, Version 2.0.