App reference

ADR-0002: Centralised RPC authorization behind auth.Allow

Metadata

  • Status: Accepted
  • Date: 2026-07-22
  • Deciders: EagraΓ­ Clainne Team
  • Context: Shipped in refactor(auth): centralise RPC authorization behind auth.Allow
  • Related: ADR-0009 (service-layer authorization, formerly RFC 003); the domain-behaviour architecture review (candidate D)

Context

The permission matrix and role helpers live in pkg/roles. internal/auth carries a thin re-export shim over that package β€” auth.Role, auth.RoleAdmin/Member/Child/Guest, auth.AllRoles, auth.ValidateRoles, auth.Permission, auth.PermissionMatrix, and auth.CheckPermission all forward to pkg/roles. Service code and the test suites import the auth aliases rather than pkg/roles directly.

Until now the interceptor made the RPC-level authorization decision inline, and did so twice β€” once in WrapUnary, once in WrapStreamingHandler β€” each path re-typing the same four steps: public-endpoint bypass, authenticate, parseProcedure + CheckPermission, and WithUser. The authorization decision was therefore split across parseProcedure (Connect procedure format) and CheckPermission (matrix lookup), with no single seam that answered "may this caller invoke this procedure?".

The domain-behaviour review (candidate D) proposed two things: collapse the re-export shim, and deepen authorization into one seam. This ADR records what we did with each.

Decision

  1. RPC-level authorization is one seam: auth.Allow(procedure, roles) bool. Allow owns both halves of the decision β€” decomposing the Connect procedure into service/method (parseProcedure), and consulting the matrix (roles.CheckPermission). A procedure that does not parse, or that no matrix entry covers, is denied. The interceptor no longer decomposes procedures or walks the matrix; it asks Allow.

  2. The auth+authz sequence is shared, not duplicated. A private (*Interceptor).authorize(ctx, procedure, headers) runs the public-endpoint bypass, authenticate, Allow, and WithUser, returning the user-scoped context or a Connect error. Both WrapUnary and WrapStreamingHandler call it, so the flow lives in one place.

  3. The internal/auth re-export shim is deliberately retained. We did not collapse it. Deleting it would rewrite the import in every service call site (create.go, assignroles.go, the integration auth context) and roughly two hundred lines of role/permission tests, for no behavioural change. The shim compiles, is stable, and is not the source of any friction today.

Consequences

Positive

  • The interface is the test surface. Authorization is asserted against procedure strings in authorize_test.go β€” no JWT, no interceptor, no request flow. TestAllow covers allow/deny, unparseable procedures, missing methods and unknown services.
  • Locality. "May this call proceed?" is answered in one function. The unary and streaming paths can no longer drift apart, because they share authorize.
  • Leverage. parseProcedure is now an implementation detail of Allow; the interceptor is blind to the procedure format.

Negative / trade-offs

  • Two ways to reach the matrix remain. New code should call roles.CheckPermission (or auth.Allow) directly; the auth.CheckPermission re-export persists for the existing callers and tests. This is the price of not doing the wide sweep, and is revisited below.
  • Allow couples the auth package to the Connect procedure format. That is the right home for it β€” auth is the transport-facing authorization module β€” but pkg/roles deliberately stays format-agnostic.

Alternatives considered

  • Collapse the re-export shim now. Rejected for the reason in Decision (3): a large mechanical change to services and ~200 lines of tests with no payoff while the shim causes no friction. If a future change makes the split load-bearing β€” a second consumer of the matrix, a divergence between the auth and roles role sets, or a move of authorization off the RPC edge β€” reopen this ADR and do the sweep then. Recorded here so the architecture review does not re-suggest the deletion in the meantime.
  • Put Allow in pkg/roles. Rejected: it would teach the pure roles/matrix package about the Connect procedure string format. The matrix stays keyed by service/method; the format knowledge stays in auth.