diff --git a/supabase/migrations/20260511090000_channel_devices_forced_device_rbac.sql b/supabase/migrations/20260511090000_channel_devices_forced_device_rbac.sql new file mode 100644 index 0000000000..cd98841313 --- /dev/null +++ b/supabase/migrations/20260511090000_channel_devices_forced_device_rbac.sql @@ -0,0 +1,93 @@ +-- Ensure direct PostgREST access to forced-device overrides uses the +-- channel-scoped RBAC permissions instead of broad app read/write rights. + +DROP POLICY IF EXISTS "Allow delete for auth, api keys (write+)" +ON public.channel_devices; + +DROP POLICY IF EXISTS "Allow insert for auth (write+)" +ON public.channel_devices; + +DROP POLICY IF EXISTS "Allow read for auth, api keys (read+)" +ON public.channel_devices; + +DROP POLICY IF EXISTS "Allow read for auth (read+)" +ON public.channel_devices; + +DROP POLICY IF EXISTS "Allow update for auth, api keys (write+)" +ON public.channel_devices; + +CREATE POLICY "Allow delete for auth, api keys (write+)" +ON public.channel_devices +FOR DELETE +TO anon, authenticated +USING ( + public.rbac_check_permission_request( + public.rbac_perm_channel_manage_forced_devices(), + owner_org, + app_id, + channel_id + ) +); + +CREATE POLICY "Allow insert for auth (write+)" +ON public.channel_devices +FOR INSERT +TO authenticated +WITH CHECK ( + public.rbac_check_permission_request( + public.rbac_perm_channel_manage_forced_devices(), + owner_org, + app_id, + channel_id + ) +); + +CREATE POLICY "Allow read for auth (read+)" +ON public.channel_devices +FOR SELECT +TO anon, authenticated +USING ( + public.rbac_check_permission_request( + public.rbac_perm_channel_read_forced_devices(), + owner_org, + app_id, + channel_id + ) +); + +CREATE POLICY "Allow update for auth, api keys (write+)" +ON public.channel_devices +FOR UPDATE +TO anon, authenticated +USING ( + public.rbac_check_permission_request( + public.rbac_perm_channel_manage_forced_devices(), + owner_org, + app_id, + channel_id + ) +) +WITH CHECK ( + public.rbac_check_permission_request( + public.rbac_perm_channel_manage_forced_devices(), + owner_org, + app_id, + channel_id + ) +); + +COMMENT ON POLICY "Allow delete for auth, api keys (write+)" +ON public.channel_devices IS +'Direct channel_devices deletes require channel.manage_forced_devices for the target channel.'; + +COMMENT ON POLICY "Allow insert for auth (write+)" +ON public.channel_devices IS +'Direct channel_devices inserts require channel.manage_forced_devices for the target channel.'; + +COMMENT ON POLICY "Allow read for auth (read+)" +ON public.channel_devices IS +'Direct channel_devices reads require channel.read_forced_devices for the target channel.'; + +COMMENT ON POLICY "Allow update for auth, api keys (write+)" +ON public.channel_devices IS +'Direct channel_devices updates require channel.manage_forced_devices for both old and new target channels.'; diff --git a/supabase/schemas/prod.sql b/supabase/schemas/prod.sql index fce7be7159..e63aa20c5e 100644 --- a/supabase/schemas/prod.sql +++ b/supabase/schemas/prod.sql @@ -19079,7 +19079,7 @@ CREATE POLICY "Allow delete for auth (admin+) (all apikey)" ON "public"."channel -CREATE POLICY "Allow delete for auth, api keys (write+)" ON "public"."channel_devices" FOR DELETE TO "anon", "authenticated" USING ("public"."check_min_rights"('write'::"public"."user_min_right", "public"."get_identity_org_appid"('{write,all}'::"public"."key_mode"[], "owner_org", "app_id"), "owner_org", "app_id", NULL::bigint)); +CREATE POLICY "Allow delete for auth, api keys (write+)" ON "public"."channel_devices" FOR DELETE TO "anon", "authenticated" USING ("public"."rbac_check_permission_request"("public"."rbac_perm_channel_manage_forced_devices"(), "owner_org", "app_id", "channel_id")); @@ -19099,7 +19099,7 @@ CREATE POLICY "Allow insert for apikey (write,all) (admin+)" ON "public"."apps" -CREATE POLICY "Allow insert for auth (write+)" ON "public"."channel_devices" FOR INSERT TO "authenticated" WITH CHECK ("public"."check_min_rights"('write'::"public"."user_min_right", "public"."get_identity"(), "owner_org", "app_id", NULL::bigint)); +CREATE POLICY "Allow insert for auth (write+)" ON "public"."channel_devices" FOR INSERT TO "authenticated" WITH CHECK ("public"."rbac_check_permission_request"("public"."rbac_perm_channel_manage_forced_devices"(), "owner_org", "app_id", "channel_id")); @@ -19231,7 +19231,7 @@ CREATE POLICY "Allow read for auth (read+)" ON "public"."app_versions_meta" FOR -CREATE POLICY "Allow read for auth (read+)" ON "public"."channel_devices" FOR SELECT TO "anon", "authenticated" USING ("public"."check_min_rights"('read'::"public"."user_min_right", "public"."get_identity_org_appid"('{read,upload,write,all}'::"public"."key_mode"[], "owner_org", "app_id"), "owner_org", "app_id", NULL::bigint)); +CREATE POLICY "Allow read for auth (read+)" ON "public"."channel_devices" FOR SELECT TO "anon", "authenticated" USING ("public"."rbac_check_permission_request"("public"."rbac_perm_channel_read_forced_devices"(), "owner_org", "app_id", "channel_id")); @@ -19313,7 +19313,7 @@ CREATE POLICY "Allow update for auth (write+)" ON "public"."app_versions" FOR UP -CREATE POLICY "Allow update for auth, api keys (write+)" ON "public"."channel_devices" FOR UPDATE TO "anon", "authenticated" USING ("public"."check_min_rights"('write'::"public"."user_min_right", "public"."get_identity_org_appid"('{write,all}'::"public"."key_mode"[], "owner_org", "app_id"), "owner_org", "app_id", NULL::bigint)) WITH CHECK ("public"."check_min_rights"('write'::"public"."user_min_right", "public"."get_identity_org_appid"('{write,all}'::"public"."key_mode"[], "owner_org", "app_id"), "owner_org", "app_id", NULL::bigint)); +CREATE POLICY "Allow update for auth, api keys (write+)" ON "public"."channel_devices" FOR UPDATE TO "anon", "authenticated" USING ("public"."rbac_check_permission_request"("public"."rbac_perm_channel_manage_forced_devices"(), "owner_org", "app_id", "channel_id")) WITH CHECK ("public"."rbac_check_permission_request"("public"."rbac_perm_channel_manage_forced_devices"(), "owner_org", "app_id", "channel_id")); @@ -19555,6 +19555,22 @@ ALTER TABLE "public"."channel_devices" ENABLE ROW LEVEL SECURITY; ALTER TABLE "public"."channel_permission_overrides" ENABLE ROW LEVEL SECURITY; +COMMENT ON POLICY "Allow delete for auth, api keys (write+)" ON "public"."channel_devices" IS 'Direct channel_devices deletes require channel.manage_forced_devices for the target channel.'; + + + +COMMENT ON POLICY "Allow insert for auth (write+)" ON "public"."channel_devices" IS 'Direct channel_devices inserts require channel.manage_forced_devices for the target channel.'; + + + +COMMENT ON POLICY "Allow read for auth (read+)" ON "public"."channel_devices" IS 'Direct channel_devices reads require channel.read_forced_devices for the target channel.'; + + + +COMMENT ON POLICY "Allow update for auth, api keys (write+)" ON "public"."channel_devices" IS 'Direct channel_devices updates require channel.manage_forced_devices for both old and new target channels.'; + + + CREATE POLICY "channel_permission_overrides_admin_delete" ON "public"."channel_permission_overrides" FOR DELETE TO "authenticated" USING ((EXISTS ( SELECT 1 FROM ("public"."channels" JOIN "public"."apps" ON ((("channels"."app_id")::"text" = ("apps"."app_id")::"text"))) @@ -22918,8 +22934,6 @@ ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT SELECT,INS - - diff --git a/supabase/tests/26_test_rls_policies.sql b/supabase/tests/26_test_rls_policies.sql index 9b4e6a195a..8acb52b46b 100644 --- a/supabase/tests/26_test_rls_policies.sql +++ b/supabase/tests/26_test_rls_policies.sql @@ -65,7 +65,7 @@ SELECT ARRAY[ 'Allow delete for auth, api keys (write+)', 'Allow insert for auth (write+)', - 'Allow read for auth, api keys (read+)', + 'Allow read for auth (read+)', 'Allow update for auth, api keys (write+)', 'Prevent non 2FA access' ], diff --git a/supabase/tests/55_test_channel_devices_forced_device_rbac.sql b/supabase/tests/55_test_channel_devices_forced_device_rbac.sql new file mode 100644 index 0000000000..ec472ad795 --- /dev/null +++ b/supabase/tests/55_test_channel_devices_forced_device_rbac.sql @@ -0,0 +1,83 @@ +BEGIN; + +SELECT plan(6); + +SELECT + ok( + ( + SELECT qual + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'channel_devices' + AND policyname = 'Allow read for auth (read+)' + ) LIKE '%rbac_check_permission_request%rbac_perm_channel_read_forced_devices%channel_id%', + 'channel_devices SELECT policy uses channel.read_forced_devices with channel scope' + ); + +SELECT + ok( + ( + SELECT qual + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'channel_devices' + AND policyname = 'Allow delete for auth, api keys (write+)' + ) LIKE '%rbac_check_permission_request%rbac_perm_channel_manage_forced_devices%channel_id%', + 'channel_devices DELETE policy uses channel.manage_forced_devices with channel scope' + ); + +SELECT + ok( + ( + SELECT with_check + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'channel_devices' + AND policyname = 'Allow insert for auth (write+)' + ) LIKE '%rbac_check_permission_request%rbac_perm_channel_manage_forced_devices%channel_id%', + 'channel_devices INSERT policy uses channel.manage_forced_devices with channel scope' + ); + +SELECT + ok( + ( + SELECT qual + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'channel_devices' + AND policyname = 'Allow update for auth, api keys (write+)' + ) LIKE '%rbac_check_permission_request%rbac_perm_channel_manage_forced_devices%channel_id%', + 'channel_devices UPDATE USING policy uses channel.manage_forced_devices with channel scope' + ); + +SELECT + ok( + ( + SELECT with_check + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'channel_devices' + AND policyname = 'Allow update for auth, api keys (write+)' + ) LIKE '%rbac_check_permission_request%rbac_perm_channel_manage_forced_devices%channel_id%', + 'channel_devices UPDATE WITH CHECK policy uses channel.manage_forced_devices with channel scope' + ); + +SELECT + ok( + NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'channel_devices' + AND ( + COALESCE(qual, '') LIKE '%check_min_rights%' + OR COALESCE(with_check, '') LIKE '%check_min_rights%' + ) + ), + 'channel_devices policies do not retain legacy check_min_rights grants' + ); + +SELECT * +FROM finish(); + +ROLLBACK;