Modified by www.SourceFiles.app

This commit is contained in:
2026-08-07 04:45:54 +00:00
parent ed2aa101ec
commit 381dabe139
@@ -0,0 +1,227 @@
-- Audit logging, administrative controls, and third-party integrations —
-- spec sections 16 and 15.
--
-- The audit log is append-only by construction: authenticated is granted
-- SELECT and nothing else, and rows are written by a SECURITY DEFINER trigger
-- rather than by the client. A user who can edit a student cannot edit the
-- record of them having edited it.
-- ============================================================================
-- 1. AUDIT LOG
-- ============================================================================
CREATE TABLE IF NOT EXISTS public.audit_log (
id BIGGENERATED_PLACEHOLDER
);
DROP TABLE IF EXISTS public.audit_log;
CREATE TABLE public.audit_log (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
table_name TEXT NOT NULL,
record_id UUID,
action TEXT NOT NULL,
actor_id UUID,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
old_data JSONB,
new_data JSONB,
-- Columns that actually changed, so a diff does not have to be recomputed.
changed_columns TEXT[],
CONSTRAINT audit_action_valid CHECK (action IN ('INSERT','UPDATE','DELETE'))
);
-- Deliberately no INSERT/UPDATE/DELETE for authenticated: evidence only.
GRANT SELECT ON public.audit_log TO authenticated;
GRANT ALL ON public.audit_log TO service_role;
ALTER TABLE public.audit_log ENABLE ROW LEVEL SECURITY;
CREATE INDEX audit_table_record_idx ON public.audit_log (table_name, record_id, occurred_at DESC);
CREATE INDEX audit_actor_idx ON public.audit_log (actor_id, occurred_at DESC);
CREATE INDEX audit_occurred_idx ON public.audit_log (occurred_at DESC);
DROP POLICY IF EXISTS "audit read" ON public.audit_log;
CREATE POLICY "audit read" ON public.audit_log FOR SELECT TO authenticated
USING (public.is_org_admin() OR public.is_auditor());
-- Generic row auditor. Attach with:
-- CREATE TRIGGER trg_audit_<t> AFTER INSERT OR UPDATE OR DELETE ON <t>
-- FOR EACH ROW EXECUTE FUNCTION public.audit_row();
CREATE OR REPLACE FUNCTION public.audit_row()
RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE
v_old JSONB;
v_new JSONB;
v_id UUID;
v_changed TEXT[];
BEGIN
IF TG_OP = 'DELETE' THEN
v_old := to_jsonb(OLD);
v_new := NULL;
ELSIF TG_OP = 'INSERT' THEN
v_old := NULL;
v_new := to_jsonb(NEW);
ELSE
v_old := to_jsonb(OLD);
v_new := to_jsonb(NEW);
SELECT array_agg(key) INTO v_changed
FROM jsonb_each(v_new)
WHERE v_old -> key IS DISTINCT FROM v_new -> key;
-- Nothing actually changed; do not manufacture an audit row.
IF v_changed IS NULL THEN RETURN NULL; END IF;
END IF;
BEGIN
v_id := COALESCE((v_new ->> 'id')::uuid, (v_old ->> 'id')::uuid);
EXCEPTION WHEN OTHERS THEN
v_id := NULL; -- tables whose primary key is not a UUID
END;
INSERT INTO public.audit_log (table_name, record_id, action, actor_id,
old_data, new_data, changed_columns)
VALUES (TG_TABLE_NAME, v_id, TG_OP, (SELECT auth.uid()), v_old, v_new, v_changed);
RETURN NULL; -- AFTER trigger; return value is ignored
END;
$$;
-- Attach to the tables where "who changed this, and when" is a real question:
-- money, safety, permissions, and legally sensitive records.
DO $$
DECLARE
t TEXT;
BEGIN
FOREACH t IN ARRAY ARRAY[
'students','user_roles','invoices','invoice_line_items','ledger_entries',
'student_credits','student_alerts','parenting_plans','households',
'household_members','staff_certifications','tuition_rates',
'tuition_adjustment_rules','student_tuition_assignments','scholarships',
'payment_plans','staff_campus_assignments','campuses','student_campus_schedules'
]
LOOP
EXECUTE format('DROP TRIGGER IF EXISTS trg_audit_%1$s ON public.%1$I', t);
EXECUTE format(
'CREATE TRIGGER trg_audit_%1$s AFTER INSERT OR UPDATE OR DELETE ON public.%1$I
FOR EACH ROW EXECUTE FUNCTION public.audit_row()', t);
END LOOP;
END $$;
-- ============================================================================
-- 2. ADMINISTRATIVE CONTROLS
-- ============================================================================
-- Section 3 reserves "deletion controls" to the Super Administrator. Rather
-- than scatter that rule, destructive intent is recorded here and the actual
-- deletion is gated on a super-admin-approved request.
CREATE TABLE IF NOT EXISTS public.deletion_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
reason TEXT NOT NULL,
requested_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status TEXT NOT NULL DEFAULT 'pending',
reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
reviewed_at TIMESTAMPTZ,
review_note TEXT,
CONSTRAINT dr_status_valid CHECK (status IN ('pending','approved','denied','completed'))
);
GRANT SELECT, INSERT, UPDATE ON public.deletion_requests TO authenticated;
GRANT ALL ON public.deletion_requests TO service_role;
ALTER TABLE public.deletion_requests ENABLE ROW LEVEL SECURITY;
CREATE INDEX IF NOT EXISTS dr_status_idx ON public.deletion_requests (status, requested_at DESC);
CREATE INDEX IF NOT EXISTS dr_requested_by_idx ON public.deletion_requests (requested_by);
DROP POLICY IF EXISTS "deletion requests read" ON public.deletion_requests;
CREATE POLICY "deletion requests read" ON public.deletion_requests FOR SELECT TO authenticated
USING (public.is_org_admin() OR public.is_auditor() OR requested_by = (SELECT auth.uid()));
DROP POLICY IF EXISTS "deletion requests insert" ON public.deletion_requests;
CREATE POLICY "deletion requests insert" ON public.deletion_requests FOR INSERT TO authenticated
WITH CHECK (requested_by = (SELECT auth.uid()) AND public.is_org_admin());
-- Only a super administrator may approve.
DROP POLICY IF EXISTS "deletion requests review" ON public.deletion_requests;
CREATE POLICY "deletion requests review" ON public.deletion_requests FOR UPDATE TO authenticated
USING (public.is_super_admin()) WITH CHECK (public.is_super_admin());
-- ============================================================================
-- 3. INTEGRATIONS
-- ============================================================================
-- Credentials are NOT stored here. `credential_ref` names a secret held
-- elsewhere — the same separation the existing mail_server_settings /
-- mail-crypto.server.ts pair already uses — so a read of this table never
-- discloses a usable key.
CREATE TABLE IF NOT EXISTS public.integrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
provider TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'other',
campus_id UUID REFERENCES public.campuses(id) ON DELETE CASCADE,
-- Non-secret configuration only: endpoints, IDs, feature switches.
config JSONB NOT NULL DEFAULT '{}'::jsonb,
credential_ref TEXT,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
last_sync_at TIMESTAMPTZ,
last_sync_status TEXT,
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider, campus_id),
CONSTRAINT int_category_valid CHECK (category IN
('payment','accounting','email','sms','calendar','sis','background_check','storage','other'))
);
GRANT SELECT, INSERT, UPDATE, DELETE ON public.integrations TO authenticated;
GRANT ALL ON public.integrations TO service_role;
ALTER TABLE public.integrations ENABLE ROW LEVEL SECURITY;
CREATE INDEX IF NOT EXISTS int_campus_idx ON public.integrations (campus_id);
CREATE INDEX IF NOT EXISTS int_active_idx ON public.integrations (category) WHERE is_active;
DROP TRIGGER IF EXISTS trg_int_upd ON public.integrations;
CREATE TRIGGER trg_int_upd BEFORE UPDATE ON public.integrations
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
CREATE TABLE IF NOT EXISTS public.integration_sync_logs (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
integration_id UUID NOT NULL REFERENCES public.integrations(id) ON DELETE CASCADE,
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
finished_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'running',
records_processed INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
detail JSONB NOT NULL DEFAULT '{}'::jsonb,
CONSTRAINT isl_status_valid CHECK (status IN ('running','success','partial','failed'))
);
GRANT SELECT ON public.integration_sync_logs TO authenticated;
GRANT ALL ON public.integration_sync_logs TO service_role;
ALTER TABLE public.integration_sync_logs ENABLE ROW LEVEL SECURITY;
CREATE INDEX IF NOT EXISTS isl_integration_idx
ON public.integration_sync_logs (integration_id, started_at DESC);
-- Integrations are org-level configuration: super admins manage, org admins
-- and auditors read. Credentials never appear here regardless.
DROP POLICY IF EXISTS "integrations read" ON public.integrations;
CREATE POLICY "integrations read" ON public.integrations FOR SELECT TO authenticated
USING (public.is_org_admin() OR public.is_auditor());
DROP POLICY IF EXISTS "integrations manage" ON public.integrations;
CREATE POLICY "integrations manage" ON public.integrations FOR ALL TO authenticated
USING (public.is_super_admin()) WITH CHECK (public.is_super_admin());
DROP POLICY IF EXISTS "sync logs read" ON public.integration_sync_logs;
CREATE POLICY "sync logs read" ON public.integration_sync_logs FOR SELECT TO authenticated
USING (public.is_org_admin() OR public.is_auditor());