diff --git a/packages/database/features/getContext.feature b/packages/database/features/getContext.feature index 532fb4700..e079a5d52 100644 --- a/packages/database/features/getContext.feature +++ b/packages/database/features/getContext.feature @@ -14,11 +14,13 @@ Feature: Get Context When the user user1 opens the Roam plugin in space abc Then the database should contain 2 PlatformAccount And the database should contain 1 AgentIdentifier - And the database should contain 2 SpaceAccess + And the database should contain 1 SpaceAccess + And the database should contain 1 LocalAccess And the database should contain 1 Space And a user logged in space abc should see 1 Space in the database And a user logged in space abc should see 1 AgentIdentifier in the database - And a user logged in space abc should see 2 SpaceAccess in the database + And a user logged in space abc should see 1 SpaceAccess in the database + And a user logged in space abc should see 1 LocalAccess in the database And a user logged in space abc should see 2 PlatformAccount in the database Scenario Outline: Calling getContext again @@ -26,4 +28,5 @@ Feature: Get Context Then the database should contain 2 PlatformAccount And the database should contain 1 Space And the database should contain 1 AgentIdentifier - And the database should contain 2 SpaceAccess + And the database should contain 1 SpaceAccess + And the database should contain 1 LocalAccess diff --git a/packages/database/schema.puml b/packages/database/schema.puml index de8a0ee6c..9cac789fd 100644 --- a/packages/database/schema.puml +++ b/packages/database/schema.puml @@ -15,18 +15,21 @@ class "PlatformAccount" [[{An account for an agent on a platform}]] { class "Users" [[{A database user account. Managed by the auth infrastructure.}]] { {field} id : integer } -"PlatformAccount" --> "0..1" "Users" : "dg_account" class "SpaceAccess" [[{An access control entry for a space}]] { {field} editor : boolean } +"PlatformAccount" --> "0..1" "Users" : "dg_account" +"SpaceAccess" --> "1" "Users" : "account" class "Space" [[{A space on a platform representing a community engaged in a conversation}]] { {field} id : integer {field} url : string {field} name : string {field} platform : Platform } -"SpaceAccess" --> "1" "PlatformAccount" : "account" "SpaceAccess" --> "0..1" "Space" : "space" +class "LocalAccess" [[{A record of which platform accounts have used this space}]] { + +} class "Content" [[{A unit of content}]] { {field} id : integer {field} source_local_id : string @@ -57,6 +60,7 @@ class "Concept" [[{An abstract concept, claim or relation}]] { {field} literal_content : JSON {field} is_schema : boolean } +"LocalAccess" --> "0..1" "Space" : "space" "Content" --> "0..1" "Space" : "space" "Document" --> "0..1" "Space" : "space" "Concept" --> "0..1" "Space" : "space" @@ -73,6 +77,7 @@ class "AgentIdentifier" [[{An identifying attribute associated with an agent, ca "Document" --> "0..*" "PlatformAccount" : "contributors" "Concept" --> "0..*" "PlatformAccount" : "contributors" "Content" --> "1" "PlatformAccount" : "creator" +"LocalAccess" --> "1" "PlatformAccount" : "account" "Content" --> "1" "Document" : "document" abstract "ContentEmbedding" [[{An embedding for a piece of content.}]] { {field} model : EmbeddingName diff --git a/packages/database/schema.svg b/packages/database/schema.svg index 78e2c9368..ab85a5238 100644 --- a/packages/database/schema.svg +++ b/packages/database/schema.svg @@ -1 +1 @@ -PlatformAccountid : integername : stringplatform : Platformaccount_local_id : stringwrite_permission : booleanactive : booleanagent_type : AgentTypemetadata : JSONUsersid : integerSpaceAccesseditor : booleanSpaceid : integerurl : stringname : stringplatform : PlatformContentid : integersource_local_id : stringcreated : datetimetext : stringmetadata : JSONscale : Scalelast_modified : datetimeDocumentid : integersource_local_id : stringcreated : datetimemetadata : JSONlast_modified : datetimeurl : stringcontents : blobConceptid : integerepistemic_status : EpistemicStatusname : stringdescription : stringcreated : datetimelast_modified : datetimearity : integerreference_content : JSONliteral_content : JSONis_schema : booleanAgentIdentifieridentifier_type : AgentIdentifierTypevalue : stringtrusted : booleanContentEmbeddingmodel : EmbeddingNamevector : vectorobsolete : booleanContentEmbedding_openai_text_embedding_3_small_1536model(i) : EmbeddingNamevector(i) : vectorobsolete(i) : booleanConceptSchemaid(i) : integerepistemic_status(i) : EpistemicStatusname(i) : stringdescription(i) : stringcreated(i) : datetimelast_modified(i) : datetimearity(i) : integerreference_content(i) : JSONliteral_content(i) : JSONis_schema(i) : booleandg_account0..1account1space0..1space0..1space0..1space0..1account1author1contributors0..*creator1author1contributors0..*author1contributors0..*document1target1part_of0..1represented_by0..1schema1 \ No newline at end of file +PlatformAccountid : integername : stringplatform : Platformaccount_local_id : stringwrite_permission : booleanactive : booleanagent_type : AgentTypemetadata : JSONUsersid : integerSpaceAccesseditor : booleanSpaceid : integerurl : stringname : stringplatform : PlatformLocalAccessContentid : integersource_local_id : stringcreated : datetimetext : stringmetadata : JSONscale : Scalelast_modified : datetimeDocumentid : integersource_local_id : stringcreated : datetimemetadata : JSONlast_modified : datetimeurl : stringcontents : blobConceptid : integerepistemic_status : EpistemicStatusname : stringdescription : stringcreated : datetimelast_modified : datetimearity : integerreference_content : JSONliteral_content : JSONis_schema : booleanAgentIdentifieridentifier_type : AgentIdentifierTypevalue : stringtrusted : booleanContentEmbeddingmodel : EmbeddingNamevector : vectorobsolete : booleanContentEmbedding_openai_text_embedding_3_small_1536model(i) : EmbeddingNamevector(i) : vectorobsolete(i) : booleanConceptSchemaid(i) : integerepistemic_status(i) : EpistemicStatusname(i) : stringdescription(i) : stringcreated(i) : datetimelast_modified(i) : datetimearity(i) : integerreference_content(i) : JSONliteral_content(i) : JSONis_schema(i) : booleandg_account0..1account1space0..1space0..1space0..1space0..1space0..1account1author1contributors0..*creator1author1contributors0..*author1contributors0..*account1document1target1part_of0..1represented_by0..1schema1 \ No newline at end of file diff --git a/packages/database/schema.yaml b/packages/database/schema.yaml index 9526ef1b7..5f44eef21 100644 --- a/packages/database/schema.yaml +++ b/packages/database/schema.yaml @@ -194,13 +194,21 @@ classes: space_url: unique_key_slots: - url + LocalAccess: + description: A record of which platform accounts have used this space + slots: + - space + attributes: + account: + range: PlatformAccount + required: true SpaceAccess: description: An access control entry for a space slots: - space attributes: account: - range: PlatformAccount + range: Users required: true editor: range: boolean diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index 132a482cf..c28556115 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -532,6 +532,50 @@ export type Database = { }, ] } + LocalAccess: { + Row: { + account_id: number + space_id: number + } + Insert: { + account_id: number + space_id: number + } + Update: { + account_id?: number + space_id?: number + } + Relationships: [ + { + foreignKeyName: "LocalAccess_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "my_accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "LocalAccess_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "PlatformAccount" + referencedColumns: ["id"] + }, + { + foreignKeyName: "LocalAccess_space_id_fkey" + columns: ["space_id"] + isOneToOne: false + referencedRelation: "my_spaces" + referencedColumns: ["id"] + }, + { + foreignKeyName: "LocalAccess_space_id_fkey" + columns: ["space_id"] + isOneToOne: false + referencedRelation: "Space" + referencedColumns: ["id"] + }, + ] + } PlatformAccount: { Row: { account_local_id: string @@ -603,35 +647,21 @@ export type Database = { } SpaceAccess: { Row: { - account_id: number + account_uid: string editor: boolean space_id: number } Insert: { - account_id: number + account_uid: string editor: boolean space_id: number } Update: { - account_id?: number + account_uid?: string editor?: boolean space_id?: number } Relationships: [ - { - foreignKeyName: "SpaceAccess_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "my_accounts" - referencedColumns: ["id"] - }, - { - foreignKeyName: "SpaceAccess_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "PlatformAccount" - referencedColumns: ["id"] - }, { foreignKeyName: "SpaceAccess_space_id_fkey" columns: ["space_id"] @@ -1472,7 +1502,6 @@ export type Database = { text_content: string }[] } - my_account: { Args: never; Returns: number } my_space_ids: { Args: never; Returns: number[] } propose_sync_task: { Args: { diff --git a/packages/database/supabase/functions/create-space/index.ts b/packages/database/supabase/functions/create-space/index.ts index 345c02176..cdc436b08 100644 --- a/packages/database/supabase/functions/create-space/index.ts +++ b/packages/database/supabase/functions/create-space/index.ts @@ -167,11 +167,11 @@ const processAndGetOrCreateSpace = async ( .upsert( { space_id, - account_id: anonPlatformUserResult.data.id, + account_uid: anonymousUser.id, editor: true, }, { - onConflict: "space_id,account_id", + onConflict: "account_uid,space_id", ignoreDuplicates: false, count: "estimated", }, diff --git a/packages/database/supabase/migrations/20251231203152_migrate_space_access_to_user.sql b/packages/database/supabase/migrations/20251231203152_migrate_space_access_to_user.sql new file mode 100644 index 000000000..12504a8ec --- /dev/null +++ b/packages/database/supabase/migrations/20251231203152_migrate_space_access_to_user.sql @@ -0,0 +1,207 @@ +REVOKE ALL ON TABLE public."SpaceAccess" FROM anon; +DROP POLICY IF EXISTS space_access_select_policy ON public."SpaceAccess"; +DROP POLICY IF EXISTS space_access_delete_policy ON public."SpaceAccess"; +DROP POLICY IF EXISTS space_access_insert_policy ON public."SpaceAccess"; +DROP POLICY IF EXISTS space_access_update_policy ON public."SpaceAccess"; +ALTER TABLE ONLY public."SpaceAccess" DROP CONSTRAINT "SpaceAccess_pkey"; + +ALTER TABLE public."SpaceAccess" RENAME TO "LocalAccess"; + +COMMENT ON TABLE public."LocalAccess" IS 'A record of which platform accounts have used this space'; + +COMMENT ON COLUMN public."LocalAccess".account_id IS 'The identity of the local account in this space'; + +ALTER TABLE ONLY public."LocalAccess" +RENAME CONSTRAINT "SpaceAccess_account_id_fkey" TO "LocalAccess_account_id_fkey"; + +ALTER TABLE ONLY public."LocalAccess" +RENAME CONSTRAINT "SpaceAccess_space_id_fkey" TO "LocalAccess_space_id_fkey"; + +ALTER TABLE public."LocalAccess" ADD CONSTRAINT "LocalAccess_pkey" PRIMARY KEY (account_id, space_id); + + +CREATE TABLE IF NOT EXISTS public."SpaceAccess" ( + account_uid UUID NOT NULL, + space_id bigint NOT NULL, + editor boolean NOT NULL +); + +ALTER TABLE ONLY public."SpaceAccess" +ADD CONSTRAINT "SpaceAccess_pkey" PRIMARY KEY (account_uid, space_id); + +ALTER TABLE public."SpaceAccess" OWNER TO "postgres"; + +COMMENT ON TABLE public."SpaceAccess" IS 'An access control entry for a space'; + +COMMENT ON COLUMN public."SpaceAccess".space_id IS 'The space in which the content is located'; + +COMMENT ON COLUMN public."SpaceAccess".account_uid IS 'The identity of the user account'; + +ALTER TABLE ONLY public."SpaceAccess" +ADD CONSTRAINT "SpaceAccess_account_uid_fkey" FOREIGN KEY ( + account_uid +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY public."SpaceAccess" +ADD CONSTRAINT "SpaceAccess_space_id_fkey" FOREIGN KEY ( + space_id +) REFERENCES public."Space" ( + id +) ON UPDATE CASCADE ON DELETE CASCADE; + +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; + +INSERT INTO public."SpaceAccess" +SELECT pa.dg_account, la.space_id, la.editor +FROM public."LocalAccess" AS la + JOIN public."PlatformAccount" AS pa ON (pa.id = la.account_id) +WHERE pa.dg_account IS NOT NULL; + +ALTER TABLE public."LocalAccess" DROP COLUMN editor; +DELETE FROM public."LocalAccess" WHERE account_id IN ( + SELECT id FROM public."PlatformAccount" + WHERE agent_type = 'anonymous' +); + +CREATE OR REPLACE FUNCTION public.is_my_account(account_id BIGINT) RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS ( + SELECT 1 FROM public."PlatformAccount" + WHERE id = account_id AND dg_account = auth.uid() + LIMIT 1 + ); +$$; + +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" + WHERE account_uid = auth.uid(); +$$; + +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 + WHERE sa.space_id = in_space.space_id + AND sa.account_uid=auth.uid()); +$$; + +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) + WHERE la.account_id = p_account_id + AND sa.account_uid = auth.uid() + ); +$$; + +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."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 + ); +$$; + +CREATE OR REPLACE FUNCTION public.upsert_account_in_space( + space_id_ BIGINT, + local_account public.account_local_input +) RETURNS BIGINT +SECURITY DEFINER +SET search_path = '' +LANGUAGE plpgsql +AS $$ +DECLARE + platform_ public."Platform"; + account_id_ BIGINT; + user_uid UUID; +BEGIN + SELECT platform INTO STRICT platform_ FROM public."Space" WHERE id = space_id_; + INSERT INTO public."PlatformAccount" AS pa ( + account_local_id, name, platform + ) VALUES ( + local_account.account_local_id, local_account.name, platform_ + ) ON CONFLICT (account_local_id, platform) DO UPDATE SET + name = COALESCE(NULLIF(TRIM(EXCLUDED.name), ''), pa.name) + RETURNING id, dg_account INTO STRICT account_id_, user_uid; + IF user_uid IS NOT NULL THEN + INSERT INTO public."SpaceAccess" as sa (space_id, account_uid, editor) + VALUES (space_id_, user_uid, COALESCE(local_account.space_editor, true)) + ON CONFLICT (space_id, account_uid) + DO UPDATE SET editor = COALESCE(local_account.space_editor, sa.editor, true); + END IF; + INSERT INTO public."LocalAccess" (space_id, account_id) values (space_id_, account_id_) + ON CONFLICT (space_id, account_id) + DO NOTHING; + IF local_account.email IS NOT NULL THEN + -- TODO: how to distinguish basic untrusted from platform placeholder email? + INSERT INTO public."AgentIdentifier" as ai (account_id, value, identifier_type, trusted) VALUES (account_id_, local_account.email, 'email', COALESCE(local_account.email_trusted, false)) + ON CONFLICT (value, identifier_type, account_id) + DO UPDATE SET trusted = COALESCE(local_account.email_trusted, ai.trusted, false); + END IF; + RETURN account_id_; +END; +$$; + +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) + WHERE "SpaceAccess".account_uid = auth.uid() +); + +ALTER TABLE public."SpaceAccess" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY space_access_select_policy ON public."SpaceAccess" FOR SELECT USING (public.in_space(space_id)); +CREATE POLICY space_access_delete_policy ON public."SpaceAccess" FOR DELETE USING (account_uid = auth.uid()); +CREATE POLICY space_access_insert_policy ON public."SpaceAccess" FOR INSERT WITH CHECK (account_uid = auth.uid()); +CREATE POLICY space_access_update_policy ON public."SpaceAccess" FOR UPDATE WITH CHECK (account_uid = auth.uid()); + +CREATE POLICY local_access_select_policy ON public."LocalAccess" FOR SELECT USING (public.in_space(space_id)); +CREATE POLICY local_access_delete_policy ON public."LocalAccess" FOR DELETE USING (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); +CREATE POLICY local_access_insert_policy ON public."LocalAccess" FOR INSERT WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); +CREATE POLICY local_access_update_policy ON public."LocalAccess" FOR UPDATE WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); + +DROP POLICY IF EXISTS agent_identifier_delete_policy ON public."AgentIdentifier"; +CREATE POLICY agent_identifier_delete_policy ON public."AgentIdentifier" FOR DELETE USING (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); + +DROP POLICY IF EXISTS agent_identifier_insert_policy ON public."AgentIdentifier"; +CREATE POLICY agent_identifier_insert_policy ON public."AgentIdentifier" FOR INSERT WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); + +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)); + +DROP FUNCTION public.my_account(); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 80944dc83..0182a2e9c 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -64,15 +64,45 @@ GRANT ALL ON TABLE public."AgentIdentifier" TO service_role; CREATE INDEX platform_account_dg_account_idx ON public."PlatformAccount" (dg_account); -CREATE TABLE IF NOT EXISTS public."SpaceAccess" ( - space_id bigint, +CREATE TABLE IF NOT EXISTS public."LocalAccess" ( account_id bigint NOT NULL, + space_id bigint NOT NULL +); + +ALTER TABLE public."LocalAccess" OWNER TO "postgres"; + +ALTER TABLE public."LocalAccess" ADD CONSTRAINT "LocalAccess_pkey" PRIMARY KEY (account_id, space_id); + +COMMENT ON TABLE public."LocalAccess" IS 'A record of which platform accounts have used this space'; + +COMMENT ON COLUMN public."LocalAccess".space_id IS 'The space in which the content is located'; + +COMMENT ON COLUMN public."LocalAccess".account_id IS 'The identity of the local account in this space'; + +ALTER TABLE ONLY public."LocalAccess" +ADD CONSTRAINT "LocalAccess_account_id_fkey" FOREIGN KEY ( + account_id +) REFERENCES public."PlatformAccount" (id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE ONLY public."LocalAccess" +ADD CONSTRAINT "LocalAccess_space_id_fkey" FOREIGN KEY ( + space_id +) REFERENCES public."Space" ( + id +) ON UPDATE CASCADE ON DELETE CASCADE; + +GRANT ALL ON TABLE public."LocalAccess" TO authenticated; +GRANT ALL ON TABLE public."LocalAccess" TO service_role; +REVOKE ALL ON TABLE public."LocalAccess" FROM anon; + +CREATE TABLE IF NOT EXISTS public."SpaceAccess" ( + account_uid UUID NOT NULL, + space_id bigint NOT NULL, editor boolean NOT NULL ); ALTER TABLE ONLY public."SpaceAccess" -ADD CONSTRAINT "SpaceAccess_pkey" PRIMARY KEY (space_id, account_id); - +ADD CONSTRAINT "SpaceAccess_pkey" PRIMARY KEY (account_uid, space_id); ALTER TABLE public."SpaceAccess" OWNER TO "postgres"; @@ -80,12 +110,12 @@ COMMENT ON TABLE public."SpaceAccess" IS 'An access control entry for a space'; COMMENT ON COLUMN public."SpaceAccess".space_id IS 'The space in which the content is located'; -COMMENT ON COLUMN public."SpaceAccess".account_id IS 'The identity of the account in this space'; +COMMENT ON COLUMN public."SpaceAccess".account_uid IS 'The identity of the user account'; ALTER TABLE ONLY public."SpaceAccess" -ADD CONSTRAINT "SpaceAccess_account_id_fkey" FOREIGN KEY ( - account_id -) REFERENCES public."PlatformAccount" (id) ON UPDATE CASCADE ON DELETE CASCADE; +ADD CONSTRAINT "SpaceAccess_account_uid_fkey" FOREIGN KEY ( + account_uid +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY public."SpaceAccess" ADD CONSTRAINT "SpaceAccess_space_id_fkey" FOREIGN KEY ( @@ -94,9 +124,9 @@ ADD CONSTRAINT "SpaceAccess_space_id_fkey" FOREIGN KEY ( id ) ON UPDATE CASCADE ON DELETE CASCADE; -GRANT ALL ON TABLE public."SpaceAccess" TO anon; 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 TYPE public.account_local_input AS ( -- PlatformAccount columns @@ -119,6 +149,7 @@ AS $$ DECLARE platform_ public."Platform"; account_id_ BIGINT; + user_uid UUID; BEGIN SELECT platform INTO STRICT platform_ FROM public."Space" WHERE id = space_id_; INSERT INTO public."PlatformAccount" AS pa ( @@ -127,10 +158,16 @@ BEGIN local_account.account_local_id, local_account.name, platform_ ) ON CONFLICT (account_local_id, platform) DO UPDATE SET name = COALESCE(NULLIF(TRIM(EXCLUDED.name), ''), pa.name) - RETURNING id INTO STRICT account_id_; - INSERT INTO public."SpaceAccess" as sa (space_id, account_id, editor) values (space_id_, account_id_, COALESCE(local_account.space_editor, true)) + RETURNING id, dg_account INTO STRICT account_id_, user_uid; + IF user_uid IS NOT NULL THEN + INSERT INTO public."SpaceAccess" as sa (space_id, account_uid, editor) + VALUES (space_id_, user_uid, COALESCE(local_account.space_editor, true)) + ON CONFLICT (space_id, account_uid) + DO UPDATE SET editor = COALESCE(local_account.space_editor, sa.editor, true); + END IF; + INSERT INTO public."LocalAccess" (space_id, account_id) values (space_id_, account_id_) ON CONFLICT (space_id, account_id) - DO UPDATE SET editor = COALESCE(local_account.space_editor, sa.editor, true); + DO NOTHING; IF local_account.email IS NOT NULL THEN -- TODO: how to distinguish basic untrusted from platform placeholder email? INSERT INTO public."AgentIdentifier" as ai (account_id, value, identifier_type, trusted) VALUES (account_id_, local_account.email, 'email', COALESCE(local_account.email_trusted, false)) @@ -186,35 +223,23 @@ STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ - WITH u AS (SELECT auth.uid() LIMIT 1) - SELECT true FROM public."PlatformAccount" pa - JOIN u ON pa.dg_account = u.uid - WHERE pa.id = account_id; + SELECT EXISTS ( + SELECT 1 FROM public."PlatformAccount" + WHERE id = account_id AND dg_account = auth.uid() + LIMIT 1 + ); $$; COMMENT ON FUNCTION public.is_my_account IS 'security utility: is this my own account?'; -CREATE OR REPLACE FUNCTION public.my_account() RETURNS BIGINT -STABLE SECURITY DEFINER -SET search_path = '' -LANGUAGE sql -AS $$ - WITH u AS (SELECT auth.uid() LIMIT 1) - SELECT id FROM public."PlatformAccount" pa - JOIN u ON pa.dg_account = u.uid LIMIT 1; -$$; - -COMMENT ON FUNCTION public.my_account IS 'security utility: id of my account'; - CREATE OR REPLACE FUNCTION public.my_space_ids() RETURNS BIGINT [] STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ - WITH u AS (SELECT auth.uid() LIMIT 1) - SELECT COALESCE(array_agg(distinct sa.space_id), '{}') AS ids FROM public."SpaceAccess" AS sa - JOIN public."PlatformAccount" AS pa ON pa.id=sa.account_id - JOIN u ON pa.dg_account = u.uid; + SELECT COALESCE(array_agg(distinct space_id), '{}') AS ids + FROM public."SpaceAccess" + WHERE account_uid = auth.uid(); $$; COMMENT ON FUNCTION public.my_space_ids IS 'security utility: all spaces the user has access to'; @@ -224,11 +249,9 @@ STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ - WITH u AS (SELECT auth.uid() LIMIT 1), - pa AS (SELECT sa.space_id AS id FROM public."SpaceAccess" AS sa - JOIN public."PlatformAccount" AS pa ON pa.id=sa.account_id - JOIN u ON pa.dg_account = u.uid) - SELECT EXISTS (SELECT id FROM pa WHERE id = space_id ); + SELECT EXISTS (SELECT 1 FROM public."SpaceAccess" AS sa + WHERE sa.space_id = in_space.space_id + AND sa.account_uid=auth.uid()); $$; COMMENT ON FUNCTION public.in_space IS 'security utility: does current user have access to this space?'; @@ -240,9 +263,10 @@ SET search_path = '' LANGUAGE sql AS $$ SELECT EXISTS ( SELECT 1 - FROM public."SpaceAccess" AS sa - WHERE sa.account_id = p_account_id - AND sa.space_id = ANY(public.my_space_ids()) + FROM public."LocalAccess" AS la + JOIN public."SpaceAccess" AS sa USING (space_id) + WHERE la.account_id = p_account_id + AND sa.account_uid = auth.uid() ); $$; @@ -253,12 +277,13 @@ STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ SELECT EXISTS ( - SELECT 1 - FROM public."SpaceAccess" AS sa - JOIN public."PlatformAccount" AS pa ON (pa.id = sa.account_id) - WHERE sa.account_id = p_account_id - AND sa.space_id = ANY(public.my_space_ids()) - AND pa.dg_account IS NULL + SELECT 1 + FROM public."SpaceAccess" AS sa + 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 ); $$; @@ -290,22 +315,20 @@ ALTER TABLE public."PlatformAccount" ENABLE ROW LEVEL SECURITY; CREATE OR REPLACE VIEW public.my_accounts AS SELECT - pa.id, - pa.name, - pa.platform, - pa.account_local_id, - pa.write_permission, - pa.active, - pa.agent_type, - pa.metadata, - pa.dg_account -FROM public."PlatformAccount" AS pa -WHERE EXISTS ( - SELECT 1 - FROM public."SpaceAccess" AS sa - WHERE - sa.account_id = pa.id - AND sa.space_id = any(public.my_space_ids()) + 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) + WHERE "SpaceAccess".account_uid = auth.uid() ); DROP POLICY IF EXISTS platform_account_policy ON public."PlatformAccount"; @@ -334,13 +357,29 @@ DROP POLICY IF EXISTS space_access_select_policy ON public."SpaceAccess"; CREATE POLICY space_access_select_policy ON public."SpaceAccess" FOR SELECT USING (public.in_space(space_id)); DROP POLICY IF EXISTS space_access_delete_policy ON public."SpaceAccess"; -CREATE POLICY space_access_delete_policy ON public."SpaceAccess" FOR DELETE USING (public.unowned_account_in_shared_space(account_id) OR account_id = public.my_account()); +CREATE POLICY space_access_delete_policy ON public."SpaceAccess" FOR DELETE USING (account_uid = auth.uid()); DROP POLICY IF EXISTS space_access_insert_policy ON public."SpaceAccess"; -CREATE POLICY space_access_insert_policy ON public."SpaceAccess" FOR INSERT WITH CHECK (public.unowned_account_in_shared_space(account_id) OR account_id = public.my_account()); +CREATE POLICY space_access_insert_policy ON public."SpaceAccess" FOR INSERT WITH CHECK (account_uid = auth.uid()); DROP POLICY IF EXISTS space_access_update_policy ON public."SpaceAccess"; -CREATE POLICY space_access_update_policy ON public."SpaceAccess" FOR UPDATE WITH CHECK (public.unowned_account_in_shared_space(account_id) OR account_id = public.my_account()); +CREATE POLICY space_access_update_policy ON public."SpaceAccess" FOR UPDATE WITH CHECK (account_uid = auth.uid()); + +ALTER TABLE public."LocalAccess" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS local_access_policy ON public."LocalAccess"; + +DROP POLICY IF EXISTS local_access_select_policy ON public."LocalAccess"; +CREATE POLICY local_access_select_policy ON public."LocalAccess" FOR SELECT USING (public.in_space(space_id)); + +DROP POLICY IF EXISTS local_access_delete_policy ON public."LocalAccess"; +CREATE POLICY local_access_delete_policy ON public."LocalAccess" FOR DELETE USING (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); + +DROP POLICY IF EXISTS local_access_insert_policy ON public."LocalAccess"; +CREATE POLICY local_access_insert_policy ON public."LocalAccess" FOR INSERT WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); + +DROP POLICY IF EXISTS local_access_update_policy ON public."LocalAccess"; +CREATE POLICY local_access_update_policy ON public."LocalAccess" FOR UPDATE WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); -- AgentIdentifier: Allow space members to do anything, to allow editing authors. -- Eventually: Once the account is claimed by a user, only allow this user to modify it. @@ -353,10 +392,10 @@ DROP POLICY IF EXISTS agent_identifier_select_policy ON public."AgentIdentifier" CREATE POLICY agent_identifier_select_policy ON public."AgentIdentifier" FOR SELECT USING (public.account_in_shared_space(account_id)); DROP POLICY IF EXISTS agent_identifier_delete_policy ON public."AgentIdentifier"; -CREATE POLICY agent_identifier_delete_policy ON public."AgentIdentifier" FOR DELETE USING (public.unowned_account_in_shared_space(account_id) OR account_id = public.my_account()); +CREATE POLICY agent_identifier_delete_policy ON public."AgentIdentifier" FOR DELETE USING (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); DROP POLICY IF EXISTS agent_identifier_insert_policy ON public."AgentIdentifier"; -CREATE POLICY agent_identifier_insert_policy ON public."AgentIdentifier" FOR INSERT WITH CHECK (public.unowned_account_in_shared_space(account_id) OR account_id = public.my_account()); +CREATE POLICY agent_identifier_insert_policy ON public."AgentIdentifier" FOR INSERT WITH CHECK (public.unowned_account_in_shared_space(account_id) OR public.is_my_account(account_id)); 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 account_id = public.my_account()); +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));