diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index fe658a92f..d588149b6 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -247,6 +247,36 @@ export type Database = { }, ] } + ConceptAccess: { + Row: { + account_uid: string + concept_id: number + } + Insert: { + account_uid: string + concept_id: number + } + Update: { + account_uid?: string + concept_id?: number + } + Relationships: [ + { + foreignKeyName: "ConceptAccess_concept_id_fkey" + columns: ["concept_id"] + isOneToOne: false + referencedRelation: "Concept" + referencedColumns: ["id"] + }, + { + foreignKeyName: "ConceptAccess_concept_id_fkey" + columns: ["concept_id"] + isOneToOne: false + referencedRelation: "my_concepts" + referencedColumns: ["id"] + }, + ] + } Content: { Row: { author_id: number | null @@ -424,6 +454,43 @@ export type Database = { }, ] } + ContentAccess: { + Row: { + account_uid: string + content_id: number + } + Insert: { + account_uid: string + content_id: number + } + Update: { + account_uid?: string + content_id?: number + } + Relationships: [ + { + foreignKeyName: "ContentAccess_content_id_fkey" + columns: ["content_id"] + isOneToOne: false + referencedRelation: "Content" + referencedColumns: ["id"] + }, + { + foreignKeyName: "ContentAccess_content_id_fkey" + columns: ["content_id"] + isOneToOne: false + referencedRelation: "my_contents" + referencedColumns: ["id"] + }, + { + foreignKeyName: "ContentAccess_content_id_fkey" + columns: ["content_id"] + isOneToOne: false + referencedRelation: "my_contents_with_embedding_openai_text_embedding_3_small_1536" + referencedColumns: ["id"] + }, + ] + } ContentEmbedding_openai_text_embedding_3_small_1536: { Row: { model: Database["public"]["Enums"]["EmbeddingName"] @@ -1320,10 +1387,17 @@ export type Database = { isSetofReturn: true } } + can_access_account: { Args: { account_uid: string }; Returns: boolean } + can_view_specific_concept: { Args: { id: number }; Returns: boolean } + can_view_specific_content: { Args: { id: number }; Returns: boolean } compute_arity_local: { Args: { lit_content: Json; schema_id: number } Returns: number } + concept_in_editable_space: { + Args: { concept_id: number } + Returns: boolean + } concept_in_relations: | { Args: { concept: Database["public"]["Tables"]["Concept"]["Row"] } @@ -1377,6 +1451,10 @@ export type Database = { isSetofReturn: true } } + content_in_editable_space: { + Args: { content_id: number } + Returns: boolean + } content_in_space: { Args: { content_id: number }; Returns: boolean } content_of_concept: { Args: { concept: Database["public"]["Views"]["my_concepts"]["Row"] } @@ -1434,6 +1512,7 @@ export type Database = { isSetofReturn: true } } + editor_in_space: { Args: { space_id: number }; Returns: boolean } end_sync_task: { Args: { s_function: string @@ -1511,6 +1590,7 @@ export type Database = { text_content: string }[] } + my_editable_space_ids: { Args: never; Returns: number[] } my_space_ids: { Args: never; Returns: number[] } my_user_accounts: { Args: never; Returns: string[] } propose_sync_task: { diff --git a/packages/database/supabase/migrations/20260102140646_content_and_concept_access_tables.sql b/packages/database/supabase/migrations/20260102140646_content_and_concept_access_tables.sql new file mode 100644 index 000000000..2a8bcda48 --- /dev/null +++ b/packages/database/supabase/migrations/20260102140646_content_and_concept_access_tables.sql @@ -0,0 +1,231 @@ +CREATE OR REPLACE FUNCTION public.can_access_account(account_uid UUID) RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT account_uid = auth.uid() OR EXISTS ( + SELECT 1 FROM public.group_membership + WHERE member_id = auth.uid() AND group_id=account_uid + LIMIT 1 + ); +$$; + +COMMENT ON FUNCTION public.can_access_account IS 'security utility: Is this my account or one of my groups?'; + +CREATE OR REPLACE FUNCTION public.my_editable_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) + WHERE editor; +$$; +COMMENT ON FUNCTION public.my_editable_space_ids IS 'security utility: all spaces the user has edit access to'; + + +CREATE OR REPLACE FUNCTION public.editor_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 = editor_in_space.space_id AND sa.editor); +$$; + +COMMENT ON FUNCTION public.editor_in_space IS 'security utility: does current user have edit access to this space?'; + +CREATE OR REPLACE FUNCTION public.content_in_editable_space(content_id BIGINT) RETURNS boolean +STABLE +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT public.editor_in_space(space_id) FROM public."Content" WHERE id=content_id +$$; + +COMMENT ON FUNCTION public.content_in_editable_space IS 'security utility: does current user have editor access to this content''s space?'; + +CREATE OR REPLACE FUNCTION public.concept_in_editable_space(concept_id BIGINT) RETURNS boolean +STABLE +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT public.editor_in_space(space_id) FROM public."Concept" WHERE id=concept_id +$$; + +COMMENT ON FUNCTION public.concept_in_editable_space IS 'security utility: does current user have editor access to this concept''s space?'; + +CREATE TABLE IF NOT EXISTS public."ContentAccess" ( + account_uid UUID NOT NULL, + content_id bigint NOT NULL +); + +ALTER TABLE ONLY public."ContentAccess" +ADD CONSTRAINT "ContentAccess_pkey" PRIMARY KEY (account_uid, content_id); + +ALTER TABLE public."ContentAccess" OWNER TO "postgres"; + +COMMENT ON TABLE public."ContentAccess" IS 'An access control entry for a content'; + +COMMENT ON COLUMN public."ContentAccess".content_id IS 'The content item for which access is granted'; + +COMMENT ON COLUMN public."ContentAccess".account_uid IS 'The identity of the user account'; + +ALTER TABLE ONLY public."ContentAccess" +ADD CONSTRAINT "ContentAccess_account_uid_fkey" FOREIGN KEY ( + account_uid +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +CREATE INDEX content_access_content_id_idx ON public."ContentAccess" (content_id); + +ALTER TABLE ONLY public."ContentAccess" +ADD CONSTRAINT "ContentAccess_content_id_fkey" FOREIGN KEY ( + content_id +) REFERENCES public."Content" ( + id +) ON UPDATE CASCADE ON DELETE CASCADE; + +GRANT ALL ON TABLE public."ContentAccess" TO authenticated; +GRANT ALL ON TABLE public."ContentAccess" TO service_role; +REVOKE ALL ON TABLE public."ContentAccess" FROM anon; + +CREATE OR REPLACE FUNCTION public.can_view_specific_content(id BIGINT) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS( + SELECT true FROM public."ContentAccess" + JOIN public.my_user_accounts() ON (account_uid=my_user_accounts) + WHERE content_id=id + LIMIT 1); +$$; + +CREATE OR REPLACE VIEW public.my_contents AS +SELECT + id, + document_id, + source_local_id, + variant, + author_id, + creator_id, + created, + text, + metadata, + scale, + space_id, + last_modified, + part_of_id +FROM public."Content" +WHERE ( + space_id = any(public.my_space_ids()) + OR public.can_view_specific_content(id) +); + +DROP POLICY IF EXISTS content_policy ON public."Content"; +CREATE POLICY content_select_policy ON public."Content" FOR SELECT USING (public.in_space(space_id) OR public.can_view_specific_content(id)); +CREATE POLICY content_delete_policy ON public."Content" FOR DELETE USING (public.in_space(space_id)); +CREATE POLICY content_insert_policy ON public."Content" FOR INSERT WITH CHECK (public.in_space(space_id)); +CREATE POLICY content_update_policy ON public."Content" FOR UPDATE WITH CHECK (public.in_space(space_id)); + +ALTER TABLE public."ContentAccess" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS content_access_policy ON public."ContentAccess"; +DROP POLICY IF EXISTS content_access_select_policy ON public."ContentAccess"; +CREATE POLICY content_access_select_policy ON public."ContentAccess" FOR SELECT USING (public.content_in_space(content_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS content_access_delete_policy ON public."ContentAccess"; +CREATE POLICY content_access_delete_policy ON public."ContentAccess" FOR DELETE USING (public.content_in_editable_space(content_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS content_access_insert_policy ON public."ContentAccess"; +CREATE POLICY content_access_insert_policy ON public."ContentAccess" FOR INSERT WITH CHECK (public.content_in_editable_space(content_id)); +DROP POLICY IF EXISTS content_access_update_policy ON public."ContentAccess"; +CREATE POLICY content_access_update_policy ON public."ContentAccess" FOR UPDATE WITH CHECK (public.content_in_editable_space(content_id)); + + +CREATE TABLE IF NOT EXISTS public."ConceptAccess" ( + account_uid UUID NOT NULL, + concept_id bigint NOT NULL +); + +ALTER TABLE ONLY public."ConceptAccess" +ADD CONSTRAINT "ConceptAccess_pkey" PRIMARY KEY (account_uid, concept_id); + +ALTER TABLE public."ConceptAccess" OWNER TO "postgres"; + +COMMENT ON TABLE public."ConceptAccess" IS 'An access control entry for a concept'; + +COMMENT ON COLUMN public."ConceptAccess".concept_id IS 'The concept item for which access is granted'; + +COMMENT ON COLUMN public."ConceptAccess".account_uid IS 'The identity of the user account'; + +ALTER TABLE ONLY public."ConceptAccess" +ADD CONSTRAINT "ConceptAccess_account_uid_fkey" FOREIGN KEY ( + account_uid +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +CREATE INDEX concept_access_concept_id_idx ON public."ConceptAccess" (concept_id); + +ALTER TABLE ONLY public."ConceptAccess" +ADD CONSTRAINT "ConceptAccess_concept_id_fkey" FOREIGN KEY ( + concept_id +) REFERENCES public."Concept" ( + id +) ON UPDATE CASCADE ON DELETE CASCADE; + +GRANT ALL ON TABLE public."ConceptAccess" TO authenticated; +GRANT ALL ON TABLE public."ConceptAccess" TO service_role; +REVOKE ALL ON TABLE public."ConceptAccess" FROM anon; + +CREATE OR REPLACE FUNCTION public.can_view_specific_concept(id BIGINT) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS( + SELECT true FROM public."ConceptAccess" + JOIN public.my_user_accounts() ON (account_uid=my_user_accounts) + WHERE concept_id=id + LIMIT 1); +$$; + +CREATE OR REPLACE VIEW public.my_concepts AS +SELECT + id, + epistemic_status, + name, + description, + author_id, + created, + last_modified, + space_id, + arity, + schema_id, + literal_content, + reference_content, + refs, + is_schema, + represented_by_id +FROM public."Concept" +WHERE ( + space_id = any(public.my_space_ids()) + OR public.can_view_specific_concept(id) +); + +DROP POLICY IF EXISTS concept_policy ON public."Concept"; +CREATE POLICY concept_select_policy ON public."Concept" FOR SELECT USING (public.in_space(space_id) OR public.can_view_specific_concept(id)); +CREATE POLICY concept_delete_policy ON public."Concept" FOR DELETE USING (public.in_space(space_id)); +CREATE POLICY concept_insert_policy ON public."Concept" FOR INSERT WITH CHECK (public.in_space(space_id)); +CREATE POLICY concept_update_policy ON public."Concept" FOR UPDATE WITH CHECK (public.in_space(space_id)); + +ALTER TABLE public."ConceptAccess" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS concept_access_policy ON public."ConceptAccess"; +DROP POLICY IF EXISTS concept_access_select_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_select_policy ON public."ConceptAccess" FOR SELECT USING (public.concept_in_space(concept_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS concept_access_delete_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_delete_policy ON public."ConceptAccess" FOR DELETE USING (public.concept_in_editable_space(concept_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS concept_access_insert_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_insert_policy ON public."ConceptAccess" FOR INSERT WITH CHECK (public.concept_in_editable_space(concept_id)); +DROP POLICY IF EXISTS concept_access_update_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_update_policy ON public."ConceptAccess" FOR UPDATE WITH CHECK (public.concept_in_editable_space(concept_id)); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index ac4525683..7d3f075be 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -263,6 +263,20 @@ $$; COMMENT ON FUNCTION public.is_my_account IS 'security utility: is this my own account?'; +CREATE OR REPLACE FUNCTION public.can_access_account(account_uid UUID) RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT account_uid = auth.uid() OR EXISTS ( + SELECT 1 FROM public.group_membership + WHERE member_id = auth.uid() AND group_id=account_uid + LIMIT 1 + ); +$$; + +COMMENT ON FUNCTION public.can_access_account IS 'security utility: Is this my account or one of my groups?'; + CREATE OR REPLACE FUNCTION public.my_user_accounts() RETURNS SETOF UUID STABLE SECURITY DEFINER SET search_path = '' @@ -325,6 +339,31 @@ $$; COMMENT ON FUNCTION public.in_space IS 'security utility: does current user have access to this space?'; +CREATE OR REPLACE FUNCTION public.my_editable_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) + WHERE editor; +$$; +COMMENT ON FUNCTION public.my_editable_space_ids IS 'security utility: all spaces the user has edit access to'; + + +CREATE OR REPLACE FUNCTION public.editor_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 = editor_in_space.space_id AND sa.editor); +$$; + +COMMENT ON FUNCTION public.editor_in_space IS 'security utility: does current user have edit access to this space?'; + CREATE OR REPLACE FUNCTION public.account_in_shared_space(p_account_id BIGINT) RETURNS boolean STABLE SECURITY DEFINER diff --git a/packages/database/supabase/schemas/concept.sql b/packages/database/supabase/schemas/concept.sql index 21b73cd2a..a7c0c33c4 100644 --- a/packages/database/supabase/schemas/concept.sql +++ b/packages/database/supabase/schemas/concept.sql @@ -115,6 +115,52 @@ REVOKE ALL ON TABLE public."Concept" FROM anon; GRANT ALL ON TABLE public."Concept" TO authenticated; GRANT ALL ON TABLE public."Concept" TO service_role; +CREATE TABLE IF NOT EXISTS public."ConceptAccess" ( + account_uid UUID NOT NULL, + concept_id bigint NOT NULL +); + +ALTER TABLE ONLY public."ConceptAccess" +ADD CONSTRAINT "ConceptAccess_pkey" PRIMARY KEY (account_uid, concept_id); + +ALTER TABLE public."ConceptAccess" OWNER TO "postgres"; + +COMMENT ON TABLE public."ConceptAccess" IS 'An access control entry for a concept'; + +COMMENT ON COLUMN public."ConceptAccess".concept_id IS 'The concept item for which access is granted'; + +COMMENT ON COLUMN public."ConceptAccess".account_uid IS 'The identity of the user account'; + +ALTER TABLE ONLY public."ConceptAccess" +ADD CONSTRAINT "ConceptAccess_account_uid_fkey" FOREIGN KEY ( + account_uid +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +CREATE INDEX concept_access_concept_id_idx ON public."ConceptAccess" (concept_id); + +ALTER TABLE ONLY public."ConceptAccess" +ADD CONSTRAINT "ConceptAccess_concept_id_fkey" FOREIGN KEY ( + concept_id +) REFERENCES public."Concept" ( + id +) ON UPDATE CASCADE ON DELETE CASCADE; + +GRANT ALL ON TABLE public."ConceptAccess" TO authenticated; +GRANT ALL ON TABLE public."ConceptAccess" TO service_role; +REVOKE ALL ON TABLE public."ConceptAccess" FROM anon; + +CREATE OR REPLACE FUNCTION public.can_view_specific_concept(id BIGINT) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS( + SELECT true FROM public."ConceptAccess" + JOIN public.my_user_accounts() ON (account_uid=my_user_accounts) + WHERE concept_id=id + LIMIT 1); +$$; + CREATE OR REPLACE VIEW public.my_concepts AS SELECT id, @@ -132,7 +178,11 @@ SELECT refs, is_schema, represented_by_id -FROM public."Concept" WHERE space_id = any(public.my_space_ids()); +FROM public."Concept" +WHERE ( + space_id = any(public.my_space_ids()) + OR public.can_view_specific_concept(id) +); -- following https://docs.postgrest.org/en/v13/references/api/resource_embedding.html#recursive-relationships CREATE OR REPLACE FUNCTION public.schema_of_concept(concept public."Concept") @@ -388,8 +438,37 @@ $$; COMMENT ON FUNCTION public.concept_in_space IS 'security utility: does current user have access to this concept''s space?'; +CREATE OR REPLACE FUNCTION public.concept_in_editable_space(concept_id BIGINT) RETURNS boolean +STABLE +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT public.editor_in_space(space_id) FROM public."Concept" WHERE id=concept_id +$$; + +COMMENT ON FUNCTION public.concept_in_editable_space IS 'security utility: does current user have editor access to this concept''s space?'; + ALTER TABLE public."Concept" ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS concept_policy ON public."Concept"; -CREATE POLICY concept_policy ON public."Concept" FOR ALL USING (public.in_space(space_id)); +DROP POLICY IF EXISTS concept_select_policy ON public."Concept"; +CREATE POLICY concept_select_policy ON public."Concept" FOR SELECT USING (public.in_space(space_id) OR public.can_view_specific_concept(id)); +DROP POLICY IF EXISTS concept_delete_policy ON public."Concept"; +CREATE POLICY concept_delete_policy ON public."Concept" FOR DELETE USING (public.in_space(space_id)); +DROP POLICY IF EXISTS concept_insert_policy ON public."Concept"; +CREATE POLICY concept_insert_policy ON public."Concept" FOR INSERT WITH CHECK (public.in_space(space_id)); +DROP POLICY IF EXISTS concept_update_policy ON public."Concept"; +CREATE POLICY concept_update_policy ON public."Concept" FOR UPDATE WITH CHECK (public.in_space(space_id)); + +ALTER TABLE public."ConceptAccess" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS concept_access_policy ON public."ConceptAccess"; +DROP POLICY IF EXISTS concept_access_select_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_select_policy ON public."ConceptAccess" FOR SELECT USING (public.concept_in_space(concept_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS concept_access_delete_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_delete_policy ON public."ConceptAccess" FOR DELETE USING (public.concept_in_editable_space(concept_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS concept_access_insert_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_insert_policy ON public."ConceptAccess" FOR INSERT WITH CHECK (public.concept_in_editable_space(concept_id)); +DROP POLICY IF EXISTS concept_access_update_policy ON public."ConceptAccess"; +CREATE POLICY concept_access_update_policy ON public."ConceptAccess" FOR UPDATE WITH CHECK (public.concept_in_editable_space(concept_id)); diff --git a/packages/database/supabase/schemas/content.sql b/packages/database/supabase/schemas/content.sql index b8a4c8694..1cf16b4e7 100644 --- a/packages/database/supabase/schemas/content.sql +++ b/packages/database/supabase/schemas/content.sql @@ -165,6 +165,39 @@ COMMENT ON COLUMN public."Content".last_modified IS 'The last time the content w COMMENT ON COLUMN public."Content".part_of_id IS 'This content is part of a larger content unit'; +CREATE TABLE IF NOT EXISTS public."ContentAccess" ( + account_uid UUID NOT NULL, + content_id bigint NOT NULL +); + +ALTER TABLE ONLY public."ContentAccess" +ADD CONSTRAINT "ContentAccess_pkey" PRIMARY KEY (account_uid, content_id); + +ALTER TABLE public."ContentAccess" OWNER TO "postgres"; + +COMMENT ON TABLE public."ContentAccess" IS 'An access control entry for a content'; + +COMMENT ON COLUMN public."ContentAccess".content_id IS 'The content item for which access is granted'; + +COMMENT ON COLUMN public."ContentAccess".account_uid IS 'The identity of the user account'; + +ALTER TABLE ONLY public."ContentAccess" +ADD CONSTRAINT "ContentAccess_account_uid_fkey" FOREIGN KEY ( + account_uid +) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE; + +CREATE INDEX content_access_content_id_idx ON public."ContentAccess" (content_id); + +ALTER TABLE ONLY public."ContentAccess" +ADD CONSTRAINT "ContentAccess_content_id_fkey" FOREIGN KEY ( + content_id +) REFERENCES public."Content" ( + id +) ON UPDATE CASCADE ON DELETE CASCADE; + +GRANT ALL ON TABLE public."ContentAccess" TO authenticated; +GRANT ALL ON TABLE public."ContentAccess" TO service_role; +REVOKE ALL ON TABLE public."ContentAccess" FROM anon; REVOKE ALL ON TABLE public."Document" FROM anon; GRANT ALL ON TABLE public."Document" TO authenticated; @@ -174,6 +207,19 @@ REVOKE ALL ON TABLE public."Content" FROM anon; GRANT ALL ON TABLE public."Content" TO authenticated; GRANT ALL ON TABLE public."Content" TO service_role; +CREATE OR REPLACE FUNCTION public.can_view_specific_content(id BIGINT) RETURNS BOOLEAN +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT EXISTS( + SELECT true FROM public."ContentAccess" + JOIN public.my_user_accounts() ON (account_uid=my_user_accounts) + WHERE content_id=id + LIMIT 1); +$$; + + CREATE OR REPLACE VIEW public.my_contents AS SELECT id, @@ -189,7 +235,11 @@ SELECT space_id, last_modified, part_of_id -FROM public."Content" WHERE space_id = any(public.my_space_ids()); +FROM public."Content" +WHERE ( + space_id = any(public.my_space_ids()) + OR public.can_view_specific_content(id) +); CREATE OR REPLACE FUNCTION public.document_of_content(content public.my_contents) RETURNS SETOF public.my_documents STRICT STABLE @@ -436,7 +486,7 @@ COMMENT ON FUNCTION public.upsert_content_embedding IS 'single content embedding -- The data should be an array of LocalContentDataInput -- Contents are upserted, based on space_id and local_id. New (or old) IDs are returned. -- This may trigger creation of PlatformAccounts and Documents appropriately. -CREATE OR REPLACE FUNCTION public.upsert_content(v_space_id bigint, data jsonb, v_creator_id BIGINT, content_as_document boolean DEFAULT true) +CREATE OR REPLACE FUNCTION public.upsert_content(v_space_id bigint, data jsonb, v_creator_id BIGINT, content_as_document boolean DEFAULT TRUE) RETURNS SETOF BIGINT SET search_path = '' LANGUAGE plpgsql @@ -574,6 +624,16 @@ $$; COMMENT ON FUNCTION public.content_in_space IS 'security utility: does current user have access to this content''s space?'; +CREATE OR REPLACE FUNCTION public.content_in_editable_space(content_id BIGINT) RETURNS boolean +STABLE +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT public.editor_in_space(space_id) FROM public."Content" WHERE id=content_id +$$; + +COMMENT ON FUNCTION public.content_in_editable_space IS 'security utility: does current user have editor access to this content''s space?'; + CREATE OR REPLACE FUNCTION public.document_in_space(document_id BIGINT) RETURNS boolean STABLE SET search_path = '' @@ -592,4 +652,23 @@ CREATE POLICY document_policy ON public."Document" FOR ALL USING (public.in_spac ALTER TABLE public."Content" ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS content_policy ON public."Content"; -CREATE POLICY content_policy ON public."Content" FOR ALL USING (public.in_space(space_id)); +DROP POLICY IF EXISTS content_select_policy ON public."Content"; +CREATE POLICY content_select_policy ON public."Content" FOR SELECT USING (public.in_space(space_id) OR public.can_view_specific_content(id)); +DROP POLICY IF EXISTS content_delete_policy ON public."Content"; +CREATE POLICY content_delete_policy ON public."Content" FOR DELETE USING (public.in_space(space_id)); +DROP POLICY IF EXISTS content_insert_policy ON public."Content"; +CREATE POLICY content_insert_policy ON public."Content" FOR INSERT WITH CHECK (public.in_space(space_id)); +DROP POLICY IF EXISTS content_update_policy ON public."Content"; +CREATE POLICY content_update_policy ON public."Content" FOR UPDATE WITH CHECK (public.in_space(space_id)); + +ALTER TABLE public."ContentAccess" ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS content_access_policy ON public."ContentAccess"; +DROP POLICY IF EXISTS content_access_select_policy ON public."ContentAccess"; +CREATE POLICY content_access_select_policy ON public."ContentAccess" FOR SELECT USING (public.content_in_space(content_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS content_access_delete_policy ON public."ContentAccess"; +CREATE POLICY content_access_delete_policy ON public."ContentAccess" FOR DELETE USING (public.content_in_editable_space(content_id) OR public.can_access_account(account_uid)); +DROP POLICY IF EXISTS content_access_insert_policy ON public."ContentAccess"; +CREATE POLICY content_access_insert_policy ON public."ContentAccess" FOR INSERT WITH CHECK (public.content_in_editable_space(content_id)); +DROP POLICY IF EXISTS content_access_update_policy ON public."ContentAccess"; +CREATE POLICY content_access_update_policy ON public."ContentAccess" FOR UPDATE WITH CHECK (public.content_in_editable_space(content_id));