From 3978e5980cca310456ac552ef49b06dbf22bbbe5 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 22 Aug 2026 23:11:01 +0000 Subject: [PATCH] Terminal --- .../20260807001000_audit_and_integrations.sql | 222 ------------------ 1 file changed, 222 deletions(-) delete mode 100644 supabase/migrations/20260807001000_audit_and_integrations.sql diff --git a/supabase/migrations/20260807001000_audit_and_integrations.sql b/supabase/migrations/20260807001000_audit_and_integrations.sql deleted file mode 100644 index 289fc86..0000000 --- a/supabase/migrations/20260807001000_audit_and_integrations.sql +++ /dev/null @@ -1,222 +0,0 @@ --- 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 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 IF NOT EXISTS audit_table_record_idx ON public.audit_log (table_name, record_id, occurred_at DESC); -CREATE INDEX IF NOT EXISTS audit_actor_idx ON public.audit_log (actor_id, occurred_at DESC); -CREATE INDEX IF NOT EXISTS 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_ AFTER INSERT OR UPDATE OR DELETE ON --- 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());