Skip to content

Permission Sets

Permission Sets are tenant-specific collections of permissions that group related access rights together. They provide a reusable way to assign multiple permissions at once, making permission management more maintainable and consistent.

Key Concepts

Global Permissions vs Tenant Permission Sets

Global Permissions (auth.permission): - Defined once for the entire system - Hierarchical structure using PostgreSQL ltree - Same permission tree for all tenants - Examples: users.create, reports.view, admin.system

Permission Sets (auth.perm_set): - Tenant-specific groupings of global permissions - Each tenant can create unique sets - Reusable across users and groups within a tenant - Examples: "Administrator", "Read-Only User", "Department Manager"

Database Structure

Permission Set Tables

-- Permission sets per tenant
CREATE TABLE auth.perm_set (
    perm_set_id uuid PRIMARY KEY,
    tenant_id uuid REFERENCES auth.tenant,
    code text NOT NULL,
    name text NOT NULL,
    description text,
    is_active boolean DEFAULT true,
    created_at timestamptz DEFAULT now(),
    updated_at timestamptz DEFAULT now(),
    UNIQUE (tenant_id, code)
);

-- Links permissions to permission sets
CREATE TABLE auth.perm_set_perm (
    perm_set_id uuid REFERENCES auth.perm_set,
    permission_id uuid REFERENCES auth.permission,
    added_at timestamptz DEFAULT now(),
    PRIMARY KEY (perm_set_id, permission_id)
);

Creating Permission Sets

Basic Permission Set

-- Create a permission set
SELECT auth.create_perm_set(
    _tenant_id := 'tenant-uuid',
    _code := 'VIEWER',
    _name := 'Viewer Role',
    _description := 'Read-only access to most resources'
);

Add Permissions to Set

-- Add individual permissions to a set
SELECT auth.add_perm_set_permissions(
    _tenant_id := 'tenant-uuid',
    _perm_set_code := 'VIEWER',
    _perm_codes := ARRAY[
        'users.read',
        'reports.view',
        'dashboard.access'
    ]
);

Create Complete Permission Set

-- Create and populate a permission set in one transaction
DO $$
DECLARE
    v_perm_set_id uuid;
BEGIN
    -- Create the permission set
    v_perm_set_id := auth.create_perm_set(
        'tenant-uuid',
        'MANAGER',
        'Manager Role',
        'Management access with limited admin rights'
    );

    -- Add permissions
    INSERT INTO auth.perm_set_perm (perm_set_id, permission_id)
    SELECT
        v_perm_set_id,
        p.permission_id
    FROM auth.permission p
    WHERE p.code IN (
        'users.read',
        'users.update',
        'reports',
        'dashboard',
        'teams.manage'
    );
END $$;

Common Permission Set Patterns

Administrator Set

-- Full administrative access
SELECT auth.create_perm_set(
    _tenant_id := 'tenant-uuid',
    _code := 'ADMIN',
    _name := 'Administrator',
    _description := 'Full system access'
);

-- Add all permissions (or specific admin permissions)
INSERT INTO auth.perm_set_perm (perm_set_id, permission_id)
SELECT
    ps.perm_set_id,
    p.permission_id
FROM auth.perm_set ps
CROSS JOIN auth.permission p
WHERE ps.code = 'ADMIN'
  AND ps.tenant_id = 'tenant-uuid';

Read-Only Set

-- Read-only access
SELECT auth.create_perm_set(
    _tenant_id := 'tenant-uuid',
    _code := 'READONLY',
    _name := 'Read Only User',
    _description := 'View access without modification rights'
);

-- Add only read/view permissions
INSERT INTO auth.perm_set_perm (perm_set_id, permission_id)
SELECT
    ps.perm_set_id,
    p.permission_id
FROM auth.perm_set ps
CROSS JOIN auth.permission p
WHERE ps.code = 'READONLY'
  AND ps.tenant_id = 'tenant-uuid'
  AND (p.code LIKE '%.read%' OR p.code LIKE '%.view%');

Department-Specific Sets

-- HR Department permissions
SELECT auth.create_perm_set(
    _tenant_id := 'tenant-uuid',
    _code := 'HR_STAFF',
    _name := 'HR Staff',
    _description := 'Human Resources department permissions'
);

