ADR-0003: The catalog is read-collaborative — only mutations are permission-gated

Once authenticated, any ODD user can read the whole catalog — entities, lineage, search, activity. Only mutations are permission-gated; reads are authenticated-only by design.

Status

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

Context

A data catalog earns its value from discovery — people finding datasets, tracing lineage, and reusing what others have built across an organisation. That goal pulls against fine-grained read access control: if every entity carried a per-owner or per-role read ACL, users could not discover what they cannot see, and the catalog would fragment into silos that defeat its purpose. The platform had to choose where, on the read side, to sit on that spectrum.

ADR-0002 established how authorization is wired — one central SECURITY_RULES table and a catch-all that closes the surface. ADR-0002 deliberately left what that means for reads to this record.

Decision

The platform is read-collaborative: every gated rule in SECURITY_RULES guards a mutation, and reads fall through to authenticated-only. The rule table is made up almost entirely of POST/PUT/DELETE/PATCH rows — create, update, delete, regenerate — each requiring a specific permission. GET endpoints are, with one deliberate exception, not in the table, so they hit the pathMatchers("/**").authenticated() catch-all (ADR-0002): any authenticated user can read any entity, its lineage, the catalog search, and the activity feed, with no role, owner, or per-resource check.

The one gated read is the owner-association management queue (GET /api/owner_association_requestOWNER_ASSOCIATION_MANAGE) — an administrative surface, not catalog content.

Owner-scoping is available but opt-in: a user can narrow to their own entities through the dedicated /api/dataentities/my, /my/upstream, /my/downstream reads (and the my_objects search flag), but the default read is the whole catalog. Scoping is a convenience the caller asks for, not a boundary the platform imposes.

Consequences

  • Discovery works without read modelling. Any authenticated user can search, browse, and trace the entire catalog; there is no per-entity read ACL to author or maintain, and nothing is hidden from a logged-in user because it belongs to someone else.

  • There is no read-level isolation between users. Read-collaborative means exactly that: catalog metadata — names, descriptions, schemas, lineage, ownership, activity — is visible to every authenticated principal. An operator who must restrict who can see which metadata cannot do it with in-app permissions; that boundary has to be drawn outside the application (separate deployments, network segmentation). This is the deliberate trade-off of a collaboration-first catalog, and operators should size their deployment boundary accordingly.

  • Write and read are asymmetric by design. Editing is permission-gated per SECURITY_RULES; reading is not. A contributor adding a mutating endpoint must add its rule (ADR-0002); a new read endpoint inherits the authenticated-only floor automatically — which is the intended default, not an oversight.

  • This posture assumes an authentication mode is enabled; whether authentication is required at all is a separate decision (the platform's auth modes), covered by the security-configuration docs.

Evidence

  • odd-platform-api/.../auth/util/SecurityConstants.java:98-355SECURITY_RULES: every rule guards a mutating method (POST/PUT/DELETE/PATCH), with a single GET exception at :148-150 (/api/owner_association_requestOWNER_ASSOCIATION_MANAGE, the owner-association admin queue). No catalog read (entity, lineage, search, activity) appears in the table.

  • odd-platform-api/.../auth/authorization/AuthorizationCustomizer.java:20-31 — permits the whitelist, registers each SECURITY_RULES row, then closes with .pathMatchers("/**").authenticated() (:29-30) — the catch-all that makes every un-listed read available to any authenticated user.

  • odd-platform-api/.../controller/DataEntityController.java:284-300getMyObjects / getMyObjectsWithDownstream / getMyObjectsWithUpstream, the opt-in owner-scoped reads (/api/dataentities/my[/upstream|/downstream]); the default catalog read carries no owner predicate.

See also

  • ADR-0002 — Centralised path-matcher authorization — the mechanism behind this posture: the one rule table and the authenticated() catch-all that reads fall through to. ADR-0002 names this record as where the read posture is decided.

  • Enable security — how operators turn on authentication and authorization.

  • Permissions — the permission model the mutation rules check (and which reads do not).

Last updated