Disclaimer: This is the point-in-time version of my "Playbook - Permission and Authorisation", which I will keep up-to-date.
Authorisation is a domain capability, not a collection of 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 this matters
This becomes important 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.
| Principal | Action | Resource / container | Conditions | Expected outcome |
|---|---|---|---|---|
| Staff member | case.view |
a case in their organisation | active employment | Allow |
| Case owner | case.update |
their own open case | not locked | Allow |
| Organisation admin | user.invite |
their organisation | MFA completed | Allow |
| Any user | billing.export |
organisation billing data | no finance role | Deny |
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 authorisation 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 rule.
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.
| Layer | Verify |
|---|---|
| Decision-model tests | intended grants, explicit prohibitions, default deny, and boundary values |
| Authorisation-boundary tests | correct request-to-action mapping, canonical resource resolution, trusted data loading, and error handling |
| Endpoint/service tests | denied calls produce no protected data or side effect; allowed calls reach only valid scope |
| Data-access tests | queries, storage access, and exports retain tenant and visibility constraints |
| Regression tests | known historical incidents and cross-tenant attack cases stay denied |
| Change tests | rule/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.
Choose based on the behaviour the system needs, not the novelty of the tool.
A common evolution path
Most systems do not need their final permission model on day one. A common progression is:
Simple role checks
↓
Central authorisation module or service
↓
Resource ownership
↓
Tenant isolation
↓
Attribute conditions
↓
Delegation
↓
Shared policies
↓
Policy engine, if the demonstrated complexity justifies it
This is not a universal sequence or a maturity scorecard. A product may need tenant isolation before ownership, or never need delegation at all. Treat it as a set of capabilities: adopt the next one only when a concrete product, security, or operational need makes the current model insufficient.
Starting simple is normal. The important discipline is to centralise decisions early enough that the model can evolve deliberately, rather than accumulating inconsistent route-by-route checks.
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 rule condition.
- Tenant IDs from the client: trusting an unverified selector or URL parameter as access proof.
- Unscoped list authorisation: permitting
listbut 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
Descubra mais sobre Rafael Bernard Araujo
Assine para receber nossas notícias mais recentes por e-mail.