Multi-Organization Based Access Control for PostgreSQL
A PostgreSQL extension implementing the Multi-OrBAC access control model - enabling organization-centric, context-aware, and hierarchical access control with advanced features like delegation, separation of duty, and cross-organizational policies. ## Features - Multi-organization with organizational hierarchy - Unattributed (no-org) objects as a first-class rule target - Role-based access with full hierarchy support - Activity and view hierarchies with transitive permission inheritance - Prohibition precedence over permissions - Temporal delegation with time bounds - Temporal constraints on rules with validity periods - Separation of duty constraints - Derived roles computed from application logic - Cross-organizational policies - Context-aware rules - Audit logging for security-critical operations - Row-level security integration ## Installation ### Prerequisites - PostgreSQL 13 or higher - Development tools: `make`, `bash` ### Quick Install ```bash # Clone repository git clone https://github.com/crudylabs/pgmorbac.git cd pgmorbac # Build and install extension make build # Build versioned file from src/ sudo make install # Install to PostgreSQL # Enable in your database psql -d mydb -c "CREATE EXTENSION pgmorbac;" ``` ### Alternative: Using install script ```bash # Build first make build # Run installer sudo ./tools/install.sh ``` ### Manual Install ```bash # Build versioned file from source make build # Concatenates src/ files into pgmorbac--1.0.2.sql # Copy files to PostgreSQL extension directory sudo cp pgmorbac.control $(pg_config --sharedir)/extension/ sudo cp pgmorbac--1.0.2.sql $(pg_config --sharedir)/extension/ # Enable in PostgreSQL psql -d mydb -c "CREATE EXTENSION pgmorbac;" ``` ### From Node.js (npm) [`@crudy/pgmorbac`](https://www.npmjs.com/package/@crudy/pgmorbac) ships the install and upgrade scripts of each signed release for Node projects and migration runners, and [`@crudy/pgmorbac-fastify`](https://www.npmjs.com/package/@crudy/pgmorbac-fastify) adds Fastify helpers on top. Sources: [pgmorbac-node](https://github.com/crudylabs/pgmorbac-node), [pgmorbac-fastify](https://github.com/crudylabs/pgmorbac-fastify). ## Quick Start ```sql -- 1. Create organization INSERT INTO morbac.orgs (name) VALUES ('Acme Corp'); -- 2. Create role INSERT INTO morbac.roles (org_id, name) SELECT id, 'employee' FROM morbac.orgs WHERE name = 'Acme Corp'; -- 3. Assign user to role INSERT INTO morbac.user_roles (user_id, role_id, org_id) SELECT '00000000-0000-0000-0000-000000000001'::uuid, r.id, o.id FROM morbac.orgs o JOIN morbac.roles r ON r.org_id = o.id WHERE o.name = 'Acme Corp' AND r.name = 'employee'; -- 4. Define policy INSERT INTO morbac.policy (org_name, role_name, activity, view, modality) VALUES ('Acme Corp', 'employee', 'read', 'documents', 'permission'); -- 5. Compile policy SELECT * FROM morbac.compile_policy(); -- 6. Check authorization SELECT morbac.is_allowed( '00000000-0000-0000-0000-000000000001'::uuid, (SELECT id FROM morbac.orgs WHERE name = 'Acme Corp'), 'read', 'documents' ); -- Returns: true -- 7. Initialize performance cache (recommended for production) SELECT morbac.refresh_hierarchy_cache(); ``` ## Usage ### Authorization Check ```sql -- Production (cached by default) SELECT morbac.is_allowed(user_id, org_id, activity, view); -- Debugging (bypasses cache) SELECT morbac.is_allowed_nocache(user_id, org_id, activity, view); -- Unattributed object (no org): pass NULL as the org SELECT morbac.is_allowed(user_id, NULL, activity, view); -- Capability probe for UI gating (any org, unattributed, or global) SELECT morbac.has_permission(user_id, activity, view); ``` See [PERFORMANCE.md](docs/PERFORMANCE.md) for optimization details. ### Row-Level Security (RLS) ```sql -- Enable RLS on your table ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY; -- Table without an org column CREATE POLICY doc_access ON app.documents FOR SELECT USING (morbac.rls_check('read', 'documents')); -- Table with an org column: pass it. A NULL org_id means the record -- is unattributed (awaiting attribution). CREATE POLICY doc_access ON app.documents FOR SELECT USING (morbac.rls_check('read', 'documents', org_id)); ``` ### Unattributed (no-org) Records A record whose `org_id` is `NULL` is *unattributed*. Give a role access to that pool without granting anything org-wide: ```sql INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality, scope) SELECT o.id, r.id, 'read', 'documents', c.id, 'permission', 'unattributed' FROM morbac.orgs o JOIN morbac.roles r ON r.org_id = o.id AND r.name = 'triage' JOIN morbac.contexts c ON c.name = 'always' WHERE o.name = 'Acme Corp'; ``` Choose which records a query returns: ```sql SET morbac.org_ids = '[null]'; -- unattributed only (attribution queue) SET morbac.org_ids = '["