Files
navop/team-sync-rls.sql
T

235 lines
7.2 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
BEGIN;
-- 统一 team_members.role 为 owner/admin/member。
DO $$
DECLARE
constraint_record RECORD;
BEGIN
FOR constraint_record IN
SELECT c.conname
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public'
AND t.relname = 'team_members'
AND c.contype = 'c'
AND pg_get_constraintdef(c.oid) ILIKE '%role%'
LOOP
EXECUTE format(
'ALTER TABLE public.team_members DROP CONSTRAINT IF EXISTS %I',
constraint_record.conname
);
END LOOP;
END;
$$;
ALTER TABLE public.team_members
ADD CONSTRAINT team_members_role_check
CHECK (role IN ('owner', 'admin', 'member'));
-- SECURITY DEFINER 用于避免 team_members RLS 自递归。
CREATE OR REPLACE FUNCTION public.current_team_role(p_team_id uuid)
RETURNS text
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT tm.role
FROM public.team_members tm
WHERE tm.team_id = p_team_id AND tm.user_id = auth.uid()
LIMIT 1;
$$;
CREATE OR REPLACE FUNCTION public.get_my_team_ids()
RETURNS SETOF uuid
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT tm.team_id
FROM public.team_members tm
WHERE tm.user_id = auth.uid();
$$;
CREATE OR REPLACE FUNCTION public.can_manage_team_members(p_team_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT COALESCE(
public.current_team_role(p_team_id) IN ('owner', 'admin'), false
);
$$;
CREATE OR REPLACE FUNCTION public.can_manage_team_owners(p_team_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT COALESCE(public.current_team_role(p_team_id) = 'owner', false);
$$;
CREATE OR REPLACE FUNCTION public.can_edit_team_sync_data(p_team_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT COALESCE(
public.current_team_role(p_team_id) IN ('owner', 'admin'), false
);
$$;
-- owner_id 不能通过普通 UPDATE 转移;团队 admin 只能编辑数据,不能夺取所有权。
CREATE OR REPLACE FUNCTION public.prevent_direct_owner_change()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = public
AS $$
BEGIN
IF NEW.owner_id IS DISTINCT FROM OLD.owner_id THEN
RAISE EXCEPTION 'Record owner cannot be changed directly'
USING ERRCODE = '42501';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_prevent_direct_team_owner_change ON public.teams;
CREATE TRIGGER trg_prevent_direct_team_owner_change
BEFORE UPDATE OF owner_id ON public.teams
FOR EACH ROW
EXECUTE FUNCTION public.prevent_direct_owner_change();
DROP TRIGGER IF EXISTS trg_prevent_direct_sync_data_owner_change ON public.sync_data;
CREATE TRIGGER trg_prevent_direct_sync_data_owner_change
BEFORE UPDATE OF owner_id ON public.sync_data
FOR EACH ROW
EXECUTE FUNCTION public.prevent_direct_owner_change();
REVOKE ALL ON FUNCTION public.current_team_role(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.get_my_team_ids() FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.can_manage_team_members(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.can_manage_team_owners(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.can_edit_team_sync_data(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.prevent_direct_owner_change()
FROM PUBLIC, anon, authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.current_team_role(uuid) TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.get_my_team_ids() TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.can_manage_team_members(uuid) TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.can_manage_team_owners(uuid) TO authenticated, service_role;
GRANT EXECUTE ON FUNCTION public.can_edit_team_sync_data(uuid) TO authenticated, service_role;
ALTER TABLE public.sync_data ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.teams ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.team_members ENABLE ROW LEVEL SECURITY;
-- 删除三个表的全部旧 policy,避免 permissive policy 按 OR 继续放宽权限。
DO $$
DECLARE
policy_record RECORD;
BEGIN
FOR policy_record IN
SELECT schemaname, tablename, policyname
FROM pg_policies
WHERE schemaname = 'public'
AND tablename IN ('sync_data', 'teams', 'team_members')
LOOP
EXECUTE format(
'DROP POLICY IF EXISTS %I ON %I.%I',
policy_record.policyname,
policy_record.schemaname,
policy_record.tablename
);
END LOOP;
END;
$$;
-- sync_data:成员可读,owner/admin 可维护团队数据。
CREATE POLICY sync_data_select ON public.sync_data
FOR SELECT USING (
(team_id IS NULL AND owner_id = auth.uid())
OR (team_id IS NOT NULL AND team_id IN (SELECT public.get_my_team_ids()))
);
CREATE POLICY sync_data_insert ON public.sync_data
FOR INSERT WITH CHECK (
owner_id = auth.uid()
AND (team_id IS NULL OR public.can_edit_team_sync_data(team_id))
);
CREATE POLICY sync_data_update ON public.sync_data
FOR UPDATE USING (
(team_id IS NULL AND owner_id = auth.uid())
OR (team_id IS NOT NULL AND public.can_edit_team_sync_data(team_id))
) WITH CHECK (
(team_id IS NULL AND owner_id = auth.uid())
OR (team_id IS NOT NULL AND public.can_edit_team_sync_data(team_id))
);
CREATE POLICY sync_data_delete ON public.sync_data
FOR DELETE USING (
(team_id IS NULL AND owner_id = auth.uid())
OR (team_id IS NOT NULL AND public.can_edit_team_sync_data(team_id))
);
-- teams:成员可读,owner/admin 可修改团队,只有 owner 可以删除团队。
CREATE POLICY teams_select ON public.teams
FOR SELECT USING (id IN (SELECT public.get_my_team_ids()));
CREATE POLICY teams_insert ON public.teams
FOR INSERT WITH CHECK (owner_id = auth.uid());
CREATE POLICY teams_update ON public.teams
FOR UPDATE USING (public.can_manage_team_members(id))
WITH CHECK (public.can_manage_team_members(id));
CREATE POLICY teams_delete ON public.teams
FOR DELETE USING (owner_id = auth.uid());
-- team_membersadmin 可管理 member/admin,但 owner 记录只能由 owner 管理。
CREATE POLICY team_members_select ON public.team_members
FOR SELECT USING (team_id IN (SELECT public.get_my_team_ids()));
CREATE POLICY team_members_insert ON public.team_members
FOR INSERT WITH CHECK (
CASE
WHEN role = 'owner' THEN public.can_manage_team_owners(team_id)
ELSE public.can_manage_team_members(team_id)
END
);
CREATE POLICY team_members_update ON public.team_members
FOR UPDATE USING (
CASE
WHEN role = 'owner' THEN public.can_manage_team_owners(team_id)
ELSE public.can_manage_team_members(team_id)
END
) WITH CHECK (
CASE
WHEN role = 'owner' THEN public.can_manage_team_owners(team_id)
ELSE public.can_manage_team_members(team_id)
END
);
CREATE POLICY team_members_delete ON public.team_members
FOR DELETE USING (
(user_id = auth.uid() AND role <> 'owner')
OR (
user_id <> auth.uid()
AND CASE
WHEN role = 'owner' THEN public.can_manage_team_owners(team_id)
ELSE public.can_manage_team_members(team_id)
END
)
);
COMMIT;