Files
pgmorbac/ADMIN_GUIDE.md
T
2026-02-20 00:14:12 +01:00

11 KiB

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:

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:

-- 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:

-- 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:

-- 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

-- 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

-- 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:

-- 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:

-- 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):

-- 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

-- 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