diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b7bf1..1bec7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- RLS-Migration fuer tenant-bezogene Tabellen mit Membership-basierten Policies angelegt. - Audit-Log-Migration mit append-only Triggern und Tenant-Zeit-Index angelegt. - VMID-Allokator-Migration mit transaktionssicherer Reservation pro Cluster angelegt. - VMs-und-SSH-Keys-Migration mit Ressourcen-Mapping und eindeutigem Proxmox-VMID-Index angelegt. diff --git a/TODO.md b/TODO.md index 8f5b7cc..00358f6 100644 --- a/TODO.md +++ b/TODO.md @@ -64,6 +64,11 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda - [x] Tenant- und Profil-Referenzen gesetzt - [x] Index auf `(tenant_id, created_at)` gesetzt - [x] UPDATE/DELETE per Trigger blockiert +- [x] E2-T08: RLS-Policies + - [x] RLS auf tenant-bezogenen Tabellen aktiviert + - [x] Membership-basierte SELECT-Policies angelegt + - [x] Audit-Log auf owner/admin beschraenkt + - [x] Direkter Query als Nicht-Mitglied lokal verifiziert ## MVP-Backlog @@ -109,3 +114,4 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda - 2026-06-10: VMs-und-SSH-Keys-Migration `0005_vms_and_ssh_keys` angelegt und lokal gegen Supabase verifiziert. - 2026-06-10: VMID-Allokator-Migration `0006_vmid_allocator` angelegt und mit 50 parallelen Reservierungen lokal gegen Supabase verifiziert. - 2026-06-10: Audit-Log-Migration `0007_audit_log` angelegt und Append-only-Verhalten lokal gegen Supabase verifiziert. +- 2026-06-10: RLS-Migration `0008_rls_policies` angelegt und Mitglied/Nicht-Mitglied-Isolation lokal gegen Supabase verifiziert. diff --git a/migrations/0008_rls_policies.down.sql b/migrations/0008_rls_policies.down.sql new file mode 100644 index 0000000..81b58d5 --- /dev/null +++ b/migrations/0008_rls_policies.down.sql @@ -0,0 +1,19 @@ +DROP POLICY IF EXISTS audit_log_admin_select ON public.audit_log; +DROP POLICY IF EXISTS ssh_keys_member_select ON public.ssh_keys; +DROP POLICY IF EXISTS vms_member_select ON public.vms; +DROP POLICY IF EXISTS project_quotas_member_select ON public.project_quotas; +DROP POLICY IF EXISTS projects_member_select ON public.projects; +DROP POLICY IF EXISTS memberships_member_select ON public.memberships; +DROP POLICY IF EXISTS tenants_member_select ON public.tenants; + +ALTER TABLE public.audit_log DISABLE ROW LEVEL SECURITY; +ALTER TABLE public.ssh_keys DISABLE ROW LEVEL SECURITY; +ALTER TABLE public.vms DISABLE ROW LEVEL SECURITY; +ALTER TABLE public.project_quotas DISABLE ROW LEVEL SECURITY; +ALTER TABLE public.projects DISABLE ROW LEVEL SECURITY; +ALTER TABLE public.memberships DISABLE ROW LEVEL SECURITY; +ALTER TABLE public.tenants DISABLE ROW LEVEL SECURITY; + +DROP FUNCTION IF EXISTS public.has_tenant_role(uuid, public.membership_role[]); +DROP FUNCTION IF EXISTS public.is_tenant_member(uuid); +DROP FUNCTION IF EXISTS public.current_profile_id(); diff --git a/migrations/0008_rls_policies.up.sql b/migrations/0008_rls_policies.up.sql new file mode 100644 index 0000000..f147ec4 --- /dev/null +++ b/migrations/0008_rls_policies.up.sql @@ -0,0 +1,95 @@ +CREATE OR REPLACE FUNCTION public.current_profile_id() +RETURNS uuid +LANGUAGE sql +STABLE +AS $$ + SELECT nullif(current_setting('request.jwt.claim.sub', true), '')::uuid; +$$; + +CREATE OR REPLACE FUNCTION public.is_tenant_member(p_tenant_id uuid) +RETURNS boolean +LANGUAGE sql +STABLE +SECURITY DEFINER +SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.memberships + WHERE tenant_id = p_tenant_id + AND profile_id = public.current_profile_id() + ); +$$; + +CREATE OR REPLACE FUNCTION public.has_tenant_role(p_tenant_id uuid, p_roles public.membership_role[]) +RETURNS boolean +LANGUAGE sql +STABLE +SECURITY DEFINER +SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.memberships + WHERE tenant_id = p_tenant_id + AND profile_id = public.current_profile_id() + AND role = ANY (p_roles) + ); +$$; + +ALTER TABLE public.tenants ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.memberships ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.project_quotas ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.vms ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.ssh_keys ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.audit_log ENABLE ROW LEVEL SECURITY; + +CREATE POLICY tenants_member_select + ON public.tenants + FOR SELECT + USING (public.is_tenant_member(id)); + +CREATE POLICY memberships_member_select + ON public.memberships + FOR SELECT + USING (public.is_tenant_member(tenant_id)); + +CREATE POLICY projects_member_select + ON public.projects + FOR SELECT + USING (public.is_tenant_member(tenant_id)); + +CREATE POLICY project_quotas_member_select + ON public.project_quotas + FOR SELECT + USING ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = project_quotas.project_id + AND public.is_tenant_member(projects.tenant_id) + ) + ); + +CREATE POLICY vms_member_select + ON public.vms + FOR SELECT + USING ( + EXISTS ( + SELECT 1 + FROM public.projects + WHERE projects.id = vms.project_id + AND public.is_tenant_member(projects.tenant_id) + ) + ); + +CREATE POLICY ssh_keys_member_select + ON public.ssh_keys + FOR SELECT + USING (public.is_tenant_member(tenant_id)); + +CREATE POLICY audit_log_admin_select + ON public.audit_log + FOR SELECT + USING (public.has_tenant_role(tenant_id, ARRAY['owner', 'admin']::public.membership_role[]));