25 KiB
pg_morbac Documentation
Complete technical documentation for the Multi-OrBAC PostgreSQL extension.
Table of Contents
- Architecture
- Schema Reference
- Core Concepts
- Advanced Features
- API Reference
- Integration Guides
- Performance
- Security Model
Architecture
Design Principles
pg_morbac is built on these principles:
- Pure PostgreSQL: No external dependencies (except pgcrypto)
- Schema Isolation: All objects in
morbacschema - Default Deny: No permission = access denied
- Prohibition Precedence: Prohibitions checked first, always override permissions
- Organization-Centric: All policies scoped to organizations
- Multi-Tenant Native: Users and resources can span organizations
Authorization Flow
flowchart TD
A[Authorization Request] --> B[Get Comprehensive Roles]
B --> C[Direct Assignments]
B --> D[Delegations]
B --> E[Derived Roles]
B --> F[Role Hierarchy]
C --> G[Filter Negative Assignments]
D --> G
E --> G
F --> G
G --> H{Check Prohibitions}
H -->|Found| I[DENY]
H -->|Not Found| J{Check Permissions}
J -->|Found| K[ALLOW]
J -->|Not Found| L[DENY - Default]
Performance Optimizations
Prohibition-first checking: Prohibitions are checked before permissions for early exit on denial.
Indexed foreign keys: All foreign key relationships have indexes for fast joins.
Composite indexes: Optimized indexes for common query patterns (org_id, role_id, activity, view).
STABLE functions: RLS functions are marked STABLE for transaction-level caching.
Recursive CTEs: Efficient hierarchy traversal using PostgreSQL's recursive common table expressions.
Schema Reference
Entity Relationship Diagram
erDiagram
orgs ||--o{ orgs : "parent_id"
orgs ||--o{ roles : "has"
orgs ||--o{ user_roles : "contains"
orgs ||--o{ rules : "defines"
roles ||--o{ user_roles : "assigned_to"
roles ||--o{ role_hierarchy : "senior"
roles ||--o{ role_hierarchy : "junior"
roles ||--o{ rules : "applies_to"
activities ||--o{ rules : "action"
activities ||--o{ activity_hierarchy : "parent"
activities ||--o{ activity_hierarchy : "child"
views ||--o{ rules : "target"
views ||--o{ view_hierarchy : "parent"
views ||--o{ view_hierarchy : "child"
contexts ||--o{ rules : "condition"
roles ||--o{ delegations : "delegated"
roles ||--o{ negative_role_assignments : "prohibited"
roles ||--o{ sod_conflicts : "conflicts_with"
roles ||--o{ derived_roles : "computed"
Core Tables
morbac.orgs: Organizations with hierarchy support (parent_id self-reference). Unique name required. Supports metadata as JSONB.
morbac.roles: Roles scoped to organizations. Unique (org_id, name) constraint ensures role names are unique within each org.
morbac.role_hierarchy: Role inheritance via (senior_role_id, junior_role_id). Senior roles inherit all junior permissions. Supports transitive closure.
morbac.user_roles: Direct user-to-role assignments. Primary key on (user_id, role_id, org_id). Note: user_id is external (application manages users).
morbac.activities: Abstract actions (global scope). Text primary key. Examples: read, write, delete, approve.
morbac.activity_hierarchy: Activity inheritance via (parent_activity, child_activity). Permission to parent grants child activities.
morbac.views: Abstract object categories (global scope). Text primary key. Examples: documents, reports, financial_data.
morbac.view_hierarchy: View inheritance via (parent_view, child_view). Access to parent grants child views.
morbac.contexts: Contextual conditions as callable predicates. Column evaluator (REGPROC) references a function returning BOOLEAN (preferably STABLE). Built-in context always returns true.
morbac.rules: Core policy rules linking org, role, activity, view, context, and modality. Indexed on (org_id, role_id, activity, view, modality) for fast lookups.
morbac.policy: Policy DSL using names instead of UUIDs. Insert here, then call compile_policy() to generate rules.
Advanced Feature Tables
morbac.delegations
Temporal role delegation with time bounds.
Key columns:
delegator_user_id: User granting the roledelegate_user_id: User receiving the rolerole_id,org_id: Role being delegatedvalid_from,valid_until: Time window (NULL = indefinite)
Behavior: Automatically included in get_comprehensive_roles() when active.
morbac.negative_role_assignments
Explicit role prohibitions with highest precedence.
Key columns:
user_id,role_id,org_id: Assignment to prohibitreason: Explanation for prohibition
Behavior: Overrides direct assignments, delegations, and derived roles.
morbac.sod_conflicts
Separation of Duty constraints enforce mutually exclusive roles.
Key columns:
role1_id,role2_id: Conflicting role pairorg_id: Organization scopedescription: Explanation of conflict
Behavior: Prevents users from holding both roles simultaneously. Validated via check_sod_violation() before role assignment.
morbac.role_cardinality
Constrains the number of users per role.
Key columns:
role_id,org_id: Role to constrainmin_users: Minimum required users (default 0)max_users: Maximum allowed users (NULL = unlimited)
Behavior: Validated via check_cardinality_violation() before role assignment.
morbac.derived_roles
Dynamically computed roles via custom functions.
Key columns:
role_id,org_id: Role to computeevaluator: Function returningTABLE(user_id UUID, org_id UUID)description: Explanation of computation logic
Behavior: Function is called at runtime to determine role membership. Included in get_comprehensive_roles().
morbac.cross_org_rules
Inter-organizational access policies.
Key columns:
source_org_id: Organization where role is heldtarget_org_id: Organization where access is grantedrole_id,activity,view: Policy specificationmodality: Permission or prohibition
Behavior: Allows roles in source organization to access resources in target organization.
morbac.admin_rules
Administration meta-policies for delegated management.
Key columns:
org_id,role_id: Role receiving admin capabilitiescan_manage_policies: Can create/modify policiescan_manage_roles: Can create/modify rolescan_manage_users: Can assign/revoke user roles
Behavior: Enables organization-scoped administrators without database superuser privileges.
Core Concepts
Deontic Modalities
Multi-OrBAC implements four modalities:
- Permission: Allows action (if no prohibition)
- Prohibition: Denies action (always wins)
- Obligation: Must be done (informational only)
- Recommendation: Should be done (informational only)
Only permissions and prohibitions affect is_allowed() decisions.
Context Evaluation
Contexts are boolean predicates that determine if a rule applies:
-- Context function signature
CREATE OR REPLACE FUNCTION morbac.my_context()
RETURNS BOOLEAN
LANGUAGE plpgsql
STABLE -- Important for RLS performance
AS $$
BEGIN
RETURN <condition>;
END;
$$;
-- Register context
INSERT INTO morbac.contexts (name, evaluator) VALUES
('my_context', 'morbac.my_context'::regproc);
Context functions should:
- Use STABLE (not VOLATILE) for RLS compatibility
- Keep logic simple and fast
- Avoid external dependencies
- Consider caching if computation is expensive
Policy DSL
Simplified policy declaration:
-- Insert policies using names
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality) VALUES
('Acme Corp', 'manager', 'approve', 'requests', 'permission');
-- Compile (idempotent)
SELECT * FROM morbac.compile_policy();
Compiler behavior:
- Creates missing activities/views
- Resolves names to UUIDs
- Creates entries in
morbac.rules - Reports errors for missing orgs/roles/contexts
- Safe to run multiple times
Advanced Features
Hierarchies
Organizations, roles, activities, and views all support hierarchical relationships with transitive closure.
-- Organization hierarchy (via parent_id)
INSERT INTO morbac.orgs (name, parent_id) VALUES ('EMEA', parent_org_id);
SELECT * FROM morbac.get_org_ancestors(org_id);
SELECT * FROM morbac.get_org_descendants(org_id);
-- Role hierarchy (senior inherits from junior)
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id)
VALUES (manager_role_id, employee_role_id);
SELECT * FROM morbac.get_inherited_roles(role_id);
-- Activity hierarchy (parent includes children)
INSERT INTO morbac.activity_hierarchy (parent_activity, child_activity)
VALUES ('write', 'create'), ('write', 'update');
SELECT * FROM morbac.get_effective_activities('write');
-- View hierarchy (parent includes children)
INSERT INTO morbac.view_hierarchy (parent_view, child_view)
VALUES ('documents', 'invoices'), ('documents', 'contracts');
SELECT * FROM morbac.get_effective_views('documents');
Example hierarchy diagram:
graph TD
A[Director] -->|inherits from| B[Manager]
B -->|inherits from| C[Employee]
A -.->|transitive| C
Delegation
Temporary role delegation with time bounds:
INSERT INTO morbac.delegations (
delegator_user_id, delegate_user_id, role_id, org_id,
valid_from, valid_until
) VALUES (
alice_uuid, bob_uuid, approver_role_id, org_id,
NOW(), NOW() + INTERVAL '7 days'
);
Automatically included in get_comprehensive_roles() when active. Set valid_until to NULL for indefinite delegation.
Negative Role Assignments
Explicit prohibitions with highest precedence:
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason)
VALUES (user_uuid, admin_role_id, org_id, 'Security audit requirement');
Separation of Duty
Prevents users from holding conflicting roles that could enable fraud or abuse.
Example: Financial Controls
In an accounting system, the same person should not both create and approve invoices:
-- Create roles
INSERT INTO morbac.roles (org_id, name) VALUES
(org_id, 'invoice_creator'),
(org_id, 'invoice_approver');
-- Define conflict
INSERT INTO morbac.sod_conflicts (role1_id, role2_id, org_id, description)
VALUES (creator_role_id, approver_role_id, org_id, 'Financial controls: prevent self-approval');
-- Check before assignment
SELECT morbac.check_sod_violation(alice_uuid, org_id);
-- Returns: empty array if valid, or ['invoice_creator', 'invoice_approver'] if conflict exists
If Alice already has the invoice_creator role, attempting to assign invoice_approver will violate the constraint.
Cardinality Constraints
Enforce minimum and maximum users per role:
-- Require 1-3 administrators
INSERT INTO morbac.role_cardinality (role_id, org_id, min_users, max_users)
VALUES (admin_role_id, org_id, 1, 3);
-- Validate before assignment
SELECT morbac.check_cardinality_violation(admin_role_id, org_id);
-- Returns: error message if constraint violated, NULL if valid
Dynamically computed roles via custom functions:
CREATE OR REPLACE FUNCTION morbac.eval_project_leads()
RETURNS TABLE(user_id UUID, org_id UUID)
LANGUAGE sql STABLE AS $$
SELECT user_id, org_id FROM app.projects WHERE lead_user_id IS NOT NULL;
$$;
INSERT INTO morbac.derived_roles (role_id, org_id, evaluator)
VALUES (project_lead_role_id, org_id, 'morbac.eval_project_leads'::regproc);
Cross-Organizational Rules
Inter-organizational access policies:
INSERT INTO morbac.cross_org_rules (
source_org_id, target_org_id, role_id, activity, view, modality
) VALUES (
global_org_id, subsidiary_org_id, auditor_role_id,
'read', 'financial_reports', 'permission'
);
Administration Rules
Meta-policies for policy management:
INSERT INTO morbac.admin_rules (org_id, role_id, can_manage_policies)
VALUES (org_id, sec_admin_role_id, true);
SELECT morbac.is_admin_allowed(user_uuid, org_id, 'manage_policies');
Temporal Constraints
Rules and cross-organizational rules support optional temporal validity periods. Rules are only active within their specified time window.
Adding temporal constraints to rules:
-- Rule active only in 2024
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from, valid_until)
VALUES (org_id, contractor_role_id, 'read', 'projects', ctx_id, 'permission',
'2024-01-01 00:00:00+00', '2024-12-31 23:59:59+00');
-- Rule active starting from a specific date (no end date)
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_from)
VALUES (org_id, employee_role_id, 'read', 'handbook', ctx_id, 'permission',
'2024-06-01 00:00:00+00');
-- Rule expires at a specific date
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, valid_until)
VALUES (org_id, temp_role_id, 'write', 'reports', ctx_id, 'permission',
'2024-12-31 23:59:59+00');
Temporal constraints on cross-org rules:
INSERT INTO morbac.cross_org_rules (
source_org_id, target_org_id, role_id, activity, view, modality,
valid_from, valid_until
) VALUES (
parent_org_id, subsidiary_org_id, auditor_role_id,
'read', 'financial_data', 'permission',
'2024-01-01 00:00:00+00', '2024-03-31 23:59:59+00'
);
Checking rule validity manually:
SELECT morbac.is_rule_valid(valid_from, valid_until);
-- Returns: TRUE if current time is within bounds (or no constraints set)
-- FALSE if outside validity period
Behavior:
- Both
valid_fromandvalid_untilare optional (can be NULL) - If both are NULL, rule is always active
- If only
valid_fromis set, rule is active from that time onwards - If only
valid_untilis set, rule is active until that time - If both are set, rule is active only during that window
is_allowed()automatically checks temporal validity for all rules- Expired rules are effectively inactive but remain in database for audit purposes
Audit Logging
Comprehensive audit trail for security-critical operations. The audit system tracks all INSERT, UPDATE, and DELETE operations on specified tables.
Audit log table structure:
morbac.audit_log (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ, -- When change occurred
user_id UUID, -- User making change (if applicable)
org_id UUID, -- Organization context
table_name TEXT, -- Table being modified
operation TEXT, -- INSERT, UPDATE, DELETE
record_id UUID, -- ID of affected record
old_data JSONB, -- Record state before change (UPDATE/DELETE)
new_data JSONB, -- Record state after change (INSERT/UPDATE)
changed_fields TEXT[], -- List of modified columns (UPDATE)
session_user TEXT, -- PostgreSQL session user
client_addr INET, -- Client IP address
application_name TEXT, -- Application name from connection
metadata JSONB -- Additional custom metadata
)
Enabling audit logging:
-- Enable auditing on user_roles table
SELECT morbac.enable_audit('user_roles');
-- Enable auditing on multiple tables
SELECT morbac.enable_audit('rules');
SELECT morbac.enable_audit('delegations');
SELECT morbac.enable_audit('admin_rules');
Disabling audit logging:
SELECT morbac.disable_audit('user_roles');
Querying audit log:
-- Recent changes to user_roles
SELECT
timestamp,
operation,
session_user,
old_data->>'role_id' AS old_role,
new_data->>'role_id' AS new_role
FROM morbac.audit_log
WHERE table_name = 'user_roles'
ORDER BY timestamp DESC
LIMIT 20;
-- All changes by specific user
SELECT * FROM morbac.audit_log
WHERE user_id = '123e4567-e89b-12d3-a456-426614174000'
ORDER BY timestamp DESC;
-- Changes to specific record
SELECT * FROM morbac.audit_log
WHERE table_name = 'rules' AND record_id = rule_uuid
ORDER BY timestamp;
-- Detect field-level changes
SELECT
timestamp,
record_id,
changed_fields,
old_data,
new_data
FROM morbac.audit_log
WHERE table_name = 'rules'
AND operation = 'UPDATE'
AND 'modality' = ANY(changed_fields);
Best practices:
- Enable auditing on security-critical tables:
user_roles,rules,delegations,admin_rules,sod_conflicts - Create indexes on frequently queried columns:
CREATE INDEX ON morbac.audit_log(table_name, timestamp DESC) - Implement retention policies to archive old audit logs
- Use JSONB operators to query
old_dataandnew_dataefficiently - Consider partitioning
audit_logby timestamp for large installations
API Reference
Authorization Functions
is_allowed(user_id, org_id, activity, view): Main authorization decision. Returns BOOLEAN. Checks prohibitions first, then permissions, defaults to deny.
SELECT morbac.is_allowed(user_uuid, org_uuid, 'read', 'documents');
get_comprehensive_roles(user_id, org_id): Returns all roles for user (direct, delegated, derived, hierarchy, minus negative assignments).
get_effective_roles(user_id, org_id): Alias for get_comprehensive_roles().
Hierarchy Functions
get_org_ancestors(org_id): Returns all parent organizations with depth.
get_org_descendants(org_id): Returns all child organizations with depth.
get_inherited_roles(role_id): Returns all junior roles (transitive).
get_effective_activities(activity): Returns activity plus all child activities.
get_effective_views(view): Returns view plus all child views.
Validation Functions
check_sod_violation(user_id, org_id): Returns empty array if valid, or array of conflicting role name pairs if violations exist.
check_cardinality_violation(role_id, org_id): Returns error message if constraint violated, NULL if valid.
Administration Functions
is_admin_allowed(user_id, org_id, capability): Check administrative permissions. Capabilities: manage_policies, manage_roles, manage_users.
eval_derived_role(user_id, org_id, derived_role_id): Evaluate if user has derived role.
Context Functions
eval_context(context_id): Evaluate a context predicate.
Policy DSL Functions
compile_policy(): Compile policy DSL into rules. Returns TABLE with success status and messages. Idempotent.
RLS Helper Functions
current_user_id(): Get user ID from request.header.x-user-id (PostgREST) or current_setting('morbac.user_id').
current_org_id(): Get org ID from request.header.x-org-id (PostgREST) or current_setting('morbac.org_id').
rls_check(activity, view): Authorization check for RLS policies using current user/org context.
CREATE POLICY my_policy ON app.table
FOR SELECT USING (morbac.rls_check('read', 'documents'));
Informational Functions
pending_obligations(user_id, org_id): Returns obligations for user (informational only).
pending_recommendations(user_id, org_id): Returns recommendations for user (informational only).
user_has_role(user_id, org_id, role_name): Check if user has specific role by name.
morbac.user_has_role(
p_user_id UUID,
p_org_id UUID,
p_role_name TEXT
) RETURNS BOOLEAN
Integration Guides
PostgREST Integration
Configure PostgREST to pass user/org context via headers:
# Nginx config
proxy_set_header X-User-Id $user_id;
proxy_set_header X-Org-Id $org_id;
Enable RLS and grant permissions:
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
GRANT USAGE ON SCHEMA morbac TO postgrest_role;
GRANT SELECT ON ALL TABLES IN SCHEMA morbac TO postgrest_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA morbac TO postgrest_role;
Create RLS policies:
CREATE POLICY document_read ON app.documents
FOR SELECT
USING (morbac.rls_check('read', 'documents'));
CREATE POLICY document_write ON app.documents
FOR INSERT
WITH CHECK (morbac.rls_check('write', 'documents'));
Alternative method using session variables:
SET morbac.user_id = '123e4567-e89b-12d3-a456-426614174000';
SET morbac.org_id = '987fcdeb-51a2-43d7-9c6e-5a8b7c9d0e1f';
Application Integration
Python example:
import psycopg2
conn = psycopg2.connect("dbname=mydb")
cursor = conn.cursor()
cursor.execute("SET morbac.user_id = %s", [user_id])
cursor.execute("SET morbac.org_id = %s", [org_id])
cursor.execute("SELECT * FROM app.documents") # RLS enforced
Node.js example:
const client = await pool.connect();
await client.query('SET morbac.user_id = $1', [userId]);
await client.query('SET morbac.org_id = $1', [orgId]);
const res = await client.query('SELECT * FROM app.documents');
Multi-Organization Resources
Resources can belong to multiple organizations using a junction table:
-- Application defines document-org relationships
-- morbac.rls_check() enforces access rules
CREATE POLICY doc_access ON app.documents FOR SELECT USING (
EXISTS (
SELECT 1 FROM app.document_orgs
WHERE document_id = app.documents.id
AND org_id = morbac.current_org_id()
)
AND morbac.rls_check('read', 'documents')
);
Performance
Optimization Strategies
Prohibition-first checking provides early exit on denial for both security and performance.
For large hierarchies, use materialized views:
CREATE MATERIALIZED VIEW morbac.mv_role_closure AS
SELECT senior_role_id, junior_role_id FROM ...;
CREATE INDEX ON morbac.mv_role_closure(senior_role_id);
REFRESH MATERIALIZED VIEW morbac.mv_role_closure;
Context optimization:
- Use STABLE functions (not VOLATILE) for RLS caching
- Avoid expensive computations or external calls
- Keep context logic simple
All foreign keys are indexed. Critical composite indexes exist on (user_id, org_id) and (org_id, role_id, activity, view, modality).
Benchmarking
Example query performance (1M rules, 10K users, 100 orgs):
-- Simple authorization check
EXPLAIN ANALYZE
SELECT morbac.is_allowed(user_id, org_id, 'read', 'documents');
-- Typical: 5-15ms
-- With hierarchies (3 levels)
EXPLAIN ANALYZE
SELECT morbac.is_allowed(user_id, org_id, 'read', 'documents');
-- Typical: 10-30ms
-- RLS query (1M rows)
EXPLAIN ANALYZE
SELECT * FROM app.documents WHERE morbac.rls_check('read', 'documents');
-- Depends on selectivity, uses indexes
Security Model
Threat Model
Protected against:
- Unauthorized access (default deny)
- Privilege escalation (prohibition precedence)
- SQL injection (parameterized queries)
- Org boundary violations (explicit cross-org rules)
Not protected against:
- Malicious context functions (trusted code)
- Database user privilege escalation (PostgreSQL security model)
- Time-of-check/time-of-use races (transaction-based consistency only)
Security Best Practices
Context Function Security
-- BAD: Leaks information
CREATE FUNCTION morbac.bad_context()
RETURNS BOOLEAN VOLATILE AS $$
SELECT EXISTS(SELECT 1 FROM sensitive_table);
$$;
-- GOOD: Self-contained, stable
CREATE FUNCTION morbac.good_context()
RETURNS BOOLEAN STABLE AS $$
SELECT EXTRACT(HOUR FROM CURRENT_TIME) BETWEEN 9 AND 17;
$$;
User ID Validation
-- Validate user exists before authorization
SELECT morbac.is_allowed(
user_id, -- Must be from authenticated session
org_id,
activity,
view
);
Org Context Validation
-- Verify user belongs to org
SELECT EXISTS(
SELECT 1 FROM morbac.user_roles
WHERE user_id = ? AND org_id = ?
);
Comparison: Multi-OrBAC vs Traditional RBAC
| Aspect | Traditional RBAC | Multi-OrBAC (pg_morbac) |
|---|---|---|
| Multi-tenancy | Single tenant or complex workarounds | Native multi-organization |
| Prohibition | No standard support | First-class, always wins |
| Context | Static permissions | Dynamic, condition-based |
| Hierarchy | Basic (roles only) | Full (orgs, roles, activities, views) |
| Delegation | Manual implementation | Native temporal delegation |
| SoD | Application logic | Database-enforced constraints |
| Cross-tenant | Not supported | Native cross-org rules |
| Audit | Application layer | Meta-policies (admin rules) |
Additional Resources
- Multi-OrBAC Research Paper
- test_morbac.sql: Comprehensive examples
- CHANGELOG.md: Version history
- CONTRIBUTING.md: Contribution guidelines
- SECURITY.md: Security policy