> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0002-centralised-path-matcher-authorization.md).

# ADR-0002: Centralised path-matcher authorization

## Status

**Accepted.** Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.

## Context

ODD Platform exposes hundreds of endpoints, many of which mutate state — creating data sources, editing descriptions, regenerating collector tokens, managing policies and roles. Each needs an authorization decision: *which permission must the caller hold to perform this operation?*

Spring Security offers two broad places to answer that. One is **method-level** annotations — `@PreAuthorize` / `@Secured` on each controller method, keeping the rule next to the code it guards. The other is a **centralised** declaration — every rule registered in one place against the security filter chain, so the whole access matrix can be read at once. The trade-off is locality versus auditability: annotations sit next to the method but scatter the security model across the codebase; a central table concentrates the model at the cost of the controller no longer advertising its own auth posture.

ODD chose the centralised path.

## Decision

**Authorization is declared in one rule table — `SecurityConstants.SECURITY_RULES` — and enforced by a single `AuthorizationCustomizer`.** Each gated endpoint is one row mapping a request matcher to the permission it requires; controllers and the generated `*Api` interfaces carry **no** `@PreAuthorize`, `@Secured`, or programmatic permission checks.

Each row is a `SecurityRule(type, matcher, permission)`: an `AuthorizationManagerType` (whether the decision needs a resource context — `NO_CONTEXT` for global operations, or a resource type such as `DATA_ENTITY` / `TERM` for per-resource policies), a path-and-method matcher, and the `PolicyPermissionDto` the caller must hold.

`AuthorizationCustomizer` wires the chain in a fixed shape:

1. a whitelist of public paths is permitted (`actuator`, static assets, the ingestion surface, and the Slack-events webhook);
2. every `SECURITY_RULES` row is registered as a matcher guarded by that permission's authorization manager;
3. a final `pathMatchers("/**").authenticated()` catch-all closes the surface.

The catch-all is the deliberate floor: **any endpoint not named in `SECURITY_RULES` requires authentication but no specific permission** — it is available to any authenticated user.

## Consequences

* The platform has **one auditable security matrix**. To answer "what does this endpoint require?", read `SECURITY_RULES`; to change an endpoint's authorization, edit that one list.
* Because read (`GET`) endpoints are generally not listed, they fall through the catch-all to "any authenticated user." This is the platform's read-collaborative posture — a separate decision, recorded in its own record.
* The model is **path-string coupled**: a rule matches a literal path pattern. If a controller's URL changes but its `SECURITY_RULES` row is not updated to match, the rule silently stops applying. The single table makes the rules auditable, but nothing at compile time ties a rule to the route it guards — keeping the two in step is a manual discipline.
* A contributor adding a mutating endpoint must add its `SECURITY_RULES` row; reaching for a `@PreAuthorize` annotation instead would not follow the convention and would split the security model across two places.

## Evidence

* `odd-platform-api/.../auth/authorization/AuthorizationCustomizer.java:20-31` — the **sole** consumer of `SECURITY_RULES`: it permits `WHITELIST_PATHS`, loops the rules registering `.matchers(rule.matcher()).access(manager(rule.type(), extractors, permissionService, rule.permission()))`, then closes with `.pathMatchers("/**").authenticated()`.
* `odd-platform-api/.../auth/util/SecurityConstants.java:95-96` — `WHITELIST_PATHS = {"/actuator/**", "/favicon.ico", "/ingestion/**", "/img/**", "/api/slack/events"}`; `:98` — `public static final List<SecurityRule> SECURITY_RULES = List.of(...)`, the rule table (one `new SecurityRule(type, new PathPatternParserServerWebExchangeMatcher(path, METHOD), permission)` per gated endpoint).
* `odd-platform-api/.../auth/util/SecurityRule.java` — `record SecurityRule(AuthorizationManagerType type, ServerWebExchangeMatcher matcher, PolicyPermissionDto permission)`.
* No `@PreAuthorize`, `@Secured`, `@PostAuthorize`, or `@RolesAllowed` annotation exists anywhere in `odd-platform-api/src/main` — the authorization model is entirely table-driven, confirming the annotation-free decision by exhaustive absence.

## See also

* [ADR-0001 — Contract-first HTTP layer](/developer-guides/architecture-decision-log/adr-0001-openapi-generated-controller-interfaces.md) — controllers are thin delegates over generated interfaces, which is why they carry no authorization annotations either: both routing and authorization live outside the controller class.
* [Policies](/configuration-and-deployment/enable-security/authorization/policies.md) — how operators author the policies whose permissions these rules check.
* [Permissions](/configuration-and-deployment/enable-security/authorization/permissions.md) — the permission model the rule table references.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0002-centralised-path-matcher-authorization.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
