cosec-custom-matcher · git:20260527.808710c · 2026-05-27 · sha256 c37e755960df67b8

cosec-custom-matcher git:20260527.808710cA

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

---
name: cosec-custom-matcher
description: "Use when extending CoSec policy matching with custom ActionMatcher or ConditionMatcher implementations, condition types, matcher factories, ServiceLoader SPI registration, or Spring bean matcher registration."
---

# CoSec Custom Matcher Development

This skill helps you create custom `ActionMatcher` and `ConditionMatcher` implementations to extend CoSec's policy evaluation logic. CoSec uses Java SPI (ServiceLoader) to discover matcher factories.

## Architecture Overview

Policy matching has two sides:
- **ActionMatcher** — determines if a request's action (path + method) matches a policy pattern
- **ConditionMatcher** — determines if contextual conditions are met (user attributes, request properties, etc.)

Both extend `RequestMatcher` and are created by corresponding factory classes registered via SPI.

```
Policy
├── condition: ConditionMatcher (policy-level gate)
└── statements[]
    ├── Statement (effect: DENY)
    │   ├── action: ActionMatcher
    │   └── condition: ConditionMatcher
    └── Statement (effect: ALLOW)
        ├── action: ActionMatcher
        └── condition: ConditionMatcher
```

## Creating a Custom ConditionMatcher

### Step 1: Implement the ConditionMatcher

```kotlin
package com.example.cosec.condition

import me.ahoo.cosec.api.context.SecurityContext
import me.ahoo.cosec.api.context.request.Request
import me.ahoo.cosec.api.policy.ConditionMatcher
import me.ahoo.cosec.api.configuration.Configuration

class PremiumUserConditionMatcher(
    override val configuration: Configuration
) : ConditionMatcher {

    override val type: String = "premiumUser"

    override fun match(request: Request, securityContext: SecurityContext): Boolean {
        val isPremium = securityContext.principal.attributes["premium"]
        return isPremium == "true"
    }
}
```

Key points:
- `type` — unique string identifier used in policy JSON
- `configuration` — arbitrary key-value config passed from the policy JSON
- `match()` — return `true` if the condition is satisfied

### Step 2: Implement the Factory

```kotlin
package com.example.cosec.condition

import me.ahoo.cosec.api.configuration.Configuration
import me.ahoo.cosec.api.policy.ConditionMatcher
import me.ahoo.cosec.policy.condition.ConditionMatcherFactory

class PremiumUserConditionMatcherFactory : ConditionMatcherFactory {

    override val type: String = "premiumUser"

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

### Step 3: Register via SPI

Create file: `src/main/resources/META-INF/services/me.ahoo.cosec.policy.condition.ConditionMatcherFactory`

```
com.example.cosec.condition.PremiumUserConditionMatcherFactory
```

### Step 4: Use in Policy JSON

```json
{
  "name": "PremiumEndpoints",
  "action": "/api/premium/**",
  "condition": {
    "premiumUser": {}
  }
}
```

With configuration:
```json
{
  "name": "TieredAccess",
  "action": "/api/**",
  "condition": {
    "premiumUser": {
      "minTier": "gold"
    }
  }
}
```

Access configuration in the matcher:
```kotlin
val minTier = configuration.getRequiredString("minTier")
```

## Creating a Custom ActionMatcher

### Step 1: Implement the ActionMatcher

```kotlin
package com.example.cosec.action

import me.ahoo.cosec.api.context.SecurityContext
import me.ahoo.cosec.api.context.request.Request
import me.ahoo.cosec.api.policy.ActionMatcher
import me.ahoo.cosec.api.configuration.Configuration

class HttpMethodActionMatcher(
    override val configuration: Configuration
) : ActionMatcher {

    override val type: String = "httpMethod"

    private val allowedMethods: Set<String> = configuration.getRequiredString("methods")
        .split(",")
        .map { it.trim().uppercase() }
        .toSet()

    override fun match(request: Request, securityContext: SecurityContext): Boolean {
        return request.method.uppercase() in allowedMethods
    }
}
```

### Step 2: Implement the Factory

```kotlin
package com.example.cosec.action

