class

KemalIdentity::Authz::RBAC

Inherits KemalIdentity::Authz::Authorizer < Reference < Object

Role-based authorization over a Repository, with tenancy.

The order the checks run in

  1. Is the permission declared? An undeclared name is a typo or a half-finished rename, and it is refused before anything is read.
  2. Is the principal bound to a different tenant? A session issued inside tenant A asking about tenant B is refused without consulting membership at all. This is the horizontal escalation attempt — the identifier in the URL swapped for somebody else's — and it is the one check that must not depend on a database row being correct.
  3. Does any role the principal actually holds grant it? Global roles always count. Tenant roles count only for a member.
  4. Have they proved who they are strongly enough? Last, so that a denial for weak assurance is distinguishable in the trail from a denial for having no grant at all.

Tenant roles are inert without a membership

Holding finance_admin in tenant A grants nothing unless there is also a membership row for A. Two rows to say one thing, and the redundancy is the point: removing somebody from a tenant is then a single row that revokes everything at once, and it cannot be defeated by a role assignment that was missed in the cleanup.

A global assignment — tenant_id nil — is not gated this way. It applies everywhere, including inside every tenant, which is what makes it the dangerous kind of grant and why Assignment#granted_by exists.

Constructors

new(catalog : RoleCatalog, store : Repository, clock : Clock = SystemClock.new, random : RandomSource = SecureRandomSource.new, cache : Cache | Nil = nil)
Source

Instance methods

add_member(account_id : String, tenant_id : String) : Bool
Source
cache
Source
catalog
Source
decide(principal : Principal, permission : String, context : Context) : Decision

context.resource and context.attributes are read by neither this method nor anything it calls, and that is what an RBAC implementation should do: a role grants a permission everywhere or nowhere. An application whose rules turn on the object being acted on implements Authorizer itself — which is the whole reason authorization is a contract rather than a table (blueprints/0018-authorization-and-tenancy.md).

Source
grant(account_id : String, role : String, tenant_id : String | Nil = nil, granted_by : String | Nil = nil) : Bool

Grants a role, and drops this process's cached copy of the account's grants.

Administration goes through here rather than straight to the repository so that the cache cannot be left holding an answer the database no longer agrees with. Behind several processes the others still wait out the TTL — Cache says why that is the honest bound.

Raises ArgumentError for a role the catalog does not define. A row naming a role that does not exist grants nothing, forever, and looks like it grants something.

Source
invalidate(account_id : String) : Nil

Drops this process's cached grants for an account. Public because an application that writes to the repository through some other path — a migration, an admin tool sharing the process — has to be able to say so.

Source
member?(account_id : String, tenant_id : String) : Bool
Source
permissions_for(principal : Principal, tenant_id : String | Nil = nil) : Array(String)

Every permission this principal currently holds, for a screen that renders a menu.

Not for guarding an action — a route guards with #decide, against the permission it is about to perform, at the moment it performs it. A list handed to a template is a snapshot, and a snapshot used as an authorization decision is the stale-grant problem this whole module exists to avoid.

Assurance is not applied here: the list says what the account has been granted, not what it can do at this instant, so a menu can show an action and let the step-up prompt happen when it is clicked.

Source
remove_account(account_id : String) : Int32

Deletes every membership and assignment for an account. What account deletion calls.

Source
remove_member(account_id : String, tenant_id : String) : Bool

Removes somebody from a tenant, taking that tenant's roles with them — see Repository#remove_member for why those are one operation.

Source
revoke(account_id : String, role : String, tenant_id : String | Nil = nil) : Bool
Source
store
Source