Merge: urgent RLS function-body fix after rename (t-paliad-036)
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
-- Best-effort revert of 021. NOT a clean roll-back — this migration only
|
||||
-- restores the broken German-bodied versions of can_see_project /
|
||||
-- note_is_visible / projekte_*; if the underlying tables have already been
|
||||
-- renamed to English (i.e. 020 hasn't been rolled back yet), the German
|
||||
-- bodies will still error. Run 020 / 018 down first if you really want to
|
||||
-- get back to a working German state.
|
||||
--
|
||||
-- We do not recreate the dropped policies under their previous German names;
|
||||
-- migration 018 / 020 down rebuilds the German-named policy set, and the
|
||||
-- English-named policies created by 021.up are the right starting point for
|
||||
-- those rollbacks.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Revert trigger functions
|
||||
-- ---------------------------------------------------------------------------
|
||||
DROP TRIGGER IF EXISTS projects_sync_path_before ON paliad.projects;
|
||||
DROP TRIGGER IF EXISTS projects_rewrite_subtree_after ON paliad.projects;
|
||||
DROP FUNCTION IF EXISTS paliad.projects_sync_path() CASCADE;
|
||||
DROP FUNCTION IF EXISTS paliad.projects_rewrite_subtree() CASCADE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION paliad.projekte_sync_path()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
parent_path text;
|
||||
BEGIN
|
||||
IF NEW.parent_id IS NULL THEN
|
||||
NEW.path := NEW.id::text;
|
||||
ELSE
|
||||
SELECT path INTO parent_path
|
||||
FROM paliad.projekte
|
||||
WHERE id = NEW.parent_id;
|
||||
IF parent_path IS NULL THEN
|
||||
RAISE EXCEPTION 'parent projekt % not found', NEW.parent_id;
|
||||
END IF;
|
||||
IF parent_path = NEW.id::text
|
||||
OR parent_path LIKE '%.' || NEW.id::text
|
||||
OR parent_path LIKE NEW.id::text || '.%'
|
||||
OR parent_path LIKE '%.' || NEW.id::text || '.%'
|
||||
THEN
|
||||
RAISE EXCEPTION 'cannot set parent to own descendant';
|
||||
END IF;
|
||||
NEW.path := parent_path || '.' || NEW.id::text;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION paliad.projekte_rewrite_subtree()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD.path IS DISTINCT FROM NEW.path THEN
|
||||
UPDATE paliad.projekte
|
||||
SET path = NEW.path || substring(path FROM length(OLD.path) + 1)
|
||||
WHERE path LIKE OLD.path || '.%';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- The triggers can only be bound to a table that exists; pick whichever of
|
||||
-- paliad.projekte / paliad.projects is currently around.
|
||||
DO $$ BEGIN
|
||||
EXECUTE 'CREATE TRIGGER projekte_sync_path_before BEFORE INSERT OR UPDATE OF parent_id ON paliad.projekte FOR EACH ROW EXECUTE FUNCTION paliad.projekte_sync_path()';
|
||||
EXCEPTION WHEN undefined_table THEN
|
||||
EXECUTE 'CREATE TRIGGER projekte_sync_path_before BEFORE INSERT OR UPDATE OF parent_id ON paliad.projects FOR EACH ROW EXECUTE FUNCTION paliad.projekte_sync_path()';
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
EXECUTE 'CREATE TRIGGER projekte_rewrite_subtree_after AFTER UPDATE OF path ON paliad.projekte FOR EACH ROW EXECUTE FUNCTION paliad.projekte_rewrite_subtree()';
|
||||
EXCEPTION WHEN undefined_table THEN
|
||||
EXECUTE 'CREATE TRIGGER projekte_rewrite_subtree_after AFTER UPDATE OF path ON paliad.projects FOR EACH ROW EXECUTE FUNCTION paliad.projekte_rewrite_subtree()';
|
||||
END $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Revert visibility functions to their pre-021 (broken) state
|
||||
-- ---------------------------------------------------------------------------
|
||||
DROP FUNCTION IF EXISTS paliad.note_is_visible(uuid, uuid, uuid, uuid) CASCADE;
|
||||
DROP FUNCTION IF EXISTS paliad.can_see_project(uuid) CASCADE;
|
||||
|
||||
CREATE FUNCTION paliad.can_see_project(_projekt_id uuid)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = paliad, public
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM paliad.users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'admin'
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM paliad.projekte target
|
||||
JOIN paliad.projekt_teams pt
|
||||
ON pt.user_id = auth.uid()
|
||||
AND pt.projekt_id = ANY(string_to_array(target.path, '.')::uuid[])
|
||||
WHERE target.id = _projekt_id
|
||||
);
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION paliad.note_is_visible(
|
||||
_projekt_id uuid,
|
||||
_frist_id uuid,
|
||||
_termin_id uuid,
|
||||
_projekt_event_id uuid
|
||||
) RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = paliad, public
|
||||
AS $$
|
||||
SELECT CASE
|
||||
WHEN _projekt_id IS NOT NULL THEN paliad.can_see_projekt(_projekt_id)
|
||||
WHEN _frist_id IS NOT NULL THEN paliad.can_see_projekt(
|
||||
(SELECT projekt_id FROM paliad.fristen WHERE id = _frist_id))
|
||||
WHEN _termin_id IS NOT NULL THEN
|
||||
CASE
|
||||
WHEN (SELECT projekt_id FROM paliad.termine WHERE id = _termin_id) IS NULL
|
||||
THEN (SELECT created_by FROM paliad.termine WHERE id = _termin_id) = auth.uid()
|
||||
ELSE paliad.can_see_projekt(
|
||||
(SELECT projekt_id FROM paliad.termine WHERE id = _termin_id))
|
||||
END
|
||||
WHEN _projekt_event_id IS NOT NULL THEN paliad.can_see_projekt(
|
||||
(SELECT projekt_id FROM paliad.projekt_events WHERE id = _projekt_event_id))
|
||||
ELSE false
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,316 @@
|
||||
-- Hotfix for migration 020: the German→English rename renamed function NAMES
|
||||
-- via ALTER FUNCTION but never rewrote the function BODIES. On production
|
||||
-- (which had been seeded from the original German-named migration 018), the
|
||||
-- bodies still referenced paliad.projekte / paliad.projekt_teams /
|
||||
-- paliad.fristen / paliad.termine / paliad.projekt_events / the German
|
||||
-- column names — all of which had been renamed/dropped in 018+020. Any
|
||||
-- RLS-enforced query against the new English tables exploded with
|
||||
-- `relation "paliad.projekte" does not exist`, breaking /api/projects,
|
||||
-- /api/deadlines, /api/appointments, etc.
|
||||
--
|
||||
-- The trigger functions paliad.projekte_sync_path() and
|
||||
-- paliad.projekte_rewrite_subtree() suffered the same fate — kept their
|
||||
-- German names and German bodies. Triggers on paliad.projects still pointed
|
||||
-- at them, so any INSERT/UPDATE on projects tripped them too.
|
||||
--
|
||||
-- Strategy: DROP FUNCTION ... CASCADE drops the broken function and its
|
||||
-- dependent RLS policies (whose names are still German on production —
|
||||
-- projekte_*, projekt_teams_*, fristen_all, termine_*, parteien_all,
|
||||
-- dokumente_all, projekt_events_all, notizen_all). Recreate the functions
|
||||
-- with English bodies + English parameter names, then rebuild every
|
||||
-- dependent policy under its canonical English name (matching migration
|
||||
-- 018). Trigger functions are dropped+recreated under English names with
|
||||
-- English-table bodies; triggers are recreated to point at the new names.
|
||||
--
|
||||
-- Idempotent: on a fresh DB everything is already English, so the CASCADE
|
||||
-- drops the same English-named policies and they're recreated identically.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1. Drop broken visibility functions (CASCADE drops dependent policies).
|
||||
-- ---------------------------------------------------------------------------
|
||||
DROP FUNCTION IF EXISTS paliad.note_is_visible(uuid, uuid, uuid, uuid) CASCADE;
|
||||
DROP FUNCTION IF EXISTS paliad.can_see_project(uuid) CASCADE;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2. Recreate can_see_project() with English body + English parameter.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE FUNCTION paliad.can_see_project(_project_id uuid)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = paliad, public
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM paliad.users u
|
||||
WHERE u.id = auth.uid() AND u.role = 'admin'
|
||||
)
|
||||
OR EXISTS (
|
||||
-- Any team membership (direct or ancestor). `path` always includes
|
||||
-- the target's own id as the last label, so this handles direct too.
|
||||
SELECT 1
|
||||
FROM paliad.projects target
|
||||
JOIN paliad.project_teams pt
|
||||
ON pt.user_id = auth.uid()
|
||||
AND pt.project_id = ANY(string_to_array(target.path, '.')::uuid[])
|
||||
WHERE target.id = _project_id
|
||||
);
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION paliad.can_see_project(uuid) IS
|
||||
'Team-based visibility predicate for paliad.projects. Direct or inherited '
|
||||
'(ancestor) membership grants access. Admins see all.';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3. Recreate note_is_visible() with English body + English parameters.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE FUNCTION paliad.note_is_visible(
|
||||
_project_id uuid,
|
||||
_deadline_id uuid,
|
||||
_appointment_id uuid,
|
||||
_project_event_id uuid
|
||||
) RETURNS boolean
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = paliad, public
|
||||
AS $$
|
||||
SELECT CASE
|
||||
WHEN _project_id IS NOT NULL THEN paliad.can_see_project(_project_id)
|
||||
WHEN _deadline_id IS NOT NULL THEN paliad.can_see_project(
|
||||
(SELECT project_id FROM paliad.deadlines WHERE id = _deadline_id))
|
||||
WHEN _appointment_id IS NOT NULL THEN
|
||||
CASE
|
||||
WHEN (SELECT project_id FROM paliad.appointments WHERE id = _appointment_id) IS NULL
|
||||
THEN (SELECT created_by FROM paliad.appointments WHERE id = _appointment_id) = auth.uid()
|
||||
ELSE paliad.can_see_project(
|
||||
(SELECT project_id FROM paliad.appointments WHERE id = _appointment_id))
|
||||
END
|
||||
WHEN _project_event_id IS NOT NULL THEN paliad.can_see_project(
|
||||
(SELECT project_id FROM paliad.project_events WHERE id = _project_event_id))
|
||||
ELSE false
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 4. Rebuild RLS policies that the CASCADE just dropped. Names match the
|
||||
-- canonical English set in migration 018. On production the dropped
|
||||
-- policies were named projekte_*, projekt_teams_*, fristen_all,
|
||||
-- termine_*, parteien_all, dokumente_all, projekt_events_all,
|
||||
-- notizen_all — those names are gone after this migration.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- projects ------------------------------------------------------------------
|
||||
CREATE POLICY projects_select ON paliad.projects
|
||||
FOR SELECT TO authenticated
|
||||
USING (paliad.can_see_project(id));
|
||||
|
||||
CREATE POLICY projects_insert ON paliad.projects
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
parent_id IS NULL
|
||||
OR paliad.can_see_project(parent_id)
|
||||
);
|
||||
|
||||
CREATE POLICY projects_update ON paliad.projects
|
||||
FOR UPDATE TO authenticated
|
||||
USING (paliad.can_see_project(id))
|
||||
WITH CHECK (paliad.can_see_project(id));
|
||||
|
||||
CREATE POLICY projects_delete ON paliad.projects
|
||||
FOR DELETE TO authenticated
|
||||
USING (
|
||||
paliad.can_see_project(id)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM paliad.users u
|
||||
WHERE u.id = auth.uid() AND u.role IN ('partner','admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- project_teams -------------------------------------------------------------
|
||||
CREATE POLICY project_teams_select ON paliad.project_teams
|
||||
FOR SELECT TO authenticated
|
||||
USING (paliad.can_see_project(project_id));
|
||||
|
||||
CREATE POLICY project_teams_insert ON paliad.project_teams
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
user_id = auth.uid()
|
||||
OR paliad.can_see_project(project_id)
|
||||
);
|
||||
|
||||
CREATE POLICY project_teams_update ON paliad.project_teams
|
||||
FOR UPDATE TO authenticated
|
||||
USING (paliad.can_see_project(project_id))
|
||||
WITH CHECK (paliad.can_see_project(project_id));
|
||||
|
||||
CREATE POLICY project_teams_delete ON paliad.project_teams
|
||||
FOR DELETE TO authenticated
|
||||
USING (
|
||||
paliad.can_see_project(project_id)
|
||||
AND (
|
||||
user_id = auth.uid()
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM paliad.users u
|
||||
WHERE u.id = auth.uid() AND u.role IN ('partner','admin')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- parties / deadlines / documents / project_events --------------------------
|
||||
CREATE POLICY parties_all ON paliad.parties
|
||||
FOR ALL TO authenticated
|
||||
USING (paliad.can_see_project(project_id))
|
||||
WITH CHECK (paliad.can_see_project(project_id));
|
||||
|
||||
CREATE POLICY deadlines_all ON paliad.deadlines
|
||||
FOR ALL TO authenticated
|
||||
USING (paliad.can_see_project(project_id))
|
||||
WITH CHECK (paliad.can_see_project(project_id));
|
||||
|
||||
CREATE POLICY documents_all ON paliad.documents
|
||||
FOR ALL TO authenticated
|
||||
USING (paliad.can_see_project(project_id))
|
||||
WITH CHECK (paliad.can_see_project(project_id));
|
||||
|
||||
CREATE POLICY project_events_all ON paliad.project_events
|
||||
FOR ALL TO authenticated
|
||||
USING (paliad.can_see_project(project_id))
|
||||
WITH CHECK (paliad.can_see_project(project_id));
|
||||
|
||||
-- appointments (project_id nullable; personal appts stay creator-only) -----
|
||||
CREATE POLICY appointments_select ON paliad.appointments
|
||||
FOR SELECT TO authenticated
|
||||
USING (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
CREATE POLICY appointments_insert ON paliad.appointments
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
CREATE POLICY appointments_update ON paliad.appointments
|
||||
FOR UPDATE TO authenticated
|
||||
USING (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
)
|
||||
WITH CHECK (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
CREATE POLICY appointments_delete ON paliad.appointments
|
||||
FOR DELETE TO authenticated
|
||||
USING (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
-- notes (polymorphic parent dispatch) --------------------------------------
|
||||
CREATE POLICY notes_all ON paliad.notes
|
||||
FOR ALL TO authenticated
|
||||
USING (paliad.note_is_visible(project_id, deadline_id, appointment_id, project_event_id))
|
||||
WITH CHECK (paliad.note_is_visible(project_id, deadline_id, appointment_id, project_event_id));
|
||||
|
||||
-- checklist_instances (project_id nullable; personal stays creator-only) ---
|
||||
CREATE POLICY checklist_instances_select ON paliad.checklist_instances
|
||||
FOR SELECT TO authenticated
|
||||
USING (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
CREATE POLICY checklist_instances_insert ON paliad.checklist_instances
|
||||
FOR INSERT TO authenticated
|
||||
WITH CHECK (
|
||||
created_by = auth.uid()
|
||||
AND (project_id IS NULL OR paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
CREATE POLICY checklist_instances_update ON paliad.checklist_instances
|
||||
FOR UPDATE TO authenticated
|
||||
USING (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
)
|
||||
WITH CHECK (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
CREATE POLICY checklist_instances_delete ON paliad.checklist_instances
|
||||
FOR DELETE TO authenticated
|
||||
USING (
|
||||
(project_id IS NULL AND created_by = auth.uid())
|
||||
OR (project_id IS NOT NULL AND paliad.can_see_project(project_id))
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 5. Trigger functions: drop the German-named pair (and their bound
|
||||
-- triggers), recreate under English names with English-table bodies,
|
||||
-- rebind the triggers. Migration 020 missed these entirely.
|
||||
-- ---------------------------------------------------------------------------
|
||||
DROP TRIGGER IF EXISTS projekte_sync_path_before ON paliad.projects;
|
||||
DROP TRIGGER IF EXISTS projekte_rewrite_subtree_after ON paliad.projects;
|
||||
DROP FUNCTION IF EXISTS paliad.projekte_sync_path() CASCADE;
|
||||
DROP FUNCTION IF EXISTS paliad.projekte_rewrite_subtree() CASCADE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION paliad.projects_sync_path()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
parent_path text;
|
||||
BEGIN
|
||||
IF NEW.parent_id IS NULL THEN
|
||||
NEW.path := NEW.id::text;
|
||||
ELSE
|
||||
SELECT path INTO parent_path
|
||||
FROM paliad.projects
|
||||
WHERE id = NEW.parent_id;
|
||||
IF parent_path IS NULL THEN
|
||||
RAISE EXCEPTION 'parent project % not found', NEW.parent_id;
|
||||
END IF;
|
||||
-- Reject cycles: parent's path cannot contain this row's id.
|
||||
IF parent_path = NEW.id::text
|
||||
OR parent_path LIKE '%.' || NEW.id::text
|
||||
OR parent_path LIKE NEW.id::text || '.%'
|
||||
OR parent_path LIKE '%.' || NEW.id::text || '.%'
|
||||
THEN
|
||||
RAISE EXCEPTION 'cannot set parent to own descendant';
|
||||
END IF;
|
||||
NEW.path := parent_path || '.' || NEW.id::text;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION paliad.projects_rewrite_subtree()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD.path IS DISTINCT FROM NEW.path THEN
|
||||
UPDATE paliad.projects
|
||||
SET path = NEW.path || substring(path FROM length(OLD.path) + 1)
|
||||
WHERE path LIKE OLD.path || '.%';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS projects_sync_path_before ON paliad.projects;
|
||||
DROP TRIGGER IF EXISTS projects_rewrite_subtree_after ON paliad.projects;
|
||||
|
||||
CREATE TRIGGER projects_sync_path_before
|
||||
BEFORE INSERT OR UPDATE OF parent_id ON paliad.projects
|
||||
FOR EACH ROW EXECUTE FUNCTION paliad.projects_sync_path();
|
||||
|
||||
CREATE TRIGGER projects_rewrite_subtree_after
|
||||
AFTER UPDATE OF path ON paliad.projects
|
||||
FOR EACH ROW EXECUTE FUNCTION paliad.projects_rewrite_subtree();
|
||||
Reference in New Issue
Block a user