OMAR
Field NotesCV
Authorisation Rules Belong in One Place
← All Notes
Security15 August 2026 · 3 min read

Authorisation Rules Belong in One Place

When 'who can do what' is scattered across handlers, no one can answer the question. Policy as code makes the rules readable, testable and reviewable.

Ask a team "who can delete a project?" and watch what happens. Someone opens a controller. There is an if checking a role. Someone else remembers a middleware. There is a second check in the service layer that is subtly different. The answer takes twenty minutes and nobody is confident in it.

That is not a documentation problem. It is what happens when authorisation logic lives wherever it was first needed.

Policy as code

Move the decision out of the handlers and into rules that can be read as a unit.

package authz

default allow := false

# Owners can do anything in their own workspace
allow if {
  input.user.role == "OWNER"
  input.user.workspace == input.resource.workspace
}

# Managers can edit content but not users
allow if {
  input.user.role == "MANAGER"
  input.resource.type in {"project", "post"}
  input.action in {"read", "create", "update"}
}

# Everyone can read published content
allow if {
  input.action == "read"
  input.resource.published == true
}

The application asks a question and gets an answer:

const { allow } = await policy.evaluate({
  user, action: 'update', resource: { type: 'project', workspace, published },
})
if (!allow) return fail(res, 403, 'forbidden', 'Not allowed')

Now "who can delete a project" is answered by reading one file. So is "what changed in permissions this quarter" — it is a diff.

Rules you can test

test_manager_cannot_delete_users if {
  not allow with input as {
    "user": {"role": "MANAGER"},
    "action": "delete",
    "resource": {"type": "user"}
  }
}

Authorisation is exactly the kind of logic that should have tests, and exactly the kind that usually does not, because it is spread too thin to test. Consolidating it makes the tests possible; writing the negative cases is what makes them valuable.

Do not over-adopt

Running a separate policy service is real operational weight. For a single application, most of the benefit comes from logical centralisation: one authorisation module, one function every route calls, rules expressed as data rather than nested conditionals. You can adopt that today without deploying anything new.

The dedicated engine earns its keep when several services need the same rules, when the rules change independently of application releases, or when someone outside the team needs to audit them.

The part people forget

Authorising the action is half of it. Also constrain the data: a manager allowed to read projects should get their workspace's projects, not every project with a filter applied in the frontend. Row-level scoping belongs next to the policy decision, or you have built a careful lock on a door with a window beside it.

Resources

SecurityArchitectureOpen Source

Need this built properly?

I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.

Keep Reading