SELECT auth.add_perm_set_permissions(
    _tenant_id := 'tenant-uuid',
    _perm_set_code := 'HR_STAFF',
    _perm_codes := ARRAY[
        'employees',
        'payroll.view',
        'benefits.manage',
        'recruitment'
    ]
);

Managing Permission Sets

Update Permission Set

-- Update permission set details
UPDATE auth.perm_set
SET
    name = 'Updated Name',
    description = 'Updated description',
    updated_at = now()
WHERE tenant_id = 'tenant-uuid'
  AND code = 'VIEWER';

Add Permissions

-- Add new permissions to existing set
INSERT INTO auth.perm_set_perm (perm_set_id, permission_id)
SELECT
    ps.perm_set_id,
    p.permission_id
FROM auth.perm_set ps
CROSS JOIN auth.permission p
WHERE ps.tenant_id = 'tenant-uuid'
  AND ps.code = 'MANAGER'
  AND p.code = 'new.permission'
ON CONFLICT DO NOTHING;

Remove Permissions

-- Remove specific permissions from set
DELETE FROM auth.perm_set_perm
WHERE perm_set_id = (
    SELECT perm_set_id FROM auth.perm_set
    WHERE tenant_id = 'tenant-uuid' AND code = 'MANAGER'
)
AND permission_id IN (
    SELECT permission_id FROM auth.permission
    WHERE code IN ('permission.to.remove', 'another.permission')
);

Deactivate Permission Set

-- Soft delete permission set
UPDATE auth.perm_set
SET is_active = false, updated_at = now()
WHERE tenant_id = 'tenant-uuid' AND code = 'DEPRECATED_SET';

Copying Permission Sets

Within Same Tenant

-- Copy a permission set within tenant
SELECT auth.copy_perm_set(
    _tenant_id := 'tenant-uuid',
    _from_perm_set_code := 'VIEWER',
    _to_perm_set_code := 'VIEWER_PLUS',
    _to_name := 'Viewer Plus',
    _to_description := 'Extended viewer permissions'
);

Across Tenants

-- Duplicate permission set to another tenant
SELECT auth.duplicate_perm_set(
    _from_tenant_id := 'source-tenant-uuid',
    _to_tenant_id := 'target-tenant-uuid',
    _perm_set_code := 'STANDARD_USER',
    _new_code := 'STANDARD_USER',
    _new_name := 'Standard User'
);

Assigning Permission Sets

Assign to User

-- Assign permission set to user
SELECT auth.assign_permission(
    _tenant_id := 'tenant-uuid',
    _user_id := 'user-uuid',
    _user_group_id := NULL,
    _perm_set_code := 'MANAGER',
    _perm_code := NULL
);

Assign to Group

-- Assign permission set to group
SELECT auth.assign_permission(
    _tenant_id := 'tenant-uuid',
    _user_id := NULL,
    _user_group_id := 'group-uuid',
    _perm_set_code := 'VIEWER',
    _perm_code := NULL
);

Multiple Assignments

-- Assign multiple permission sets
DO $$
BEGIN
    -- User gets both viewer and editor sets
    PERFORM auth.assign_permission('tenant-uuid', 'user-uuid', NULL, 'VIEWER', NULL);
    PERFORM auth.assign_permission('tenant-uuid', 'user-uuid', NULL, 'EDITOR', NULL);
END $$;

Querying Permission Sets

List Tenant Permission Sets

-- Get all permission sets for a tenant
SELECT
    ps.code,
    ps.name,
    ps.description,
    COUNT(psp.permission_id) as permission_count,
    ps.is_active
FROM auth.perm_set ps
LEFT JOIN auth.perm_set_perm psp ON ps.perm_set_id = psp.perm_set_id
WHERE ps.tenant_id = 'tenant-uuid'
GROUP BY ps.perm_set_id
ORDER BY ps.name;

View Permission Set Contents

-- List all permissions in a set
SELECT
    p.code,
    p.name,
    p.description,
    p.path::text as hierarchy
FROM auth.perm_set ps
JOIN auth.perm_set_perm psp ON ps.perm_set_id = psp.perm_set_id
JOIN auth.permission p ON psp.permission_id = p.permission_id
WHERE ps.tenant_id = 'tenant-uuid'
  AND ps.code = 'MANAGER'
ORDER BY p.path;

Find Users with Permission Set

