DWPlatform lifecycle
Field guide · access control

Who may do what — decided in one place, on every single request.

Six swimlane diagrams covering every way a decision gets made in DWPlatform: a person clicking a button, an administrator handing out access, a colleague covering a delegation, an engineer breaking glass in an incident, and one service calling another. They all run through the same evaluation, because the moment one of them gets a shortcut, that shortcut is the security model.

Updated 27 Jul 2026 6 BPMN swimlanes 90 permissions · 7 scopes

The five rules everything else follows from

These are not aspirations written on a wiki. Each one is enforced by code, and each one exists because its absence has a known, specific failure mode.

Rule 01

No role checks in code

Roles are bundles of permissions, nothing more. Nothing branches on a role name. Grepping for one outside the role-definition module returns zero.

Rule 02

One entry point

Every protected operation calls the same function. Ad-hoc checks scattered through controllers are how two endpoints that should agree quietly stop agreeing.

Rule 03

The server is the only gate

The screen receives a permission list purely to decide what to draw. Assume the client is hostile: it can be edited, and eventually it will be.

Rule 04

Deny by default

Absence of a grant is a refusal. There is no implicit inheritance — a scope that should cover both a manager and their reports lists both, explicitly.

Rule 05

No permission without a scope

There is no such thing as salary.read. There is only salary.read over a defined set of people. A permission with no scope is a permission over everybody.

Why rule 5 is the one that bites. A permission list is easy to reason about and easy to get wrong. In an earlier build, an administrator correctly held payslip.read at self scope — and the payroll list still returned every row in the tenant, because the gate could only ask “do you hold this at all?” An attribute has no resource, so it cannot ask “over whom?”. That question has to be answered inside the query, which is why every list, search, report and export composes a scope filter into its SQL rather than filtering the rows after fetching them. Filtering afterwards still leaks the row count and the pagination total, which is often the sensitive part.

The shape of it

Authorization lives in one service. Every other module asks it, briefly caches the answer, and re-asks when a version stamp moves. Nothing about authorization is baked into the login token any more.

Browser renders what it is told it may HR · WorkFlow Drive · Mail · Chat… authorize() scopeFilter() · visibleFields() svc:hr · svc:workflow services are principals too The engine in DWPlatform.Identity grants · denials · delegation break-glass · SoD · org chart append-only audit SQL Server row-level security is the outer tenant wall; scope narrows inside it asks token carries NO authorization
What changed, in one line. The login token used to carry the answer. It now carries only who you are and which tenant you are in — dropping it from 4,593 characters to 2,408. Access is looked up live, so revoking something takes effect in seconds instead of waiting out a 24-hour token.

1 · One decision

This is the whole model. Everything below is a variation on it.

Read the refusal branches, not the happy path. Denials beat grants, so an explicit “this manager must not see compensation during the review cycle” cannot be undone by someone helpfully adding a role. Validity dates are evaluated on every check rather than at login, so a grant that expires at noon stops working at noon, not at next sign-in. And the refusal is recorded: repeated denials are the clearest signal that either a permission is misconfigured or somebody is trying doors.

2 · Handing access out

Escalation needs two people

You cannot grant yourself something you do not already hold. This is refused before the conflict check even runs, which is why self-escalation returns 403 rather than 409 — both are refusals, and the order is deliberate.

Conflicts are data, not memory

The pairs that must never meet in one person — preparing a payroll run and releasing it, changing a bank account and releasing payment, proposing and approving the same raise — live in a table, checked when access is granted and again when it is used. A rule enforced only at grant time is a rule that lapses the first time data changes underneath it.

3 · Covering for someone

Three properties make delegation safe rather than a second identity: it can never exceed what the delegator holds (re-checked at use, because they may have lost it since), it must expire, and it is never transitive — a delegate cannot re-delegate. Every action reads “X on behalf of Y” in both the audit log and the visible approval trail. Recording it as Y would be forgery, however convenient the screen looks.

4 · Break-glass

The uncomfortable trade-off, stated plainly. A system with no emergency door gets one anyway — usually a shared admin account with a password in a chat thread. So the door is built in, but it is loud: a typed justification that must be substantial, a 60-minute limit, an immediate alert to the security channel, and every action taken through it flagged for review. The point is not to prevent the emergency. It is to make the emergency impossible to hide.

5 · One service calling another

This is the diagram that came from a real, silent failure. HR posted org-chart updates to Identity using a token whose subject named nobody. The permission check did the correct thing and refused — and the client logged a warning and carried on. Nothing broke visibly. But the org chart is what self, direct_reports, reporting_chain and department all resolve through, so an org chart drifting behind the employment record means access quietly computed from last month's reporting lines.

The fix is not an exemption for “trusted callers”. That is ambient authority, and an exemption is never scoped and never audited. A service is now a principal in its own right: it holds grants, in a named tenant, evaluated by the same code as a human, and its actions are attributed to it in the audit log — never to a person who was not there.