import me.ahoo.cosec.api.configuration.Configuration
import me.ahoo.cosec.api.policy.ActionMatcher
import me.ahoo.cosec.policy.action.ActionMatcherFactory

class HttpMethodActionMatcherFactory : ActionMatcherFactory {

    override val type: String = "httpMethod"

    override fun create(configuration: Configuration): ActionMatcher {
        return HttpMethodActionMatcher(configuration)
    }
}
```

### Step 3: Register via SPI

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

```
com.example.cosec.action.HttpMethodActionMatcherFactory
```

### Step 4: Use in Policy JSON

```json
{
  "name": "ReadOnlyAccess",
  "action": {
    "httpMethod": {
      "methods": "GET,HEAD,OPTIONS"
    }
  }
}
```

## Accessing Configuration Values

The `Configuration` interface provides typed accessors:

```kotlin
// Required (throws if missing)
val value: String = configuration.getRequiredString("key")

// Optional with default
val value: String = configuration.get("key", "default")

// Nested configuration
val nested: Configuration = configuration.getRequiredConfiguration("nested")
```

## Accessing Request and Context Data

### Request properties
```kotlin
request.path           // URL path
request.method         // HTTP method
request.remoteIp       // client IP
request.origin         // Origin header
request.referer        // Referer header
request.appId          // application ID
request.spaceId        // space ID
request.deviceId       // device ID
request.requestId      // request ID
request.getHeader("X-Custom")    // any header
request.getQuery("param")        // query parameter
request.getCookieValue("name")   // cookie value
```

### SecurityContext properties
```kotlin
securityContext.principal                    // CoSecPrincipal
securityContext.principal.id                 // user ID
securityContext.principal.authenticated      // boolean
securityContext.principal.anonymous          // boolean
securityContext.principal.roles              // Set<String>
securityContext.principal.policies           // Set<String>
securityContext.principal.attributes         // Map<String, String>
securityContext.tenant                       // Tenant info
securityContext.attributes                   // MutableMap<String, Any>
```

## Built-in ConditionMatcher Types Reference

For reference, here are all built-in types:

| Type | Description | Key Config |
|------|-------------|------------|
| `authenticated` | User must be logged in | — |
| `inRole` | User must have role | `value`: role name |
| `inTenant` | Must be from tenant | `value`: tenant ID |
| `eq` | Exact match | `part`, `value` |
| `contains` | Substring match | `part`, `value` |
| `startsWith` | Prefix match | `part`, `value` |
| `endsWith` | Suffix match | `part`, `value` |
| `in` | Value in list | `part`, `value`: array |
| `regular` | Regex match | `part`, `pattern`, `negate` |
| `path` | Path pattern match | `part`, `pattern`, `options` |
| `bool` | Boolean logic | `and`: array, `or`: array |
| `spel` | Spring Expression | `expression` |
| `ognl` | OGNL expression | `expression` |
| `rateLimiter` | Rate limiting | `permitsPerSecond` |
| `groupedRateLimiter` | Grouped rate limit | `permitsPerSecond`, `groupKey` |

## Built-in ActionMatcher Types Reference

| Type | Description | Key Config |
|------|-------------|------------|
| `path` | URL path matching | `pattern`, `method`, `options` |
| `all` | Wildcard | `method` (optional) |
| `composite` | OR combination | array of matchers |

## Spring Registration (Alternative to SPI)

You can also register matcher factories as Spring beans. The `MatcherFactoryRegister` auto-configuration picks them up from the `ApplicationContext`:

```kotlin
@Configuration
class CustomMatcherConfig {

    @Bean
    fun premiumUserConditionMatcherFactory(): ConditionMatcherFactory {
        return PremiumUserConditionMatcherFactory()
    }

    @Bean
    fun httpMethodActionMatcherFactory(): ActionMatcherFactory {
        return HttpMethodActionMatcherFactory()
    }
}
```

This approach is simpler when your matcher needs Spring dependencies (e.g., a database or external service).