feat(t-paliad-154) commit 1.5: extend migration 062 with policy_audit_log
Q8 of locked design: policy CRUD audits to /admin/audit-log only, NOT to per-project /verlauf. The 4 existing audit sources (project_events, caldav_sync_log, reminder_log, partner_unit_events) don't fit cleanly: project_events would surface on /verlauf (rejected by Q8); partner_unit_events constrains event_type and requires unit_name + a non-null partner_unit_id which doesn't fit project-scoped policy changes. Added paliad.policy_audit_log as a fifth audit source — admin-only, scoped either to a project or a partner unit, snapshots scope_name so post-cascade rows still render. RLS: select for any authenticated user (route gate is the actual control); write for global_admin only. AuditService.ListEntries will union this source in commit 2 of this PR. Validated insert/select live in BEGIN ... ROLLBACK.
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
-- t-paliad-154 down migration. Reverses 062_approval_policy_unit_defaults.up.sql.
|
||||
--
|
||||
-- Order is the reverse of up:
|
||||
-- 0. Drop policy_audit_log table.
|
||||
-- 1. Drop seeded unit-default rows (anything where partner_unit_id IS NOT NULL).
|
||||
-- 2. Drop the resolver function.
|
||||
-- 3. Restore required_role CHECK without 'none'.
|
||||
-- 4. Drop the two partial unique indexes + restore the original UNIQUE composite.
|
||||
-- 5. Drop XOR check + partner_unit_id column.
|
||||
-- 6. Restore project_id NOT NULL.
|
||||
|
||||
-- 0. Drop the audit table.
|
||||
DROP TABLE IF EXISTS paliad.policy_audit_log;
|
||||
--
|
||||
-- Best-effort reversibility: any project-specific row with required_role='none'
|
||||
-- will fail the CHECK restoration. We coerce those to 'associate' before
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
-- ancestor + unit_default candidates.
|
||||
-- 4. Conservative seed defaults for every existing partner_unit:
|
||||
-- deadline+appointment × create/update/delete=associate, complete=none.
|
||||
-- 5. paliad.policy_audit_log — fifth audit source for /admin/audit-log only.
|
||||
-- Q8 of the locked design: policy changes audit on /admin/audit-log,
|
||||
-- NOT on per-project /verlauf. Distinct table keeps the verlauf union
|
||||
-- clean while letting AuditService union the new source.
|
||||
--
|
||||
-- Idempotent on re-run (ON CONFLICT DO NOTHING on the seed). Safe on a DB
|
||||
-- with 0 partner_units (the seed simply inserts zero rows).
|
||||
@@ -25,6 +29,7 @@
|
||||
-- 3. Extend required_role CHECK with 'none' sentinel.
|
||||
-- 4. CREATE FUNCTION paliad.approval_policy_effective(uuid, text, text).
|
||||
-- 5. Seed conservative defaults for every existing partner_unit.
|
||||
-- 6. CREATE TABLE paliad.policy_audit_log (admin-only audit source).
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. project_id becomes nullable; add partner_unit_id; XOR check.
|
||||
@@ -212,3 +217,69 @@ SELECT NULL, pu.id, t.entity_type, t.lifecycle_event, t.required_role
|
||||
ON CONFLICT (partner_unit_id, entity_type, lifecycle_event)
|
||||
WHERE partner_unit_id IS NOT NULL
|
||||
DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. paliad.policy_audit_log — admin-only audit source.
|
||||
--
|
||||
-- Q8 of the design: emit audit on /admin/audit-log, NOT on per-project
|
||||
-- /verlauf. Distinct table from project_events so AuditService can union it
|
||||
-- as a fifth source without polluting the verlauf SELECT.
|
||||
--
|
||||
-- One row per policy mutation (set or cleared, project-scoped or unit-scoped).
|
||||
-- The actor is always a global_admin (the route gate enforces this).
|
||||
--
|
||||
-- scope_type / scope_id: ('project', project_id) or ('unit', partner_unit_id).
|
||||
-- The FK columns (project_id, partner_unit_id) are nullable so a deleted
|
||||
-- target row doesn't cascade-purge the audit history.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE paliad.policy_audit_log (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_id uuid NOT NULL REFERENCES paliad.users(id) ON DELETE RESTRICT,
|
||||
event_type text NOT NULL CHECK (event_type IN (
|
||||
'approval_policy_set', 'approval_policy_cleared'
|
||||
)),
|
||||
scope_type text NOT NULL CHECK (scope_type IN ('project', 'unit')),
|
||||
project_id uuid REFERENCES paliad.projects(id) ON DELETE SET NULL,
|
||||
partner_unit_id uuid REFERENCES paliad.partner_units(id) ON DELETE SET NULL,
|
||||
-- Snapshot of the target's name at event time so a later cascade-set-null
|
||||
-- doesn't lose the human label.
|
||||
scope_name text NOT NULL,
|
||||
entity_type text NOT NULL CHECK (entity_type IN ('deadline', 'appointment')),
|
||||
lifecycle_event text NOT NULL CHECK (lifecycle_event IN ('create', 'update', 'complete', 'delete')),
|
||||
old_required_role text,
|
||||
new_required_role text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT policy_audit_log_scope_xor CHECK (
|
||||
(scope_type = 'project' AND project_id IS NOT NULL AND partner_unit_id IS NULL) OR
|
||||
(scope_type = 'unit' AND partner_unit_id IS NOT NULL AND project_id IS NULL) OR
|
||||
(scope_type = 'project' AND project_id IS NULL AND partner_unit_id IS NULL) OR -- post-cascade
|
||||
(scope_type = 'unit' AND partner_unit_id IS NULL AND project_id IS NULL) -- post-cascade
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX policy_audit_log_time_idx ON paliad.policy_audit_log (created_at DESC);
|
||||
CREATE INDEX policy_audit_log_actor_idx ON paliad.policy_audit_log (actor_id, created_at DESC);
|
||||
|
||||
ALTER TABLE paliad.policy_audit_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Read access: any authenticated user (the audit-log page itself is
|
||||
-- admin-gated at the route layer; this is defence-in-depth).
|
||||
CREATE POLICY policy_audit_log_select ON paliad.policy_audit_log
|
||||
FOR SELECT USING (auth.uid() IS NOT NULL);
|
||||
|
||||
-- Write access: only global_admin (defence-in-depth; service layer also
|
||||
-- gates).
|
||||
CREATE POLICY policy_audit_log_write ON paliad.policy_audit_log
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM paliad.users u
|
||||
WHERE u.id = auth.uid() AND u.global_role = 'global_admin'
|
||||
)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE paliad.policy_audit_log IS
|
||||
'Audit trail for paliad.approval_policies CRUD (t-paliad-154). '
|
||||
'Surfaces on /admin/audit-log only — not on per-project /verlauf, per '
|
||||
'design Q8 lock-in. Unioned by services.AuditService alongside '
|
||||
'project_events / partner_unit_events / caldav_sync_log / reminder_log.';
|
||||
|
||||
Reference in New Issue
Block a user