# A Practical Specification for Roles, Permissions, and Policy Decisions

> The full specification is available here: [Authorization Model Specification](https://mahdavipanah.github.io/authorization-model/).

Authorization logic often starts simple.

At first, a role like `admin`, `editor`, or `viewer` is enough. Then the product grows. You add organizations, projects, machine clients, service accounts, field-level rules, exceptions, audit logs, and suddenly the question "can this principal do this action?" is answered differently in different parts of the codebase.

This specification is my attempt to make that question explicit, portable, and easy to evaluate.

The goal is not to define authentication, token issuance, identity provisioning, or transport security. Those are separate concerns. This spec focuses only on authorization decisions: how roles, permissions, scopes, and policy evaluation should work together.

* * *

## The Core Idea

The model is built around a small chain:

```plaintext
Principal -> Role -> Permission -> allow | deny
```

A principal can be a human user, a service account, or a machine-to-machine client. A role is a named bundle of permission statements, such as `auditor`, `editor`, or `billing-admin`.

Roles are not magical. Their names do not grant authority by themselves. A role only matters because of the permission statements it contains, and a principal only receives authority through an explicit binding:

```plaintext
(principal, role, scope)
```

This is important because it keeps the model auditable. If someone has access, there should be a concrete binding and a concrete permission statement that explains why.

* * *

## The Design Principles

The specification is based on five principles:

**Default-deny**: if nothing explicitly allows an action, the answer is deny.

**Deny-overrides**: if both an allow and a deny match the same request, the deny wins.

**Explicit over implicit**: authority is never inferred from role names, group names, or conventions.

**Decision and enforcement separation**: application code should ask a Policy Decision Point (PDP) for a decision. It should not spread authorization rules across controllers, services, and handlers.

**Auditability**: decisions should be loggable with enough context to reconstruct what happened.

These principles make the model predictable. There is no hidden privilege, no role-name guessing, and no ambiguous conflict resolution.

* * *

## Permission Strings

Permission statements are serialized as compact strings:

```plaintext
<organization>:<service>/<resource>[:<field>[:<resource_id>]]/<effect>/<action>
```

For example:

```plaintext
acme:api/suppliers/allow/update
```

This means: in the `acme` organization, for the `api` service, allow updating `suppliers`.

The format also supports field-level and instance-level rules:

```plaintext
acme:api/contacts:email/allow/read
acme:api/suppliers:*:12345/deny/read
```

The first statement allows reading only the `email` field of contacts. The second denies reading supplier `12345`.

Wildcards are supported too:

```plaintext
acme:api/suppliers/allow/*
acme:api/suppliers/deny/delete
```

Together, these allow every action on suppliers except deletion.

The string format is intentionally compact enough to be transported in places like JWT claims, while still being readable for operators and auditors.

* * *

## How Evaluation Works

The evaluator receives a request:

```plaintext
(principal, action, resource_uri)
```

Then it evaluates the principal's effective permission set in three steps.

First, it keeps only the permission statements that match the request. A segment matches if it is exactly equal to the request segment or if it is `*`.

Second, if any matching statement has `effect = deny`, the result is deny.

Third, if at least one matching statement has `effect = allow` and no deny matched, the result is allow. Otherwise, the result is deny.

In pseudocode:

```python
def evaluate(request, permissions):
    applicable = [p for p in permissions if matches(p, request)]

    if any(p.effect == "deny" for p in applicable):
        return Decision.DENY

    if any(p.effect == "allow" for p in applicable):
        return Decision.ALLOW

    return Decision.DENY
```

One important detail: the evaluator is specificity-agnostic. A specific deny does not beat a wildcard allow because it is more specific. It wins because all denies override all allows.

For example:

```plaintext
acme:api/suppliers/allow/read
acme:api/suppliers:*:12345/deny/read
```

Reading suppliers is generally allowed, but reading supplier `12345` is denied.

* * *

## What This Spec Is For

This specification is useful for teams building a Policy Decision Point, integrating a Policy Enforcement Point, or simply trying to make authorization rules easier to audit.

It gives you:

*   a portable permission string format
    
*   clear role and binding semantics
    
*   default-deny behavior
    
*   deny-overrides conflict handling
    
*   a small evaluation algorithm
    
*   decision logging requirements
    

The main benefit is consistency. Instead of each service inventing its own interpretation of roles and permissions, services can ask the same kind of question and receive the same kind of answer.

Authorization should be boring, predictable, and explainable. That is what this spec is designed to support.

You can read the complete spec, including the grammar, validation rules, examples, and versioning notes, at [mahdavipanah.github.io/authorization-model](https://mahdavipanah.github.io/authorization-model/).