-- Find all users assigned a specific permission set
SELECT DISTINCT
    u.user_id,
    u.username,
    u.display_name,
    CASE
        WHEN pa.user_id IS NOT NULL THEN 'Direct'
        ELSE 'Via Group'
    END as assignment_type
FROM auth.permission_assignment pa
JOIN auth.perm_set ps ON pa.perm_set_id = ps.perm_set_id
LEFT JOIN auth.user_info u ON pa.user_id = u.user_id
LEFT JOIN auth.user_group ug ON pa.user_group_id = ug.user_group_id
LEFT JOIN auth.user_group_member ugm ON ug.user_group_id = ugm.user_group_id
LEFT JOIN auth.user_info u2 ON ugm.user_id = u2.user_id
WHERE ps.tenant_id = 'tenant-uuid'
  AND ps.code = 'ADMIN'
  AND (u.user_id IS NOT NULL OR u2.user_id IS NOT NULL);

Permission Set Inheritance

Hierarchical Sets

-- Create base and extended permission sets
DO $$
DECLARE
    v_base_set_id uuid;
    v_extended_set_id uuid;
BEGIN
    -- Base set
    v_base_set_id := auth.create_perm_set(
        'tenant-uuid', 'BASE_USER', 'Base User', 'Minimum permissions'
    );

    -- Extended set (includes base + more)
    v_extended_set_id := auth.create_perm_set(
        'tenant-uuid', 'POWER_USER', 'Power User', 'Extended permissions'
    );

    -- Copy base permissions to extended
    INSERT INTO auth.perm_set_perm (perm_set_id, permission_id)
    SELECT v_extended_set_id, permission_id
    FROM auth.perm_set_perm
    WHERE perm_set_id = v_base_set_id;

    -- Add additional permissions to extended
    INSERT INTO auth.perm_set_perm
    SELECT v_extended_set_id, permission_id
    FROM auth.permission
    WHERE code IN ('advanced.feature1', 'advanced.feature2');
END $$;

Best Practices

Naming Conventions

  1. Use Clear Codes: ADMIN, MANAGER, VIEWER, EDITOR
  2. Descriptive Names: "Financial Administrator" not just "Admin"
  3. Document Purpose: Clear descriptions for each set
  4. Version in Code: VIEWER_V2 when updating significantly

Permission Set Design

  1. Start Minimal: Begin with least privilege
  2. Group Logically: Related permissions together
  3. Avoid Overlap: Minimize duplicate permissions across sets
  4. Regular Review: Audit and update sets periodically

Performance Optimization

-- Create index for permission lookups
CREATE INDEX idx_perm_set_perm_lookup
ON auth.perm_set_perm (perm_set_id, permission_id);

-- Materialized view for permission set analysis
CREATE MATERIALIZED VIEW permission_set_summary AS
SELECT
    ps.tenant_id,
    ps.code,
    ps.name,
    array_agg(p.code ORDER BY p.code) as permissions
FROM auth.perm_set ps
JOIN auth.perm_set_perm psp ON ps.perm_set_id = psp.perm_set_id
JOIN auth.permission p ON psp.permission_id = p.permission_id
WHERE ps.is_active = true
GROUP BY ps.tenant_id, ps.perm_set_id;

Common Issues and Solutions

Duplicate Permissions

-- Find users with redundant permission assignments
SELECT
    u.username,
    array_agg(DISTINCT ps.code) as permission_sets,
    COUNT(DISTINCT p.permission_id) as unique_permissions,
    COUNT(p.permission_id) as total_assignments
FROM auth.permission_assignment pa
JOIN auth.user_info u ON pa.user_id = u.user_id
JOIN auth.perm_set ps ON pa.perm_set_id = ps.perm_set_id
JOIN auth.perm_set_perm psp ON ps.perm_set_id = psp.perm_set_id
JOIN auth.permission p ON psp.permission_id = p.permission_id
GROUP BY u.user_id
HAVING COUNT(p.permission_id) > COUNT(DISTINCT p.permission_id);

Orphaned Assignments

-- Find assignments to inactive permission sets
SELECT
    pa.*,
    ps.code as inactive_set
FROM auth.permission_assignment pa
JOIN auth.perm_set ps ON pa.perm_set_id = ps.perm_set_id
WHERE ps.is_active = false;

What's Next