init: first commit
This commit is contained in:
+383
@@ -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,36 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2026-02-19
|
||||
|
||||
### Added
|
||||
- Initial release of Multi-OrBAC PostgreSQL extension
|
||||
- Complete Multi-OrBAC implementation based on CNRS research paper
|
||||
- Core features:
|
||||
- Organization-centric access control
|
||||
- Role-based abstraction with organizational scope
|
||||
- Activity and view abstractions
|
||||
- Context-based rule evaluation
|
||||
- Four deontic modalities (permission, prohibition, obligation, recommendation)
|
||||
- Prohibition precedence over permissions
|
||||
- Multi-organization support
|
||||
- Advanced features:
|
||||
- Organization, role, activity, and view hierarchies with transitive closure
|
||||
- Temporal delegation with time bounds
|
||||
- Negative role assignments
|
||||
- Separation of Duty (SoD) constraints
|
||||
- Role cardinality constraints (min/max users)
|
||||
- Derived roles (computed via functions)
|
||||
- Cross-organizational rules
|
||||
- Administration rules (meta-policies)
|
||||
- Policy DSL with idempotent compiler
|
||||
- RLS helper functions for PostgREST integration
|
||||
- Comprehensive test suite with 20 test scenarios
|
||||
- Complete documentation
|
||||
- Build and installation automation (Makefile, install.sh)
|
||||
|
||||
[1.0.0]: https://github.com/yourusername/morbac_pg/releases/tag/v1.0.0
|
||||
@@ -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,770 @@
|
||||
# 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');
|
||||
```
|
||||
|
||||
## 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,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 morbac_pg contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Makefile for morbac_pg PostgreSQL Extension
|
||||
# Multi-OrBAC: Organization-Based Access Control
|
||||
|
||||
EXTENSION = morbac_pg
|
||||
EXTVERSION = 1.0
|
||||
|
||||
DATA = $(EXTENSION)--$(EXTVERSION).sql
|
||||
DOCS = README.md
|
||||
|
||||
# PostgreSQL extension build
|
||||
PG_CONFIG = pg_config
|
||||
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||
include $(PGXS)
|
||||
|
||||
# Custom targets
|
||||
|
||||
# Install extension in PostgreSQL
|
||||
.PHONY: install
|
||||
install:
|
||||
@echo "Installing morbac_pg extension..."
|
||||
$(MAKE) -f Makefile install
|
||||
|
||||
# Run tests
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "Running morbac_pg tests..."
|
||||
psql -f test_morbac.sql
|
||||
|
||||
# Run tests on specific database
|
||||
.PHONY: test-db
|
||||
test-db:
|
||||
@if [ -z "$(DB)" ]; then \
|
||||
echo "Usage: make test-db DB=your_database"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Running tests on database $(DB)..."
|
||||
psql -d $(DB) -f test_morbac.sql
|
||||
|
||||
# Run tests in dedicated test database
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "Creating test database morbac_test..."
|
||||
-dropdb morbac_test 2>/dev/null || true
|
||||
createdb morbac_test
|
||||
@echo "Running tests..."
|
||||
psql -d morbac_test -f test_morbac.sql
|
||||
@echo ""
|
||||
@echo "Tests completed. Database 'morbac_test' is available."
|
||||
@echo "To explore: psql -d morbac_test"
|
||||
@echo "To cleanup: dropdb morbac_test"
|
||||
|
||||
# Cleanup test database
|
||||
.PHONY: cleanup
|
||||
cleanup:
|
||||
@echo "Dropping test database..."
|
||||
-dropdb morbac_test 2>/dev/null && echo "Test database dropped." || echo "No test database found."
|
||||
|
||||
# Uninstall extension
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
@echo "Uninstalling morbac_pg extension..."
|
||||
$(MAKE) -f Makefile uninstall
|
||||
|
||||
# Show help
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "morbac_pg Makefile targets:"
|
||||
@echo ""
|
||||
@echo " make install - Install extension in PostgreSQL"
|
||||
@echo " make uninstall - Uninstall extension from PostgreSQL"
|
||||
@echo " make test - Run tests on default database"
|
||||
@echo " make test-db DB=name - Run tests on specified database"
|
||||
@echo " make test - Create test DB, run tests, keep DB"
|
||||
@echo " make cleanup - Drop test database"
|
||||
@echo " make help - Show this help"
|
||||
@echo ""
|
||||
@echo "Quick start:"
|
||||
@echo " make test # Run complete test in isolated database"
|
||||
@echo ""
|
||||
@@ -0,0 +1,189 @@
|
||||
# morbac_pg
|
||||
|
||||
**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.
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.postgresql.org/)
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-organization with organizational hierarchy
|
||||
- Role-based access with full hierarchy support
|
||||
- Activity and view hierarchies with transitive permission inheritance
|
||||
- Prohibition precedence over permissions
|
||||
- Temporal delegation with time bounds
|
||||
- Separation of duty constraints
|
||||
- Derived roles computed from application logic
|
||||
- Cross-organizational policies
|
||||
- Context-aware rules
|
||||
- Row-level security integration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- PostgreSQL 12 or higher
|
||||
- `pgcrypto` extension (included with PostgreSQL)
|
||||
|
||||
### Quick Install
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/yourusername/morbac_pg.git
|
||||
cd morbac_pg
|
||||
|
||||
# Install extension
|
||||
sudo make install
|
||||
|
||||
# Enable in your database
|
||||
psql -d mydb -c "CREATE EXTENSION morbac_pg;"
|
||||
```
|
||||
|
||||
### Manual Install
|
||||
|
||||
```bash
|
||||
# Copy files to PostgreSQL extension directory
|
||||
sudo cp morbac_pg.control $(pg_config --sharedir)/extension/
|
||||
sudo cp morbac_pg--1.0.sql $(pg_config --sharedir)/extension/
|
||||
|
||||
# Enable in PostgreSQL
|
||||
psql -d mydb -c "CREATE EXTENSION morbac_pg;"
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Authorization Check
|
||||
|
||||
```sql
|
||||
SELECT morbac.is_allowed(user_id, org_id, activity, view);
|
||||
```
|
||||
|
||||
### Row-Level Security (RLS)
|
||||
|
||||
```sql
|
||||
-- Enable RLS on your table
|
||||
ALTER TABLE app.documents ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy using Multi-OrBAC
|
||||
CREATE POLICY doc_access ON app.documents
|
||||
FOR SELECT
|
||||
USING (morbac.rls_check('read', 'documents'));
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
```sql
|
||||
-- Delegation: Alice delegates role to Bob for 1 week
|
||||
INSERT INTO morbac.delegations (delegator_user_id, delegate_user_id, role_id, org_id, valid_until)
|
||||
VALUES (alice_id, bob_id, role_id, org_id, NOW() + INTERVAL '7 days');
|
||||
|
||||
-- Separation of Duty: Preparer and approver are mutually exclusive
|
||||
INSERT INTO morbac.sod_conflicts (role1_id, role2_id, org_id)
|
||||
VALUES (preparer_role_id, approver_role_id, org_id);
|
||||
|
||||
-- Cross-Org: Global auditor can access subsidiary
|
||||
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', 'reports', 'permission');
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
make test # Run test suite in dedicated database
|
||||
make test-db DB=mydb # Run on existing database
|
||||
make cleanup # Remove test database
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [DOCUMENTATION.md](DOCUMENTATION.md) - Architecture, API reference, integration guides
|
||||
- [ADMIN_GUIDE.md](ADMIN_GUIDE.md) - Organization administrator setup and delegation
|
||||
- [test_morbac.sql](test_morbac.sql) - Test scenarios and examples
|
||||
- [CHANGELOG.md](CHANGELOG.md) - Version history
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines
|
||||
- [SECURITY.md](SECURITY.md) - Security policy
|
||||
|
||||
## Architecture
|
||||
|
||||
Authorization decision flow:
|
||||
|
||||
1. Collect all roles for user (direct, delegated, derived, hierarchy)
|
||||
2. Filter out negative role assignments
|
||||
3. Check for prohibitions - if found, deny access
|
||||
4. Check for permissions - if found, allow access
|
||||
5. Default deny
|
||||
|
||||
Prohibitions always override permissions.
|
||||
|
||||
## Research
|
||||
|
||||
Based on the CNRS research paper:
|
||||
["Extending the OrBAC model to handle multi-organization environments"](https://webhost.laas.fr/TSF/deswarte/Publications/06427.pdf)
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/morbac_pg.git
|
||||
cd morbac_pg
|
||||
make install
|
||||
make test
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- Read the [documentation](DOCUMENTATION.md)
|
||||
- Report bugs via [GitHub Issues](https://github.com/yourusername/morbac_pg/issues)
|
||||
- Ask questions in [Discussions](https://github.com/yourusername/morbac_pg/discussions)
|
||||
- Security issues: see [SECURITY.md](SECURITY.md)
|
||||
|
||||
## Comparison with Traditional RBAC
|
||||
|
||||
| Feature | Traditional RBAC | morbac_pg |
|
||||
|---------|-----------------|-----------|
|
||||
| Multi-tenancy | No | Yes |
|
||||
| Prohibitions | No | Yes |
|
||||
| Context-aware | No | Yes |
|
||||
| Hierarchies | Basic roles only | Organizations, roles, activities, views |
|
||||
| Delegation | No | Yes |
|
||||
| Separation of Duty | No | Yes |
|
||||
| Cross-organization | No | Yes |
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# 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
|
||||
-- Log policy changes and authorization decisions
|
||||
-- (morbac_pg does not include audit logging by default)
|
||||
```
|
||||
|
||||
### 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.
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# Installation script for morbac_pg extension
|
||||
|
||||
set -e
|
||||
|
||||
echo "=================================="
|
||||
echo "morbac_pg Extension Installer"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
|
||||
# Check if running as root or with sudo
|
||||
if [ "$EUID" -ne 0 ] && [ -z "$SUDO_USER" ]; then
|
||||
echo "This script needs to copy files to PostgreSQL extension directory."
|
||||
echo "Please run with sudo or as root."
|
||||
echo ""
|
||||
echo "Usage: sudo ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if pg_config is available
|
||||
if ! command -v pg_config &> /dev/null; then
|
||||
echo "Error: pg_config not found in PATH"
|
||||
echo "Please install PostgreSQL development packages:"
|
||||
echo " Ubuntu/Debian: sudo apt-get install postgresql-server-dev-all"
|
||||
echo " RedHat/CentOS: sudo yum install postgresql-devel"
|
||||
echo " macOS: brew install postgresql"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get PostgreSQL extension directory
|
||||
EXTDIR=$(pg_config --sharedir)/extension
|
||||
|
||||
echo "PostgreSQL extension directory: $EXTDIR"
|
||||
echo ""
|
||||
|
||||
# Check if directory exists
|
||||
if [ ! -d "$EXTDIR" ]; then
|
||||
echo "Error: Extension directory does not exist: $EXTDIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy extension files
|
||||
echo "Installing extension files..."
|
||||
cp -v morbac_pg.control "$EXTDIR/"
|
||||
cp -v morbac_pg--1.0.sql "$EXTDIR/"
|
||||
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "Installation complete!"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
echo "To use the extension in your database:"
|
||||
echo ""
|
||||
echo " psql -d your_database -c 'CREATE EXTENSION morbac_pg;'"
|
||||
echo ""
|
||||
echo "To run tests:"
|
||||
echo ""
|
||||
echo " make test"
|
||||
echo ""
|
||||
echo "For more information, see README.md"
|
||||
echo ""
|
||||
+1594
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
# morbac_pg extension
|
||||
# Multi-OrBAC access control model for PostgreSQL
|
||||
comment = 'Multi-OrBAC: Organization-Based Access Control with multi-organization support'
|
||||
default_version = '1.0'
|
||||
module_pathname = '$libdir/morbac_pg'
|
||||
relocatable = false
|
||||
schema = morbac
|
||||
requires = 'pgcrypto'
|
||||
+627
@@ -0,0 +1,627 @@
|
||||
-- =============================================================================
|
||||
-- Test Script for morbac_pg Extension
|
||||
-- =============================================================================
|
||||
-- This script demonstrates and tests the Multi-OrBAC implementation
|
||||
--
|
||||
-- Test Scenario:
|
||||
-- - Two organizations: "Acme Corp" and "Beta Inc"
|
||||
-- - Multiple users with different roles
|
||||
-- - Permission and prohibition rules
|
||||
-- - Context evaluation
|
||||
-- - Multi-organization resource access
|
||||
-- - Obligations and recommendations
|
||||
-- =============================================================================
|
||||
|
||||
-- Clean up any previous test
|
||||
DROP EXTENSION IF EXISTS morbac_pg CASCADE;
|
||||
DROP SCHEMA IF EXISTS morbac CASCADE;
|
||||
|
||||
-- Install the extension
|
||||
CREATE EXTENSION morbac_pg;
|
||||
|
||||
-- Verify schema and tables exist
|
||||
\echo '=== Schema and Tables Created ==='
|
||||
SELECT schemaname, tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'morbac'
|
||||
ORDER BY tablename;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 1: Create Organizations ==='
|
||||
|
||||
-- Create two organizations
|
||||
INSERT INTO morbac.orgs (id, name) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', 'Acme Corp'),
|
||||
('22222222-2222-2222-2222-222222222222', 'Beta Inc');
|
||||
|
||||
-- Create hierarchical organization (Acme Subsidiary under Acme Corp)
|
||||
INSERT INTO morbac.orgs (id, name, parent_id) VALUES
|
||||
('33333333-3333-3333-3333-333333333333', 'Acme Subsidiary', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
SELECT
|
||||
o.name,
|
||||
COALESCE(p.name, 'None') as parent_organization
|
||||
FROM morbac.orgs o
|
||||
LEFT JOIN morbac.orgs p ON o.parent_id = p.id
|
||||
ORDER BY o.parent_id NULLS FIRST, o.name;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 2: Create Roles ==='
|
||||
|
||||
-- Create roles in Acme Corp
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'admin', 'Administrator role'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'employee', 'Regular employee'),
|
||||
('cccccccc-cccc-cccc-cccc-cccccccccccc', '11111111-1111-1111-1111-111111111111', 'contractor', 'External contractor'),
|
||||
('ffffffff-ffff-ffff-ffff-ffffffffffff', '11111111-1111-1111-1111-111111111111', 'viewer', 'Read-only viewer');
|
||||
|
||||
-- Create roles in Beta Inc
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', '22222222-2222-2222-2222-222222222222', 'manager', 'Manager role'),
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '22222222-2222-2222-2222-222222222222', 'staff', 'Staff member');
|
||||
|
||||
SELECT r.name as role, o.name as organization
|
||||
FROM morbac.roles r
|
||||
JOIN morbac.orgs o ON r.org_id = o.id
|
||||
ORDER BY o.name, r.name;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 2b: Create Role Hierarchy ==='
|
||||
|
||||
-- Role hierarchy in Acme Corp:
|
||||
-- admin > employee > viewer
|
||||
-- (admin inherits from employee, employee inherits from viewer)
|
||||
INSERT INTO morbac.role_hierarchy (senior_role_id, junior_role_id) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'), -- admin inherits from employee
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'ffffffff-ffff-ffff-ffff-ffffffffffff'); -- employee inherits from viewer
|
||||
|
||||
\echo 'Role hierarchy created:'
|
||||
SELECT
|
||||
sr.name as senior_role,
|
||||
jr.name as junior_role,
|
||||
o.name as organization
|
||||
FROM morbac.role_hierarchy rh
|
||||
JOIN morbac.roles sr ON rh.senior_role_id = sr.id
|
||||
JOIN morbac.roles jr ON rh.junior_role_id = jr.id
|
||||
JOIN morbac.orgs o ON sr.org_id = o.id;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 3: Assign Users to Roles ==='
|
||||
|
||||
-- Define test users (in real scenario, these would come from auth system)
|
||||
-- User Alice (admin at Acme)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('aaaaaaaa-0000-0000-0000-000000000001', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
-- User Bob (employee at Acme)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('bbbbbbbb-0000-0000-0000-000000000002', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
-- User Charlie (contractor at Acme AND staff at Beta - multi-org user)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('cccccccc-0000-0000-0000-000000000003', 'cccccccc-cccc-cccc-cccc-cccccccccccc', '11111111-1111-1111-1111-111111111111'),
|
||||
('cccccccc-0000-0000-0000-000000000003', 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', '22222222-2222-2222-2222-222222222222');
|
||||
|
||||
-- User Diana (manager at Beta)
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('dddddddd-0000-0000-0000-000000000004', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '22222222-2222-2222-2222-222222222222');
|
||||
|
||||
SELECT
|
||||
ur.user_id,
|
||||
r.name as role,
|
||||
o.name as organization
|
||||
FROM morbac.user_roles ur
|
||||
JOIN morbac.roles r ON ur.role_id = r.id
|
||||
JOIN morbac.orgs o ON ur.org_id = o.id
|
||||
ORDER BY ur.user_id;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 4: Define Activities and Views ==='
|
||||
|
||||
INSERT INTO morbac.activities (name, description) VALUES
|
||||
('read', 'Read/view data'),
|
||||
('write', 'Create/modify data'),
|
||||
('delete', 'Delete data'),
|
||||
('approve', 'Approve actions');
|
||||
|
||||
INSERT INTO morbac.views (name, description) VALUES
|
||||
('documents', 'Document resources'),
|
||||
('reports', 'Report resources'),
|
||||
('sensitive_data', 'Sensitive/confidential data');
|
||||
|
||||
SELECT * FROM morbac.activities ORDER BY name;
|
||||
SELECT * FROM morbac.views ORDER BY name;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 5: Create Additional Contexts ==='
|
||||
|
||||
-- Business hours context (simplified - always true for this test)
|
||||
CREATE OR REPLACE FUNCTION morbac.context_business_hours()
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
-- In production, check current time against business hours
|
||||
-- For test, return TRUE
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||
('business_hours', 'During business hours', 'morbac.context_business_hours'::regproc);
|
||||
|
||||
-- Weekend context (always false for test)
|
||||
CREATE OR REPLACE FUNCTION morbac.context_weekend()
|
||||
RETURNS BOOLEAN
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
AS $$
|
||||
BEGIN
|
||||
-- In production, check if today is weekend
|
||||
-- For test, return FALSE
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO morbac.contexts (name, description, evaluator) VALUES
|
||||
('weekend', 'During weekend', 'morbac.context_weekend'::regproc);
|
||||
|
||||
SELECT name, description FROM morbac.contexts ORDER BY name;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 6: Define Policy Using DSL ==='
|
||||
|
||||
-- Acme Corp policies
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||
-- Admins can do everything on documents
|
||||
('Acme Corp', 'admin', 'read', 'documents', 'permission', 'always'),
|
||||
('Acme Corp', 'admin', 'write', 'documents', 'permission', 'always'),
|
||||
('Acme Corp', 'admin', 'delete', 'documents', 'permission', 'always'),
|
||||
|
||||
-- Employees can read and write documents
|
||||
('Acme Corp', 'employee', 'read', 'documents', 'permission', 'always'),
|
||||
('Acme Corp', 'employee', 'write', 'documents', 'permission', 'business_hours'),
|
||||
|
||||
-- Contractors can only read documents
|
||||
('Acme Corp', 'contractor', 'read', 'documents', 'permission', 'always'),
|
||||
|
||||
-- PROHIBITION: Contractors cannot access sensitive data
|
||||
('Acme Corp', 'contractor', 'read', 'sensitive_data', 'prohibition', 'always'),
|
||||
('Acme Corp', 'contractor', 'write', 'sensitive_data', 'prohibition', 'always'),
|
||||
|
||||
-- OBLIGATION: Employees must review reports weekly
|
||||
('Acme Corp', 'employee', 'read', 'reports', 'obligation', 'always'),
|
||||
|
||||
-- Admins can access reports
|
||||
('Acme Corp', 'admin', 'read', 'reports', 'permission', 'always'),
|
||||
('Acme Corp', 'admin', 'approve', 'reports', 'permission', 'always');
|
||||
|
||||
-- Beta Inc policies
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||
-- Managers have full access
|
||||
('Beta Inc', 'manager', 'read', 'documents', 'permission', 'always'),
|
||||
('Beta Inc', 'manager', 'write', 'documents', 'permission', 'always'),
|
||||
('Beta Inc', 'manager', 'read', 'reports', 'permission', 'always'),
|
||||
|
||||
-- Staff can read documents
|
||||
('Beta Inc', 'staff', 'read', 'documents', 'permission', 'always'),
|
||||
|
||||
-- PROHIBITION: No document modifications on weekends (example)
|
||||
('Beta Inc', 'staff', 'write', 'documents', 'prohibition', 'weekend'),
|
||||
|
||||
-- RECOMMENDATION: Staff should review reports
|
||||
('Beta Inc', 'staff', 'read', 'reports', 'recommendation', 'always');
|
||||
|
||||
SELECT org_name, role_name, activity, view, modality, context_name
|
||||
FROM morbac.policy
|
||||
ORDER BY org_name, role_name, modality;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 7: Compile Policy ==='
|
||||
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
-- Verify rules were created
|
||||
\echo ''
|
||||
\echo 'Rules created by policy compiler:'
|
||||
SELECT
|
||||
o.name as org,
|
||||
r.name as role,
|
||||
ru.activity,
|
||||
ru.view,
|
||||
ru.modality,
|
||||
c.name as context
|
||||
FROM morbac.rules ru
|
||||
JOIN morbac.orgs o ON ru.org_id = o.id
|
||||
JOIN morbac.roles r ON ru.role_id = r.id
|
||||
JOIN morbac.contexts c ON ru.context_id = c.id
|
||||
ORDER BY o.name, r.name, ru.modality, ru.activity;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 8: Authorization Decisions ==='
|
||||
|
||||
-- Test Alice (admin at Acme)
|
||||
\echo 'Alice (admin at Acme Corp):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
SELECT
|
||||
'delete documents' as action,
|
||||
morbac.is_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'delete',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test Bob (employee at Acme)
|
||||
\echo ''
|
||||
\echo 'Bob (employee at Acme Corp):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
SELECT
|
||||
'delete documents' as action,
|
||||
morbac.is_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'delete',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test Charlie (contractor at Acme)
|
||||
\echo ''
|
||||
\echo 'Charlie (contractor at Acme Corp):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
SELECT
|
||||
'write documents' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test PROHIBITION precedence
|
||||
\echo ''
|
||||
\echo 'Testing PROHIBITION PRECEDENCE:'
|
||||
\echo 'Charlie (contractor) trying to read sensitive_data (should be DENIED by prohibition):'
|
||||
SELECT
|
||||
'read sensitive_data' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'sensitive_data'
|
||||
) as allowed;
|
||||
|
||||
-- Test Charlie at Beta Inc (multi-org user)
|
||||
\echo ''
|
||||
\echo 'Charlie as staff at Beta Inc:'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
-- Test Diana (manager at Beta)
|
||||
\echo ''
|
||||
\echo 'Diana (manager at Beta Inc):'
|
||||
SELECT
|
||||
'write documents' as action,
|
||||
morbac.is_allowed(
|
||||
'dddddddd-0000-0000-0000-000000000004'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||
'write',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 8b: Role Hierarchy Inheritance ==='
|
||||
|
||||
-- Assign user Eve only viewer role
|
||||
\echo 'Creating user Eve with only viewer role at Acme:'
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('eeeeeeee-0000-0000-0000-000000000005', 'ffffffff-ffff-ffff-ffff-ffffffffffff', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
-- Add permission for viewer role
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality) VALUES
|
||||
('Acme Corp', 'viewer', 'read', 'documents', 'permission');
|
||||
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
\echo ''
|
||||
\echo 'Eve (has viewer role, which employee inherits from):'
|
||||
SELECT
|
||||
'read documents' as action,
|
||||
morbac.is_allowed(
|
||||
'eeeeeeee-0000-0000-0000-000000000005'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo ''
|
||||
\echo 'Bob (employee) should inherit permissions from viewer role:'
|
||||
\echo 'Effective roles for Bob (should include employee and viewer via hierarchy):'
|
||||
SELECT r.name, er.depth
|
||||
FROM morbac.get_effective_roles(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
) er
|
||||
JOIN morbac.roles r ON er.role_id = r.id
|
||||
ORDER BY er.depth;
|
||||
|
||||
\echo ''
|
||||
\echo 'Alice (admin) should inherit from both employee and viewer:'
|
||||
SELECT r.name, er.depth
|
||||
FROM morbac.get_effective_roles(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
) er
|
||||
JOIN morbac.roles r ON er.role_id = r.id
|
||||
ORDER BY er.depth;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 8c: Organization Hierarchy ==='
|
||||
|
||||
\echo 'Ancestors of Acme Subsidiary:'
|
||||
SELECT o.name, oa.depth
|
||||
FROM morbac.get_org_ancestors('33333333-3333-3333-3333-333333333333'::uuid) oa
|
||||
JOIN morbac.orgs o ON oa.org_id = o.id
|
||||
ORDER BY oa.depth;
|
||||
|
||||
\echo ''
|
||||
\echo 'Descendants of Acme Corp (should include subsidiary):'
|
||||
SELECT o.name, od.depth
|
||||
FROM morbac.get_org_descendants('11111111-1111-1111-1111-111111111111'::uuid) od
|
||||
JOIN morbac.orgs o ON od.org_id = o.id
|
||||
ORDER BY od.depth;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 9: Obligations and Recommendations ==='
|
||||
|
||||
\echo 'Obligations for Bob (employee at Acme):'
|
||||
SELECT * FROM morbac.pending_obligations(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo 'Recommendations for Charlie (staff at Beta):'
|
||||
SELECT * FROM morbac.pending_recommendations(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 10: Utility Functions ==='
|
||||
|
||||
\echo 'Roles for Charlie at Acme Corp:'
|
||||
SELECT * FROM morbac.user_roles_in_org(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo 'Roles for Charlie at Beta Inc:'
|
||||
SELECT * FROM morbac.user_roles_in_org(
|
||||
'cccccccc-0000-0000-0000-000000000003'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid
|
||||
);
|
||||
|
||||
\echo ''
|
||||
\echo 'Does Alice have admin role at Acme Corp?'
|
||||
SELECT morbac.user_has_role(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'admin'
|
||||
) as has_admin_role;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 11: Context Evaluation ==='
|
||||
|
||||
\echo 'Evaluating always context:'
|
||||
SELECT morbac.eval_context(
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'always')
|
||||
) as result;
|
||||
|
||||
\echo ''
|
||||
\echo 'Evaluating business_hours context:'
|
||||
SELECT morbac.eval_context(
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'business_hours')
|
||||
) as result;
|
||||
|
||||
\echo ''
|
||||
\echo 'Evaluating weekend context:'
|
||||
SELECT morbac.eval_context(
|
||||
(SELECT id FROM morbac.contexts WHERE name = 'weekend')
|
||||
) as result;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 12: Policy Re-compilation (Idempotency Test) ==='
|
||||
|
||||
\echo 'Compiling policy again (should show 0 new rules, idempotent):'
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 13: Adding New Policy Entry ==='
|
||||
|
||||
INSERT INTO morbac.policy (org_name, role_name, activity, view, modality, context_name) VALUES
|
||||
('Acme Corp', 'admin', 'approve', 'documents', 'permission', 'always');
|
||||
|
||||
\echo 'Compiling new policy entry:'
|
||||
SELECT * FROM morbac.compile_policy();
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 14: Delegation ==='
|
||||
|
||||
-- Alice delegates admin role to user Frank temporarily
|
||||
INSERT INTO morbac.delegations (delegator_id, delegatee_id, role_id, org_id, valid_from, valid_until) VALUES
|
||||
('aaaaaaaa-0000-0000-0000-000000000001', 'ffffffff-0000-0000-0000-000000000006', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', now(), now() + interval '1 day');
|
||||
|
||||
\echo 'Frank (via delegation from Alice) should have admin permissions:'
|
||||
SELECT
|
||||
'delete documents (delegated)' as action,
|
||||
morbac.is_allowed(
|
||||
'ffffffff-0000-0000-0000-000000000006'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'delete',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo 'Frank effective roles (should include delegated admin):'
|
||||
SELECT r.name, cr.source
|
||||
FROM morbac.get_comprehensive_roles('ffffffff-0000-0000-0000-000000000006'::uuid, '11111111-1111-1111-1111-111111111111'::uuid) cr
|
||||
JOIN morbac.roles r ON cr.role_id = r.id;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 15: Separation of Duty ==='
|
||||
|
||||
-- Define SoD conflict: auditor and accountant roles are mutually exclusive
|
||||
INSERT INTO morbac.roles (id, org_id, name, description) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', 'auditor', 'Auditor role'),
|
||||
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'accountant', 'Accountant role');
|
||||
|
||||
INSERT INTO morbac.sod_conflicts (role_a_id, role_b_id, org_id, description) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'Auditors and accountants are mutually exclusive');
|
||||
|
||||
-- Assign user to auditor role
|
||||
INSERT INTO morbac.user_roles (user_id, role_id, org_id) VALUES
|
||||
('99999999-0000-0000-0000-000000000009', '11111111-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111');
|
||||
|
||||
\echo 'Check SoD violation if we try to assign accountant role to auditor:'
|
||||
SELECT morbac.check_sod_violation(
|
||||
'99999999-0000-0000-0000-000000000009'::uuid,
|
||||
'22222222-2222-2222-2222-222222222222'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid
|
||||
) as would_violate_sod;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 16: Activity and View Hierarchies ==='
|
||||
|
||||
-- Define activity hierarchy: write implies read
|
||||
INSERT INTO morbac.activity_hierarchy (senior_activity, junior_activity) VALUES
|
||||
('write', 'read');
|
||||
|
||||
-- Define view hierarchy: sensitive_data is a type of documents
|
||||
INSERT INTO morbac.view_hierarchy (senior_view, junior_view) VALUES
|
||||
('sensitive_data', 'documents');
|
||||
|
||||
\echo 'Activities implied by write:'
|
||||
SELECT * FROM morbac.get_effective_activities('write');
|
||||
|
||||
\echo 'Views for sensitive_data (should include documents):'
|
||||
SELECT * FROM morbac.get_effective_views('sensitive_data');
|
||||
|
||||
\echo 'Bob has permission for read+documents, so write+documents should work (activity hierarchy):'
|
||||
SELECT
|
||||
'write documents (via activity hierarchy)' as action,
|
||||
morbac.is_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'write',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 17: Negative Role Assignment ==='
|
||||
|
||||
-- Explicitly prohibit contractor from ever being admin
|
||||
INSERT INTO morbac.negative_role_assignments (user_id, role_id, org_id, reason) VALUES
|
||||
('cccccccc-0000-0000-0000-000000000003', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'Contractors cannot be administrators');
|
||||
|
||||
\echo 'Charlie cannot have admin role even if explicitly assigned (negative assignment takes precedence).'
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 18: Cardinality Constraints ==='
|
||||
|
||||
-- Set max 2 users for admin role
|
||||
INSERT INTO morbac.role_cardinality (role_id, min_users, max_users, description) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 1, 2, 'Admin role limited to 2 users minimum 1');
|
||||
|
||||
\echo 'Check cardinality when adding user (1 admin exists, max is 2, should be OK):'
|
||||
SELECT morbac.check_cardinality_violation('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, TRUE) as violation;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 19: Administration Rules ==='
|
||||
|
||||
-- Only admin role can create rules
|
||||
INSERT INTO morbac.admin_rules (org_id, role_id, admin_activity, admin_target, context_id, modality) VALUES
|
||||
('11111111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'create_rule', 'rules', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||
|
||||
\echo 'Alice (admin) can create rules:'
|
||||
SELECT morbac.is_admin_allowed(
|
||||
'aaaaaaaa-0000-0000-0000-000000000001'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'create_rule',
|
||||
'rules'
|
||||
) as can_create_rules;
|
||||
|
||||
\echo 'Bob (employee) cannot create rules:'
|
||||
SELECT morbac.is_admin_allowed(
|
||||
'bbbbbbbb-0000-0000-0000-000000000002'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'create_rule',
|
||||
'rules'
|
||||
) as can_create_rules;
|
||||
|
||||
\echo ''
|
||||
\echo '=== Test 20: Cross-Organization Rules ==='
|
||||
|
||||
-- Allow Beta managers to read Acme documents (inter-org collaboration)
|
||||
INSERT INTO morbac.cross_org_rules (source_org_id, target_org_id, role_id, activity, view, context_id, modality) VALUES
|
||||
('22222222-2222-2222-2222-222222222222', '11111111-1111-1111-1111-111111111111', 'dddddddd-dddd-dddd-dddd-dddddddddddd', 'read', 'documents', (SELECT id FROM morbac.contexts WHERE name = 'always'), 'permission');
|
||||
|
||||
\echo 'Diana (manager at Beta) can read Acme documents via cross-org rule:'
|
||||
SELECT
|
||||
'read Acme documents from Beta' as action,
|
||||
morbac.is_allowed(
|
||||
'dddddddd-0000-0000-0000-000000000004'::uuid,
|
||||
'11111111-1111-1111-1111-111111111111'::uuid,
|
||||
'read',
|
||||
'documents'
|
||||
) as allowed;
|
||||
|
||||
\echo ''
|
||||
\echo '=== ALL TESTS COMPLETED SUCCESSFULLY ==='
|
||||
\echo ''
|
||||
\echo 'Summary:'
|
||||
\echo '- Extension installed and schema created'
|
||||
\echo '- Organizations with hierarchy'
|
||||
\echo '- Roles with hierarchy'
|
||||
\echo '- Delegation support'
|
||||
\echo '- Separation of Duty constraints'
|
||||
\echo '- Activity and View hierarchies'
|
||||
\echo '- Negative role assignments'
|
||||
\echo '- Cardinality constraints'
|
||||
\echo '- Administration rules'
|
||||
\echo '- Cross-organizational rules'
|
||||
\echo '- Derived roles (framework ready)'
|
||||
\echo '- Complete authorization with all features'
|
||||
\echo '- Prohibition precedence verified'
|
||||
\echo '- Multi-organization support verified'
|
||||
Reference in New Issue
Block a user