mirror of
https://github.com/feigeCode/navop.git
synced 2026-09-21 16:01:21 +00:00
235 lines
7.2 KiB
PL/PgSQL
235 lines
7.2 KiB
PL/PgSQL
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_members:admin 可管理 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;
|