Playbook – Permissions and Authorisation

Treat authorisation as a domain capability, not scattered endpoint conditionals. For every protected operation, decide consistently whether a verified principal may perform a business action on a specific resource in a defined context—and enforce that decision server-side before data or side effects are exposed.

Choose the simplest mechanism that preserves one consistent, server-side decision model. A few stable rules may be clearer as centrally tested application code; a policy engine becomes valuable when rules are expressive, auditable, shared across services, or need to evolve independently of individual endpoints.

To definitions:

Permission is a grant or entitlement.

Authorisation is the request-time decision and server-side enforcement that applies those grants to a principal, action, resource, and context.

When to use this playbook

Use this when a system has any of the following:

  • users with more than one role or organisation;
  • tenant, ownership, project, folder, account, or team boundaries;
  • permissions that depend on resource state, relationships, time, location, MFA, or other trusted context;
  • customer-configurable or delegated access;
  • several services or endpoints that must make the same access decision;
  • audit, compliance, support, or incident needs that require explaining why access was allowed or denied.

A small internal tool with a stable administrator/member distinction may need only a simple, centrally tested server-side check. Do not introduce a policy engine merely to avoid a short, clear rule.

The mental model

Keep authentication and authorisation distinct

  • Authentication: who is making this request? Verify identity and session/token integrity.
  • Authorisation: may that verified identity perform this action on this resource now?
  • Entitlements / relationships: which roles, tenants, ownership relations, subscriptions, and delegations are true?
  • Enforcement: where is the decision applied before sensitive data or side effects occur?

An authenticated user is not automatically authorised. Client-side checks improve the user experience but never replace server-side authorisation. A role label alone is not a complete permission model.

Separate permissions from business rules

Permission answers whether a principal may perform an action on a resource:

Can Alice approve invoice #123?

Business rules answer whether that action is valid in the resource's current state:

Can an approved invoice still be edited?

These are different questions. An authorised user can still fail business validation, and a valid workflow transition can still be forbidden for that user. Workflow state may be a deliberate authorisation input, but permissions must not become a substitute for state-transition validation or domain invariants.

Ask the same question everywhere

Express every authorisation decision as:

Can principal P perform action A on resource R in context C?

Use business actions such as invoice.approve, project.member.invite, or document.download, rather than HTTP verbs or UI labels. Make the resource explicit even for create and list operations, where the resource is normally the destination container or tenant.

Design for default deny and least privilege

Start with no access. Add narrow, reviewable grants only where a business rule justifies them. Keep explicit prohibitions for non-negotiable guardrails, such as suspended accounts, cross-tenant access, or actions requiring MFA.

Every server-side path that reads protected data, changes state, invokes a privileged integration, or produces a sensitive export needs an enforcement point. Database row-level security, query predicates, and storage access controls remain necessary defence-in-depth; an application authorisation decision does not replace them.

Build the model before choosing the mechanism

1. Map protected capabilities

Create an authorisation matrix from the domain, not from framework routes.

PrincipalActionResource / containerConditionsExpected outcome
Staff membercase.viewa case in their organisationactive employmentAllow
Case ownercase.updatetheir own open casenot lockedAllow
Organisation adminuser.invitetheir organisationMFA completedAllow
Any userbilling.exportorganisation billing datano finance roleDeny

Include the negative cases deliberately: cross-tenant access, inactive users, deleted/locked resources, delegated access expiry, and privileged actions without step-up authentication.

2. Model the stable domain nouns and relationships

Identify principals, resources, containers, roles/groups, and the relationships that determine access. Use opaque, immutable IDs in policy-facing data; display names, email addresses, and mutable slugs are poor policy identifiers.

For a multi-tenant system, model the tenant/organisation as a first-class container. A resource should have a clear containment path, and a principal’s tenant membership should be supplied from a trusted source. Do not rely on a tenant ID supplied by the browser without resolving and validating it server-side.

3. Identify the enforcement boundaries

Document where checks occur for:

  • single-resource reads, updates, deletes, and downloads;
  • resource creation and listing (against the target container);
  • batch operations (per resource or via a deliberately designed compound decision);
  • asynchronous jobs, webhooks, internal APIs, and service accounts;
  • data stores, object storage, search indexes, and reporting/export paths.

A middleware can be a useful adapter, but it is not the model. The policy decision must be close enough to the domain operation that route additions cannot silently bypass it.

4. Define the data contract

Authorisation is only as trustworthy as its inputs. Define the canonical source, freshness expectation, and ownership for each principal attribute, resource attribute, and relationship. Normalise application data into a single request model before evaluating a policy.

