Access Control

This is the standard the Quality Gate's Security lens holds work to for knowing who a user is and deciding what they may do. It expands the Security Policy β€” which sets the organisational rules (account MFA, least privilege, quarterly access review) β€” into a working standard for the authentication and authorization built inside an application: how we procure it, how we model it, and how we wall off the most dangerous surface of all, the admin plane. It sits under the Infrastructure Planning Policy's Zero Trust posture ("never trust, always verify") and works alongside the Database Guidelines, because the last line of defence for row-level rules is the database itself. Deviations are allowed, but β€” as everywhere in the handbook β€” they must be deliberate and justified in the project's design notes.

Access control is where OSBR's values point outward, at the people whose data we hold. Be Kind: a home-grown login screen can look identical to a bought one and be one missing check away from leaking every account, so we do not make users the test subjects for auth code a hardened solution would have gotten right. Be Strong: we choose the boring, adversarially-tested path over the flattering one of building it ourselves, and we hold the admin boundary as a hard wall, not a role flag. Be Nice: the authorization model is documentation a teammate reads to learn who may touch what, so it lives in one legible place, in the same pull request as the data it guards. Humans and AI agents build these controls as collaborators, and both are held to exactly the same bar.

How to read this policy

1. Goal

Every protected action in an OSBR system is guarded by an access decision that is procured responsibly, modelled at the lowest sufficient power, enforced where it cannot be bypassed, and β€” for administrative capability β€” entered as a separate, logged, deliberate act. Concretely:

A control that does not serve one of these goals is either missing or waste. We optimise for a decision a reviewer can read end to end, not for a policy engine nobody yet needs β€” unread complexity someone debugs at 3am is not "secure".

2. Responsibility

3. Practices β€” Procure authentication before you build it

3-1. Buy-before-build is the default; the burden of proof is on building

The oldest rule in the field applies: do not roll your own auth or crypto. The failure modes β€” credential theft, session fixation, account takeover, privilege escalation β€” land on users, not on us.

3-2. Express auth against named standards, not improvisation

Auth requirements and designs MUST be grounded in named standards so the evaluation is checkable rather than improvised:

3-3. Keep authentication separate from authorization

The two concerns MUST NOT be conflated in design or procurement:

3-4. Document what the chosen solution does not cover

