221 lines
9.9 KiB
PL/PgSQL
221 lines
9.9 KiB
PL/PgSQL
-- rbac_ingest.sql
|
|
-- One-shot ingestion of the RBAC tables: roles, permissions (bundles) and
|
|
-- permission_tags. Produces the same end state as manual migrations
|
|
-- 001, 004, 005, 007, 008, 019, 024, 026, 028 and 038 combined, so a fresh
|
|
-- database can be seeded in one pass.
|
|
--
|
|
-- Deliberately NOT under migrations/manual/ — run_manual_sql() only globs that
|
|
-- folder, so this file never auto-applies at startup. Run it by hand:
|
|
-- psql "$DATABASE_URL" -f migrations/seed/rbac_ingest.sql
|
|
--
|
|
-- Idempotent: every insert is ON CONFLICT DO NOTHING and bundle ids are only
|
|
-- appended to a role when missing, so re-running it is a no-op.
|
|
-- Users must log in again afterwards — the frontend caches /users/me permissions.
|
|
|
|
BEGIN;
|
|
|
|
-- =============================================================================
|
|
-- 1. permission_tags — every module x action (17 x 8 = 136), matches
|
|
-- users/permissions.py PermissionModule / PermissionAction
|
|
-- =============================================================================
|
|
INSERT INTO app.permission_tags
|
|
(tag_name, module, action, description, created_at, updated_at, is_active, is_deleted)
|
|
SELECT
|
|
m.module || '.' || a.action,
|
|
m.module,
|
|
a.action,
|
|
NULL,
|
|
NOW(),
|
|
NOW(),
|
|
true,
|
|
false
|
|
FROM unnest(ARRAY[
|
|
'dashboard', 'inbox', 'jobs', 'candidates', 'pipeline', 'interviews',
|
|
'assessments', 'offers', 'reports', 'analytics', 'job_board', 'settings',
|
|
'rbac_users', 'tasks', 'talent', 'requisitions', 'department'
|
|
]) WITH ORDINALITY AS m(module, m_ord)
|
|
CROSS JOIN unnest(ARRAY[
|
|
'view', 'create', 'edit', 'delete', 'approve', 'export', 'manage', 'configure'
|
|
]) WITH ORDINALITY AS a(action, a_ord)
|
|
ORDER BY m.m_ord, a.a_ord
|
|
ON CONFLICT (tag_name) DO NOTHING;
|
|
|
|
-- =============================================================================
|
|
-- 2. roles — explicit ids: the app hardcodes candidate = 8 and
|
|
-- hiring_manager = 4 (inbox/models.py, users/models.py)
|
|
-- =============================================================================
|
|
INSERT INTO app.roles
|
|
(id, role_name, description, permissions, is_system, created_at, updated_at, is_active, is_deleted)
|
|
VALUES
|
|
(1, 'system_administrator', 'Full system access', '[]'::jsonb, true, NOW(), NOW(), true, false),
|
|
(2, 'hr_administrator', 'HR administration', '[]'::jsonb, true, NOW(), NOW(), false, true),
|
|
(3, 'recruiter', 'Recruiting staff', '[]'::jsonb, true, NOW(), NOW(), true, false),
|
|
(4, 'hiring_manager', 'Hiring manager for own requisitions', '[]'::jsonb, true, NOW(), NOW(), true, false),
|
|
(5, 'department_head', 'Head of a department', '[]'::jsonb, true, NOW(), NOW(), true, false),
|
|
(6, 'interviewer', 'Interview panel member', '[]'::jsonb, true, NOW(), NOW(), false, true),
|
|
(7, 'ceo', 'Chief executive', '[]'::jsonb, true, NOW(), NOW(), false, true),
|
|
(8, 'candidate', 'Applicant account', '[]'::jsonb, true, NOW(), NOW(), true, false)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
-- Explicit ids bypass the sequence; move it past them so UI-created roles don't collide.
|
|
SELECT setval(
|
|
pg_get_serial_sequence('app.roles', 'id'),
|
|
GREATEST((SELECT MAX(id) FROM app.roles), 1)
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- 3. permissions — named bundles; each resolves to permission_tags ids by
|
|
-- module list and/or explicit tag names
|
|
-- =============================================================================
|
|
INSERT INTO app.permissions
|
|
(name, description, permission_tags, is_system, created_at, updated_at, is_active, is_deleted)
|
|
SELECT
|
|
b.name,
|
|
b.description,
|
|
(
|
|
SELECT COALESCE(jsonb_agg(t.id ORDER BY t.id), '[]'::jsonb)
|
|
FROM app.permission_tags t
|
|
WHERE t.is_deleted = false
|
|
AND (t.module = ANY(b.modules) OR t.tag_name = ANY(b.tags))
|
|
),
|
|
true,
|
|
NOW(),
|
|
NOW(),
|
|
true,
|
|
false
|
|
FROM (VALUES
|
|
('all_access',
|
|
'Every permission in the original 13 modules',
|
|
ARRAY['dashboard', 'inbox', 'jobs', 'candidates', 'pipeline', 'interviews', 'assessments',
|
|
'offers', 'reports', 'analytics', 'job_board', 'settings', 'rbac_users']::text[],
|
|
ARRAY[]::text[]),
|
|
('analytics_dashboard',
|
|
'Dashboard KPI tiles, analytics charts, offers, and interview list',
|
|
ARRAY['dashboard', 'analytics', 'offers']::text[],
|
|
ARRAY['interviews.view']::text[]),
|
|
('tasks_management',
|
|
'Recruiting task list: view, create, complete and manage tasks',
|
|
ARRAY['tasks']::text[],
|
|
ARRAY[]::text[]),
|
|
('tasks_viewer',
|
|
'Recruiting task list: read-only access',
|
|
ARRAY[]::text[],
|
|
ARRAY['tasks.view', 'tasks.export']::text[]),
|
|
('talent_sourcing',
|
|
'LinkedIn talent sourcing: run Apify searches and view sourced profiles',
|
|
ARRAY['talent']::text[],
|
|
ARRAY[]::text[]),
|
|
('hiring_forms',
|
|
'Fill and amend candidate hiring forms (requisition, interview analysis, cultural fit)',
|
|
ARRAY[]::text[],
|
|
ARRAY['interviews.create', 'interviews.edit', 'interviews.delete']::text[]),
|
|
('requisitions_management',
|
|
'Employee requisition forms: view, create, edit and manage requisitions',
|
|
ARRAY['requisitions']::text[],
|
|
ARRAY[]::text[]),
|
|
('manager_candidates',
|
|
'Hiring manager: list candidates on own requisition jobs, view profiles, write notes',
|
|
ARRAY[]::text[],
|
|
ARRAY['candidates.view', 'candidates.create', 'candidates.edit']::text[]),
|
|
('requisitions_self',
|
|
'Own employee requisition forms: view, create, edit (not org-wide manage)',
|
|
ARRAY[]::text[],
|
|
ARRAY['requisitions.view', 'requisitions.create', 'requisitions.edit']::text[]),
|
|
('interviews_tab',
|
|
'Interviews and Calendar tabs: list, schedule, reschedule',
|
|
ARRAY[]::text[],
|
|
ARRAY['interviews.view', 'interviews.create', 'interviews.edit']::text[]),
|
|
('department_management',
|
|
'Departments: view, create, edit and manage departments',
|
|
ARRAY['department']::text[],
|
|
ARRAY[]::text[])
|
|
) AS b(name, description, modules, tags)
|
|
ON CONFLICT (name) DO NOTHING;
|
|
|
|
-- =============================================================================
|
|
-- 4. roles.permissions — attach bundle ids (append only what is missing).
|
|
-- requisitions_self and interviews_tab are for custom roles, so unattached.
|
|
-- =============================================================================
|
|
WITH role_bundles(role_name, bundle_name) AS (
|
|
VALUES
|
|
('system_administrator', 'all_access'),
|
|
('system_administrator', 'analytics_dashboard'),
|
|
('system_administrator', 'tasks_management'),
|
|
('system_administrator', 'talent_sourcing'),
|
|
('system_administrator', 'hiring_forms'),
|
|
('system_administrator', 'requisitions_management'),
|
|
('system_administrator', 'department_management'),
|
|
|
|
('hr_administrator', 'analytics_dashboard'),
|
|
('hr_administrator', 'tasks_management'),
|
|
('hr_administrator', 'talent_sourcing'),
|
|
('hr_administrator', 'hiring_forms'),
|
|
('hr_administrator', 'requisitions_management'),
|
|
('hr_administrator', 'department_management'),
|
|
|
|
('recruiter', 'analytics_dashboard'),
|
|
('recruiter', 'tasks_management'),
|
|
('recruiter', 'talent_sourcing'),
|
|
('recruiter', 'hiring_forms'),
|
|
('recruiter', 'requisitions_management'),
|
|
|
|
('hiring_manager', 'analytics_dashboard'),
|
|
('hiring_manager', 'tasks_viewer'),
|
|
('hiring_manager', 'talent_sourcing'),
|
|
('hiring_manager', 'hiring_forms'),
|
|
('hiring_manager', 'requisitions_management'),
|
|
('hiring_manager', 'manager_candidates'),
|
|
|
|
('department_head', 'analytics_dashboard'),
|
|
('department_head', 'tasks_viewer'),
|
|
('department_head', 'talent_sourcing'),
|
|
('department_head', 'hiring_forms'),
|
|
('department_head', 'requisitions_management'),
|
|
|
|
('ceo', 'analytics_dashboard'),
|
|
('ceo', 'tasks_viewer'),
|
|
('ceo', 'talent_sourcing'),
|
|
('ceo', 'hiring_forms'),
|
|
('ceo', 'requisitions_management')
|
|
),
|
|
wanted AS (
|
|
SELECT rb.role_name, p.id AS permission_id
|
|
FROM role_bundles rb
|
|
JOIN app.permissions p ON p.name = rb.bundle_name AND p.is_deleted = false
|
|
)
|
|
UPDATE app.roles r
|
|
SET permissions = (
|
|
SELECT COALESCE(jsonb_agg(ids.id ORDER BY ids.id), '[]'::jsonb)
|
|
FROM (
|
|
SELECT value::int AS id
|
|
FROM jsonb_array_elements_text(COALESCE(r.permissions, '[]'::jsonb))
|
|
UNION
|
|
SELECT w.permission_id FROM wanted w WHERE w.role_name = r.role_name
|
|
) ids
|
|
),
|
|
updated_at = NOW()
|
|
WHERE EXISTS (
|
|
SELECT 1 FROM wanted w
|
|
WHERE w.role_name = r.role_name
|
|
AND NOT (COALESCE(r.permissions, '[]'::jsonb) @> jsonb_build_array(w.permission_id))
|
|
);
|
|
|
|
COMMIT;
|
|
|
|
-- =============================================================================
|
|
-- 5. Verify: role -> bundles -> resolved tag count
|
|
-- =============================================================================
|
|
SELECT
|
|
r.id,
|
|
r.role_name,
|
|
r.is_deleted,
|
|
string_agg(DISTINCT p.name, ', ' ORDER BY p.name) AS bundles,
|
|
COUNT(DISTINCT t.id) AS tag_count
|
|
FROM app.roles r
|
|
LEFT JOIN LATERAL jsonb_array_elements_text(COALESCE(r.permissions, '[]'::jsonb)) rp(pid) ON true
|
|
LEFT JOIN app.permissions p ON p.id = rp.pid::int AND p.is_deleted = false
|
|
LEFT JOIN LATERAL jsonb_array_elements_text(COALESCE(p.permission_tags, '[]'::jsonb)) pt(tid) ON true
|
|
LEFT JOIN app.permission_tags t ON t.id = pt.tid::int AND t.is_deleted = false
|
|
GROUP BY r.id, r.role_name, r.is_deleted
|
|
ORDER BY r.id;
|