cosec-custom-matcher · git:20260818.6d5c642 · 2026-08-18 · sha256 c7352fd07f319ab1

cosec-custom-matcher git:20260818.6d5c642A

Immutable. This exact content is served forever at /api/v1/blob/c7352fd07f319ab1.

---
name: cosec-custom-matcher
description: Extend CoSec with custom ActionMatcher or ConditionMatcher implementations and register their factories through ServiceLoader or Spring. Do not use for policies that built-in matchers already express.
---

# CoSec Custom Matchers

Add a matcher only after confirming that the built-ins cannot express the rule. Prefer composing `path`, `bool`, part matchers, or the local/Redis rate limiters over new code.

## Extension contract

- `ActionMatcher` and `ConditionMatcher` live in `cosec-api` and extend `RequestMatcher`.
- Their factory interfaces and providers live in `cosec-core` under `me.ahoo.cosec.policy.action` and `me.ahoo.cosec.policy.condition`.
- A factory's `type` is the object key used in policy JSON. It must not collide with another factory; later registration replaces the existing entry.
- Do not change the SPI interfaces to add a matcher. Implement them and register the factory.
- Keep matching synchronous and side-effect free unless the matcher intentionally enforces a stateful control such as rate limiting.

Before editing, inspect the target CoSec version's interfaces and the nearest built-in matcher. In a CoSec checkout, the authoritative files are:

- `cosec-api/src/main/kotlin/me/ahoo/cosec/api/principal/RequestMatcher.kt`
- `cosec-core/src/main/kotlin/me/ahoo/cosec/policy/action/`
- `cosec-core/src/main/kotlin/me/ahoo/cosec/policy/condition/`

## Minimal condition matcher

Use `AbstractConditionMatcher` when `negate` support is desirable. Parse and validate configuration once during construction, not on every request.

```kotlin
class PremiumUserConditionMatcher(configuration: Configuration) :
    AbstractConditionMatcher(PremiumUserConditionMatcherFactory.TYPE, configuration) {

    private val requiredTier = configuration.getRequired("tier").asString()

    override fun internalMatch(request: Request, securityContext: SecurityContext): Boolean {
        val tier = securityContext.principal.attributes["tier"]?.toString()
        return tier == requiredTier
    }
}

class PremiumUserConditionMatcherFactory : ConditionMatcherFactory {
    companion object {
        const val TYPE = "premiumUser"
    }

    override val type: String = TYPE

    override fun create(configuration: Configuration): ConditionMatcher =
        PremiumUserConditionMatcher(configuration)
}
```

Register it for non-Spring and Spring environments with:

```text
# src/main/resources/META-INF/services/me.ahoo.cosec.policy.condition.ConditionMatcherFactory
com.example.cosec.PremiumUserConditionMatcherFactory
```

```json
{
  "action": "/api/premium/**",
  "condition": {
    "premiumUser": { "tier": "gold" }
  }
}
```

For an action matcher, use the same pattern with `AbstractActionMatcher`, `ActionMatcherFactory`, and:

```text
src/main/resources/META-INF/services/me.ahoo.cosec.policy.action.ActionMatcherFactory
```

## Spring registration

When a factory needs Spring-managed dependencies, expose it as a bean instead of adding a service file:

```kotlin
@Bean
fun premiumUserConditionMatcherFactory(): ConditionMatcherFactory =
    PremiumUserConditionMatcherFactory()
```

`MatcherFactoryRegister` registers all action and condition factory beans at startup. Do not register the same type through both mechanisms unless replacement is intentional.

## Useful API surface

Read configuration through `get`/`getRequired` followed by `asString`, `asBoolean`, `asInt`, `asLong`, `asDouble`, `asList`, or `asMap`.

Request matching commonly uses `path`, `method`, `remoteIp`, `origin`, `referer`, `appId`, `spaceId`, `deviceId`, `requestId`, headers, queries, cookies, and request attributes. Context matching commonly uses principal ID/authentication/roles/policies/attributes, tenant ID, and context attributes. Inspect the target interfaces rather than assuming fields from another CoSec version.

## Existing type names

Do not replace these unless the user explicitly wants an override:

- Actions: `all`, `path`, `composite`
- Conditions: `all`, `authenticated`, `inRole`, `inTenant`, `eq`, `contains`, `startsWith`, `endsWith`, `in`, `regular`, `path`, `bool`, `spel`, `ognl`, `rateLimiter`, `groupedRateLimiter`
- With Redis cache support: `redisRateLimiter`, `redisGroupedRateLimiter`

## Verification

Add one focused test that creates the matcher through its factory and checks a match and non-match. If registration changed, also deserialize one policy using the custom type so a missing service entry or Spring bean fails the test. Run the narrow module test and the repository's static analysis command.