# morbac_pg Documentation Complete technical documentation for the Multi-OrBAC PostgreSQL extension. ## Table of Contents - [Architecture](#architecture) - [Schema Reference](#schema-reference) - [Core Concepts](#core-concepts) - [Advanced Features](#advanced-features) - [API Reference](#api-reference) - [Integration Guides](#integration-guides) - [Performance](#performance) - [Security Model](#security-model) ## Architecture ### Design Principles morbac_pg is built on these principles: 1. **Pure PostgreSQL** - No external dependencies (except pgcrypto) 2. **Schema Isolation** - All objects in `morbac` schema 3. **Default Deny** - No permission = access denied 4. **Prohibition Precedence** - Prohibitions checked first, always override permissions 5. **Organization-Centric** - All policies scoped to organizations 6. **Multi-Tenant Native** - Users and resources can span organizations ### Authorization Flow ```mermaid 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 ```mermaid 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. ```sql CREATE TABLE morbac.delegations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), delegator_user_id UUID NOT NULL, delegate_user_id UUID NOT NULL, role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, valid_from TIMESTAMPTZ NOT NULL DEFAULT now(), valid_until TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (valid_until IS NULL OR valid_until > valid_from) ); ``` **Columns:** - `valid_from` - Delegation start time - `valid_until` - Optional end time (NULL = indefinite) **Automatic handling:** Included in `get_comprehensive_roles()` when active. #### morbac.negative_role_assignments Explicit role prohibitions. ```sql CREATE TABLE morbac.negative_role_assignments ( user_id UUID NOT NULL, role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, reason TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (user_id, role_id, org_id) ); ``` **Precedence:** Overrides direct assignments, delegations, and derived roles. #### morbac.sod_conflicts Separation of Duty constraints. ```sql CREATE TABLE morbac.sod_conflicts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role1_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, role2_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(role1_id, role2_id, org_id), CHECK (role1_id < role2_id) ); ``` **Usage:** Validated via `check_sod_violation()` before role assignment. #### morbac.role_cardinality Min/max users per role. ```sql CREATE TABLE morbac.role_cardinality ( role_id UUID PRIMARY KEY REFERENCES morbac.roles(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, min_users INTEGER NOT NULL DEFAULT 0, max_users INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (min_users >= 0), CHECK (max_users IS NULL OR max_users >= min_users) ); ``` **Usage:** Validated via `check_cardinality_violation()`. #### morbac.derived_roles Dynamically computed roles. ```sql CREATE TABLE morbac.derived_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, evaluator REGPROC NOT NULL, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` **Columns:** - `evaluator` - Function returning `TABLE(user_id UUID, org_id UUID)` #### morbac.cross_org_rules Inter-organizational rules. ```sql CREATE TABLE morbac.cross_org_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, target_org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, activity TEXT NOT NULL REFERENCES morbac.activities(name) ON DELETE CASCADE, view TEXT NOT NULL REFERENCES morbac.views(name) ON DELETE CASCADE, modality morbac.modality NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ``` **Usage:** Allows roles in `source_org` to access resources in `target_org`. #### morbac.admin_rules Administration meta-policies. ```sql CREATE TABLE morbac.admin_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES morbac.orgs(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES morbac.roles(id) ON DELETE CASCADE, can_manage_policies BOOLEAN NOT NULL DEFAULT false, can_manage_roles BOOLEAN NOT NULL DEFAULT false, can_manage_users BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(org_id, role_id) ); ``` ## Core Concepts ### Deontic Modalities Multi-OrBAC implements four modalities: 1. **Permission** - Allows action (if no prohibition) 2. **Prohibition** - Denies action (always wins) 3. **Obligation** - Must be done (informational only) 4. **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: ```sql -- Context function signature CREATE OR REPLACE FUNCTION morbac.my_context() RETURNS BOOLEAN LANGUAGE plpgsql STABLE -- Important for RLS performance AS $$ BEGIN RETURN ; 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: ```sql -- 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. ```sql -- 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: ```mermaid graph TD A[Director] -->|inherits from| B[Manager] B -->|inherits from| C[Employee] A -.->|transitive| C ``` ### Delegation Temporary role delegation with time bounds: ```sql 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: ```sql 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 Mutually exclusive role constraints: ```sql INSERT INTO morbac.sod_conflicts (role1_id, role2_id, org_id, description) VALUES (preparer_role_id, approver_role_id, org_id, 'Cannot prepare and approve same transaction'); SELECT morbac.check_sod_violation(user_uuid, org_id); ``` ### Cardinality Constraints Min/max users per role: ```sql INSERT INTO morbac.role_cardinality (role_id, org_id, min_users, max_users) VALUES (admin_role_id, org_id, 1, 3); SELECT morbac.check_cardinality_violation(admin_role_id, org_id); ``` Check before INSERT INTO user_roles ```sql -- Require 1-3 admins INSERT INTO morbac.role_cardinality (role_id, org_id, min_users, max_users) VALUES (admin_role_id, org_id, 1, 3); -- Validate SELECT morbac.check_cardinality_violation(admin_role_id, org_id); -- Returns: TEXT (error message) or NULL (valid) ``` ### Derived Roles ```sql -- Define evaluator ### Derived Roles Dynamically computed roles via custom functions: ```sql 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: ```sql 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: ```sql 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:** ```sql -- 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:** ```sql 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:** ```sql 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_from` and `valid_until` are optional (can be NULL) - If both are NULL, rule is always active - If only `valid_from` is set, rule is active from that time onwards - If only `valid_until` is 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:** ```sql 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:** ```sql -- 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:** ```sql SELECT morbac.disable_audit('user_roles'); ``` **Querying audit log:** ```sql -- 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_data` and `new_data` efficiently - Consider partitioning `audit_log` by 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. ```sql 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 TEXT[] of conflicting role pairs or empty array. **`check_cardinality_violation(role_id, org_id)`** - Returns error message or 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. ```sql 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. ```sql morbac.user_has_role( p_user_id UUID, p_org_id UUID, p_role_name TEXT ) RETURNS BOOLEAN ``` ## Integration Guides ### PostgREST Integration #### 1. Setup Headers Configure PostgREST to pass user/org context: ```nginx # Nginx config proxy_set_header X-User-Id $user_id; proxy_set_header X-Org-Id $org_id; ``` #### 2. Enable RLS ```sql ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY; -- Grant usage to PostgREST role 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; ``` #### 3. Create RLS Policies ```sql 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')); ``` #### 4. Set Context (alternative to headers) ```sql -- In application connection SET morbac.user_id = '123e4567-e89b-12d3-a456-426614174000'; SET morbac.org_id = '987fcdeb-51a2-43d7-9c6e-5a8b7c9d0e1f'; ``` ## Integration Guides ### PostgREST Integration Configure PostgREST to pass headers, enable RLS, and create policies: ```sql -- Enable RLS 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')); -- Alternative: Set context directly SET morbac.user_id = '123e4567...'; SET morbac.org_id = '987fcdeb...'; ``` ### Application Integration Python example: ```python 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: ```javascript 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: ```sql CREATE TABLE app.document_orgs ( document_id UUID, org_id UUID, PRIMARY KEY (document_id, org_id) ); 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: ```sql 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)`. CREATE INDEX idx_rules_lookup ON morbac.rules(org_id, role_id, activity, view, modality); -- Hierarchy traversal CREATE INDEX idx_role_hierarchy_senior ON morbac.role_hierarchy(senior_role_id); CREATE INDEX idx_role_hierarchy_junior ON morbac.role_hierarchy(junior_role_id); ``` ### Benchmarking Example query performance (1M rules, 10K users, 100 orgs): ```sql -- 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 #### 1. Context Function Security ```sql -- 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; $$; ``` #### 2. User ID Validation ```sql -- Validate user exists before authorization SELECT morbac.is_allowed( user_id, -- Must be from authenticated session org_id, activity, view ); ``` #### 3. Org Context Validation ```sql -- Verify user belongs to org SELECT EXISTS( SELECT 1 FROM morbac.user_roles WHERE user_id = ? AND org_id = ? ); ``` #### 4. Delegation Auditing ```sql -- Log delegations CREATE TABLE app.delegation_audit ( delegation_id UUID REFERENCES morbac.delegations(id), action TEXT, by_user UUID, at_time TIMESTAMPTZ DEFAULT now() ); ``` ### Comparison: Multi-OrBAC vs Traditional RBAC | Aspect | Traditional RBAC | Multi-OrBAC (morbac_pg) | |--------|-----------------|-------------------------| | **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](https://webhost.laas.fr/TSF/deswarte/Publications/06427.pdf) - [test_morbac.sql](test_morbac.sql) - Comprehensive examples - [CHANGELOG.md](CHANGELOG.md) - Version history - [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines - [SECURITY.md](SECURITY.md) - Security policy