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
    Concepts

    Organizations

    The org tree, ancestors, descendants, and scope resolution.

    Organizations are the backbone of Multi-OrBAC. morbac.orgs is a tree: every org has a unique name, an optional parent_id, and free-form JSONB metadata. Deleting an org cascades to its subtree, its roles and its rules.

    INSERT INTO morbac.orgs (name) VALUES ('Groupe Hospitalier Valmont');
    INSERT INTO morbac.orgs (name, parent_id)
    SELECT 'Clinique Valmont Nord', id FROM morbac.orgs WHERE name = 'Groupe Hospitalier Valmont';
    INSERT INTO morbac.orgs (name, parent_id)
    SELECT 'Pharmacie Valmont Nord', id FROM morbac.orgs WHERE name = 'Clinique Valmont Nord';

    Traversal functions

    FunctionReturns
    get_org_ancestors(org)the org and every ancestor, with depth (0 = self)
    get_org_descendants(org)the org and every descendant, with depth
    get_org_scope(org, scope, max_depth)a named slice of the tree (below)
    SELECT o.name, s.depth
    FROM morbac.get_org_scope(
        (SELECT id FROM morbac.orgs WHERE name = 'Clinique Valmont Nord'),
        'subtree', NULL) s
    JOIN morbac.orgs o ON o.id = s.org_id;
    -- Clinique Valmont Nord (0), Pharmacie Valmont Nord (1), Cardiologie Valmont Nord (1)

    Scope names

    Rules carry a scope that is resolved against the tree at query time, so new organizations are covered the moment they are created:

    ScopeMeaning
    selfexactly this org (default)
    childrendirect children only
    descendantsall descendants, excluding self
    subtreeself plus all descendants
    parentdirect parent only
    ancestorsall ancestors, excluding self
    lineageself plus all ancestors
    rootthe topmost ancestor only
    unattributedobjects with no org at all
    allevery org, unattributed objects included

    A rule written at Groupe Hospitalier Valmont with scope subtree applies to both hospitals, every department, and any clinic acquired next year, with no further writes. See the org hierarchies recipe.

    The last two are not positions in the tree; they are covered next.

    Records with no organization

    Some records belong to no organization. An import still in flight, an inbound webhook, a patient mid-admission, a catalog entry that will never have a tenant: their org_id is NULL. pgmorbac calls these unattributed, and they are a first-class target rather than a gap in the model.

    NULL in the org dimension always means exactly this. It never means "any org" and never means "all orgs".

    That gives every rule a choice of three org targets, and the vocabulary is the same whichever kind of rule you write:

    TargetRole rule (morbac.rules)User rule (no role)
    A specific organizationscope = self, subtree, ...org_id = that org
    Records with no orgscope = unattributedorg_id = NULL
    Every org, no-org includedscope = alluse global rules

    The first two are partitioned. An unattributed rule can never reach a record that has an org, and a tree scope can never reach one that does not. Only all and global rules deliberately span both.

    Granting the unassigned pile

    The declaring organization stays the policy authority and the role is still held there; only the records it reaches are the org-less ones.

    INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, scope)
    SELECT o.id, r.id, 'read', 'patients', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission', 'unattributed'
    FROM morbac.orgs o
    JOIN morbac.roles r ON r.org_id = o.id AND r.name = 'nurse'
    WHERE o.name = 'Clinique Valmont Nord';

    Grant, revoke and delegate that role exactly as usual: access to the pile follows the role, and nothing org-wide is handed over with it. Several organizations may each declare their own policy over the same pool, which is ordinary Multi-OrBAC: independent authorities over one shared object space.

    Prohibitions, priorities, contexts, validity windows, role hierarchy, delegation, derived roles, negative assignments and separation of duty all apply unchanged, because unattributed rules flow through the same steps of the decision algorithm.

    To grant one person without a role, use a user rule with no org:

    INSERT INTO morbac.user_rules (user_id, org_id, activity, view, context_id, modality)
    SELECT :julien, NULL, 'read', 'patients', c.id, 'permission'
    FROM morbac.contexts c WHERE c.name = 'always';

    Checking one record means passing its org, or NULL when it has none:

    SELECT morbac.is_allowed(:user, :org_id, 'read', 'patients');  -- in an org
    SELECT morbac.is_allowed(:user, NULL,    'read', 'patients');  -- unattributed

    Choosing which unattributed records a query returns is a separate question, answered by the session variables in row-level security.

    Two kinds of no-org

    The database cannot tell "should have an org and does not yet" from "will never have one". Both are NULL, and the unattributed target reaches both alike. If they must be granted separately, keep an explicit state column and give each kind its own view; the view dimension separates them while the org dimension stays unattributed.

    Design guidance

    • Model authority boundaries, not org charts. If Pharmacie Valmont Nord never needs its own roles or rules, it does not need to be an org.
    • Never invent an "Unassigned" organization to stand in for a missing one. Records from different tenants would share it, so a triage role there would see across customers, and it contradicts the principle that organizations are real places. The unattributed target exists so no such placeholder is needed.
    • Org names are globally unique. Use qualified names (Pharmacie Valmont Nord, not Pharmacy) rather than relying on the tree for disambiguation.
    • metadata is yours: store display labels, external ids, feature flags. The engine never reads it.
    PreviousUpgradeNext Roles
    pgmorbac - Multi-OrBAC permission engine for PostgreSQL
    DocumentationDownloadGitea