fix(salary): stop RLS from failing vab/parental absence registration (#1568)
Migration 20260517135000 rewrote the franvaro-specifikationsnummer trigger functions to insert audit rows into salary_absence_franvaro_audit, a table with RLS enabled and zero policies, while leaving the functions SECURITY INVOKER (its comment claimed implicit SECURITY DEFINER, which is false in Postgres). Every vab/parental insert from role authenticated (dashboard absence POST, web /pending approval, in-app Assistenten chat) then failed with 42501, surfaced as a generic 500, and left no diagnosable trace. - New migration 20260813120000: ALTER both trigger functions to SECURITY DEFINER with search_path pinned to public, pg_temp. No RLS policy is added on the audit table: trigger/service-only writes stay the design intent. - mapInsertError: 42501 now maps to the new bilingual DB_PERMISSION_DENIED code instead of INTERNAL_ERROR, and 23514 is split so only the 24h-cap trigger's 'Total tid' message becomes ABSENCE_HOURS_CONFLICT; other CHECK violations map to VALIDATION_ERROR. - commitRegisterAbsence/commitDeleteAbsence: log the underlying PG details and persist the sanitized structured code in result_data.error_code so the next failure is traceable from the op row. - Dashboard absence route: only ABSENCE_HOURS_CONFLICT passes details.message through to the client; every other code shows the registry Swedish message instead of raw Postgres text. - New pg-real regression test locks the authenticated-role parental/vab insert path, the shared per-month specnummer sequence, the audit rows, and idempotent upsert retries. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
ebeaeec80e
commit
c4adc8eb7d
@@ -0,0 +1,33 @@
|
||||
-- Fix: register_absence 500 for foraldraledighet/VAB from user-scoped surfaces.
|
||||
--
|
||||
-- Migration 20260517135000_skatteverket_audit_franvaro_lock.sql created
|
||||
-- salary_absence_franvaro_audit with ENABLE ROW LEVEL SECURITY and ZERO
|
||||
-- policies, and rewrote the specifikationsnummer trigger functions to INSERT
|
||||
-- an audit row into it. That migration's comment claims the trigger "runs
|
||||
-- SECURITY DEFINER (implicit in plpgsql functions that own the table)"; the
|
||||
-- claim is false: plpgsql functions default to SECURITY INVOKER, so the audit
|
||||
-- INSERT executes as the calling role. Under any non-BYPASSRLS role (role
|
||||
-- authenticated: the dashboard absence POST, the web /pending approval, the
|
||||
-- in-app Assistenten chat committing staged operations) the INSERT is denied
|
||||
-- with SQLSTATE 42501, which aborts the whole salary_absence_days write. The
|
||||
-- trigger fires only for absence_type IN ('vab', 'parental'), which is why
|
||||
-- exactly those registrations failed with a generic 500 while 'sick' and
|
||||
-- every other type kept working. Service-role paths (BYPASSRLS) were never
|
||||
-- affected.
|
||||
--
|
||||
-- Fix: make both trigger functions SECURITY DEFINER so the audit INSERT runs
|
||||
-- as the function owner (the migration runner, which also owns the audit
|
||||
-- table; RLS is not FORCEd, so the owner is exempt). search_path is pinned
|
||||
-- because SECURITY DEFINER without it is a privilege-escalation footgun.
|
||||
--
|
||||
-- Deliberately NO RLS policy is added on salary_absence_franvaro_audit: the
|
||||
-- design intent is trigger/service-only writes, and an INSERT policy for
|
||||
-- authenticated would let clients forge audit rows.
|
||||
|
||||
ALTER FUNCTION public.assign_franvaro_specifikationsnummer()
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, pg_temp;
|
||||
|
||||
ALTER FUNCTION public.assign_franvaro_specifikationsnummer_on_update()
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, pg_temp;
|
||||
Reference in New Issue
Block a user