6 · Access that maintains itself

Manager access is derived from the org chart at the moment of asking, never stored as a grant. A transfer therefore needs no cleanup task and no offboarding checklist item: the old manager loses visibility because they stopped being the manager, not because somebody remembered to revoke something. The permissions that get forgotten are the ones that had to be remembered.

The seven scopes

Scopes do not nest. If a bundle should cover the holder and their reports, it lists both — explicit beats clever, and every implicit inheritance rule is a rule somebody will misread.

ScopeCoversResolved from
selfOnly the actor's own recordThe person bound to the account
direct_reportsPeople reporting straight to themOrg chart, live
reporting_chainEveryone beneath them, to any depthOrg chart, live
departmentA department and its sub-departmentsTree-path prefix
legal_entityOne company inside the tenantCompany on the employment record
locationOne branch or siteBranch on the employment record
allThe whole tenant — never widerRow-level security is still the outer wall

Measured against the platforms people compare us to

An honest scorecard is more useful than a confident one. The comparison below is against the access models of Workday and SAP SuccessFactors (HCM), Microsoft 365 and Google Workspace (collaboration), and ServiceNow (multi-tenant operations). Where we fall short, it says so.

PracticeWhat the best doDWPlatform
Role/permission splitRoles are bundles; nothing evaluates a role name Meets. 90 permissions, 17 bundles, zero role-name branches in code
Target populationSuccessFactors pairs a permission role with a target population Meets. Seven scopes, org-derived, composed into the query rather than applied after
Relationship accessDerived from the org chart at evaluation time Meets. Never stored as a grant, so a transfer needs no cleanup
Field-level securityRecord access does not imply field access Meets, and slightly beyond. Pay can be coarsened to a band rather than hidden, so a manager can plan without reading salaries. Masked values are absent or explicitly redacted — never a plausible fake
Separation of dutiesConflict pairs refused at grant and at call time Meets, with one residual. Seeded as data, refused at grant time, and re-checked on every authorization call — which is what catches a conflict that appeared after the grant: a role edit, a delegation handing over the other half, a future-dated grant coming into force, or a new pair added to the table retroactively. Refusal names the pair rather than returning a generic 403, and sits above break-glass: an emergency is for an unreachable approver, not for completing both halves of a separated duty. Self-approval stays hard-coded impossible. The residual: a pair marked same-record-only can only be judged when the request names the record, so a write endpoint that gates on the permission alone, without saying which record it is about to change, gets no per-record enforcement
DelegationTime-boxed, non-transitive, recorded as on-behalf-of Meets. Subset re-validated at use, not only at creation
Service identityGoogle Cloud service accounts, Microsoft Graph app-only permissions Meets. Services are principals holding tenant-scoped grants, evaluated by the same engine, audited as themselves
Mid-session revocationShort-lived answers, refreshed on change Meets. The token carries no authorization; a version stamp forces a refetch within seconds
Audit of denialsDenials logged, not just successes Meets. Append-only, enforced by database permission rather than by convention
Break-glassJustified, time-boxed, alerted, flagged Meets. 60 minutes, substantial justification required
Access certificationPeriodic recertification campaigns — managers re-attest who holds what Gap. Grants are visible and revocable, but nothing schedules a review. Entitlement creep is the normal failure mode of a system this expressive
Policy simulation“Explain this decision” and “what would change if…” tooling Partial. A single decision can be checked and a scope resolved, but there is no what-if against a proposed grant
Customer-managed keys / residencyPer-tenant encryption keys, regional pinning Gap. Not applicable yet at this scale, but it is what enterprise procurement asks for on day one
ABAC on record attributesConditions on record state, not just relationship Partial. Grants carry a constraint, and an unparseable one narrows to nothing rather than widening — but it is not a general condition language
The honest summary. On the mechanics of deciding access, this is at parity with the systems it is measured against, and ahead of most regional HR products in two specific places: pay coarsened to a band rather than hidden, and a scope filter composed into the query so list totals cannot leak. The real gaps are governance rather than enforcement — nothing schedules a recertification, and there is no way to ask what a proposed grant would change before making it. Those are the next two things worth building, and neither is hard; they were simply not what this program set out to do.

What we deliberately did not do

No “trusted service” flag

It would have closed the service-to-service gap in an afternoon. It also would have created an unscoped, unaudited path that every future integration would reach for. A service holding real grants costs more to set up and cannot be quietly widened.

No stale answer on failure

When the engine is unreachable, callers hold nothing rather than their last known permissions. An outage therefore refuses work instead of opening it. Serving a cached set on failure would reopen exactly the revocation hole this design exists to close.

No permission renames

The catalogue may be added to, never renamed. A rename is invisible at compile time and silently turns a grant into a no-op — which reads as “that person lost access for no reason” months later.

No role names in routing

Where a role name is still used — a chief executive's queue reaching their secretary, a post addressed to a desk — it names who to route to, never what they may do. Membership and authority are looked up separately and deliberately.