> 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-0079-ingestion-authentication-filter-coverage.md).

# ADR-0079: Ingestion authentication is one gate over /ingestion/\*\*, not per-route filters

## Status

**Accepted** (2026-06-22). Ships in **0.29.0** — resolves [odd-platform#1740](https://github.com/opendatadiscovery/odd-platform/issues/1740). Extends [ADR-0002 — Centralised path-matcher authorization](/developer-guides/architecture-decision-log/adr-0002-centralised-path-matcher-authorization.md) and [ADR-0074 — Pluggable auth modes](/developer-guides/architecture-decision-log/adr-0074-pluggable-auth-modes.md).

## Context

ODD Platform has two independent authentication surfaces (see [Enable security](/configuration-and-deployment/enable-security.md)): the UI/API surface governed by `auth.type`, and the ingestion ingress governed by `auth.ingestion.filter.enabled`. The `/ingestion/**` namespace is in `SecurityConstants.WHITELIST_PATHS`, so it never traverses the central `SECURITY_RULES` table that ADR-0002 governs — it is protected instead by dedicated `WebFilter`s.

Before this decision those filters bound to **one exact path each**: `IngestionDataSourceFilter` matched `POST /ingestion/datasources` (always on, collector-token), and `IngestionDataEntitiesFilter` matched `POST /ingestion/entities` (conditional on the flag, per-datasource token). Every other ingestion route — `POST /ingestion/entities/datasets/stats`, `POST /ingestion/metrics`, the `POST /ingestion/alert/alertmanager` webhook, and the `GET /ingestion/entities/degs/children` read — matched neither filter, so it accepted requests with **no token even when `auth.ingestion.filter.enabled=true`**. An operator who enabled the flag expecting the whole ingestion surface to require a token got only one of its routes protected.

Widening the entities filter's matcher was not viable: its body decorator parses the request as a `DataEntityList` to resolve the datasource oddrn, and the sibling routes carry different body shapes (`DatasetStatisticsList`, `MetricSetList`) — or, for the DEG-children read, no body at all. One body decorator cannot serve them, and adding one exact-path filter per route scatters the security model across N beans, the very pattern that let four routes ship unprotected.

## Decision

**Introduce one uniform `IngestionAuthenticationFilter` that matches `/ingestion/**` and, when `auth.ingestion.filter.enabled=true`, requires a registered collector- or datasource-`Authorization: Bearer` token on every ingestion route except the two that already have a dedicated filter (`/ingestion/entities`, `/ingestion/datasources`).** It enforces *authentication* — the token must resolve to a known collector or datasource — without parsing the request body, so it covers the body-less `GET` read uniformly. The per-datasource *authorization* on `/ingestion/entities` (the token must match the datasource named in the body) is unchanged.

The check is **eager** (run before the chain proceeds, not inside a body decorator), which is why it protects the `GET /ingestion/entities/degs/children` read that a body-triggered check would miss. The filter is `@ConditionalOnProperty("auth.ingestion.filter.enabled", havingValue="true")`, mirroring the entities filter — so the flag now means what operators read it to mean: *on* ⇒ the whole ingestion surface needs a token.

This conforms to ADR-0002's principle of one auditable place for an authorization concern, applied to the ingestion-filter sub-system ADR-0002 leaves to the whitelist.

## Consequences

* With the flag on, the previously-open routes (stats, metrics, the AlertManager webhook, the DEG-children read) return **401** to an unauthenticated caller. The [Enable security deployment matrix](/configuration-and-deployment/enable-security.md#deployment-matrix-per-endpoint-per-auth-config) reflects this for 0.29.0.
* **Backward-compatible for compliant collectors.** `odd-collector-sdk` already sends `Authorization: Bearer <token>` on every ingestion call, so enabling the flag does not require collector-side changes. The Prometheus AlertManager webhook is now covered too — configure Alertmanager's `http_config` to send a registered token (see [Notifications](/features/active-platform-features/notifications.md) and [Prometheus AlertManager Integration](/configuration-and-deployment/odd-platform.md#prometheus-alertmanager-integration)).
* **The shipped default is unchanged** (`auth.ingestion.filter.enabled=false`). With the default in place every ingestion route — including the webhook — remains open on a network-reachable deployment; that caveat is documented on the security, notifications, AlertManager-integration, and integrations pages. Whether to make the flag implicit when `auth.type != DISABLED` is a separate decision.
* A future `/ingestion/*` route is authenticated by default — it falls under the uniform matcher rather than silently shipping open until someone remembers to add a filter.
* Per-resource *authorization* (the token must own the specific dataset) for stats and metrics is a deliberate non-goal here; this decision delivers authentication parity, and the stronger property can layer on without rework.

## See also

* [ADR-0002 — Centralised path-matcher authorization](/developer-guides/architecture-decision-log/adr-0002-centralised-path-matcher-authorization.md) — the auditable-rule-table principle this extends into the ingestion-filter sub-system.
* [ADR-0074 — Authentication is a pluggable mode selected by `auth.type`](/developer-guides/architecture-decision-log/adr-0074-pluggable-auth-modes.md) — the UI/API surface; this ADR is the ingestion surface's counterpart.
* [Enable security](/configuration-and-deployment/enable-security.md) — the two-surface model and the per-endpoint deployment matrix.
