From 07de7b514068a7fa07514486c2ccff45d244301b Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 1 Jan 2026 14:43:03 -0500 Subject: [PATCH 1/4] eng-1232 create group table --- packages/database/src/dbTypes.ts | 22 +++ .../20260101183250_group_access.sql | 145 ++++++++++++++++++ .../database/supabase/schemas/account.sql | 92 ++++++++++- 3 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 packages/database/supabase/migrations/20260101183250_group_access.sql diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index c28556115..10eecd427 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -532,6 +532,24 @@ export type Database = { }, ] } + group_membership: { + Row: { + admin: boolean | null + group_id: string + member_id: string + } + Insert: { + admin?: boolean | null + group_id: string + member_id: string + } + Update: { + admin?: boolean | null + group_id?: string + member_id?: string + } + Relationships: [] + } LocalAccess: { Row: { account_id: number @@ -1452,6 +1470,8 @@ export type Database = { } Returns: string } + group_exists: { Args: { group_id_: string }; Returns: boolean } + in_group: { Args: { group_id_: string }; Returns: boolean } in_space: { Args: { space_id: number }; Returns: boolean } instances_of_schema: | { @@ -1478,6 +1498,7 @@ export type Database = { isSetofReturn: true } } + is_group_admin: { Args: { group_id_: string }; Returns: boolean } is_my_account: { Args: { account_id: number }; Returns: boolean } match_content_embeddings: { Args: { @@ -1503,6 +1524,7 @@ export type Database = { }[] } my_space_ids: { Args: never; Returns: number[] } + my_user_accounts: { Args: never; Returns: string[] } propose_sync_task: { Args: { s_function: string diff --git a/packages/database/supabase/migrations/20260101183250_group_access.sql b/packages/database/supabase/migrations/20260101183250_group_access.sql new file mode 100644 index 000000000..cadbf67b4 --- /dev/null +++ b/packages/database/supabase/migrations/20260101183250_group_access.sql @@ -0,0 +1,145 @@ +CREATE TABLE public.group_membership ( + member_id UUID, + group_id UUID, + admin BOOLEAN default true +); + +ALTER TABLE public.group_membership ADD CONSTRAINT group_membership_pkey PRIMARY KEY (member_id, group_id); + +ALTER TABLE public.group_membership OWNER TO "postgres"; + +COMMENT ON TABLE public.group_membership IS 'A group membership table'; +COMMENT ON COLUMN public.group_membership.member_id IS 'The member of the group'; +COMMENT ON COLUMN public.group_membership.group_id IS 'The group identifier'; + +ALTER TABLE ONLY public.group_membership +ADD CONSTRAINT "group_membership_member_id_fkey" FOREIGN KEY ( + member_id +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY public.group_membership +ADD CONSTRAINT "group_membership_group_id_fkey" FOREIGN KEY ( + group_id +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + + +GRANT ALL ON TABLE public.group_membership TO authenticated; +GRANT ALL ON TABLE public.group_membership TO service_role; +REVOKE ALL ON TABLE public.group_membership FROM anon; + +CREATE OR REPLACE FUNCTION public.in_group(group_id_ UUID) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT true FROM public.group_membership + WHERE member_id = auth.uid() AND group_id = group_id_); +$$; + +CREATE OR REPLACE FUNCTION public.is_group_admin(group_id_ UUID) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT true FROM public.group_membership + WHERE member_id = auth.uid() AND group_id = group_id_ AND admin); +$$; + +CREATE OR REPLACE FUNCTION public.group_exists(group_id_ UUID) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT true FROM public.group_membership WHERE group_id = group_id_ LIMIT 1); +$$; + +ALTER TABLE public.group_membership ENABLE ROW LEVEL SECURITY; +CREATE POLICY group_membership_select_policy ON public.group_membership FOR SELECT USING (public.in_group(group_id)); +CREATE POLICY group_membership_delete_policy ON public.group_membership FOR DELETE USING (member_id = auth.uid() OR public.is_group_admin(group_id)); +CREATE POLICY group_membership_insert_policy ON public.group_membership FOR INSERT WITH CHECK (public.is_group_admin(group_id) OR NOT public.group_exists(group_id)); +CREATE POLICY group_membership_update_policy ON public.group_membership FOR UPDATE WITH CHECK (public.is_group_admin(group_id)); + + +CREATE OR REPLACE FUNCTION public.my_user_accounts() RETURNS SETOF UUID +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT auth.uid() UNION + SELECT group_id FROM public.group_membership + WHERE member_id = auth.uid(); +$$; + +COMMENT ON FUNCTION public.my_user_accounts IS 'security utility: The uids which give me access, either as myself or as a group member.'; + +CREATE OR REPLACE FUNCTION public.my_space_ids() RETURNS BIGINT [] +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT COALESCE(array_agg(distinct space_id), '{}') AS ids + FROM public."SpaceAccess" + JOIN public.my_user_accounts() ON (account_uid = my_user_accounts); +$$; + +CREATE OR REPLACE FUNCTION public.in_space(space_id BIGINT) RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT 1 FROM public."SpaceAccess" AS sa + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) + WHERE sa.space_id = in_space.space_id); +$$; + +CREATE OR REPLACE FUNCTION public.account_in_shared_space(p_account_id BIGINT) RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public."LocalAccess" AS la + JOIN public."SpaceAccess" AS sa USING (space_id) + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) + WHERE la.account_id = p_account_id + ); +$$; + +CREATE OR REPLACE FUNCTION public.unowned_account_in_shared_space(p_account_id BIGINT) RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public."SpaceAccess" AS sa + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) + JOIN public."LocalAccess" AS la USING (space_id) + JOIN public."PlatformAccount" AS pa ON (pa.id=la.account_id) + WHERE la.account_id = p_account_id + AND pa.dg_account IS NULL + ); +$$; + +CREATE OR REPLACE VIEW public.my_accounts AS +SELECT + id, + name, + platform, + account_local_id, + write_permission, + active, + agent_type, + metadata, + dg_account +FROM public."PlatformAccount" +WHERE id IN ( + SELECT "LocalAccess".account_id FROM public."LocalAccess" + JOIN public."SpaceAccess" USING (space_id) + JOIN public.my_user_accounts() ON (account_uid = my_user_accounts) +); + + + + + +-- ALTER TYPE public."ContentVariant" ADD VALUE 'full'; diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 0182a2e9c..713586214 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -128,6 +128,34 @@ GRANT ALL ON TABLE public."SpaceAccess" TO authenticated; GRANT ALL ON TABLE public."SpaceAccess" TO service_role; REVOKE ALL ON TABLE public."SpaceAccess" FROM anon; +CREATE TABLE IF NOT EXISTS public.group_membership ( + member_id UUID, + group_id UUID, + admin BOOLEAN default true +); + +ALTER TABLE public.group_membership ADD CONSTRAINT group_membership_pkey PRIMARY KEY (member_id, group_id); + +ALTER TABLE public.group_membership OWNER TO "postgres"; + +COMMENT ON TABLE public.group_membership IS 'A group membership table'; +COMMENT ON COLUMN public.group_membership.member_id IS 'The member of the group'; +COMMENT ON COLUMN public.group_membership.group_id IS 'The group identifier'; + +ALTER TABLE ONLY public.group_membership +ADD CONSTRAINT "group_membership_member_id_fkey" FOREIGN KEY ( + member_id +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY public.group_membership +ADD CONSTRAINT "group_membership_group_id_fkey" FOREIGN KEY ( + group_id +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +REVOKE ALL ON TABLE public.group_membership FROM anon; +GRANT ALL ON TABLE public.group_membership TO authenticated; +GRANT ALL ON TABLE public.group_membership TO service_role; + CREATE TYPE public.account_local_input AS ( -- PlatformAccount columns name VARCHAR, @@ -232,6 +260,44 @@ $$; COMMENT ON FUNCTION public.is_my_account IS 'security utility: is this my own account?'; +CREATE OR REPLACE FUNCTION public.my_user_accounts() RETURNS SETOF UUID +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT auth.uid() UNION + SELECT group_id FROM public.group_membership + WHERE member_id = auth.uid(); +$$; + +COMMENT ON FUNCTION public.my_user_accounts IS 'security utility: The uids which give me access, either as myself or as a group member.'; + +CREATE OR REPLACE FUNCTION public.in_group(group_id_ UUID) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT true FROM public.group_membership + WHERE member_id = auth.uid() AND group_id = group_id_); +$$; + +CREATE OR REPLACE FUNCTION public.is_group_admin(group_id_ UUID) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT true FROM public.group_membership + WHERE member_id = auth.uid() AND group_id = group_id_ AND admin); +$$; + +CREATE OR REPLACE FUNCTION public.group_exists(group_id_ UUID) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS (SELECT true FROM public.group_membership WHERE group_id = group_id_ LIMIT 1); +$$; + CREATE OR REPLACE FUNCTION public.my_space_ids() RETURNS BIGINT [] STABLE SECURITY DEFINER SET search_path = '' @@ -239,7 +305,7 @@ LANGUAGE sql AS $$ SELECT COALESCE(array_agg(distinct space_id), '{}') AS ids FROM public."SpaceAccess" - WHERE account_uid = auth.uid(); + JOIN public.my_user_accounts() ON (account_uid = my_user_accounts); $$; COMMENT ON FUNCTION public.my_space_ids IS 'security utility: all spaces the user has access to'; @@ -250,8 +316,8 @@ SET search_path = '' LANGUAGE sql AS $$ SELECT EXISTS (SELECT 1 FROM public."SpaceAccess" AS sa - WHERE sa.space_id = in_space.space_id - AND sa.account_uid=auth.uid()); + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) + WHERE sa.space_id = in_space.space_id); $$; COMMENT ON FUNCTION public.in_space IS 'security utility: does current user have access to this space?'; @@ -265,8 +331,8 @@ LANGUAGE sql AS $$ SELECT 1 FROM public."LocalAccess" AS la JOIN public."SpaceAccess" AS sa USING (space_id) + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) WHERE la.account_id = p_account_id - AND sa.account_uid = auth.uid() ); $$; @@ -279,10 +345,10 @@ LANGUAGE sql AS $$ SELECT EXISTS ( SELECT 1 FROM public."SpaceAccess" AS sa + JOIN public.my_user_accounts() ON (sa.account_uid = my_user_accounts) JOIN public."LocalAccess" AS la USING (space_id) JOIN public."PlatformAccount" AS pa ON (pa.id=la.account_id) WHERE la.account_id = p_account_id - AND sa.account_uid = auth.uid() AND pa.dg_account IS NULL ); $$; @@ -328,7 +394,7 @@ FROM public."PlatformAccount" WHERE id IN ( SELECT "LocalAccess".account_id FROM public."LocalAccess" JOIN public."SpaceAccess" USING (space_id) - WHERE "SpaceAccess".account_uid = auth.uid() + JOIN public.my_user_accounts() ON (account_uid = my_user_accounts) ); DROP POLICY IF EXISTS platform_account_policy ON public."PlatformAccount"; @@ -399,3 +465,17 @@ CREATE POLICY agent_identifier_insert_policy ON public."AgentIdentifier" FOR INS DROP POLICY IF EXISTS agent_identifier_update_policy ON public."AgentIdentifier"; CREATE POLICY agent_identifier_update_policy ON public."AgentIdentifier" FOR UPDATE WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); + +ALTER TABLE public.group_membership ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS group_membership_select_policy ON public.group_membership; +CREATE POLICY group_membership_select_policy ON public.group_membership FOR SELECT USING (public.in_group(group_id)); + +DROP POLICY IF EXISTS group_membership_delete_policy ON public.group_membership; +CREATE POLICY group_membership_delete_policy ON public.group_membership FOR DELETE USING (member_id = auth.uid() OR public.is_group_admin(group_id)); + +DROP POLICY IF EXISTS group_membership_insert_policy ON public.group_membership; +CREATE POLICY group_membership_insert_policy ON public.group_membership FOR INSERT WITH CHECK (public.is_group_admin(group_id) OR NOT public.group_exists(group_id)); + +DROP POLICY IF EXISTS group_membership_update_policy ON public.group_membership; +CREATE POLICY group_membership_update_policy ON public.group_membership FOR UPDATE WITH CHECK (public.is_group_admin(group_id)); From b583bb566dcd0b6d26dee852ad20dfffdaacd166 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 1 Jan 2026 15:21:00 -0500 Subject: [PATCH 2/4] coderabbit comments --- .../supabase/migrations/20260101183250_group_access.sql | 8 +------- packages/database/supabase/schemas/account.sql | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/database/supabase/migrations/20260101183250_group_access.sql b/packages/database/supabase/migrations/20260101183250_group_access.sql index cadbf67b4..c08f34bee 100644 --- a/packages/database/supabase/migrations/20260101183250_group_access.sql +++ b/packages/database/supabase/migrations/20260101183250_group_access.sql @@ -65,7 +65,7 @@ STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ - SELECT auth.uid() UNION + SELECT auth.uid() WHERE auth.uid() IS NOT NULL UNION SELECT group_id FROM public.group_membership WHERE member_id = auth.uid(); $$; @@ -137,9 +137,3 @@ WHERE id IN ( JOIN public."SpaceAccess" USING (space_id) JOIN public.my_user_accounts() ON (account_uid = my_user_accounts) ); - - - - - --- ALTER TYPE public."ContentVariant" ADD VALUE 'full'; diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 713586214..8d8694f1a 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -265,7 +265,7 @@ STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ - SELECT auth.uid() UNION + SELECT auth.uid() WHERE auth.uid() IS NOT NULL UNION SELECT group_id FROM public.group_membership WHERE member_id = auth.uid(); $$; From c8a9d1653b3153faaa51f963292188ebf140dad2 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 1 Jan 2026 15:25:36 -0500 Subject: [PATCH 3/4] forgot to make columns non-nullable --- .../supabase/migrations/20260101183250_group_access.sql | 4 ++-- packages/database/supabase/schemas/account.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/database/supabase/migrations/20260101183250_group_access.sql b/packages/database/supabase/migrations/20260101183250_group_access.sql index c08f34bee..997d52789 100644 --- a/packages/database/supabase/migrations/20260101183250_group_access.sql +++ b/packages/database/supabase/migrations/20260101183250_group_access.sql @@ -1,6 +1,6 @@ CREATE TABLE public.group_membership ( - member_id UUID, - group_id UUID, + member_id UUID NOT NULL, + group_id UUID NOT NULL, admin BOOLEAN default true ); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 8d8694f1a..7afe74582 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -129,8 +129,8 @@ GRANT ALL ON TABLE public."SpaceAccess" TO service_role; REVOKE ALL ON TABLE public."SpaceAccess" FROM anon; CREATE TABLE IF NOT EXISTS public.group_membership ( - member_id UUID, - group_id UUID, + member_id UUID NOT NULL, + group_id UUID NOT NULL, admin BOOLEAN default true ); From 817022233cb147bba0c005ae086261a472c3ce08 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 1 Jan 2026 21:09:12 -0500 Subject: [PATCH 4/4] adding group index. Type regeneration noticed something unrelated --- packages/database/src/dbTypes.ts | 18 +++--------------- .../migrations/20260101183250_group_access.sql | 7 +++++-- packages/database/supabase/schemas/account.sql | 7 +++++-- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index 10eecd427..fe658a92f 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -630,18 +630,6 @@ export type Database = { } Relationships: [] } - result: { - Row: { - max: string | null - } - Insert: { - max?: string | null - } - Update: { - max?: string | null - } - Relationships: [] - } Space: { Row: { id: number @@ -702,7 +690,7 @@ export type Database = { id: number last_success_start: string | null last_task_end: string | null - last_task_start: string | null + last_task_start: string status: Database["public"]["Enums"]["task_status"] | null sync_function: string | null sync_target: number | null @@ -715,7 +703,7 @@ export type Database = { id?: number last_success_start?: string | null last_task_end?: string | null - last_task_start?: string | null + last_task_start: string status?: Database["public"]["Enums"]["task_status"] | null sync_function?: string | null sync_target?: number | null @@ -728,7 +716,7 @@ export type Database = { id?: number last_success_start?: string | null last_task_end?: string | null - last_task_start?: string | null + last_task_start?: string status?: Database["public"]["Enums"]["task_status"] | null sync_function?: string | null sync_target?: number | null diff --git a/packages/database/supabase/migrations/20260101183250_group_access.sql b/packages/database/supabase/migrations/20260101183250_group_access.sql index 997d52789..a211acd63 100644 --- a/packages/database/supabase/migrations/20260101183250_group_access.sql +++ b/packages/database/supabase/migrations/20260101183250_group_access.sql @@ -1,10 +1,13 @@ CREATE TABLE public.group_membership ( member_id UUID NOT NULL, group_id UUID NOT NULL, - admin BOOLEAN default true + admin BOOLEAN DEFAULT true ); -ALTER TABLE public.group_membership ADD CONSTRAINT group_membership_pkey PRIMARY KEY (member_id, group_id); +ALTER TABLE public.group_membership +ADD CONSTRAINT group_membership_pkey PRIMARY KEY (member_id, group_id); + +CREATE INDEX IF NOT EXISTS group_membership_group_idx ON public.group_membership (group_id); ALTER TABLE public.group_membership OWNER TO "postgres"; diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 7afe74582..ac4525683 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -131,10 +131,13 @@ REVOKE ALL ON TABLE public."SpaceAccess" FROM anon; CREATE TABLE IF NOT EXISTS public.group_membership ( member_id UUID NOT NULL, group_id UUID NOT NULL, - admin BOOLEAN default true + admin BOOLEAN DEFAULT true ); -ALTER TABLE public.group_membership ADD CONSTRAINT group_membership_pkey PRIMARY KEY (member_id, group_id); +ALTER TABLE public.group_membership +ADD CONSTRAINT group_membership_pkey PRIMARY KEY (member_id, group_id); + +CREATE INDEX IF NOT EXISTS group_membership_group_idx ON public.group_membership (group_id); ALTER TABLE public.group_membership OWNER TO "postgres";