How the CoSec ActionMatcher Path Matcher Works: A Technical Deep Dive

The CoSec ActionMatcher path matcher uses PathActionMatcherFactory to instantiate either static PathActionMatcher objects for fixed patterns or ReplaceablePathActionMatcher for dynamic SpEL templates, evaluating requests against Spring's PathPattern and persisting extracted URI variables in the SecurityContext.

The CoSec ActionMatcher path matcher is the primary mechanism for URL-based authorization decisions in the ahoo-wang/cosec policy engine. Located in cosec-core/src/main/kotlin/me/ahoo/cosec/policy/action, this component determines whether incoming requests satisfy policy rules by matching their paths against configured patterns. Understanding its internal implementation reveals how the framework balances high-performance static matching with flexible, context-aware dynamic authorization.

Core Architecture and Factory Pattern

PathActionMatcherFactory

The entry point for creating matchers is PathActionMatcherFactory (lines 70-95 in PathActionMatcher.kt). This factory reads the configuration value associated with the constant PATTERN_KEY ("pattern") and inspects the string for SpEL template syntax (#{…}). Based on this inspection, it returns either a pre-compiled static matcher or a dynamic replaceable matcher.

Static vs. Dynamic Implementations

When the pattern contains no SpEL expressions, the factory returns a PathActionMatcher (lines 37-50). This class pre-parses the pattern using Spring's PathPatternParser during construction, storing the resulting PathPattern instance for reuse across all requests.

If the pattern includes SpEL templates such as /users/#{principal.id}/**, the factory creates a ReplaceablePathActionMatcher (lines 53-66). This implementation evaluates the template against the current SecurityContext at request time, generating a concrete pattern string before parsing and matching.

Internal Matching Flow and Implementation

The matching logic resides in the internalMatch() methods. For static patterns, the PathActionMatcher executes the following sequence:

override fun internalMatch(request: Request, securityContext: SecurityContext): Boolean {
    // 1️⃣ Turn the request path into a PathContainer using the same parser options.
    PathContainer.parsePath(request.path, patternParser.pathOptions)
        .let { pathContainer ->
            // 2️⃣ Try to match the container against the stored pattern.
            val pathMatchInfo = pathPattern.matchAndExtract(pathContainer) ?: return false
            // 3️⃣ If matched, expose any URI variables (e.g. {id}) to downstream policies.
            securityContext.setPathVariables(pathMatchInfo.uriVariables)
            return true
        }
}

For replaceable matchers, the flow adds a template resolution step before parsing:

val pathPattern = requireNotNull(expression.getValue(securityContext))
val pathContainer = PathContainer.parsePath(request.path)
patternParser.parse(pathPattern).let {
    val match = it.matchAndExtract(pathContainer) ?: return false
    securityContext.setPathVariables(match.uriVariables)
    return true
}

Both implementations store extracted URI variables in the SecurityContext under the PATH_VARIABLES attribute key, making them available to subsequent condition matchers in the policy chain.

Configuration Syntax and Practical Examples

Static Path Patterns

Define fixed URL patterns using Ant-style syntax in your policy JSON:

{
  "type": "path",
  "pattern": "/api/orders/**"
}

This configuration matches any request starting with /api/orders/. You can capture specific segments using {variable} syntax for later use.

Dynamic SpEL Templates

For context-dependent authorization, use SpEL expressions within the pattern:

{
  "type": "path",
  "pattern": "/users/#{principal.id}/profile"
}

At runtime, #{principal.id} evaluates against the current SecurityContext, substituting the authenticated user's ID. This ensures the pattern only matches the requesting user's own profile endpoint, enabling resource-level authorization.

Accessing Extracted Variables

When patterns contain variables like /orders/{orderId}, the matcher extracts values into the security context. Downstream conditions can reference these using the PATH_VARIABLES key:

{
  "type": "path",
  "pattern": "/orders/{orderId}"
},
{
  "condition": {
    "type": "equals",
    "left": "${PATH_VARIABLES.orderId}",
    "right": "${request.query.id}"
  }
}

Parser Configuration and Customization

The PathPatternParsers utility class (PathPatternParsers.kt) handles optional parser configuration. You can specify custom PathPatternParser options—such as case sensitivity or trailing slash handling—through the options key in your matcher configuration. By default, the implementation uses PathPatternParser.defaultInstance from Spring.

Key source files for reference:

Summary

  • PathActionMatcherFactory instantiates matchers based on the pattern configuration key, detecting SpEL templates to choose between static and dynamic implementations.
  • PathActionMatcher provides high-performance matching for fixed patterns by pre-parsing the PathPattern at construction time.
  • ReplaceablePathActionMatcher evaluates SpEL templates against the SecurityContext at request time, enabling dynamic path construction for user-specific resources.
  • URI variables captured from patterns like {id} are stored in the SecurityContext as PATH_VARIABLES, accessible to subsequent policy conditions.
  • The implementation leverages Spring's PathPatternParser with configurable options for case sensitivity and path normalization.

Frequently Asked Questions

What is the difference between PathActionMatcher and ReplaceablePathActionMatcher?

PathActionMatcher handles static patterns and parses the PathPattern once during initialization for optimal performance. ReplaceablePathActionMatcher handles patterns containing SpEL templates (#{expression}), evaluating the template against the current SecurityContext on every request to generate the actual pattern string before matching. Use the static variant for fixed API paths and the replaceable variant for user-specific or dynamic resource paths.

How do I access path variables in downstream condition matchers?

When a pattern like /orders/{orderId} matches, the PathActionMatcher automatically stores the extracted value in the SecurityContext under the PATH_VARIABLES key. You can reference these variables in subsequent conditions using the syntax ${PATH_VARIABLES.orderId} or access them programmatically via securityContext.getPathVariables() in custom components.

Can I customize the path parsing behavior for case sensitivity?

Yes. According to the PathPatternParsers implementation in PathPatternParsers.kt, you can provide custom parser options through the options configuration key in your matcher definition. These options are passed directly to Spring's PathPatternParser, allowing you to control case sensitivity, trailing slash handling, and other parsing behaviors beyond the default settings.

Where is the path matcher factory registered in the CoSec policy engine?

The PathActionMatcherFactory is registered through ActionMatcherFactoryProvider, which makes the path matcher type available to the policy engine's JsonActionMatcherSerializer. This registration occurs in ActionMatcherFactoryProvider.kt, ensuring the factory is discoverable when deserializing policy JSON configurations and associating the "type": "path" identifier with the correct implementation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →