pgmorbacMulti-OrBAC for PostgreSQL
    DocumentationDownloadSource
    Use cases
    • How do I...?
    Getting Started
    • Introduction
    • Quick start
    • How pgmorbac compares
    • Upgrade
    Concepts
    • Organizations
    • Roles
    • Activities
    • Views
    • Contexts
    • Rules & Modalities
    • Global Rules
    • System Principals
    • Constraints
    • Row-Level Security
    • Decision Cache
    Resolution
    • is_allowed
    Recipes
    • Org Hierarchies
    • Multi-Tenant Isolation
    • Wildcard Admin
    • Delegation
    • Service Accounts
    • Deny Rules
    • Separation of Duty
    • Derived Roles
    • Cross-Org Access
    • Obligations & Recommendations
    • Temporal Access
    Integration
    • Fastify
    • PostgREST & RLS Context
    • SQL Only
    Reference
    • Function Reference
    Resolution

    is_allowed

    The full decision algorithm, step by step.

    morbac.is_allowed(user_id, org_id, activity, view) is the single entry point. This page specifies exactly what it does, in order. The uncached twin is_allowed_nocache implements the algorithm; is_allowed adds the cache in front.

    Inputs are expanded first

    • Roles: get_comprehensive_roles(user, org) = direct + delegated + inherited + derived, minus negative assignments.
    • Activities: get_effective_activities(a) = the activity plus every senior activity that implies it.
    • Views: get_effective_views(v) = the view plus its hierarchy expansion.
    • Org scope: each org rule matches when org_in_scope(asked_org, rule_org, rule_scope) holds, so subtree rules apply without copies.

    The steps

    Steps 1-3.5 collect the highest-priority applicable prohibition; steps 4-6.5 collect the highest-priority applicable permission. "Applicable" means: matches the expanded roles/activities/views, its validity window covers now (is_rule_valid), and its context evaluates true. Within each store, candidates are visited in descending priority and the first whose context passes wins that store.

    StepStoreNotes
    1rules (prohibition)org target resolved via org_in_scope: a tree scope, unattributed, or all
    2cross_org_rules (prohibition)role held in source_org_id, resource in target_org_id
    3user_rules (prohibition)direct user bans for this org, or for no-org records when org_id is NULL
    3.5global_rules (prohibition)NULL user_id/activity/view columns are wildcards; every org and no-org alike
    4rules (permission)
    5cross_org_rules (permission)
    6user_rules (permission)
    6.5global_rules (permission)the wildcard-admin path

    Step 7: priority resolution

    no prohibition found        -> allow iff any permission found
    prohibition at priority P   -> allow iff a permission has priority > P

    Equal priority denies: ties go to prohibition, and no rules at all means deny. The engine fails closed in both directions.

    System principals

    For users registered in morbac.system_principals, steps 1-3.5 are skipped entirely: no prohibition of any kind can stop them. They still need a permission from steps 4-6.5.

    Unattributed objects

    The org_id argument is a specific organization, or NULL when the object belongs to none. NULL never means "any org".

    Calling is_allowed(user, NULL, activity, view) runs the same seven steps, and the stores that can answer are exactly those that target no-org records:

    StoreApplies to a NULL org?
    rules with scope = unattributed or allyes
    rules with a tree scope (self, subtree, ...)no
    user_rules with org_id NULLyes
    user_rules with an orgno
    cross_org_rulesno, both ends are real orgs
    global_rulesyes, they span every org and no-org alike

    The partition is enforced in org_in_scope: an unattributed rule matches only a NULL target, and a tree scope never matches one. So org policy and no-org policy cannot contaminate each other, and only all and global rules span both deliberately.

    Prohibitions, priorities and contexts behave identically on this path. It is the same algorithm, not a special case. Unattributed decisions are the one thing the cache never stores, since it is keyed by a non-null org, so they always reflect current policy.

    Asking without an object

    is_allowed authorizes one object. To ask whether a user holds a permission anywhere at all, say to gate a menu item or a feature flag, use the capability probe instead:

    SELECT morbac.has_permission(:user, 'read', 'patients');

    It is true when any org the user is a member of, or the unattributed bucket, resolves to allowed. Use it for display decisions only; enforcement stays with is_allowed and RLS, which still run per object.

    Debugging a decision

    -- bypass the cache
    SELECT morbac.is_allowed_nocache(:user, :org, 'read', 'prescriptions');
     
    -- what roles did the engine see?
    SELECT r.name, cr.source FROM morbac.get_comprehensive_roles(:user, :org) cr
    JOIN morbac.roles r ON r.id = cr.role_id;
     
    -- which rules could match?
    SELECT * FROM morbac.rules
    WHERE activity IN (SELECT activity FROM morbac.get_effective_activities('read'))
      AND view IN (SELECT view FROM morbac.get_effective_views('prescriptions'));

    Every function used above, and the rest of the morbac API, is listed in the function reference.

    PreviousDecision CacheNext Org Hierarchies
    pgmorbac - Multi-OrBAC permission engine for PostgreSQL
    DocumentationDownloadGitea