Skip to content

Contributor Guide

Welcome to CoSec -- an RBAC-based and Policy-based Multi-Tenant Reactive Security Framework for the JVM. This guide is designed to get you from zero to productive contributor as quickly as possible, whether you are coming from Python, JavaScript, or another JVM language.


Part I: Foundations

Kotlin for Python and JavaScript Engineers

Kotlin is a statically typed language that runs on the JVM. If you have written Python or JavaScript, many Kotlin concepts will feel familiar, but the type system and null safety require some adjustment. This section gives you a side-by-side reference so you can read CoSec code fluently.

Variables and Types

ConceptPythonJavaScriptKotlin
Immutable variablex = 1 (convention: uppercase)const x = 1val x = 1
Mutable variablex = 1let x = 1var x = 1
Type annotationx: int = 1x: number = 1 (TS)val x: Int = 1
String templatef"Hello {name}"`Hello ${name}`"Hello $name"
Null typeOptional[str]string | null (TS)String?
Non-null assertionx! (TS)x! (TS)x!!
Safe callN/Ax?.method() (TS)x?.method()
Elvis (default)x or defaultx ?? defaultx ?: default

Functions and Lambdas

ConceptPythonJavaScriptKotlin
Functiondef add(a, b):function add(a, b) {fun add(a: Int, b: Int): Int
Lambdalambda x: x + 1(x) => x + 1{ x -> x + 1 }
Single-expressiondef double(x): return x * 2const double = (x) => x * 2fun double(x: Int) = x * 2
Default parameterdef f(x=1):function f(x=1) {fun f(x: Int = 1)
Named argsf(name="alice")f({ name: "alice" })f(name = "alice")

Classes and Interfaces

ConceptPythonJavaScriptKotlin
Classclass Foo:class Foo { }class Foo { }
Interface(abstract base class)(not standard)interface Foo { }
Data class@dataclass(manual)data class Foo(val x: Int)
Implement interfaceclass Bar(Foo):class Bar extends Fooclass Bar : Foo
EnumEnum moduleenum (TS)enum class Foo { A, B }
Companion object@classmethodstaticcompanion object { }
Extension function(monkey-patch)prototype.methodfun String.first() = this[0]

Collections

ConceptPythonJavaScriptKotlin
List[1, 2, 3][1, 2, 3]listOf(1, 2, 3)
Mutable list[][]mutableListOf<Int>()
Map / dict{"a": 1}{ a: 1 }mapOf("a" to 1)
Set{1, 2}new Set([1, 2])setOf(1, 2)
Filter[x for x in l if x > 0]l.filter(x => x > 0)l.filter { it > 0 }
Map (transform)[f(x) for x in l]l.map(x => f(x))l.map { f(it) }
Flat map[y for x in l for y in f(x)]l.flatMap(x => f(x))l.flatMap { f(it) }
Fold / reducefunctools.reduce(op, l, init)l.reduce((a, b) => a + b)l.fold(0) { acc, x -> acc + x }
it keywordN/AN/AImplicit single lambda param

Nullable Types and Smart Casts

Kotlin enforces null safety at the compiler level. A String cannot hold null; only String? can.

kotlin
val name: String = "Alice"   // cannot be null
val maybe: String? = null    // can be null

// Smart cast after null check
if (maybe != null) {
    println(maybe.length)    // compiler knows maybe is non-null here
}

You will see ?. (safe call), ?: (elvis), and !! (non-null assertion) throughout CoSec. The requireNotNull() function is also common -- it throws IllegalArgumentException if the value is null (PermissionVerifier.kt:69).

Key Kotlin Idioms in CoSec

  1. fun interface (SAM interfaces): CoSec uses fun interface to define single-abstract-method interfaces that can be instantiated with lambdas. For example, PermissionVerifier is a fun interface (PermissionVerifier.kt:29).

  2. companion object: Kotlin's replacement for static members. Used extensively for constants and factory methods (e.g., CoSecPrincipal.ROOT_ID at CoSecPrincipal.kt:80).

  3. Extension functions: Functions added to existing types without modifying them. For example, CoSecPrincipal.isRoot is an extension property (CoSecPrincipal.kt:94).

  4. Type aliases: Shorthand for complex types. typealias PolicyId = String at Policy.kt:22 and typealias RoleId = String at RoleCapable.kt:19.

  5. Delegation (by): Kotlin supports class and property delegation. The Delegated.kt file in cosec-core provides delegation utilities (Delegated.kt).

  6. Sealed classes and enums: Used for fixed sets of values. Effect is an enum with ALLOW and DENY values (Effect.kt:28).

Spring Boot and Project Reactor from First Principles

CoSec is built on Spring Boot 4 and Project Reactor. Understanding the reactive paradigm is essential for contributing.

What is Reactive Programming?

Reactive programming is an asynchronous, event-driven paradigm where data flows through streams. Instead of blocking a thread while waiting for a result, you declare what should happen when data arrives.

In Python terms, think of generators (yield) plus asyncio. In JavaScript terms, think of Promises and Observable (RxJS).

Mono and Flux

Project Reactor provides two core types:

  • Mono<T>: A stream that emits at most one item, then completes. Equivalent to a Promise<T> in JavaScript or Future<T> in Python.
  • Flux<T>: A stream that emits zero or more items, then completes. Equivalent to an Observable<T> in RxJS.

CoSec uses Mono extensively because authentication and authorization typically produce a single result:

kotlin
// Authentication returns Mono<CoSecPrincipal> (one result or empty)
fun authenticate(credentials: Credentials): Mono<out CoSecPrincipal>
// Source: cosec-api/.../Authentication.kt:44

// Authorization returns Mono<AuthorizeResult> (one result)
fun authorize(request: Request, context: SecurityContext): Mono<AuthorizeResult>
// Source: cosec-api/.../Authorization.kt:43

The Reactive Pipeline

Reactive streams form pipelines using operators. Here are the most common operators you will see in CoSec:

OperatorPython equivalentJavaScript equivalentPurpose
mapmap(fn, iterable)promise.then(fn)Transform the value
flatMapasync for x in gen: yield xpromise.then(asyncFn)Async transform
filterfilter(fn, iterable)N/A (use if in .then)Keep matching items
switchIfEmptyif not result: fallback()promise.catch(fallback)Provide alternative when empty
onErrorResumetry/exceptpromise.catch(handler)Handle errors reactively
toMono()Future(result)Promise.resolve(result)Wrap value in Mono

A real example from SimpleAuthorization.authorize() at SimpleAuthorization.kt:213:

kotlin
override fun authorize(request: Request, context: SecurityContext): Mono<AuthorizeResult> {
    val verifyResult = verifyRoot(context)
    if (verifyResult == VerifyResult.ALLOW) {
        return AuthorizeResult.ALLOW.toMono()        // wrap value in Mono
    }
    return blacklistChecker
        .check(request, context)                      // Mono<Boolean>
        .flatMap { allowed ->                         // async transform
            if (!allowed) {
                return@flatMap AuthorizeResult.EXPLICIT_DENY.toMono()
            }
            verify(request, context)                  // chain to next step
        }
}

Testing Reactive Code with StepVerifier

Reactor provides StepVerifier to test reactive streams declaratively. You will see this pattern in every test:

kotlin
authorization.authorize(request, securityContext)
    .test()                                        // convert Mono to StepVerifier
    .expectNext(AuthorizeResult.ALLOW)             // assert the emitted value
    .verifyComplete()                              // assert stream completes
// Source: cosec-core/.../SimpleAuthorizationTest.kt:52-54

Spring Boot Auto-Configuration

Spring Boot auto-configuration is a convention-over-configuration mechanism. Classes annotated with @AutoConfiguration register beans (Spring-managed objects) automatically when certain conditions are met.

In CoSec, CoSecAutoConfiguration is the entry point (CoSecAutoConfiguration.kt:37):

kotlin
@ConditionalOnCoSecEnabled                    // only active when cosec.enabled=true
@AutoConfiguration(before = [JacksonAutoConfiguration::class])
@EnableConfigurationProperties(CoSecProperties::class)
class CoSecAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    fun coSecModule(): CoSecModule = CoSecModule()
    // ...
}

Conditional annotations control when features are activated:

  • @ConditionalOnCoSecEnabled -- checks cosec.enabled property (ConditionalOnCoSecEnabled.kt:23)
  • @ConditionalOnJwtEnabled -- activates JWT support
  • @ConditionalOnAuthorizationEnabled -- activates authorization
  • @ConditionalOnGatewayEnabled -- activates Spring Cloud Gateway integration

The MatcherFactoryRegister bean (MatcherFactoryRegister.kt:24) is a SmartLifecycle that discovers all ActionMatcherFactory and ConditionMatcherFactory beans from the Spring context and registers them with the SPI providers.


Part II: Architecture and Domain Model

Big Picture

CoSec is a multi-module Gradle project. Every public API is defined as an interface in cosec-api; all implementations live in cosec-core or integration modules. This separation means you can understand the contract by reading cosec-api alone.

mermaid
graph TB
    subgraph "Integration Layer"
        GW["cosec-gateway<br>Spring Cloud Gateway<br>GlobalFilter"]
        WF["cosec-webflux<br>Reactive WebFilter"]
        WM["cosec-webmvc<br>Servlet Filter"]
        ST["cosec-spring-boot-starter<br>Auto-Configuration"]
    end

    subgraph "Core Layer"
        CORE["cosec-core<br>Policy Evaluation<br>Authentication<br>Authorization"]
    end

    subgraph "API Layer"
        API["cosec-api<br>Interfaces Only<br>No Dependencies"]
    end

    subgraph "Extensions"
        JWT["cosec-jwt"]
        SOCIAL["cosec-social"]
        CACHE["cosec-cocache"]
        IP["cosec-ip2region"]
        OTEL["cosec-opentelemetry"]
        OPENAPI["cosec-openapi"]
    end

    GW --> CORE
    WF --> CORE
    WM --> CORE
    ST --> GW
    ST --> WF
    ST --> WM
    CORE --> API
    JWT --> CORE
    SOCIAL --> CORE
    CACHE --> CORE
    IP --> CORE
    OTEL --> CORE
    OPENAPI --> CORE

    style API fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CORE fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ST fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style GW fill:#2d333b,stroke:#30363d,color:#e6edf3
    style WF fill:#2d333b,stroke:#30363d,color:#e6edf3
    style WM fill:#2d333b,stroke:#30363d,color:#e6edf3
    style JWT fill:#161b22,stroke:#30363d,color:#e6edf3
    style SOCIAL fill:#161b22,stroke:#30363d,color:#e6edf3
    style CACHE fill:#161b22,stroke:#30363d,color:#e6edf3
    style IP fill:#161b22,stroke:#30363d,color:#e6edf3
    style OTEL fill:#161b22,stroke:#30363d,color:#e6edf3
    style OPENAPI fill:#161b22,stroke:#30363d,color:#e6edf3

Module Dependency Graph

The module dependency order is defined in settings.gradle.kts (settings.gradle.kts) and the build logic in build.gradle.kts (build.gradle.kts):

ModuleDepends OnPublishedPurpose
cosec-apiNothingYesCore interfaces, zero framework deps
cosec-corecosec-apiYesPolicy evaluation, authN/authZ implementations
cosec-jwtcosec-coreYesJWT token handling
cosec-socialcosec-coreYesOAuth social login via JustAuth
cosec-cocachecosec-coreYesRedis caching for policies/permissions
cosec-ip2regioncosec-coreYesIP geolocation enrichment
cosec-opentelemetrycosec-coreYesOpenTelemetry tracing
cosec-openapicosec-coreYesSwagger/OpenAPI integration
cosec-webfluxcosec-coreYesReactive WebFilter integration
cosec-webmvccosec-coreYesServlet filter integration
cosec-gatewaycosec-webfluxYesSpring Cloud Gateway GlobalFilter
cosec-spring-boot-starterall aboveYesAuto-configuration aggregator
cosec-gateway-servercosec-spring-boot-starterNoStandalone gateway application
cosec-dependenciesNothingYes (BOM)Version management
cosec-bomNothingYes (BOM)Bill of Materials

The cosec-gateway-server module is the only non-publishable module, as configured in build.gradle.kts at build.gradle.kts:33:

kotlin
val serverProjects = setOf(
    project(":cosec-gateway-server"),
)

Domain Model

The CoSec domain model is inspired by AWS IAM. Understanding the relationships between the core types is essential.

mermaid
classDiagram
    class CoSecPrincipal {
        +String id
        +Map attributes
        +boolean anonymous
        +boolean authenticated
        +isRoot: boolean
        +ROOT_ID: String$
        +ANONYMOUS_ID: String$
    }

    class RoleCapable {
        +Set~RoleId~ roles
    }

    class PolicyCapable {
        +Set~String~ policies
    }

    class TenantPrincipal {
        +String tenantId
    }

    class SecurityContext {
        +CoSecPrincipal principal
        +String tenantId
        +MutableMap attributes
        +getAttributeValue(key) V
        +setAttributeValue(key, value) SecurityContext
    }

    class Request {
        +String path
        +String method
        +String remoteIp
        +String appId
        +String spaceId
        +String deviceId
        +String requestId
        +getHeader(key) String
        +getQuery(key) String
    }

    class Policy {
        +String id
        +String name
        +String category
        +String description
        +PolicyType type
        +String tenantId
        +ConditionMatcher condition
        +List~Statement~ statements
        +verify(Request, SecurityContext) VerifyResult
    }

    class Statement {
        +String name
        +Effect effect
        +ActionMatcher action
        +ConditionMatcher condition
        +verify(Request, SecurityContext) VerifyResult
    }

    class Effect {
        <<enumeration>>
        ALLOW
        DENY
    }

    class ActionMatcher {
        <<interface>>
        +match(Request, SecurityContext) boolean
    }

    class ConditionMatcher {
        <<interface>>
        +match(Request, SecurityContext) boolean
    }

    class Authentication {
        <<interface>>
        +authenticate(Credentials) Mono~CoSecPrincipal~
    }

    class Authorization {
        <<interface>>
        +authorize(Request, SecurityContext) Mono~AuthorizeResult~
    }

    class AuthorizeResult {
        +boolean authorized
        +String reason
        +ALLOW: AuthorizeResult$
        +EXPLICIT_DENY: AuthorizeResult$
        +IMPLICIT_DENY: AuthorizeResult$
    }

    CoSecPrincipal --|> RoleCapable : implements
    CoSecPrincipal --|> PolicyCapable : implements
    TenantPrincipal --|> CoSecPrincipal : extends
    TenantPrincipal --|> TenantCapable : implements
    SecurityContext o-- CoSecPrincipal : holds
    Policy o-- Statement : contains many
    Statement --> Effect : has
    Statement --> ActionMatcher : has
    Statement --> ConditionMatcher : has
    Policy --> ConditionMatcher : has
    Authorization ..> Request : evaluates
    Authorization ..> SecurityContext : uses
    Authorization ..> AuthorizeResult : returns

    style CoSecPrincipal fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Policy fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Statement fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style SecurityContext fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Authorization fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Authentication fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Request fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ActionMatcher fill:#161b22,stroke:#30363d,color:#e6edf3
    style ConditionMatcher fill:#161b22,stroke:#30363d,color:#e6edf3
    style Effect fill:#161b22,stroke:#30363d,color:#e6edf3
    style AuthorizeResult fill:#161b22,stroke:#30363d,color:#e6edf3
    style RoleCapable fill:#161b22,stroke:#30363d,color:#e6edf3
    style PolicyCapable fill:#161b22,stroke:#30363d,color:#e6edf3
    style TenantPrincipal fill:#161b22,stroke:#30363d,color:#e6edf3

Core Interfaces in Detail

CoSecPrincipal (CoSecPrincipal.kt:35) -- The central identity type. Extends java.security.Principal, PolicyCapable, and RoleCapable. Key properties:

  • id -- unique identifier (line 42)
  • anonymous -- true when id == ANONYMOUS_ID ("(0)") (line 61)
  • authenticated -- inverse of anonymous (line 68)
  • ROOT_ID -- defaults to "cosec", configurable via system property cosec.root (line 80)
  • ANONYMOUS_ID -- "(0)" (line 87)
  • isRoot -- extension property checking ROOT_ID == id (line 94)

TenantPrincipal (TenantPrincipal.kt:26) -- Extends CoSecPrincipal with TenantCapable, adding tenantId. Policies are tenant-scoped.

Policy (Policy.kt:45) -- A named collection of Statements with an optional ConditionMatcher. The verify() method at line 76 implements the deny-first evaluation algorithm:

  1. Check policy-level condition. If it fails, return IMPLICIT_DENY.
  2. Evaluate all DENY statements. If any matches, return EXPLICIT_DENY.
  3. Evaluate all ALLOW statements. If any matches, return ALLOW.
  4. Return IMPLICIT_DENY.

Statement (Statement.kt:37) -- A single permission rule with Effect, ActionMatcher, and ConditionMatcher. Its verify() method at line 60 checks the action matcher first, then the condition matcher, then maps the effect to a VerifyResult.

Authentication (Authentication.kt:32) -- Generic interface parameterized by credential type C and principal type P. Returns Mono<out P>.

Authorization (Authorization.kt:35) -- A fun interface (SAM) that takes a Request and SecurityContext and returns Mono<AuthorizeResult>.

Request (Request.kt:36) -- Represents an incoming HTTP request with path, method, remoteIp, appId, spaceId, deviceId, and requestId.

SecurityContext (SecurityContext.kt:34) -- Holds the CoSecPrincipal, tenant info, and mutable attributes for passing data between components during authorization.

Tenant Types

CoSec supports three tenant types, defined in the Tenant interface (Tenant.kt:22):

Tenant TypeID ConstantDescription
Platform"(platform)"Root platform tenant, manages all other tenants
Default"(0)"Default tenant for non-multi-tenant scenarios
Userany other IDCustomer-specific tenant

Authorization Data Flow

The following diagram shows how a request flows through the CoSec authorization pipeline:

mermaid
flowchart TD
    REQ["Incoming Request<br>GET /api/users/123"] --> FILTER["Integration Filter<br>(WebFilter / GlobalFilter / Servlet)"]
    FILTER --> PARSE["Parse Request<br>RequestParser"]
    PARSE --> CTX["Parse Security Context<br>SecurityContextParser<br>(JWT / Token)"]
    CTX --> AUTHZ["Authorization.authorize()"]
    AUTHZ --> ROOT{"Is Root<br>User?"}
    ROOT -- "Yes" --> ALLOW["ALLOW"]
    ROOT -- "No" --> BL{"Blacklist<br>Check"}
    BL -- "Blocked" --> DENY["EXPLICIT_DENY"]
    BL -- "Passed" --> GP["Global Policies<br>(DENY first, then ALLOW)"]
    GP -- "Match found" --> RESULT["Return result"]
    GP -- "No match" --> PP["Principal Policies<br>(DENY first, then ALLOW)"]
    PP -- "Match found" --> RESULT
    PP -- "No match" --> RP["Role-based Permissions<br>(DENY first, then ALLOW)"]
    RP -- "Match found" --> RESULT
    RP -- "No match" --> IMPLICIT["IMPLICIT_DENY"]
    RESULT --> FILTER
    ALLOW --> FILTER
    DENY --> FILTER
    IMPLICIT --> FILTER
    FILTER --> RESP{"Authorized?"}
    RESP -- "Yes" --> PASS["Pass to next filter<br>in chain"]
    RESP -- "No (anonymous)" --> UNAUTH["HTTP 401<br>Unauthorized"]
    RESP -- "No (authenticated)" --> FORBID["HTTP 403<br>Forbidden"]

    style REQ fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style FILTER fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style AUTHZ fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ALLOW fill:#161b22,stroke:#30363d,color:#e6edf3
    style DENY fill:#161b22,stroke:#30363d,color:#e6edf3
    style IMPLICIT fill:#161b22,stroke:#30363d,color:#e6edf3
    style PASS fill:#161b22,stroke:#30363d,color:#e6edf3
    style UNAUTH fill:#161b22,stroke:#30363d,color:#e6edf3
    style FORBID fill:#161b22,stroke:#30363d,color:#e6edf3
    style GP fill:#2d333b,stroke:#30363d,color:#e6edf3
    style PP fill:#2d333b,stroke:#30363d,color:#e6edf3
    style RP fill:#2d333b,stroke:#30363d,color:#e6edf3

Authorization Flow Detail (SimpleAuthorization)

SimpleAuthorization at SimpleAuthorization.kt:48 is the core authorization implementation. The authorize() method at line 213 follows this exact order:

  1. Root user check (line 217) -- If context.principal.isRoot, return ALLOW immediately. Root bypasses everything.

  2. Blacklist check (line 221) -- If the BlacklistChecker (BlacklistChecker.kt:29) blocks the request, return EXPLICIT_DENY. Default is NoOp which passes everything.

  3. Verify pipeline (line 194) -- A reactive chain using switchIfEmpty:

    • verifyGlobalPolicies (line 156) -- Evaluate global policies from PolicyRepository.getGlobalPolicy().
    • verifyPrincipalPolicies (line 166) -- Evaluate policies assigned to the principal.
    • verifyAppRolePermission (line 180) -- Evaluate role-based permissions.
    • If none matches, return IMPLICIT_DENY.

Each policy verification uses the evaluateDenyFirst helper (line 61) which always processes DENY statements before ALLOW statements.

Integration Layer: How Requests Enter CoSec

CoSec integrates with three Spring request handling models. All three follow the same pattern: parse the request, extract the security context, run authorization, and either pass the request along or reject it.

mermaid
flowchart LR
    subgraph "Request Sources"
        A1["HTTP Request"]
    end

    subgraph "Spring Cloud Gateway"
        B1["AuthorizationGatewayFilter<br>implements GlobalFilter"]
    end

    subgraph "Spring WebFlux"
        B2["ReactiveAuthorizationFilter<br>implements WebFilter"]
    end

    subgraph "Spring WebMVC"
        B3["AuthorizationFilter<br>(Servlet)"]
    end

    subgraph "Shared Logic"
        C1["ReactiveSecurityFilter<br>filterInternal()"]
        C2["SecurityContextParser"]
        C3["Authorization.authorize()"]
    end

    A1 --> B1
    A1 --> B2
    A1 --> B3
    B1 --> C1
    B2 --> C1
    B3 --> C3
    C1 --> C2
    C2 --> C3

    style B1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C1 fill:#161b22,stroke:#30363d,color:#e6edf3
    style C2 fill:#161b22,stroke:#30363d,color:#e6edf3
    style C3 fill:#161b22,stroke:#30363d,color:#e6edf3

WebFlux (cosec-webflux): ReactiveAuthorizationFilter at ReactiveAuthorizationFilter.kt:36 implements WebFilter and Ordered. It extends ReactiveSecurityFilter which contains the shared filtering logic at ReactiveSecurityFilter.kt:57. The filter order is 1000, set at line 49.

Gateway (cosec-gateway): AuthorizationGatewayFilter at AuthorizationGatewayFilter.kt:31 implements Spring Cloud Gateway's GlobalFilter. It also extends ReactiveSecurityFilter. The filter order is HIGHEST_PRECEDENCE + 10 at line 42.

Security Filter Logic: The ReactiveSecurityFilter.filterInternal() method at ReactiveSecurityFilter.kt:66 handles the full request lifecycle:

  • Parse the request using RequestParser (line 70)
  • Extract security context using SecurityContextParser (line 73)
  • On TokenVerificationException, fall back to anonymous context (line 75)
  • Call authorization.authorize() (line 85)
  • If authorized, set principal on exchange and continue filter chain (line 89)
  • If unauthorized and anonymous: HTTP 401 (line 99)
  • If unauthorized and authenticated: HTTP 403 (line 101)
  • Handle TooManyRequestsException: HTTP 429 (line 106)

SPI Extension Points

CoSec uses Java's Service Provider Interface (SPI) for extensibility. Custom matchers are discovered at runtime via ServiceLoader.

mermaid
flowchart TD
    subgraph "ServiceLoader Discovery"
        SPI1["META-INF/services/<br>me.ahoo.cosec.policy.action<br>.ActionMatcherFactory"]
        SPI2["META-INF/services/<br>me.ahoo.cosec.policy.condition<br>.ConditionMatcherFactory"]
    end

    subgraph "ActionMatcherFactories"
        A1["AllActionMatcherFactory<br>type: all"]
        A2["PathActionMatcherFactory<br>type: path"]
        A3["CompositeActionMatcherFactory<br>type: composite"]
    end

    subgraph "ConditionMatcherFactories"
        C1["AllConditionMatcherFactory"]
        C2["AuthenticatedConditionMatcherFactory"]
        C3["InRoleConditionMatcherFactory"]
        C4["InTenantConditionMatcherFactory"]
        C5["SpelConditionMatcherFactory"]
        C6["OgnlConditionMatcherFactory"]
        C7["PathConditionMatcherFactory"]
        C8["RateLimiterConditionMatcherFactory"]
        C9["Eq / Contains / StartsWith<br>/ EndsWith / In / Regular"]
        C10["BoolConditionMatcherFactory"]
        C11["GroupedRateLimiterConditionMatcherFactory"]
    end

    subgraph "Registration"
        PROV["ActionMatcherFactoryProvider<br>(SPI + Spring beans)"]
        CPRV["ConditionMatcherFactoryProvider<br>(SPI + Spring beans)"]
    end

    SPI1 --> A1
    SPI1 --> A2
    SPI1 --> A3
    SPI2 --> C1
    SPI2 --> C2
    SPI2 --> C3
    SPI2 --> C4
    SPI2 --> C5
    SPI2 --> C6
    SPI2 --> C7
    SPI2 --> C8
    SPI2 --> C9
    SPI2 --> C10
    SPI2 --> C11

    A1 --> PROV
    A2 --> PROV
    A3 --> PROV
    C1 --> CPRV
    C2 --> CPRV
    C3 --> CPRV
    C4 --> CPRV
    C5 --> CPRV
    C6 --> CPRV
    C7 --> CPRV
    C8 --> CPRV
    C9 --> CPRV
    C10 --> CPRV
    C11 --> CPRV

    style SPI1 fill:#161b22,stroke:#30363d,color:#e6edf3
    style SPI2 fill:#161b22,stroke:#30363d,color:#e6edf3
    style PROV fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CPRV fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

How SPI Works

  1. SPI files declare the built-in factory implementations. The action matcher SPI file (ActionMatcherFactory) lists:

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

    The condition matcher SPI file (ConditionMatcherFactory) lists 16 factories.

  2. Provider classes load SPI factories at startup. ActionMatcherFactoryProvider at ActionMatcherFactoryProvider.kt:20 uses ServiceLoader.load() in its init block to discover and register factories by type name.

  3. Spring integration also registers factories via MatcherFactoryRegister at MatcherFactoryRegister.kt:24, which discovers all ActionMatcherFactory and ConditionMatcherFactory beans from the Spring ApplicationContext.

Adding a Custom Matcher

To add a new action or condition matcher:

  1. Implement the factory interface (ActionMatcherFactory or ConditionMatcherFactory) -- e.g., ActionMatcherFactory.kt:30.
  2. If standalone, create a SPI file at META-INF/services/me.ahoo.cosec.policy.action.ActionMatcherFactory listing your class.
  3. If using Spring Boot, register your factory as a @Bean and MatcherFactoryRegister will pick it up automatically.

Policy Evaluation Flow

The DefaultPolicyEvaluator at DefaultPolicyEvaluator.kt:25 provides policy validation (not authorization). It creates a mock EvaluateRequest (line 64) and runs through all conditions, actions, and statements to catch configuration errors before the policy is used in production.

Authentication Pipeline

mermaid
sequenceDiagram
    participant C as Client
    participant F as Security Filter
    participant SP as SecurityContextParser
    participant A as CompositeAuthentication
    participant AP as AuthenticationProvider
    participant Auth as Concrete Authentication
    participant P as CoSecPrincipal

    C->>F: HTTP Request + Credentials
    F->>SP: parse(request)
    SP->>A: authenticate(credentials)
    A->>AP: getRequired(credentialsType)
    AP-->>A: Authentication instance
    A->>Auth: authenticate(credentials)
    Auth-->>A: Mono<CoSecPrincipal>
    A-->>SP: Mono<CoSecPrincipal>
    SP-->>F: SecurityContext(principal)
    F->>F: authorize(request, context)

CompositeAuthentication at CompositeAuthentication.kt:22 delegates to the appropriate Authentication implementation based on the credential type, using the AuthenticationProvider registry.

Serialization

CoSec uses Jackson for JSON serialization of policies, permissions, and configuration. The CoSecModule (CoSecModule) registers custom serializers for domain types including:

  • JsonPolicySerializer -- serializes Policy objects
  • JsonStatementSerializer -- serializes Statement objects
  • JsonActionMatcherSerializer -- serializes ActionMatcher objects
  • JsonConditionMatcherSerializer -- serializes ConditionMatcher objects
  • JsonEffectSerializer -- serializes Effect enum
  • JsonAppPermissionSerializer -- serializes AppPermission objects

The SPI-registered tools.jackson.databind.JacksonModule file ensures the module is auto-discovered (JacksonModule SPI).


Part III: Development Workflow

Development Environment Setup

Prerequisites

ToolVersionWhy
JDK17+Required by jvmToolchain(17) in build.gradle.kts:83
GradleWrapper included (./gradlew)No separate install needed
GitAny recent versionVersion control
IDEIntelliJ IDEA recommendedBest Kotlin support, Gradle integration

Clone and Build

bash
# Clone the repository
git clone https://github.com/Ahoo-Wang/CoSec.git
cd CoSec

# Build all modules (compile + test)
./gradlew build

# Build without tests (faster for initial setup)
./gradlew build -x test

The first build will download dependencies, which may take a few minutes. All dependency versions are centralized in gradle/libs.versions.toml (libs.versions.toml).

Verify Your Setup

bash
# Run all tests (should pass on a clean checkout)
./gradlew test

# Run Detekt static analysis
./gradlew detekt

# Generate code coverage report
./gradlew :code-coverage-report:codeCoverageReport

Key Build Commands Reference

CommandWhat It Does
./gradlew buildCompile, run tests, run Detekt, build JARs
./gradlew testRun all tests across all modules
./gradlew :cosec-core:testRun tests for a single module
./gradlew :cosec-core:test --tests "me.ahoo.cosec.policy.DefaultPolicyEvaluatorTest"Run a single test class
./gradlew :cosec-core:test --tests "me.ahoo.cosec.authorization.SimpleAuthorizationTest.authorizeWhenPrincipalIsRoot"Run a single test method
./gradlew detektRun static analysis on all modules
./gradlew :cosec-core:jmhRun JMH benchmarks
./gradlew :cosec-core:jmh -PjmhIncludes=*.SomeBenchmarkRun specific JMH benchmark

Project Structure Conventions

Every library module follows this structure:

cosec-<module>/
  build.gradle.kts           # Module build configuration
  src/
    main/
      kotlin/me/ahoo/cosec/  # Production code
        <feature>/
          *.kt
      resources/
        META-INF/services/    # SPI registration files
    test/
      kotlin/me/ahoo/cosec/  # Test code (mirrors main structure)
        <feature>/
          *Test.kt
      resources/              # Test fixtures

Package Naming

The base package is me.ahoo.cosec. Subpackages map to domain concepts:

PackagePurpose
me.ahoo.cosec.apiPublic interfaces (in cosec-api)
me.ahoo.cosec.api.principalPrincipal, role, policy capability interfaces
me.ahoo.cosec.api.policyPolicy, Statement, Effect, matchers
me.ahoo.cosec.api.authorizationAuthorization, AuthorizeResult
me.ahoo.cosec.api.authenticationAuthentication, Credentials
me.ahoo.cosec.api.contextSecurityContext, Attributes
me.ahoo.cosec.api.context.requestRequest, AppIdCapable, DeviceIdCapable
me.ahoo.cosec.api.tenantTenant, TenantCapable
me.ahoo.cosec.api.permissionPermission, AppPermission, RolePermission
me.ahoo.cosec.api.tokenToken, AccessToken, CompositeToken
me.ahoo.cosec.policyPolicy evaluation, data classes
me.ahoo.cosec.policy.actionActionMatcher implementations
me.ahoo.cosec.policy.conditionConditionMatcher implementations
me.ahoo.cosec.policy.condition.partPath-based condition matchers
me.ahoo.cosec.policy.condition.contextContext-based condition matchers
me.ahoo.cosec.policy.condition.limiterRate limiter conditions
me.ahoo.cosec.authorizationAuthorization implementations
me.ahoo.cosec.authenticationAuthentication implementations
me.ahoo.cosec.contextSecurityContext implementations
me.ahoo.cosec.principalPrincipal implementations
me.ahoo.cosec.serializationJackson serializers
me.ahoo.cosec.tokenToken handling
me.ahoo.cosec.blacklistBlacklist checking
me.ahoo.cosec.tenantTenant implementations
me.ahoo.cosec.permissionPermission data classes
me.ahoo.cosec.webfluxWebFlux integration
me.ahoo.cosec.gatewaySpring Cloud Gateway integration

Your First Contribution

Finding Something to Work On

  1. Check the GitHub Issues for issues labeled good first issue or help wanted.
  2. Look for TODO comments in the codebase.
  3. Review the test coverage report for untested code paths.

Typical Workflow

  1. Fork and branch: Fork the repository on GitHub, then create a feature branch:

    bash
    git checkout -b feature/my-new-feature
  2. Write the code: Follow the conventions described below.

  3. Write tests: Every new feature or bug fix must include tests.

  4. Run checks locally:

    bash
    ./gradlew build        # compile + test + detekt
    ./gradlew detekt       # static analysis only
    ./gradlew test         # tests only
  5. Commit and push: Use clear commit messages following the Conventional Commits format:

    feat(policy): add regex action matcher
    fix(authorization): handle empty policy list correctly
    test(core): add tests for RateLimiterConditionMatcher
  6. Open a Pull Request: Push your branch to your fork and open a PR against the main branch.

Adding a New Condition Matcher: Step-by-Step Example

Let's say you want to add a TimeRangeConditionMatcher that only allows access during specific hours.

Step 1: Create the matcher class in cosec-core/src/main/kotlin/me/ahoo/cosec/policy/condition/part/TimeRangeConditionMatcher.kt:

kotlin
package me.ahoo.cosec.policy.condition.part

import me.ahoo.cosec.api.configuration.Configuration
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.policy.condition.AbstractConditionMatcher

class TimeRangeConditionMatcher(configuration: Configuration) :
    AbstractConditionMatcher(TYPE, configuration) {

    private val startHour: Int = configuration.get("startHour")?.asInt() ?: 0
    private val endHour: Int = configuration.get("endHour")?.asInt() ?: 23

    override fun internalMatch(request: Request, securityContext: SecurityContext): Boolean {
        val currentHour = java.time.LocalTime.now().hour
        return currentHour in startHour..endHour
    }

    companion object {
        const val TYPE = "timeRange"
    }
}

Step 2: Create the factory class:

kotlin
class TimeRangeConditionMatcherFactory : ConditionMatcherFactory {
    override val type: String = TimeRangeConditionMatcher.TYPE

    override fun create(configuration: Configuration): ConditionMatcher =
        TimeRangeConditionMatcher(configuration)
}

Step 3: Register via SPI -- add to cosec-core/src/main/resources/META-INF/services/me.ahoo.cosec.policy.condition.ConditionMatcherFactory:

me.ahoo.cosec.policy.condition.part.TimeRangeConditionMatcherFactory

Step 4: Write tests in cosec-core/src/test/kotlin/me/ahoo/cosec/policy/condition/part/TimeRangeConditionMatcherTest.kt:

kotlin
package me.ahoo.cosec.policy.condition.part

import me.ahoo.cosec.configuration.JsonConfiguration
import me.ahoo.cosec.context.SimpleSecurityContext
import me.ahoo.cosec.policy.EvaluateRequest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow

class TimeRangeConditionMatcherTest {
    @Test
    fun `match within range`() {
        val config = """{"type":"timeRange","startHour":0,"endHour":23}"""
            .let { JsonConfiguration.Companion.asConfiguration(it) }
        val matcher = TimeRangeConditionMatcher(config)
        val request = EvaluateRequest()
        val context = SimpleSecurityContext.anonymous()
        assertDoesNotThrow { matcher.match(request, context) }
    }
}

Step 5: Register the Jackson serializer if the matcher has a custom serialization format. Add a serializer class in me.ahoo.cosec.serialization and register it in CoSecModule.

Testing Guide

CoSec uses a layered testing approach with specific libraries at each level.

Test Libraries

LibraryPurposeCoordinates
JUnit 5Test frameworkorg.junit.jupiter (version from libs.versions.toml:18)
MockKKotlin mockingio.mockk:mockk (libs.versions.toml:47)
FluentAssertFluent assertionsme.ahoo.test:fluent-assert-core (libs.versions.toml:45)
HamcrestMatcher libraryorg.hamcrest:hamcrest (libs.versions.toml:46)
Reactor TestStepVerifier for Mono/FluxIncluded via Spring BOM

Writing Tests for Authorization Logic

The test for SimpleAuthorization at SimpleAuthorizationTest.kt:41 is a good reference. Key patterns:

1. Mocking with MockK:

kotlin
val policyRepository = mockk<PolicyRepository>()
val permissionRepository = mockk<AppRolePermissionRepository>()
val request = mockk<Request>()
val securityContext = mockk<SecurityContext> {
    every { principal.id } returns CoSecPrincipal.ROOT_ID
}

2. Testing Reactive Streams with StepVerifier:

kotlin
authorization.authorize(request, securityContext)
    .test()                                    // convert Mono to StepVerifier
    .expectNext(AuthorizeResult.ALLOW)         // assert the emitted value
    .verifyComplete()                          // assert stream completes

3. Using Data Classes for Test Fixtures:

kotlin
val statement = StatementData(
    effect = Effect.ALLOW,
    action = AllActionMatcher.INSTANCE,
)
val policy = PolicyData(
    id = "test-policy",
    category = "test",
    name = "Test Policy",
    description = "A test policy",
    type = PolicyType.GLOBAL,
    tenantId = Tenant.DEFAULT_TENANT_ID,
    statements = listOf(statement),
)

StatementData (StatementData.kt:31) and PolicyData (PolicyData.kt:35) are the data class implementations used for constructing test objects.

Testing Auto-Configuration

Tests in cosec-spring-boot-starter use Spring Boot's test context framework. See CoSecAutoConfigurationTest for examples (CoSecAutoConfigurationTest.kt).

Testing Policy Evaluation

The DefaultPolicyEvaluatorTest at DefaultPolicyEvaluatorTest.kt demonstrates how to test policy evaluation. You can construct policies using the data classes and verify they evaluate correctly.

Debugging Tips

Enable Debug Logging

CoSec uses io.github.oshai:kotlin-logging-jvm for logging (libs.versions.toml:37). SimpleAuthorization logs detailed decision reasoning at DEBUG level:

Verify [Request(...)] [Context(...)] matched Policy[policyId] Statement[0][statementName] - [ALLOW].

To enable debug logging, configure Logback in config/logback.xml:

xml
<logger name="me.ahoo.cosec" level="DEBUG"/>

Common Debugging Scenarios

1. "ActionMatcherFactory[type] not found": This means you referenced a matcher type in a policy JSON that is not registered. Check that the factory is listed in the SPI file and that ActionMatcherFactoryProvider loaded it (ActionMatcherFactoryProvider.kt:46).

2. Unexpected IMPLICIT_DENY: The most common cause is a policy condition that does not match. Use debug logging to trace which policies and statements are being evaluated. Check that the policy's top-level condition is AllConditionMatcher (matches everything) or that it matches your request context.

3. TokenVerificationException swallowed: The ReactiveSecurityFilter catches TokenVerificationException and falls back to an anonymous context (ReactiveSecurityFilter.kt:75). If you see unexpected 403 responses, check whether the token parsing is failing silently.

4. StepVerifier timeouts: If a Mono never completes, StepVerifier.verifyComplete() will hang. Use .verify(Duration) to set a timeout and check that all reactive chains produce a value.

5. Detekt failures: Run ./gradlew detekt before pushing. The config is at config/detekt/detekt.yml (detekt.yml). Auto-correct is enabled by default in the build (build.gradle.kts:51).

Code Style and Conventions

Kotlin Conventions

  • Interfaces in cosec-api, implementations in cosec-core: This is the most important convention. Never put an implementation class in cosec-api.
  • Reactive throughout: Core interfaces return Mono<T>. Never block on a Mono in production code.
  • -Xjsr305=strict and -Xjvm-default=all-compatibility: These compiler flags are set in build.gradle.kts:86. The all-compatibility flag generates default method bodies in interfaces for Java interoperability.
  • Data classes for DTOs: Use data class for objects that represent data (e.g., PolicyData, StatementData). Use regular classes for objects with behavior.
  • Null safety: Prefer non-null types. Use ? only when null is a valid value. Use requireNotNull() for required values.

Naming Conventions

ElementConventionExample
InterfacePascalCase, no prefix/suffixPolicy, ActionMatcher
ImplementationSimple or descriptive prefixSimpleAuthorization, PathActionMatcher
Data classNoun + Data suffixPolicyData, StatementData
FactoryNoun + Factory suffixPathActionMatcherFactory
ProviderNoun + Provider suffixActionMatcherFactoryProvider
Test classClass name + TestSimpleAuthorizationTest
Test methodcamelCase, descriptiveauthorizeWhenPrincipalIsRoot
ConstantSCREAMING_SNAKE_CASEROOT_ID, ANONYMOUS_ID

Detekt Configuration

Detekt rules are configured in config/detekt/detekt.yml (detekt.yml). Notable relaxations:

  • LongParameterList, NestedBlockDepth, TooManyFunctions are disabled
  • MaxLineLength is set to 200
  • ReturnCount is disabled
  • MagicNumber is disabled
  • WildcardImport excludes common test imports (java.util.*, org.hamcrest.*, org.junit.jupiter.api.*)

Common Pitfalls

1. Blocking on Mono in Production

Never call .block() on a Mono in code that runs on a reactive thread (Netty event loop). It will deadlock. Use the reactive operators (map, flatMap, switchIfEmpty) instead.

2. Missing SPI Registration

If you add a new ActionMatcherFactory or ConditionMatcherFactory but forget to add it to the SPI file, it will not be discovered. Always update the corresponding META-INF/services/ file.

3. Wrong Test Imports

The project uses FluentAssert (me.ahoo.test:fluent-assert-core) for assertions. Import me.ahoo.test.asserts.assert -- not AssertJ's assertThat(), which is verbose and not null-safe in Kotlin. This is enforced by convention.

4. Forgetting JVM Toolchain

All library modules must target Java 17. This is configured in the root build.gradle.kts at build.gradle.kts:83. Do not override this in module-specific build files.

5. Adding Dependencies to cosec-api

The cosec-api module has no framework dependencies by design. Never add Spring, Reactor, or other framework dependencies to cosec-api. If you need a framework type in an interface, it belongs in cosec-core or an integration module.

6. Not Testing the Deny-First Logic

When writing policy evaluation tests, always test both the ALLOW and DENY paths. The deny-first evaluation order means that a DENY statement will override an ALLOW statement even if the ALLOW comes first in the list. This is a critical security property.

7. RateLimiter Exceptions in Policy Evaluation

DefaultPolicyEvaluator catches TooManyRequestsException during evaluation (DefaultPolicyEvaluator.kt:48). If your policy uses rate limiter conditions, do not expect them to throw during evaluation -- they are silently skipped.

8. Thread Safety of SecurityContext

SimpleSecurityContext uses ConcurrentHashMap for its attributes (SimpleSecurityContext.kt:41). The class is marked @ThreadSafe. When creating custom SecurityContext implementations, use thread-safe collections.


Appendices

Appendix A: Glossary

TermDefinition
ActionMatcherAn interface that determines whether a request matches a specific action pattern (e.g., URL path). Registered via SPI.
AllActionMatcherA built-in ActionMatcher that matches all requests. Type key: "*".
AllConditionMatcherA built-in ConditionMatcher that always returns true. Used as a default.
AllowAnonymousA policy or statement that applies to unauthenticated (anonymous) users.
AppIdThe application identifier extracted from the request header or query parameter.
AppPermissionPermissions scoped to an application, containing groups of individual permissions.
AppPermissionEvaluatorInterface for evaluating whether a request matches an app's permission configuration.
AppRolePermissionThe mapping between application roles and their permissions. Used in the third phase of authorization.
AttributeA key-value pair stored in SecurityContext.attributes for passing data between components.
AuthenticateThe process of verifying a user's identity by validating their credentials.
AuthenticationThe core interface for credential verification, returning Mono<CoSecPrincipal>.
AuthenticationProviderA registry that maps credential types to Authentication implementations.
AuthorizeThe process of determining whether an authenticated user has permission to perform an action.
AuthorizationThe core interface for access control decisions, returning Mono<AuthorizeResult>.
AuthorizeResultThe result of an authorization decision: ALLOW, EXPLICIT_DENY, IMPLICIT_DENY, TOKEN_EXPIRED, or TOO_MANY_REQUESTS.
BlacklistCheckerAn optional component that blocks requests from banned users or IPs before policy evaluation.
CoSecThe name of the framework, derived from "Co" (collaborative) + "Sec" (security). Also the constant prefix for configuration properties.
CoSecPrincipalThe core identity type representing an authenticated or anonymous user, extending java.security.Principal.
CompositeActionMatcherAn ActionMatcher that matches if any of its child matchers match. Used for combining multiple path patterns.
CompositeAuthenticationAn Authentication implementation that delegates to the appropriate handler based on credential type.
CompositeTokenA token containing both an access token and a refresh token.
ConditionMatcherAn interface that evaluates whether a condition is met for a policy or statement to apply.
CredentialsThe data required to authenticate a user (e.g., username/password, token).
DefaultPolicyEvaluatorA utility that validates policy configurations by testing all matchers against mock data.
DENYAn Effect value that explicitly prohibits an action. Takes precedence over ALLOW.
DeviceIdA unique identifier for the client device, extracted from request headers or query parameters.
EffectAn enum with two values: ALLOW and DENY. Determines whether a statement permits or blocks an action.
EXPLICIT_DENYAn AuthorizeResult indicating the request was actively denied by a DENY statement.
Fun interfaceA Kotlin SAM (Single Abstract Method) interface that can be instantiated with a lambda.
Global PolicyA policy with PolicyType.GLOBAL that applies to all requests across all applications.
IMPLICIT_DENYAn AuthorizeResult indicating no policy or permission matched the request. The default result.
JMHJava Microbenchmark Harness. Used for performance benchmarks in every module.
JWTJSON Web Token. Used for stateless authentication tokens.
LocalPolicyLoaderA component that loads policies from local configuration files.
MatcherFactoryThe factory interface for creating ActionMatcher or ConditionMatcher instances from JSON configuration.
MatcherFactoryRegisterA Spring SmartLifecycle bean that discovers and registers matcher factories from the Spring context.
MonoA Project Reactor type representing an asynchronous stream of 0 or 1 elements.
Multi-TenancyThe ability to isolate data and policies by customer (tenant).
Null SafetyKotlin's type system distinguishing between nullable (T?) and non-null (T) types.
OGNLObject-Graph Navigation Language. Used as an expression language for condition matchers.
PathActionMatcherA built-in ActionMatcher that matches requests by URL path patterns (Ant-style).
PermissionA single access control rule with an effect, action matcher, and optional condition.
PermissionVerifierA fun interface that verifies whether a request is permitted, returning VerifyResult.
PolicyA named collection of Statements with an optional condition. The primary unit of access control configuration.
PolicyCapableAn interface for entities that have policy assignments. Provides policies: Set<String>.
PolicyDataThe data class implementation of Policy.
PolicyEvaluatorAn interface for validating policy configurations.
PolicyIdA type alias for String, representing a policy identifier.
PolicyRepositoryA repository interface for retrieving policies by ID or getting global policies.
PolicyTypeAn enum: GLOBAL (applies everywhere), SYSTEM (managed by admins), CUSTOM (user-defined).
PrincipalA security identity. In CoSec, CoSecPrincipal extends java.security.Principal.
Project ReactorThe reactive programming library used throughout CoSec for asynchronous operations.
RateLimiterConditionMatcherA condition matcher that limits the rate of requests.
ReactiveAuthorizationFilterA Spring WebFlux WebFilter that performs authorization on each request.
ReactiveSecurityFilterThe base class for reactive authorization filters, containing shared filtering logic.
RequestThe interface representing an incoming HTTP request with path, method, headers, and metadata.
RequestIdA unique identifier for tracing a request through the system.
RoleA named collection of permissions assigned to a user.
RoleCapableAn interface for entities that have role assignments. Provides roles: Set<RoleId>.
RoleIdA type alias for String, representing a role identifier.
SAM InterfaceSingle Abstract Method interface. Kotlin's fun interface creates these.
SecurityContextAn object holding the current principal, tenant info, and mutable attributes for a request.
ServiceLoaderJava's SPI mechanism for discovering implementations at runtime. Used for matcher factories.
SimpleAuthorizationThe core Authorization implementation with root bypass, blacklist, policy, and role evaluation.
SimplePrincipalThe basic CoSecPrincipal implementation.
SimpleSecurityContextThe thread-safe SecurityContext implementation using ConcurrentHashMap.
SpaceIdA tenant/workspace identifier extracted from request headers.
SpELSpring Expression Language. Used as an expression language for condition matchers.
SPIService Provider Interface. Java's mechanism for discovering and loading implementations at runtime.
StatementA single permission rule within a Policy with an Effect, ActionMatcher, and ConditionMatcher.
StatementDataThe data class implementation of Statement.
StepVerifierA Reactor Test utility for verifying Mono and Flux streams in tests.
TenantAn organizational boundary for isolating policies and data. Three types: Platform, Default, User.
TenantCapableAn interface for entities that belong to a tenant. Provides tenantId.
TenantPrincipalA CoSecPrincipal that also implements TenantCapable, associating the user with a tenant.
TokenA credential (typically JWT) that represents an authenticated session.
TokenVerificationExceptionAn exception thrown when a token cannot be verified (e.g., invalid signature, expired).
TooManyRequestsExceptionAn exception thrown when a rate limiter condition is exceeded.
VerifyResultAn enum: ALLOW, EXPLICIT_DENY, IMPLICIT_DENY. The internal result of policy verification.

Appendix B: Key File Reference

cosec-api -- Core Interfaces

FileKey InterfacePurpose
CoSec.ktCoSecFramework constants (COSEC, DEFAULT)
CoSecPrincipal.ktCoSecPrincipalCore identity with id, roles, policies, attributes
TenantPrincipal.ktTenantPrincipalPrincipal with tenant context
RoleCapable.ktRoleCapable, RoleIdRole assignment capability
PolicyCapable.ktPolicyCapablePolicy assignment capability
Authentication.ktAuthentication<C, P>Credential verification
Authorization.ktAuthorizationAccess control decision
AuthorizeResult.ktAuthorizeResultDecision result (ALLOW/DENY/IMPLICIT_DENY)
Policy.ktPolicyNamed collection of Statements
Statement.ktStatementSingle permission rule
Effect.ktEffectALLOW or DENY
PolicyType.ktPolicyTypeGLOBAL, SYSTEM, CUSTOM
ActionMatcher.ktActionMatcherRequest action matching
ConditionMatcher.ktConditionMatcherCondition evaluation
PermissionVerifier.ktPermissionVerifier, VerifyResultPermission checking
SecurityContext.ktSecurityContextRequest security context
Request.ktRequestIncoming HTTP request
Tenant.ktTenantTenant with ID and type checks

cosec-core -- Implementations

FileKey ClassPurpose
SimpleAuthorization.ktSimpleAuthorizationCore authorization logic
PolicyRepository.ktPolicyRepositoryPolicy storage interface
AppRolePermissionRepository.ktAppRolePermissionRepositoryRole permission storage
PolicyVerifyContext.ktVerifyContextContext for tracking evaluation results
CompositeAuthentication.ktCompositeAuthenticationCredential-type-based auth delegation
DefaultPolicyEvaluator.ktDefaultPolicyEvaluatorPolicy validation utility
PolicyData.ktPolicyDataPolicy data class
StatementData.ktStatementDataStatement data class
PathActionMatcher.ktPathActionMatcherURL path pattern matching
AllActionMatcher.ktAllActionMatcherMatch-all wildcard
ActionMatcherFactory.ktActionMatcherFactoryFactory SPI interface
ActionMatcherFactoryProvider.ktActionMatcherFactoryProviderSPI registry
ConditionMatcherFactory.ktConditionMatcherFactoryFactory SPI interface
AbstractConditionMatcher.ktAbstractConditionMatcherBase class with negate support
SimplePrincipal.ktSimplePrincipalBasic CoSecPrincipal implementation
SimpleSecurityContext.ktSimpleSecurityContextThread-safe SecurityContext
BlacklistChecker.ktBlacklistCheckerBlacklist check interface

SPI Registration Files

FileRegistered Factories
ActionMatcherFactoryAllActionMatcherFactory, PathActionMatcherFactory, CompositeActionMatcherFactory
ConditionMatcherFactory16 factories including Spel, Ognl, RateLimiter, path-based, and context-based matchers

Integration Modules

FileKey ClassPurpose
ReactiveSecurityFilter.ktReactiveSecurityFilterBase filter logic (parse, auth, decide)
ReactiveAuthorizationFilter.ktReactiveAuthorizationFilterWebFlux WebFilter
AuthorizationGatewayFilter.ktAuthorizationGatewayFilterSpring Cloud Gateway GlobalFilter
CoSecAutoConfiguration.ktCoSecAutoConfigurationAuto-configuration entry point
MatcherFactoryRegister.ktMatcherFactoryRegisterSpring bean factory registration
ConditionalOnCoSecEnabled.kt@ConditionalOnCoSecEnabledFeature toggle annotation

Build Configuration

FilePurpose
build.gradle.ktsRoot build: plugins, Detekt, Jacoco, JMH, publishing
settings.gradle.ktsModule includes
gradle/libs.versions.tomlVersion catalog
config/detekt/detekt.ymlDetekt static analysis config

Test Files (Key Examples)

FileWhat It Tests
SimpleAuthorizationTest.ktRoot bypass, blacklist, global/principal/role policies
DefaultPolicyEvaluatorTest.ktPolicy validation
PathActionMatcherTest.ktURL path matching
ReactiveAuthorizationFilterTest.ktWebFlux filter integration
CoSecAutoConfigurationTest.ktAuto-configuration

Appendix C: Quick Reference Card

Build Commands

bash
./gradlew build                                                    # Full build
./gradlew test                                                     # All tests
./gradlew :cosec-core:test                                         # Single module tests
./gradlew :cosec-core:test --tests "*.SimpleAuthorizationTest"     # Single test class
./gradlew detekt                                                   # Static analysis
./gradlew :code-coverage-report:codeCoverageReport                 # Coverage report
./gradlew :cosec-core:jmh -PjmhIncludes=*.SomeBenchmark            # Benchmark

Key Type Hierarchy

java.security.Principal
  └── CoSecPrincipal (interface)
        ├── PolicyCapable (policies: Set<String>)
        ├── RoleCapable (roles: Set<RoleId>)
        └── TenantPrincipal (tenantId: String)

Request (path, method, remoteIp, appId, spaceId, deviceId)
  └── EvaluateRequest (mock for testing)

SecurityContext (principal, tenantId, attributes)
  └── SimpleSecurityContext (ConcurrentHashMap attributes)

Policy (id, name, type, condition, statements)
  └── PolicyData (data class)

Statement (name, effect, action, condition)
  └── StatementData (data class)

Effect = ALLOW | DENY
PolicyType = GLOBAL | SYSTEM | CUSTOM
VerifyResult = ALLOW | EXPLICIT_DENY | IMPLICIT_DENY
AuthorizeResult = ALLOW | EXPLICIT_DENY | IMPLICIT_DENY | TOKEN_EXPIRED | TOO_MANY_REQUESTS

Authorization Decision Tree

1. Is principal.isRoot?     → ALLOW
2. Is request blacklisted?  → EXPLICIT_DENY
3. Do global policies match?
   - Any DENY match?        → EXPLICIT_DENY
   - Any ALLOW match?       → ALLOW
4. Do principal policies match?
   - Any DENY match?        → EXPLICIT_DENY
   - Any ALLOW match?       → ALLOW
5. Do role permissions match?
   - Any DENY match?        → EXPLICIT_DENY
   - Any ALLOW match?       → ALLOW
6. Nothing matched?         → IMPLICIT_DENY

Configuration Properties

All CoSec properties are prefixed with cosec.:

PropertyDefaultPurpose
cosec.enabledtrueMaster enable/disable toggle
cosec.authentication.enabledtrueEnable authentication
cosec.authorization.enabledtrueEnable authorization
cosec.jwt.enabledtrueEnable JWT support
cosec.gateway.enabledfalseEnable Gateway integration
cosec.social.enabledfalseEnable social authentication
cosec.cache.enabledfalseEnable Redis caching
cosec.ip2region.enabledfalseEnable IP geolocation
cosec.openapi.enabledfalseEnable OpenAPI integration
cosec.opentelemetry.enabledfalseEnable OpenTelemetry tracing
cosec.inject.enabledfalseEnable security context injection

Tech Stack Quick Reference

ComponentVersionSource
Kotlin2.3.20libs.versions.toml:26
Java Target17build.gradle.kts:83
Spring Boot4.0.5libs.versions.toml:3
Spring Cloud2025.1.1libs.versions.toml:4
JUnit6.0.3libs.versions.toml:18
MockK1.14.9libs.versions.toml:21
FluentAssert0.2.6libs.versions.toml:19
Detekt1.23.8libs.versions.toml:24
Jackson (JWT)4.5.1libs.versions.toml:12
OGNL3.4.11libs.versions.toml:11

Licensed under the Apache License, Version 2.0.