Skip to main content

ADR-005: Permission Overrides

  • Status: Accepted
  • Date: 2026-04-16
  • Author: CTO Agent (KALA-148)

Context

Custom roles cover stable tenant policy, but production systems always need exceptions:

  • temporarily grant a user access to role viewing;
  • revoke a risky permission from a user who otherwise belongs to a broad role;
  • add one department for a project;
  • remove one department from an otherwise shared role.

Creating a new role for each exception would create role sprawl. A full permission table with per-user grants would add more machinery than the current product needs.

Decision

Store overrides directly on User:

extraPermissions      String[]
revokedPermissions String[]
extraDepartmentIds String[]
revokedDepartmentIds String[]

Effective permissions are computed as:

effectivePerm = role.permissions + extraPermissions - revokedPermissions

Effective departments use the same add-then-remove pattern:

effectiveDept = role.departmentIds + primary department + extraDepartmentIds - revokedDepartmentIds

Revokes are applied last and therefore win over base role grants and extras.

Alternatives Considered

Per-user permission join table

Rejected for now. It is easier to query historically, but it adds write paths, joins, and UI complexity before permissions have enough cardinality to justify it.

Custom role per exception

Rejected. It makes the role list noisy and hides the fact that the change is an individual exception.

Deny-only overrides

Rejected. The product also needs temporary grants and extra department access.

Consequences

Positive

  • Exceptions are explicit on the user record.
  • Effective access is deterministic and easy to explain.
  • Grant and revoke APIs can write audit entries with the changed key.
  • Revokes provide a simple deny mechanism without a full policy engine.

Negative

  • Arrays are less normalized than join rows.
  • Permission key renames require array migrations.
  • Too many overrides on one user are a smell that a custom role should be created instead.

Implementation Notes

  • POST /users/:id/permissions/grant adds permission and department extras.
  • POST /users/:id/permissions/revoke adds permission and department revokes.
  • Delete routes remove grant or revoke entries.
  • GET /users/:id/effective-access returns role, overrides, and computed access for debugging.
  • Every override mutation writes an audit log entry.