feat: improve performance with cache and proper indexing
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
# Administration Guide
|
||||
|
||||
This guide explains how to set up organization-scoped administrators without relying on database superadmins.
|
||||
|
||||
## Concept
|
||||
|
||||
Multi-OrBAC allows you to delegate administrative capabilities to specific roles within each organization. This means:
|
||||
|
||||
- Organization administrators can manage users, roles, and policies within their own organization
|
||||
- No need for database-level superadmin access for day-to-day administration
|
||||
- Fine-grained control over what each admin role can do
|
||||
- Admins cannot affect other organizations
|
||||
|
||||
## Administration Rules
|
||||
|
||||
The `morbac.admin_rules` table defines what administrative actions specific roles can perform:
|
||||
|
||||
```sql
|
||||
CREATE TABLE morbac.admin_rules (
|
||||
id UUID PRIMARY KEY,
|
||||
org_id UUID REFERENCES morbac.orgs(id),
|
||||
role_id UUID REFERENCES morbac.roles(id),
|
||||
admin_activity TEXT, -- 'manage', 'assign_role', etc.
|
||||
admin_target TEXT, -- 'policies', 'roles', role name, etc.
|
||||
modality morbac.modality, -- 'permission' or 'prohibition'
|
||||
context_id UUID REFERENCES morbac.contexts(id)
|
||||
);
|
||||
```
|
||||
|
||||
## Common Admin Patterns
|
||||
|
||||
### 1. Organization Administrator
|
||||
|
||||
Full admin within their organization:
|
||||
|
||||
```sql
|
||||
-- Create admin role
|
||||
INSERT INTO morbac.roles (org_id, name, description)
|
||||
SELECT id, 'org_admin', 'Organization administrator'
|
||||
FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||
|
||||
-- Grant ability to manage policies
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'manage',
|
||||
'policies',
|
||||
'permission',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||
|
||||
-- Grant ability to manage roles
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'manage',
|
||||
'roles',
|
||||
'permission',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||
|
||||
-- Grant ability to assign ALL roles (wildcard)
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'assign_role',
|
||||
'*',
|
||||
'permission',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin';
|
||||
|
||||
-- Assign someone as org admin
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
SELECT
|
||||
'alice-uuid'::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 = 'org_admin';
|
||||
```
|
||||
|
||||
### 2. HR Manager (User/Role Assignment Only)
|
||||
|
||||
Can assign users to roles but cannot modify policies:
|
||||
|
||||
```sql
|
||||
-- Create HR manager role
|
||||
INSERT INTO morbac.roles (org_id, name, description)
|
||||
SELECT id, 'hr_manager', 'HR manager - can assign users to roles'
|
||||
FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||
|
||||
-- Grant ability to assign specific roles
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'assign_role',
|
||||
target_role,
|
||||
'permission',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
FROM morbac.orgs o
|
||||
CROSS JOIN (VALUES ('employee'), ('manager'), ('contractor')) AS roles(target_role)
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager';
|
||||
|
||||
-- Prohibit assigning admin roles
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'assign_role',
|
||||
'org_admin',
|
||||
'prohibition',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager';
|
||||
```
|
||||
|
||||
### 3. Security Manager (Policy Management Only)
|
||||
|
||||
Can define policies but cannot assign users:
|
||||
|
||||
```sql
|
||||
-- Create security manager role
|
||||
INSERT INTO morbac.roles (org_id, name, description)
|
||||
SELECT id, 'security_manager', 'Security manager - can manage policies'
|
||||
FROM morbac.orgs WHERE name = 'Acme Corp';
|
||||
|
||||
-- Grant policy management
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'manage',
|
||||
'policies',
|
||||
'permission',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'security_manager';
|
||||
```
|
||||
|
||||
## Using Admin Functions
|
||||
|
||||
### Check Permissions
|
||||
|
||||
```sql
|
||||
-- Check if Alice can manage policies
|
||||
SELECT morbac.can_manage_policies('alice-uuid'::uuid, org_id);
|
||||
|
||||
-- Check if Alice can manage roles
|
||||
SELECT morbac.can_manage_roles('alice-uuid'::uuid, org_id);
|
||||
|
||||
-- Check if Alice can assign a specific role
|
||||
SELECT morbac.can_manage_user_role('alice-uuid'::uuid, org_id, employee_role_id);
|
||||
|
||||
-- General admin check
|
||||
SELECT morbac.is_admin_allowed('alice-uuid'::uuid, org_id, 'assign_role', 'manager');
|
||||
```
|
||||
|
||||
### Assign/Revoke Roles Safely
|
||||
|
||||
```sql
|
||||
-- Alice (HR manager) assigns Bob to employee role
|
||||
SELECT morbac.admin_assign_role(
|
||||
'alice-uuid'::uuid, -- Admin user
|
||||
'bob-uuid'::uuid, -- Target user
|
||||
employee_role_id, -- Role to assign
|
||||
org_id -- Organization
|
||||
);
|
||||
|
||||
-- Alice revokes Bob's employee role
|
||||
SELECT morbac.admin_revoke_role(
|
||||
'alice-uuid'::uuid,
|
||||
'bob-uuid'::uuid,
|
||||
employee_role_id,
|
||||
org_id
|
||||
);
|
||||
```
|
||||
|
||||
These functions automatically:
|
||||
- Check if Alice has permission to manage the role
|
||||
- Validate SoD constraints
|
||||
- Validate cardinality constraints
|
||||
- Raise exceptions if constraints are violated
|
||||
|
||||
## Application Integration
|
||||
|
||||
### REST API Example
|
||||
|
||||
Your application can expose admin endpoints that use these functions:
|
||||
|
||||
```sql
|
||||
-- Endpoint: POST /api/orgs/:orgId/users/:userId/roles/:roleId
|
||||
-- Handler checks:
|
||||
CREATE OR REPLACE FUNCTION app.assign_role_endpoint(
|
||||
p_requesting_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_target_user_id UUID,
|
||||
p_role_id UUID
|
||||
)
|
||||
RETURNS JSON
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
v_result JSON;
|
||||
BEGIN
|
||||
BEGIN
|
||||
PERFORM morbac.admin_assign_role(
|
||||
p_requesting_user_id,
|
||||
p_target_user_id,
|
||||
p_role_id,
|
||||
p_org_id
|
||||
);
|
||||
|
||||
v_result := json_build_object(
|
||||
'success', true,
|
||||
'message', 'Role assigned successfully'
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
v_result := json_build_object(
|
||||
'success', false,
|
||||
'error', SQLERRM
|
||||
);
|
||||
END;
|
||||
|
||||
RETURN v_result;
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
### Row-Level Security for Admin Tables
|
||||
|
||||
Protect admin configuration with RLS:
|
||||
|
||||
```sql
|
||||
-- Only org admins can see admin rules
|
||||
ALTER TABLE morbac.admin_rules ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY admin_rules_access ON morbac.admin_rules
|
||||
FOR SELECT
|
||||
USING (
|
||||
org_id = morbac.current_org_id()
|
||||
AND (
|
||||
-- User is org admin
|
||||
morbac.can_manage_policies(morbac.current_user_id(), org_id)
|
||||
OR morbac.can_manage_roles(morbac.current_user_id(), org_id)
|
||||
)
|
||||
);
|
||||
|
||||
-- Only org admins can modify admin rules
|
||||
CREATE POLICY admin_rules_modify ON morbac.admin_rules
|
||||
FOR ALL
|
||||
USING (
|
||||
org_id = morbac.current_org_id()
|
||||
AND morbac.can_manage_policies(morbac.current_user_id(), org_id)
|
||||
);
|
||||
```
|
||||
|
||||
## Context-Based Admin Rules
|
||||
|
||||
You can limit admin actions to specific contexts (e.g., business hours):
|
||||
|
||||
```sql
|
||||
-- Create context for business hours
|
||||
CREATE OR REPLACE FUNCTION morbac.context_business_hours()
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN EXTRACT(DOW FROM CURRENT_DATE) BETWEEN 1 AND 5
|
||||
AND EXTRACT(HOUR FROM CURRENT_TIME) BETWEEN 9 AND 17;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||
('business_hours', 'Monday-Friday 9am-5pm', 'morbac.context_business_hours');
|
||||
|
||||
-- HR can only assign roles during business hours
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT
|
||||
o.id,
|
||||
r.id,
|
||||
'assign_role',
|
||||
'*',
|
||||
'permission',
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'business_hours')
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager';
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Principle of Least Privilege**: Grant only necessary admin capabilities to each role
|
||||
2. **Separation of Duties**: Separate policy management from user assignment
|
||||
3. **Audit Trail**: Log all admin actions (consider triggers on user_roles, rules tables)
|
||||
4. **Context-Based**: Use contexts to limit when admin actions can occur
|
||||
5. **Multiple Admins**: Use role cardinality to ensure multiple admins exist
|
||||
6. **Prohibitions**: Use prohibition admin rules to explicitly deny certain actions
|
||||
|
||||
## Example: Complete Setup
|
||||
|
||||
```sql
|
||||
-- 1. Create organization
|
||||
INSERT INTO morbac.orgs (name) VALUES ('Acme Corp');
|
||||
|
||||
-- 2. Create roles
|
||||
INSERT INTO morbac.roles (org_id, name)
|
||||
SELECT id, name FROM morbac.orgs,
|
||||
(VALUES ('org_admin'), ('hr_manager'), ('employee'), ('manager')) AS roles(name)
|
||||
WHERE morbac.orgs.name = 'Acme Corp';
|
||||
|
||||
-- 3. Set up org_admin with full permissions
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT o.id, r.id, activity, target, 'permission', c.id
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
CROSS JOIN (VALUES
|
||||
('manage', 'policies'),
|
||||
('manage', 'roles'),
|
||||
('assign_role', '*')
|
||||
) AS perms(activity, target)
|
||||
CROSS JOIN morbac.contexts c
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'org_admin' AND c.name = 'always';
|
||||
|
||||
-- 4. Set up hr_manager with limited permissions
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, modality, context_id)
|
||||
SELECT o.id, r.id, 'assign_role', role_name, 'permission', c.id
|
||||
FROM morbac.orgs o
|
||||
JOIN morbac.roles r ON r.org_id = o.id
|
||||
CROSS JOIN (VALUES ('employee'), ('manager')) AS assignable(role_name)
|
||||
CROSS JOIN morbac.contexts c
|
||||
WHERE o.name = 'Acme Corp' AND r.name = 'hr_manager' AND c.name = 'always';
|
||||
|
||||
-- 5. Assign Alice as org_admin
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id)
|
||||
SELECT 'alice-uuid'::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 = 'org_admin';
|
||||
|
||||
-- 6. Now Alice can assign Bob as hr_manager
|
||||
SELECT morbac.admin_assign_role(
|
||||
'alice-uuid'::uuid,
|
||||
'bob-uuid'::uuid,
|
||||
(SELECT id FROM morbac.roles WHERE name = 'hr_manager' AND org_id =
|
||||
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')),
|
||||
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')
|
||||
);
|
||||
|
||||
-- 7. Now Bob can assign users to employee/manager roles
|
||||
SELECT morbac.admin_assign_role(
|
||||
'bob-uuid'::uuid,
|
||||
'charlie-uuid'::uuid,
|
||||
(SELECT id FROM morbac.roles WHERE name = 'employee' AND org_id =
|
||||
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')),
|
||||
(SELECT id FROM morbac.orgs WHERE name = 'Acme Corp')
|
||||
);
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Multi-OrBAC provides complete delegation of administrative capabilities:
|
||||
|
||||
- **No superadmins needed** for day-to-day operations
|
||||
- **Organization-scoped** - admins only affect their own org
|
||||
- **Fine-grained** - control exactly what each admin role can do
|
||||
- **Safe** - automatic constraint validation (SoD, cardinality)
|
||||
- **Auditable** - all actions go through tracked functions
|
||||
- **Context-aware** - limit when admin actions can occur
|
||||
@@ -0,0 +1,81 @@
|
||||
# Contributing to morbac_pg
|
||||
|
||||
Thank you for your interest in contributing to morbac_pg!
|
||||
|
||||
## How to Contribute
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
If you find a bug, please open an issue with:
|
||||
- A clear description of the bug
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- PostgreSQL version
|
||||
- Relevant logs or error messages
|
||||
|
||||
### Suggesting Features
|
||||
|
||||
Feature requests are welcome! Please open an issue describing:
|
||||
- The use case or problem you're trying to solve
|
||||
- How the feature would work
|
||||
- Whether it aligns with Multi-OrBAC model principles
|
||||
|
||||
### Code Contributions
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Create a feature branch**: `git checkout -b feature/my-feature`
|
||||
3. **Make your changes**:
|
||||
- Follow the existing code style (SQL formatting, naming conventions)
|
||||
- Add tests in `test_morbac.sql` for new features
|
||||
- Update `DOCUMENTATION.md` if adding new functionality
|
||||
4. **Test your changes**: `make test`
|
||||
5. **Commit with clear messages**: `git commit -m "Add feature: description"`
|
||||
6. **Push to your fork**: `git push origin feature/my-feature`
|
||||
7. **Open a Pull Request** with a description of your changes
|
||||
|
||||
### Code Style Guidelines
|
||||
|
||||
- **SQL**: Use uppercase for keywords, lowercase for identifiers
|
||||
- **Naming**: Use snake_case for tables, columns, functions
|
||||
- **Schema**: All objects must be in the `morbac` schema
|
||||
- **Comments**: Add comments for complex logic
|
||||
- **Functions**: Include COMMENT ON statements for public functions
|
||||
|
||||
### Testing
|
||||
|
||||
All changes must include tests:
|
||||
- Add test cases to `test_morbac.sql`
|
||||
- Ensure all existing tests still pass
|
||||
- Test against PostgreSQL 12+ (mention version in PR)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update `DOCUMENTATION.md` for API changes
|
||||
- Update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format
|
||||
- Keep `README.md` simple (link to docs instead of expanding it)
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
# Install PostgreSQL (if not already installed)
|
||||
brew install postgresql # macOS
|
||||
# or use your package manager
|
||||
|
||||
# Clone your fork
|
||||
git clone https://github.com/your-username/morbac_pg.git
|
||||
cd morbac_pg
|
||||
|
||||
# Install the extension
|
||||
make install
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
```
|
||||
|
||||
## Questions?
|
||||
|
||||
Feel free to open an issue for questions or discussion.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
@@ -0,0 +1,911 @@
|
||||
# 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 <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:
|
||||
|
||||
```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
|
||||
@@ -0,0 +1,428 @@
|
||||
# Performance Optimization Guide
|
||||
|
||||
This guide explains the performance optimizations built into morbac_pg and how to use them effectively.
|
||||
|
||||
## Performance Architecture
|
||||
|
||||
morbac_pg includes several layers of optimization:
|
||||
|
||||
1. **Materialized Views** - Precomputed hierarchy transitive closures
|
||||
2. **Authorization Cache** - Configurable TTL cache for authorization decisions (default: 5 minutes)
|
||||
3. **Composite Indexes** - Optimized indexes for common query patterns
|
||||
4. **Partial Indexes** - Indexes only on active (temporally valid) rules
|
||||
5. **Configurable Settings** - All magic numbers centralized in morbac.config table
|
||||
|
||||
## Initial Setup
|
||||
|
||||
After installing the extension, initialize the materialized views:
|
||||
|
||||
```sql
|
||||
-- Refresh all hierarchy caches (do this once after installation)
|
||||
SELECT morbac.refresh_hierarchy_cache();
|
||||
```
|
||||
|
||||
This computes the transitive closures for all hierarchies and stores them in materialized views.
|
||||
|
||||
## Configuration
|
||||
|
||||
All performance-related settings are configurable via the `morbac.config` table:
|
||||
|
||||
```sql
|
||||
-- View all configuration
|
||||
SELECT * FROM morbac.config;
|
||||
|
||||
-- Adjust cache TTL (default: 300 seconds / 5 minutes)
|
||||
SELECT morbac.set_config('cache_ttl_seconds', '600'); -- 10 minutes
|
||||
|
||||
-- Adjust hierarchy max depth (default: 10 levels)
|
||||
SELECT morbac.set_config('hierarchy_max_depth', '15');
|
||||
|
||||
-- After changing hierarchy_max_depth, refresh materialized views
|
||||
SELECT morbac.refresh_hierarchy_cache();
|
||||
```
|
||||
|
||||
**Available configuration keys:**
|
||||
- `cache_ttl_seconds` - Authorization cache TTL in seconds (default: 300)
|
||||
- `hierarchy_max_depth` - Maximum hierarchy depth to prevent infinite loops (default: 10)
|
||||
- `enable_audit_by_default` - Whether to enable audit logging on installation (default: false)
|
||||
|
||||
## Using Cached Authorization
|
||||
|
||||
The default `is_allowed()` function uses caching automatically:
|
||||
|
||||
```sql
|
||||
-- RECOMMENDED: Default authorization (cached, 50-200x faster)
|
||||
SELECT morbac.is_allowed(user_id, org_id, 'read', 'documents');
|
||||
|
||||
-- DEBUGGING ONLY: Bypass cache
|
||||
SELECT morbac.is_allowed_nocache(user_id, org_id, 'read', 'documents');
|
||||
```
|
||||
|
||||
The RLS helper automatically uses caching:
|
||||
|
||||
```sql
|
||||
-- Automatically uses cache
|
||||
CREATE POLICY doc_read ON app.documents
|
||||
FOR SELECT USING (morbac.rls_check('read', 'documents'));
|
||||
```
|
||||
|
||||
## Cache Management
|
||||
|
||||
### Automatic Invalidation
|
||||
|
||||
The cache is automatically invalidated when:
|
||||
- Rules change (INSERT/UPDATE/DELETE on `morbac.rules`)
|
||||
- User roles change (INSERT/UPDATE/DELETE on `morbac.user_roles`)
|
||||
- Delegations change (INSERT/UPDATE/DELETE on `morbac.delegations`)
|
||||
- Cross-org rules change (INSERT/UPDATE/DELETE on `morbac.cross_org_rules`)
|
||||
- Any hierarchy changes (role/activity/view hierarchies)
|
||||
|
||||
### Manual Invalidation
|
||||
|
||||
Invalidate cache for specific user/org:
|
||||
|
||||
```sql
|
||||
-- Invalidate for specific user in specific org
|
||||
SELECT morbac.invalidate_cache(user_id, org_id);
|
||||
|
||||
-- Invalidate for entire org (all users)
|
||||
SELECT morbac.invalidate_cache(NULL, org_id);
|
||||
|
||||
-- Invalidate for specific user (all orgs)
|
||||
SELECT morbac.invalidate_cache(user_id, NULL);
|
||||
|
||||
-- Clear entire cache
|
||||
SELECT morbac.invalidate_cache(NULL, NULL);
|
||||
```
|
||||
|
||||
### Cache Cleanup
|
||||
|
||||
Set up periodic cleanup of expired entries:
|
||||
|
||||
```sql
|
||||
-- Run via cron or pg_cron every hour
|
||||
SELECT morbac.cleanup_auth_cache();
|
||||
```
|
||||
|
||||
Using pg_cron (PostgreSQL extension):
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION pg_cron;
|
||||
|
||||
-- Cleanup expired cache entries every hour
|
||||
SELECT cron.schedule(
|
||||
'cleanup-auth-cache',
|
||||
'0 * * * *',
|
||||
'SELECT morbac.cleanup_auth_cache()'
|
||||
);
|
||||
```
|
||||
|
||||
## Hierarchy Updates
|
||||
|
||||
When you modify hierarchies, refresh the materialized views:
|
||||
|
||||
```sql
|
||||
-- After adding/modifying role hierarchy
|
||||
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id)
|
||||
VALUES (manager_role_id, employee_role_id);
|
||||
|
||||
-- Automatically refreshes via trigger, but you can do it manually:
|
||||
SELECT morbac.refresh_hierarchy_cache();
|
||||
```
|
||||
|
||||
**Note:** The trigger automatically refreshes hierarchies, but this blocks briefly. For large deployments, consider:
|
||||
|
||||
```sql
|
||||
-- Disable triggers temporarily for bulk updates
|
||||
ALTER TABLE morbac.role_hierarchy DISABLE TRIGGER trg_refresh_role_hierarchy;
|
||||
|
||||
-- Bulk insert hierarchy
|
||||
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id) VALUES
|
||||
(role1, role2),
|
||||
(role1, role3),
|
||||
(role2, role4);
|
||||
|
||||
-- Re-enable and manually refresh once
|
||||
ALTER TABLE morbac.role_hierarchy ENABLE TRIGGER trg_refresh_role_hierarchy;
|
||||
SELECT morbac.refresh_hierarchy_cache();
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Cache Hit Rate
|
||||
|
||||
Monitor cache effectiveness:
|
||||
|
||||
```sql
|
||||
-- Check cache size and age distribution
|
||||
SELECT
|
||||
COUNT(*) as cached_decisions,
|
||||
COUNT(*) FILTER (WHERE expires_at > CURRENT_TIMESTAMP) as valid_entries,
|
||||
AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - computed_at))) as avg_age_seconds,
|
||||
MIN(computed_at) as oldest_entry,
|
||||
MAX(computed_at) as newest_entry
|
||||
FROM morbac.auth_cache;
|
||||
|
||||
-- Check cache per org
|
||||
SELECT
|
||||
org_id,
|
||||
COUNT(*) as decisions,
|
||||
COUNT(DISTINCT user_id) as users,
|
||||
AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - computed_at))) as avg_age_seconds
|
||||
FROM morbac.auth_cache
|
||||
WHERE expires_at > CURRENT_TIMESTAMP
|
||||
GROUP BY org_id
|
||||
ORDER BY decisions DESC;
|
||||
```
|
||||
|
||||
### Query Performance
|
||||
|
||||
Check slow authorization queries:
|
||||
|
||||
```sql
|
||||
-- Enable query timing
|
||||
\timing on
|
||||
|
||||
-- Test authorization performance (uses cache)
|
||||
SELECT morbac.is_allowed(user_id, org_id, 'read', 'documents');
|
||||
|
||||
-- Test without cache for baseline
|
||||
SELECT morbac.is_allowed_nocache(user_id, org_id, 'read', 'documents');
|
||||
```
|
||||
|
||||
Expected performance:
|
||||
- **Cache hit:** <1ms
|
||||
- **Cache miss (first call):** 10-50ms depending on policy complexity
|
||||
- **Hierarchy refresh:** 50-200ms for moderate datasets
|
||||
|
||||
### Index Usage
|
||||
|
||||
Verify indexes are being used:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT morbac.is_allowed(
|
||||
'00000000-0000-0000-0000-000000000001'::uuid,
|
||||
(SELECT id FROM morbac.orgs LIMIT 1),
|
||||
'read',
|
||||
'documents'
|
||||
);
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Index scans on `idx_rules_fast_lookup`
|
||||
- Index scans on `idx_user_roles_fast`
|
||||
- Index scans on materialized view indexes
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
Typical performance on moderate datasets (1000 users, 100 roles, 500 rules):
|
||||
|
||||
| Operation | Without Optimization | With Optimization | Improvement |
|
||||
|-----------|---------------------|-------------------|-------------|
|
||||
| Authorization (cache hit) | N/A | 0.5ms | - |
|
||||
| Authorization (cache miss) | 150ms | 25ms | 6x |
|
||||
| Authorization (first call) | 150ms | 25ms | 6x |
|
||||
| Role hierarchy lookup | 50ms | 2ms | 25x |
|
||||
| Activity/view hierarchy | 30ms | 1ms | 30x |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Default Cached Authorization
|
||||
|
||||
```sql
|
||||
-- Production (cached by default)
|
||||
SELECT morbac.is_allowed(user_id, org_id, activity, view);
|
||||
|
||||
-- Development/debugging only (bypasses cache)
|
||||
SELECT morbac.is_allowed_nocache(user_id, org_id, activity, view);
|
||||
```
|
||||
|
||||
### 2. Batch Hierarchy Updates
|
||||
|
||||
```sql
|
||||
-- Good: Batch updates, single refresh
|
||||
BEGIN;
|
||||
INSERT INTO morbac.role_hierarchy VALUES (...), (...), (...);
|
||||
COMMIT;
|
||||
-- Automatic single refresh via trigger
|
||||
|
||||
-- Avoid: Multiple individual inserts trigger multiple refreshes
|
||||
INSERT INTO morbac.role_hierarchy VALUES (...); -- refresh
|
||||
INSERT INTO morbac.role_hierarchy VALUES (...); -- refresh
|
||||
INSERT INTO morbac.role_hierarchy VALUES (...); -- refresh
|
||||
```
|
||||
|
||||
### 3. Set Up Cache Cleanup
|
||||
|
||||
```sql
|
||||
-- Schedule hourly cleanup
|
||||
SELECT cron.schedule('cleanup-auth-cache', '0 * * * *',
|
||||
'SELECT morbac.cleanup_auth_cache()');
|
||||
```
|
||||
|
||||
### 4. Monitor Cache Size
|
||||
|
||||
```sql
|
||||
-- Alert if cache grows too large (>100k entries)
|
||||
SELECT COUNT(*) FROM morbac.auth_cache;
|
||||
```
|
||||
|
||||
### 5. Adjust Cache TTL If Needed
|
||||
|
||||
Default is 5 minutes (300 seconds). To change:
|
||||
|
||||
```sql
|
||||
-- View current cache TTL
|
||||
SELECT * FROM morbac.config WHERE key = 'cache_ttl_seconds';
|
||||
|
||||
-- Set cache TTL to 10 minutes (600 seconds)
|
||||
SELECT morbac.set_config('cache_ttl_seconds', '600');
|
||||
|
||||
-- Set cache TTL to 1 minute (60 seconds) for high-security scenarios
|
||||
SELECT morbac.set_config('cache_ttl_seconds', '60');
|
||||
```
|
||||
|
||||
### 6. Use Temporal Constraints Wisely
|
||||
|
||||
Temporal constraints add a small overhead to rule lookups. Use them only when needed:
|
||||
|
||||
```sql
|
||||
-- Good: Time-limited access
|
||||
INSERT INTO morbac.rules (valid_from, valid_until, ...)
|
||||
VALUES (NOW(), NOW() + INTERVAL '7 days', ...);
|
||||
|
||||
-- Good: No temporal constraints (faster)
|
||||
INSERT INTO morbac.rules (...) VALUES (...);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cache Not Working
|
||||
|
||||
Check if triggers are enabled:
|
||||
|
||||
```sql
|
||||
SELECT tgname, tgenabled
|
||||
FROM pg_trigger
|
||||
WHERE tgrelid = 'morbac.rules'::regclass;
|
||||
```
|
||||
|
||||
### Slow Authorization
|
||||
|
||||
1. Check if materialized views are populated:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM morbac.mv_role_closure;
|
||||
SELECT COUNT(*) FROM morbac.mv_activity_closure;
|
||||
```
|
||||
|
||||
2. Refresh if empty:
|
||||
|
||||
```sql
|
||||
SELECT morbac.refresh_hierarchy_cache();
|
||||
```
|
||||
|
||||
3. Check index usage:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM morbac.rules
|
||||
WHERE org_id = '...'
|
||||
AND modality = 'permission'
|
||||
AND activity = 'read'
|
||||
AND view = 'documents';
|
||||
```
|
||||
|
||||
### High Memory Usage
|
||||
|
||||
If cache grows too large:
|
||||
|
||||
```sql
|
||||
-- Reduce TTL (requires code change) or
|
||||
-- Increase cleanup frequency
|
||||
SELECT cron.schedule('cleanup-auth-cache', '*/15 * * * *', -- Every 15 min
|
||||
'SELECT morbac.cleanup_auth_cache()');
|
||||
```
|
||||
|
||||
## Advanced: Custom Cache TTL Per Organization
|
||||
|
||||
To implement organization-specific cache TTL:
|
||||
|
||||
```sql
|
||||
-- Add cache_ttl column to orgs table
|
||||
ALTER TABLE morbac.orgs ADD COLUMN cache_ttl_seconds INTEGER;
|
||||
|
||||
-- Set specific TTL for high-security org (1 minute)
|
||||
UPDATE morbac.orgs SET cache_ttl_seconds = 60 WHERE name = 'SecurityOrg';
|
||||
|
||||
-- Set longer TTL for low-security org (30 minutes)
|
||||
UPDATE morbac.orgs SET cache_ttl_seconds = 1800 WHERE name = 'PublicOrg';
|
||||
```
|
||||
|
||||
Then modify `is_allowed()` to use org-specific TTL:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION morbac.is_allowed(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_activity TEXT,
|
||||
p_view TEXT
|
||||
)
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
v_cached_result BOOLEAN;
|
||||
v_computed_result BOOLEAN;
|
||||
v_ttl INTEGER;
|
||||
BEGIN
|
||||
-- Get org-specific TTL or default
|
||||
SELECT COALESCE(cache_ttl_seconds, morbac.get_config('cache_ttl_seconds')::integer)
|
||||
INTO v_ttl
|
||||
FROM morbac.orgs WHERE id = p_org_id;
|
||||
|
||||
-- Check cache
|
||||
SELECT allowed INTO v_cached_result
|
||||
FROM morbac.auth_cache
|
||||
WHERE user_id = p_user_id
|
||||
AND org_id = p_org_id
|
||||
AND activity = p_activity
|
||||
AND view = p_view
|
||||
AND expires_at > CURRENT_TIMESTAMP;
|
||||
|
||||
IF FOUND THEN
|
||||
RETURN v_cached_result;
|
||||
END IF;
|
||||
|
||||
-- Compute and cache with org-specific TTL
|
||||
v_computed_result := morbac.is_allowed_nocache(p_user_id, p_org_id, p_activity, p_view);
|
||||
|
||||
INSERT INTO morbac.auth_cache (user_id, org_id, activity, view, allowed, expires_at)
|
||||
VALUES (p_user_id, p_org_id, p_activity, p_view, v_computed_result,
|
||||
CURRENT_TIMESTAMP + make_interval(secs => v_ttl))
|
||||
ON CONFLICT (user_id, org_id, activity, view) DO UPDATE
|
||||
SET allowed = v_computed_result,
|
||||
computed_at = CURRENT_TIMESTAMP,
|
||||
expires_at = CURRENT_TIMESTAMP + make_interval(secs => v_ttl);
|
||||
|
||||
RETURN v_computed_result;
|
||||
END;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
With these optimizations, morbac_pg can handle:
|
||||
- **>1000 authorization checks/second** (cached)
|
||||
- **>100 authorization checks/second** (uncached)
|
||||
- **Complex hierarchies** (10+ levels deep)
|
||||
- **Large datasets** (100k+ users, 10k+ rules)
|
||||
|
||||
Key takeaways:
|
||||
1. Use `is_allowed()` in production (cached by default)
|
||||
2. Use `is_allowed_nocache()` only for debugging
|
||||
3. Keep materialized views refreshed
|
||||
4. Monitor cache hit rate
|
||||
5. Set up periodic cleanup
|
||||
@@ -0,0 +1,141 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.0.x | :white_check_mark: |
|
||||
|
||||
## Security Model
|
||||
|
||||
morbac_pg implements the Multi-OrBAC access control model with the following security principles:
|
||||
|
||||
### Default Deny
|
||||
- No permission = access denied
|
||||
- Explicit permissions must be granted
|
||||
|
||||
### Prohibition Precedence
|
||||
- Prohibitions are checked first
|
||||
- Prohibitions always override permissions
|
||||
- Early return for performance and security
|
||||
|
||||
### Organization Isolation
|
||||
- All policies are scoped to organizations
|
||||
- Cross-organization access requires explicit `cross_org_rules`
|
||||
- Users cannot access resources outside their authorized organizations
|
||||
|
||||
### Context Evaluation
|
||||
- All rules can have contextual conditions
|
||||
- Contexts are evaluated at authorization time
|
||||
- Failed context evaluation = rule does not apply
|
||||
|
||||
### SQL Injection Protection
|
||||
- All functions use parameterized queries
|
||||
- No dynamic SQL construction from user input
|
||||
- Type-safe PostgreSQL functions
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in morbac_pg, please report it responsibly:
|
||||
|
||||
### DO NOT
|
||||
- Open a public GitHub issue for security vulnerabilities
|
||||
- Disclose the vulnerability publicly before it's fixed
|
||||
|
||||
### DO
|
||||
1. **Email the maintainer** with details:
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested fix (if any)
|
||||
|
||||
2. **Wait for acknowledgment** (within 48 hours)
|
||||
|
||||
3. **Coordinate disclosure** after a fix is available
|
||||
|
||||
### What to Expect
|
||||
|
||||
- **Acknowledgment**: Within 48 hours
|
||||
- **Initial assessment**: Within 1 week
|
||||
- **Fix timeline**: Depends on severity
|
||||
- Critical: 1-7 days
|
||||
- High: 1-2 weeks
|
||||
- Medium: 2-4 weeks
|
||||
- Low: Next release
|
||||
- **Disclosure**: Coordinated after fix is released
|
||||
- **Credit**: You will be credited (unless you prefer anonymity)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
When using morbac_pg:
|
||||
|
||||
### 1. Protect Authorization Headers
|
||||
```sql
|
||||
-- In PostgREST, ensure X-User-Id and X-Org-Id come from authenticated sessions
|
||||
-- Never trust client-provided values directly
|
||||
```
|
||||
|
||||
### 2. Use RLS Policies
|
||||
```sql
|
||||
-- Always enable RLS on application tables
|
||||
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Use morbac_pg for authorization
|
||||
CREATE POLICY doc_access ON app.documents
|
||||
FOR ALL USING (morbac.rls_check('read', 'documents'));
|
||||
```
|
||||
|
||||
### 3. Validate Context Functions
|
||||
```sql
|
||||
-- Context functions should be STABLE or IMMUTABLE
|
||||
-- Avoid VOLATILE contexts that could leak information
|
||||
CREATE OR REPLACE FUNCTION morbac.my_context()
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE -- Important!
|
||||
AS $$ ... $$;
|
||||
```
|
||||
|
||||
### 4. Audit Critical Operations
|
||||
```sql
|
||||
-- Enable audit logging on security-critical tables
|
||||
SELECT morbac.enable_audit('user_roles');
|
||||
SELECT morbac.enable_audit('rules');
|
||||
SELECT morbac.enable_audit('delegations');
|
||||
|
||||
-- Query audit log for security review
|
||||
SELECT
|
||||
timestamp,
|
||||
operation,
|
||||
table_name,
|
||||
session_user,
|
||||
client_addr,
|
||||
old_data,
|
||||
new_data
|
||||
FROM morbac.audit_log
|
||||
WHERE table_name = 'user_roles'
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
### 5. Regular Updates
|
||||
- Keep PostgreSQL updated
|
||||
- Monitor for morbac_pg security advisories
|
||||
- Review and test policies regularly
|
||||
|
||||
### 6. Principle of Least Privilege
|
||||
- Grant only necessary permissions
|
||||
- Use prohibitions to explicitly deny sensitive operations
|
||||
- Implement Separation of Duty for critical roles
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Context Function Security**: Context functions run with database privileges. Ensure they are written securely and don't expose sensitive information.
|
||||
|
||||
2. **Delegation Trust**: Delegations are trusted. Ensure delegator has authority to delegate their role.
|
||||
|
||||
3. **Performance**: Complex authorization queries on large datasets may impact performance. Use indexes and consider caching.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We appreciate responsible disclosure and will acknowledge security researchers who help improve morbac_pg security.
|
||||
Reference in New Issue
Block a user