Skip to content

Custom Matchers (SPI) ​

CoSec's policy system is fully extensible through two SPI (Service Provider Interface) extension points: ActionMatcherFactory for defining how requests match actions, and ConditionMatcherFactory for defining additional conditions on policy statements. Custom matchers are discovered automatically via Java's ServiceLoader and Spring's ApplicationContext.

Extension Architecture ​

mermaid
graph TD
    subgraph "SPI Discovery"
        A["Java ServiceLoader<br>(META-INF/services)"]
        B["Spring ApplicationContext<br>(bean discovery)"]
    end
    subgraph "Provider Registry"
        C["ActionMatcherFactoryProvider<br>(ConcurrentHashMap)"]
        D["ConditionMatcherFactoryProvider<br>(ConcurrentHashMap)"]
    end
    subgraph "Runtime"
        E["Policy Deserialization"]
        E --> F["ActionMatcherFactory.create(config)"]
        E --> G["ConditionMatcherFactory.create(config)"]
    end
    A --> C
    B --> C
    B --> D
    A --> D
    C --> F
    D --> G

    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

ActionMatcherFactory ​

Factory interface for creating ActionMatcher instances. Each factory is identified by a unique type string that is used in policy JSON to reference the matcher.

kotlin
interface ActionMatcherFactory {
    val type: String
    fun create(configuration: Configuration): ActionMatcher
}

Built-in Action Matchers ​

Factory ClassTypeDescription
AllActionMatcherFactoryallMatches all actions unconditionally
PathActionMatcherFactorypathMatches by URL path pattern and HTTP method
CompositeActionMatcherFactorycompositeCombines multiple matchers with OR logic (any match wins)

ConditionMatcherFactory ​

Factory interface for creating ConditionMatcher instances. Condition matchers add additional constraints beyond action matching.

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

Built-in Condition Matchers ​

CategoryMatchersDescription
Path-basedEq, Contains, StartsWith, EndsWith, In, Regular, PathMatch request properties against values
Context-basedAuthenticated, InRole, InTenantMatch security context properties
Rate limitingRate limiter matchersEnforce request rate limits
ExpressionOGNL, SpELEvaluate custom expressions

Registration Flow ​

Step 1: Java ServiceLoader (META-INF/services) ​

For non-Spring contexts, factories are discovered via ServiceLoader at class loading time.

File: META-INF/services/me.ahoo.cosec.policy.action.ActionMatcherFactory

me.ahoo.cosec.policy.action.AllActionMatcherFactory
me.ahoo.cosec.policy.action.PathActionMatcherFactory
me.ahoo.cosec.policy.action.CompositeActionMatcherFactory

File: META-INF/services/me.ahoo.cosec.policy.condition.ConditionMatcherFactory

me.ahoo.cosec.policy.condition.AllConditionMatcherFactory
me.ahoo.cosec.policy.condition.context.AuthenticatedConditionMatcherFactory
me.ahoo.cosec.policy.condition.part.EqConditionMatcherFactory
me.ahoo.cosec.policy.condition.part.PathConditionMatcherFactory
...

Step 2: Provider Registry ​

The ActionMatcherFactoryProvider and ConditionMatcherFactoryProvider singletons maintain a ConcurrentHashMap of all registered factories.

mermaid
sequenceDiagram
    autonumber
    participant SL as ServiceLoader
    participant Provider as ActionMatcherFactoryProvider
    participant Map as ConcurrentHashMap

    Note over Provider: Static init block
    Provider->>SL: load(ActionMatcherFactory::class.java)
    SL-->>Provider: Iterator of factories
    loop For each factory
        Provider->>Map: put(factory.type, factory)
    end
    Note over Provider: Factories now available via get() / getRequired()

Step 3: Spring SmartLifecycle (MatcherFactoryRegister) ​

When running in a Spring context, MatcherFactoryRegister implements SmartLifecycle to register all Spring-managed factory beans with the provider singletons. This runs at startup and ensures that custom factories defined as @Bean are available for policy evaluation.

kotlin
class MatcherFactoryRegister(
    private val applicationContext: ApplicationContext
) : SmartLifecycle {
    override fun start() {
        applicationContext.getBeansOfType<ConditionMatcherFactory>().values.forEach {
            ConditionMatcherFactoryProvider.register(it)
        }
        applicationContext.getBeansOfType<ActionMatcherFactory>().values.forEach {
            ActionMatcherFactoryProvider.register(it)
        }
    }
}
mermaid
graph TD
    A["Spring ApplicationContext"] --> B["MatcherFactoryRegister<br>(SmartLifecycle.start())"]
    B --> C["Get all ActionMatcherFactory beans"]
    B --> D["Get all ConditionMatcherFactory beans"]
    C --> E["ActionMatcherFactoryProvider.register()"]
    D --> F["ConditionMatcherFactoryProvider.register()"]

    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

Creating a Custom Action Matcher ​

Step 1: Implement the Matcher ​

Custom matchers extend AbstractActionMatcher, which supplies the type and configuration properties required by RequestMatcher and implements the two-parameter match(request, securityContext); subclasses only implement internalMatch:

kotlin
class HttpMethodActionMatcher(
    private val method: String,
    configuration: Configuration
) : AbstractActionMatcher(HttpMethodActionMatcherFactory.TYPE, configuration) {
    override fun internalMatch(
        request: Request,
        securityContext: SecurityContext
    ): Boolean {
        return request.method.equals(method, ignoreCase = true)
    }
}

Step 2: Implement the Factory ​

kotlin
class HttpMethodActionMatcherFactory : ActionMatcherFactory {
    companion object {
        const val TYPE = "httpMethod"
    }

    override val type: String
        get() = TYPE

    override fun create(configuration: Configuration): ActionMatcher {
        val method = configuration.getRequired("method").asString()
        return HttpMethodActionMatcher(method, configuration)
    }
}

Step 3: Register via META-INF/services ​

File: META-INF/services/me.ahoo.cosec.policy.action.ActionMatcherFactory

com.example.HttpMethodActionMatcherFactory

Step 4: Use in Policy JSON ​

In the object form, the first property's key is the factory type and its value becomes the matcher's configuration:

json
{
  "effect": "ALLOW",
  "action": {
    "httpMethod": {
      "method": "GET"
    }
  }
}

Creating a Custom Condition Matcher ​

The same pattern applies for ConditionMatcherFactory. Implement the ConditionMatcher interface, create a factory, and register it.

References ​

Licensed under the Apache License, Version 2.0.