diff --git a/packages/database/features/step-definitions/stepdefs.ts b/packages/database/features/step-definitions/stepdefs.ts index 90ef71f39..38acce74a 100644 --- a/packages/database/features/step-definitions/stepdefs.ts +++ b/packages/database/features/step-definitions/stepdefs.ts @@ -557,13 +557,12 @@ Then( await expectWriteRejected(operation, { insert: () => view.insert({ - name: "written through my_accounts", account_local_id: "written-through-my-accounts", platform: "Roam", }), update: () => view - .update({ name: "written through my_accounts" }) + .update({ account_local_id: "written-through-my-accounts" }) .eq("platform", "Roam"), delete: () => view.delete().eq("platform", "Roam"), }); diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index ff89b0bdf..264fabfca 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -962,7 +962,7 @@ export type Database = { active?: boolean | null agent_type?: Database["public"]["Enums"]["AgentType"] | null id?: number | null - name?: string | null + name?: never platform?: Database["public"]["Enums"]["Platform"] | null write_permission?: boolean | null } @@ -971,7 +971,7 @@ export type Database = { active?: boolean | null agent_type?: Database["public"]["Enums"]["AgentType"] | null id?: number | null - name?: string | null + name?: never platform?: Database["public"]["Enums"]["Platform"] | null write_permission?: boolean | null } @@ -1817,6 +1817,7 @@ export type Database = { } Returns: Json } + everyone_uid: { Args: never; Returns: string } extract_references: { Args: { refs: Json }; Returns: number[] } file_access: { Args: { hashvalue: string }; Returns: boolean } file_exists: { Args: { hashvalue: string }; Returns: boolean } @@ -1907,6 +1908,7 @@ export type Database = { isSetofReturn: true } } + my_identity_accounts: { Args: never; Returns: string[] } my_permissions_in_space: { Args: { space_id_: number } Returns: Database["public"]["Enums"]["SpaceAccessPermissions"] diff --git a/packages/database/supabase/migrations/20261008150000_public_resource_access.sql b/packages/database/supabase/migrations/20261008150000_public_resource_access.sql new file mode 100644 index 000000000..a3d276560 --- /dev/null +++ b/packages/database/supabase/migrations/20261008150000_public_resource_access.sql @@ -0,0 +1,114 @@ +INSERT INTO auth.users (instance_id, id, aud, role, created_at, updated_at, is_super_admin, is_anonymous) +VALUES ('00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', 'anon', 'anon', now(), now(), false, true); + +CREATE OR REPLACE FUNCTION public.everyone_uid() RETURNS UUID +IMMUTABLE +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT '00000000-0000-0000-0000-000000000000'::uuid; +$$; + +COMMENT ON FUNCTION public.everyone_uid IS 'The uid of the everyone pseudo-user. A grant to it applies to every caller, logged in or not.'; + +CREATE OR REPLACE FUNCTION public.my_user_accounts() RETURNS SETOF UUID +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT auth.uid() WHERE auth.uid() IS NOT NULL UNION + SELECT public.everyone_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_identity_accounts() RETURNS SETOF UUID +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT auth.uid() WHERE auth.uid() IS NOT NULL UNION + SELECT group_id FROM public.group_membership + WHERE member_id = auth.uid(); +$$; + +COMMENT ON FUNCTION public.my_identity_accounts IS 'security utility: The uids I act as, myself or a group I belong to. Excludes the everyone pseudo-user, so a public grant never counts as sharing a space.'; + +CREATE OR REPLACE FUNCTION public.account_in_shared_space(p_account_id BIGINT, access_level public."SpaceAccessPermissions" = 'reader') 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_identity_accounts() ON (sa.account_uid = my_identity_accounts) + WHERE la.account_id = p_account_id + AND sa.permissions >= access_level + ); +$$; + +CREATE OR REPLACE FUNCTION public.unowned_account_in_shared_space(p_account_id BIGINT, access_level public."SpaceAccessPermissions" = 'reader') RETURNS boolean +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public."SpaceAccess" AS sa + JOIN public.my_identity_accounts() ON (sa.account_uid = my_identity_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 + AND sa.permissions >= access_level + ); +$$; + +-- The real name only shows to callers who share a space with the account through their own grants. +-- An account visible only through a public grant shows as 'anonymous #'. +CREATE OR REPLACE VIEW public.my_accounts AS +SELECT + id, + CASE WHEN id IN ( + SELECT "LocalAccess".account_id FROM public."LocalAccess" + JOIN public."SpaceAccess" USING (space_id) + JOIN public.my_identity_accounts() ON (account_uid = my_identity_accounts) + WHERE permissions >= 'partial' + UNION + SELECT id FROM public."PlatformAccount" WHERE dg_account = auth.uid() + ) THEN name ELSE ('anonymous #' || id)::varchar END AS name, + platform, + account_local_id, + write_permission, + active, + agent_type +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) + WHERE permissions >= 'partial' + UNION + SELECT id FROM public."PlatformAccount" WHERE dg_account = auth.uid() +); + +DROP POLICY IF EXISTS resource_access_select_policy ON public."ResourceAccess"; +CREATE POLICY resource_access_select_policy ON public."ResourceAccess" FOR SELECT USING ( + account_uid = public.everyone_uid() + OR public.in_space(space_id) + OR public.can_access_account(account_uid) +); + +GRANT SELECT ON TABLE public."ResourceAccess" TO anon; +GRANT SELECT ON TABLE public."Document" TO anon; +GRANT SELECT ON TABLE public."Content" TO anon; +GRANT SELECT ON TABLE public."Concept" TO anon; +GRANT SELECT ON TABLE public."FileReference" TO anon; + +DROP POLICY IF EXISTS "storage_select_assets_access" ON storage.objects; +CREATE POLICY "storage_select_assets_access" +ON storage.objects FOR SELECT TO anon, authenticated USING ( + bucket_id = 'assets' AND file_access(name) +); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 031fc39e4..ad64dad03 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -1,3 +1,7 @@ +-- Anonymous pseudo-user +INSERT INTO auth.users (instance_id, id, aud, role, created_at, updated_at, is_super_admin, is_anonymous) +VALUES ('00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', 'anon', 'anon', now(), now(), false, true); + CREATE TYPE public."AgentType" AS ENUM ( 'person', 'organization', @@ -213,18 +217,41 @@ $$; COMMENT ON FUNCTION public.can_access_account IS 'security utility: Is this my account or one of my groups?'; +CREATE OR REPLACE FUNCTION public.everyone_uid() RETURNS UUID +IMMUTABLE +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT '00000000-0000-0000-0000-000000000000'::uuid; +$$; + +COMMENT ON FUNCTION public.everyone_uid IS 'The uid of the everyone pseudo-user. A grant to it applies to every caller, logged in or not.'; + CREATE OR REPLACE FUNCTION public.my_user_accounts() RETURNS SETOF UUID STABLE SECURITY DEFINER SET search_path = '' LANGUAGE sql AS $$ SELECT auth.uid() WHERE auth.uid() IS NOT NULL UNION + SELECT public.everyone_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_identity_accounts() RETURNS SETOF UUID +STABLE SECURITY DEFINER +SET search_path = '' +LANGUAGE sql +AS $$ + SELECT auth.uid() WHERE auth.uid() IS NOT NULL UNION + SELECT group_id FROM public.group_membership + WHERE member_id = auth.uid(); +$$; + +COMMENT ON FUNCTION public.my_identity_accounts IS 'security utility: The uids I act as, myself or a group I belong to. Excludes the everyone pseudo-user, so a public grant never counts as sharing a space.'; + CREATE OR REPLACE FUNCTION public.my_permissions_in_space( space_id_ BIGINT ) RETURNS public."SpaceAccessPermissions" @@ -294,7 +321,7 @@ 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) + JOIN public.my_identity_accounts() ON (sa.account_uid = my_identity_accounts) WHERE la.account_id = p_account_id AND sa.permissions >= access_level ); @@ -309,7 +336,7 @@ 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.my_identity_accounts() ON (sa.account_uid = my_identity_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 @@ -441,10 +468,19 @@ REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_spaces FROM anon, authentic ALTER TABLE public."PlatformAccount" ENABLE ROW LEVEL SECURITY; -- Leaves out dg_account and metadata: peers have no use for them, and a known auth uid should not be handed out. +-- The real name only shows to callers who share a space with the account through their own grants. +-- An account visible only through a public grant shows as 'anonymous #'. CREATE OR REPLACE VIEW public.my_accounts AS SELECT id, - name, + CASE WHEN id IN ( + SELECT "LocalAccess".account_id FROM public."LocalAccess" + JOIN public."SpaceAccess" USING (space_id) + JOIN public.my_identity_accounts() ON (account_uid = my_identity_accounts) + WHERE permissions >= 'partial' + UNION + SELECT id FROM public."PlatformAccount" WHERE dg_account = auth.uid() + ) THEN name ELSE ('anonymous #' || id)::varchar END AS name, platform, account_local_id, write_permission, diff --git a/packages/database/supabase/schemas/assets.sql b/packages/database/supabase/schemas/assets.sql index 0c533dbe7..f53b7227d 100644 --- a/packages/database/supabase/schemas/assets.sql +++ b/packages/database/supabase/schemas/assets.sql @@ -50,6 +50,7 @@ REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_file_references FROM anon, GRANT ALL ON TABLE public."FileReference" TO authenticated; GRANT ALL ON TABLE public."FileReference" TO service_role; REVOKE ALL ON TABLE public."FileReference" FROM anon; +GRANT SELECT ON TABLE public."FileReference" TO anon; ALTER TABLE public."FileReference" ENABLE ROW LEVEL SECURITY; @@ -143,7 +144,7 @@ ON storage.objects FOR INSERT TO authenticated WITH CHECK ( DROP POLICY IF EXISTS "storage_select_assets_access" ON storage.objects; CREATE POLICY "storage_select_assets_access" -ON storage.objects FOR SELECT TO authenticated USING ( +ON storage.objects FOR SELECT TO anon, authenticated USING ( bucket_id = 'assets' AND file_access(name) ); diff --git a/packages/database/supabase/schemas/concept.sql b/packages/database/supabase/schemas/concept.sql index fb2438b8c..0e4db1136 100644 --- a/packages/database/supabase/schemas/concept.sql +++ b/packages/database/supabase/schemas/concept.sql @@ -117,6 +117,7 @@ ADD CONSTRAINT "Concept_space_id_fkey" FOREIGN KEY ( REVOKE ALL ON TABLE public."Concept" FROM anon; +GRANT SELECT ON TABLE public."Concept" TO anon; GRANT ALL ON TABLE public."Concept" TO authenticated; GRANT ALL ON TABLE public."Concept" TO service_role; diff --git a/packages/database/supabase/schemas/content.sql b/packages/database/supabase/schemas/content.sql index 5745a8c77..6bc39480d 100644 --- a/packages/database/supabase/schemas/content.sql +++ b/packages/database/supabase/schemas/content.sql @@ -191,12 +191,15 @@ CREATE INDEX resource_access_content_local_id_idx ON public."ResourceAccess" (so GRANT ALL ON TABLE public."ResourceAccess" TO authenticated; GRANT ALL ON TABLE public."ResourceAccess" TO service_role; REVOKE ALL ON TABLE public."ResourceAccess" FROM anon; +GRANT SELECT ON TABLE public."ResourceAccess" TO anon; REVOKE ALL ON TABLE public."Document" FROM anon; +GRANT SELECT ON TABLE public."Document" TO anon; GRANT ALL ON TABLE public."Document" TO authenticated; GRANT ALL ON TABLE public."Document" TO service_role; REVOKE ALL ON TABLE public."Content" FROM anon; +GRANT SELECT ON TABLE public."Content" TO anon; GRANT ALL ON TABLE public."Content" TO authenticated; GRANT ALL ON TABLE public."Content" TO service_role; @@ -742,7 +745,11 @@ ALTER TABLE public."ResourceAccess" ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS resource_access_policy ON public."ResourceAccess"; DROP POLICY IF EXISTS resource_access_select_policy ON public."ResourceAccess"; -CREATE POLICY resource_access_select_policy ON public."ResourceAccess" FOR SELECT USING (public.in_space(space_id) OR public.can_access_account(account_uid)); +CREATE POLICY resource_access_select_policy ON public."ResourceAccess" FOR SELECT USING ( + account_uid = public.everyone_uid() + OR public.in_space(space_id) + OR public.can_access_account(account_uid) +); DROP POLICY IF EXISTS resource_access_delete_policy ON public."ResourceAccess"; CREATE POLICY resource_access_delete_policy ON public."ResourceAccess" FOR DELETE USING (public.in_space(space_id, 'editor') OR public.can_access_account(account_uid)); DROP POLICY IF EXISTS resource_access_insert_policy ON public."ResourceAccess";