Every auth decision MUST record its gaps β€” what the solution explicitly does not handle β€” so those gaps are owned rather than silently assumed away. State coverage for, at minimum: authorization / fine-grained permissions (usually ours, not the provider's); account recovery and de-provisioning; audit logging of auth events; session revocation and token lifetime; data residency of the identity store and PII; and rate limiting / brute-force and enumeration protection at our edges. A gap that is written down is a task; a gap that is assumed covered is an incident.

3-5. If you must build, treat the primitives as library, not project, code

If custom auth is genuinely unavoidable, the crypto and session/token primitives are library code, not project code (per Style Guide): use vetted, standard implementations (a maintained OIDC library, a standard password-hash such as Argon2 via its reference library), never a bespoke algorithm. Reserve original code for the authorization domain logic, which is legitimately ours. The bar to clear before any custom auth code is scoped: the written evaluation (Β§3-1), an AAL target (Β§3-2), an authn/authz boundary statement (Β§3-3), and a documented gap list (Β§3-4) β€” all four must exist first.

4. Practices β€” Choose and place the authorization model

4-1. Climb the escalation ladder; stop at the first rung that states the rule

Authorization models trade expressiveness for complexity. Pick the least powerful model that can state your rule and escalate only when a concrete, named rule cannot be expressed at the current rung β€” least privilege applied to the mechanism itself, not just the grants.

  1. Owner check β€” "the actor owns this row / is the actor." A single ownership predicate (resource.owner_id == actor.id). Most CRUD apps never need more. Start here.
  2. RBAC (roles) β€” permissions attach to named roles, users hold roles. Reach for it when access depends on a job function (admin, editor, viewer) rather than ownership. This is the NIST RBAC model (ANSI INCITS 359): roles, role hierarchies, and permission assignments.
  3. Admin escape hatch β€” a coarse "staff can see everything" capability is fine as an explicit, audited RBAC role (and see Β§5). Keep it deny-by-default for everyone else.
  4. ABAC / ReBAC / policy engine β€” only when a rule depends on attributes (time, location, resource state, clearance) or on relationships between objects that roles cannot enumerate ("editors of the parent folder", "members of the owning team"). ABAC is defined in NIST SP 800-162; relationship-based access at scale is the Zanzibar model. Externalise policy with a tool such as Open Policy Agent only when policy genuinely needs to live outside application code (multiple services sharing one policy, non-developer policy authors).

The trigger to move up a rung is a specific rule you cannot currently express ("a contractor may edit a document only during the project window, and only if they belong to the owning team"). "We might need ABAC later" is not a trigger. YAGNI applies to authorization models too.

4-2. Deny by default

The absence of an explicit grant is a denial. New endpoints, new columns, and new resource types are inaccessible until a rule says otherwise β€” never accessible until a rule forbids them. This is fail-safe defaults (Saltzer & Schroeder) and the baseline expectation of OWASP ASVS: access control fails closed and is enforced on the server.

4-3. One auditable authorization layer

The authorization decision lives in one place a reviewer can point to β€” a single middleware, guard, or policy module β€” not smeared across controllers, view templates, and query builders. A reviewer MUST be able to answer "where is this checked?" with one file, and "what does it decide?" by reading it.

4-4. Enforce row-level rules at the database

When access is scoped to rows ("a user sees only their own orders", "a tenant sees only its own data"), enforce it in the database with row-level security (for example, PostgreSQL Row-Level Security), not only in application queries. Application-only filtering is one forgotten WHERE clause away from a cross-tenant leak; a database policy holds even when a query forgets. This is defence in depth, consistent with the Infrastructure Planning Policy's "assume breach". Row-level rules belong in the database as a backstop β€” coarser role and ownership decisions can still live in the single application layer (Β§4-3). Use both: the app layer for the readable decision, the database for the guarantee.

4-5. Document the authz model in the same PR as the schema

The authorization model is part of the data model. When a PR adds or changes a table, that same PR states, in prose or a short table: who may read/write these rows, under what rule, and where the check is enforced (owner predicate, RBAC role, database policy, external policy). Schema and its access rules are reviewed together or not at all. Grants SHOULD be reviewed periodically and the unused ones removed, per the Security Policy's quarterly access review.

5. Practices β€” Isolate administrative functions

An admin panel is not "the app with more buttons." It is a different blast radius: a stolen session on the user surface exposes one account; the same on the admin surface exposes the whole tenant. We model the admin surface as the system's control plane and keep it architecturally distinct from the data plane that serves users.

5-1. A separate surface, not a route prefix

Administrative functions MUST be served from a surface distinct from the user application:

5-2. A separate authentication event (step-up, MFA minimum)

Reaching the admin surface MUST require its own authentication event:

5-3. Just-in-time, not standing privilege

Follow Privileged Access Management (PAM) practice: privilege is granted when needed and expires, not held permanently.

5-4. Break-glass (emergency access)

There MUST be a documented break-glass path for when normal admin auth is unavailable (identity-provider outage, locked-out operator):

5-5. Audit every admin write β€” immutable, attributable, retained β‰₯ 1 year

Every administrative create / update / delete MUST produce an audit record. The boundary is only trustworthy if we can prove, after the fact, who did what.

"Immutable" is right-sized: object-lock / WORM on the log store plus an isolated account is enough. You do not need a dedicated SIEM to satisfy this policy β€” you need a trail the logged party cannot alter and a retention clock of at least a year.

6. Anti-patterns

The failure modes this policy exists to prevent:

References

Foundations & least privilege

Authentication & identity (NIST / protocols)

Authorization models

Privileged access & audit

Related OSBR standards