209 lines
6.3 KiB
PL/PgSQL
209 lines
6.3 KiB
PL/PgSQL
/*
|
|
* -------------------------------------------------------
|
|
* Hierarchy-Aware Role & Permission Functions + RLS Policies
|
|
*
|
|
* Extends the flat has_role_on_account model to support
|
|
* Verband → Subverein → Verein hierarchy.
|
|
*
|
|
* Design decisions:
|
|
* - Parents can READ children (top-down visibility)
|
|
* - Children CANNOT see parents or siblings
|
|
* - Writes remain strictly per-account (no cascade)
|
|
* - Only team accounts participate in hierarchy
|
|
* -------------------------------------------------------
|
|
*/
|
|
|
|
-- -------------------------------------------------------
|
|
-- Core: check role on account OR any ancestor account
|
|
-- -------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION public.has_role_on_account_or_ancestor(
|
|
target_account_id uuid,
|
|
account_role varchar(50) DEFAULT NULL
|
|
)
|
|
RETURNS boolean
|
|
LANGUAGE sql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM public.accounts_memberships membership
|
|
WHERE membership.user_id = (SELECT auth.uid())
|
|
AND membership.account_id IN (
|
|
SELECT public.get_account_ancestors(target_account_id)
|
|
)
|
|
AND (
|
|
membership.account_role = has_role_on_account_or_ancestor.account_role
|
|
OR has_role_on_account_or_ancestor.account_role IS NULL
|
|
)
|
|
);
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.has_role_on_account_or_ancestor(uuid, varchar)
|
|
TO authenticated, service_role;
|
|
|
|
-- -------------------------------------------------------
|
|
-- Core: check permission on account OR any ancestor
|
|
-- -------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION public.has_permission_or_ancestor(
|
|
user_id uuid,
|
|
target_account_id uuid,
|
|
permission_name public.app_permissions
|
|
)
|
|
RETURNS boolean
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
BEGIN
|
|
RETURN EXISTS (
|
|
SELECT 1
|
|
FROM public.accounts_memberships am
|
|
JOIN public.role_permissions rp ON am.account_role = rp.role
|
|
WHERE am.user_id = has_permission_or_ancestor.user_id
|
|
AND am.account_id IN (
|
|
SELECT public.get_account_ancestors(target_account_id)
|
|
)
|
|
AND rp.permission = has_permission_or_ancestor.permission_name
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.has_permission_or_ancestor(uuid, uuid, public.app_permissions)
|
|
TO authenticated, service_role;
|
|
|
|
-- -------------------------------------------------------
|
|
-- Utility: get all accounts visible to the current user
|
|
-- through hierarchy (accounts they belong to + descendants)
|
|
-- -------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION public.get_user_visible_accounts()
|
|
RETURNS SETOF uuid
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
SELECT DISTINCT d.id
|
|
FROM public.accounts_memberships am
|
|
CROSS JOIN LATERAL public.get_account_descendants(am.account_id) d(id)
|
|
WHERE am.user_id = (SELECT auth.uid());
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.get_user_visible_accounts()
|
|
TO authenticated, service_role;
|
|
|
|
-- -------------------------------------------------------
|
|
-- Utility: list child accounts of a given account
|
|
-- -------------------------------------------------------
|
|
CREATE OR REPLACE FUNCTION public.get_child_accounts(parent_id uuid)
|
|
RETURNS TABLE (
|
|
id uuid,
|
|
name varchar,
|
|
slug text,
|
|
parent_account_id uuid,
|
|
created_at timestamptz
|
|
)
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
BEGIN
|
|
-- Only return children if caller has a role on the parent account
|
|
IF NOT public.has_role_on_account(get_child_accounts.parent_id) THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
RETURN QUERY
|
|
SELECT a.id, a.name, a.slug, a.parent_account_id, a.created_at
|
|
FROM public.accounts a
|
|
WHERE a.parent_account_id = get_child_accounts.parent_id
|
|
AND a.is_personal_account = false;
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.get_child_accounts(uuid)
|
|
TO authenticated, service_role;
|
|
|
|
-- -------------------------------------------------------
|
|
-- RLS: Allow parent accounts to read child account rows
|
|
--
|
|
-- We add PERMISSIVE policies alongside existing ones.
|
|
-- Existing: has_role_on_account(account_id) — direct membership
|
|
-- New: has_role_on_account_or_ancestor(account_id) — hierarchy
|
|
--
|
|
-- Only applied to SELECT (reads). Writes stay per-account.
|
|
-- -------------------------------------------------------
|
|
|
|
-- Members table: parent can see child members
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE schemaname = 'public' AND tablename = 'members'
|
|
AND policyname = 'members_hierarchy_read'
|
|
) THEN
|
|
EXECUTE 'CREATE POLICY members_hierarchy_read ON public.members
|
|
FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account_or_ancestor(account_id))';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Events table: parent can see child events
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE schemaname = 'public' AND tablename = 'events'
|
|
AND policyname = 'events_hierarchy_read'
|
|
) THEN
|
|
EXECUTE 'CREATE POLICY events_hierarchy_read ON public.events
|
|
FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account_or_ancestor(account_id))';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Courses table: parent can see child courses
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE schemaname = 'public' AND tablename = 'courses'
|
|
AND policyname = 'courses_hierarchy_read'
|
|
) THEN
|
|
EXECUTE 'CREATE POLICY courses_hierarchy_read ON public.courses
|
|
FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account_or_ancestor(account_id))';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Member clubs (Verbandsverwaltung): parent can see child clubs
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE schemaname = 'public' AND tablename = 'member_clubs'
|
|
AND policyname = 'member_clubs_hierarchy_read'
|
|
) THEN
|
|
EXECUTE 'CREATE POLICY member_clubs_hierarchy_read ON public.member_clubs
|
|
FOR SELECT TO authenticated
|
|
USING (public.has_role_on_account_or_ancestor(account_id))';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Accounts table: allow reading child accounts in hierarchy
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_policies
|
|
WHERE schemaname = 'public' AND tablename = 'accounts'
|
|
AND policyname = 'accounts_hierarchy_read'
|
|
) THEN
|
|
EXECUTE 'CREATE POLICY accounts_hierarchy_read ON public.accounts
|
|
FOR SELECT TO authenticated
|
|
USING (
|
|
id IN (SELECT public.get_user_visible_accounts())
|
|
)';
|
|
END IF;
|
|
END $$;
|