116 lines
5.2 KiB
SQL
116 lines
5.2 KiB
SQL
-- AK-P17a · P17 HR-Hub core — RLS ROLLOUT ARTIFACT (deferred, NOT in the auto-migration)
|
|
-- =============================================================================
|
|
-- Companion to R-P2b's prisma/rls/tenant-isolation.sql, AK-P4a's
|
|
-- prisma/rls/file-isolation.sql and AK-P4b's prisma/rls/task-isolation.sql. The
|
|
-- HR tables + enums + indexes ship in the tracked merge migration
|
|
-- (prisma/migrations/<ts>_p17a_hr_core, created at main-merge per AD-21). What
|
|
-- lives in THIS file is the Row-Level-Security block, DELIBERATELY kept out of
|
|
-- the auto-migration path (same stance as task-isolation.sql): turning RLS on
|
|
-- before every access sets the GUC would break the feature, so activation is a
|
|
-- coordinated cutover, not a schema diff.
|
|
--
|
|
-- WHY DEFERRED: enabling RLS + FORCE makes every HR query WITHOUT `app.tenant_id`
|
|
-- return ZERO rows. Per R-P2b's rule a table may only be RLS-enabled once ALL its
|
|
-- accesses set the GUC. The app-level scoping (where: { tenantId }) carries
|
|
-- isolation meanwhile.
|
|
--
|
|
-- CODE-SIDE PRECONDITIONS — DONE (AK-P17a):
|
|
-- 1. [x] Every HR web service runs every DB op inside withTenantRLS()
|
|
-- (apps/web/app/utils/integrations/hr*Service.server.ts). Internal
|
|
-- composition reuses the active transaction client via *WithTx helpers;
|
|
-- functions never nest withTenantRLS(). The only unwrapped access is the
|
|
-- GLOBAL User lookup (display-name resolution): User is not
|
|
-- tenant-scoped, so it stays on the db singleton.
|
|
-- Proven by apps/web/app/utils/db/.server/rls-hr-isolation.integration.test.ts
|
|
-- (own-tenant only, 0 cross-tenant, 0 without context, WITH CHECK blocks
|
|
-- foreign insert).
|
|
--
|
|
-- WHAT REMAINS — "Teil B" = the org-wide P5 RLS cutover (Raffael / R-P2b):
|
|
-- a. Ship this block as its own forward-only migration.
|
|
-- b. Switch the runtime DB connection to the restricted role `hivegrid_app`
|
|
-- (NOSUPERUSER, NOBYPASSRLS); otherwise FORCE is a no-op for a superuser and
|
|
-- an empty-read trap for a non-superuser owner. The GRANTs below already
|
|
-- cover that role for the HR tables.
|
|
-- c. Staging cutover + live cross-tenant 404 verification.
|
|
--
|
|
-- Forward-only (DoD 28). Inverse SQL as comment per block. Order-dependent DDL is
|
|
-- DO-guarded (P3018 protection: reproducible from an empty DB). Idempotent.
|
|
-- =============================================================================
|
|
|
|
-- Row-level security on all tenant-scoped HR tables ---------------------------
|
|
-- Pattern: tenant_id = current_setting('app.tenant_id', true). The second
|
|
-- argument `true` returns NULL instead of erroring when the GUC is not (yet) set
|
|
-- — the policy then matches nothing (fail-closed), and server-side scoping stays
|
|
-- the hard path until the cutover.
|
|
--
|
|
-- Inverse per table:
|
|
-- REVOKE SELECT, INSERT, UPDATE, DELETE ON "<tbl>" FROM hivegrid_app;
|
|
-- DROP POLICY IF EXISTS "<tbl>_tenant_isolation" ON "<tbl>";
|
|
-- ALTER TABLE "<tbl>" NO FORCE ROW LEVEL SECURITY;
|
|
-- ALTER TABLE "<tbl>" DISABLE ROW LEVEL SECURITY;
|
|
|
|
DO $$
|
|
DECLARE
|
|
tbl text;
|
|
BEGIN
|
|
FOREACH tbl IN ARRAY ARRAY[
|
|
'HrEmployeeProfile',
|
|
'HrLocation',
|
|
'HrEmployeeDocument',
|
|
'HrResource',
|
|
'HrQuickLink',
|
|
'EmployeePasswordSafeVault',
|
|
'EmployeePasswordSafeEntry',
|
|
'EmployeePasswordSafeAccess',
|
|
-- AK-P17b workflow tables
|
|
'EmployeeVacationRequest',
|
|
'EmployeeSickLeaveReport',
|
|
'HrAnnouncement',
|
|
'HrAnnouncementRead',
|
|
'HrAnnouncementReaction',
|
|
'HrAnnouncementComment',
|
|
'EmployeePayslip',
|
|
'EmployeeUploadReminder',
|
|
'EmployeeUploadReminderDispatch',
|
|
'HrRequestTemplate',
|
|
'HrRequest',
|
|
'HrSurvey',
|
|
'HrSurveyItem',
|
|
'HrSurveySubmission'
|
|
]
|
|
LOOP
|
|
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY;', tbl);
|
|
-- FORCE so the table owner is subject to the policies too (otherwise the
|
|
-- owner silently bypasses RLS).
|
|
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY;', tbl);
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE schemaname = current_schema()
|
|
AND tablename = tbl
|
|
AND policyname = tbl || '_tenant_isolation'
|
|
) THEN
|
|
EXECUTE format(
|
|
'CREATE POLICY %I ON %I USING ("tenantId" = current_setting(''app.tenant_id'', true)) WITH CHECK ("tenantId" = current_setting(''app.tenant_id'', true));',
|
|
tbl || '_tenant_isolation', tbl
|
|
);
|
|
END IF;
|
|
|
|
-- The restricted app role (R-P2b's hivegrid_app, NOBYPASSRLS) needs table
|
|
-- privileges to reach the rows the policy then filters.
|
|
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO hivegrid_app;', tbl);
|
|
END LOOP;
|
|
END
|
|
$$;
|
|
|
|
-- =============================================================================
|
|
-- Verify green before the cutover migration (AGENTS.md §Prisma):
|
|
-- pnpm prisma validate
|
|
-- pnpm prisma migrate status
|
|
-- CI: `prisma migrate deploy` against an EMPTY DB (reproducible from scratch,
|
|
-- no order dependency on Raffael's P2 migration).
|
|
-- Coordination: align ordering with R-P2b — P2 ships withTenantRLS()
|
|
-- (SET LOCAL app.tenant_id); without a tenant context the policies match nothing
|
|
-- (no data leak, just empty results) until the runtime role is hivegrid_app.
|
|
-- =============================================================================
|