Keep principal, action, and resource facts in their respective models. Use request context only for transient, action-specific facts such as an MFA assertion, request time, or an IP/network signal—not as a duplicate source for identity, role, tenant, or resource ownership.

Permission implementation practices

Address listings, creates, and batches deliberately

  • Create: authorise against the verified destination container before creating the resource.
  • List: authorise the container-level list action and apply equivalent tenant/visibility constraints in the query. A list decision alone does not make every item safe to reveal.
  • Batch: evaluate each target resource or use a deliberately designed compound authorisation rule; do not authorise only the first item.
  • Move/share: evaluate all affected source and destination relationships.

Make rule changes a controlled deployment

Version authorisation rules, their supporting model and mapping code, and regression tests together. Validate rules and their configuration in CI. For dynamically managed rules, use staged rollout, approval, rollback, and versioned audit records. Treat model-contract changes as compatibility changes: a change to attributes, relationships, or resource types can alter existing decisions unexpectedly.

Observe decisions without leaking data

Record enough to investigate: request/correlation ID, principal/resource type and opaque IDs where appropriate, action, decision, applicable rule identifiers and version where available, evaluation errors, and latency. Protect logs and avoid raw tokens, sensitive attributes, or full rule payloads unless access and retention are explicitly controlled.

Test strategy

Test authorisation as executable security behaviour, not merely syntax or configuration.

LayerVerify
Decision-model testsintended grants, explicit prohibitions, default deny, and boundary values
Authorisation-boundary testscorrect request-to-action mapping, canonical resource resolution, trusted data loading, and error handling
Endpoint/service testsdenied calls produce no protected data or side effect; allowed calls reach only valid scope
Data-access testsqueries, storage access, and exports retain tenant and visibility constraints
Regression testsknown historical incidents and cross-tenant attack cases stay denied
Change testsrule/model migrations preserve decisions intentionally and make changed decisions explicit

For every new permission capability, include at least one must allow, must deny, and must not leak scenario. Test actions—not UI controls—because direct API calls and background execution bypass the UI.

Choose the authorisation mechanism

Use centrally tested application code when the rules are few, stable, local to one service, and readily understood from the domain model. Keep the decision behind a small authorisation boundary rather than copying checks across routes or handlers.

Introduce a policy engine when the model combines roles with ownership, relationships, tenant containment, and attributes; when the same decision must be made in several services; or when policy review, simulation, traceability, or controlled change is a real requirement. A policy engine does not remove the need for trusted data, enforcement at every boundary, query scoping, or governance of who may change access.

Operational checklist

Before implementation

  • [ ] Define the decision to centralise and the protected business outcome.
  • [ ] Inventory all entry points and data/side-effect paths for that domain.
  • [ ] Name domain actions, resources, containers, and trusted attributes.
  • [ ] Write allow and deny scenarios, including cross-tenant and stale/disabled-user cases.
  • [ ] Identify source, freshness, and owner of each authorisation fact.
  • [ ] Choose simple central checks or a policy engine based on demonstrated complexity.

Before rollout

  • [ ] Authorisation rules and configuration validate in CI.
  • [ ] Rule changes have review, audit, and rollback paths.
  • [ ] Every protected operation uses the authorisation boundary or an equivalent verified enforcement path.
  • [ ] List, batch, create, move, export, background-job, and service-account paths are covered.
  • [ ] Decision telemetry captures implementation version and evaluation errors safely.
  • [ ] Existing data/query/storage controls still enforce tenant and ownership boundaries.
  • [ ] Negative and regression tests prove denial causes neither disclosure nor side effect.

After rollout

  • [ ] Inspect allow/deny rates, diagnostics, latency, and unexpected denials.
  • [ ] Confirm no route or job bypasses the enforcement boundary.
  • [ ] Review exception policies, stale roles, delegations, and privileged access on a defined cadence.
  • [ ] Record incidents and confusing access requests as new regression cases or model improvements.

Anti-patterns

  • UI-only permission checks: hiding a button while leaving the API callable.
  • Route-by-route conditionals: duplicating unreviewed logic until equivalent rules disagree.
  • Role explosion: adding a new role for every exception instead of modelling the relationship or policy condition.
  • Tenant IDs from the client: trusting an unverified selector or URL parameter as access proof.
  • Unscoped list authorisation: permitting list but returning objects the caller may not read.
  • Authorisation as a magic boundary: assuming one decision protects data paths it does not govern.
  • Rule/model drift: changing domain relationships or configuration without revalidating affected authorisation decisions.
  • Ignoring decision errors: treating failed or incomplete evaluations as harmless without monitoring their impact.
  • Mutable identifiers in rules: binding access to email addresses, names, or human-facing slugs.
  • No explanation trail: being unable to answer who granted access, why it applied, and when it changed.

References