diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index d664a0df2..841a9d1ac 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -35,17 +35,25 @@ jobs: runs-on: blacksmith-8vcpu-ubuntu-2404 env: POSTGRES_IMAGE: ${{ matrix.postgres_image }} + DB_USER: ${{ matrix.db_user }} strategy: fail-fast: false matrix: - postgres: [pg14, pg15, pg17] + postgres: [pg14, pg15, pg15_latest, pg17] include: - postgres: pg14 - postgres_image: supabase/postgres:14.1.0.85 + postgres_image: supabase/postgres:14.1.0.82 + db_user: supabase_admin + # missing supautils.policy_grants - postgres: pg15 - postgres_image: supabase/postgres:15.14.1.127 + postgres_image: supabase/postgres:15.1.0.1 + db_user: supabase_admin + - postgres: pg15_latest + postgres_image: supabase/postgres:15.14.1.129 + db_user: supabase_realtime_admin - postgres: pg17 postgres_image: supabase/postgres:17.6.1.127 + db_user: supabase_realtime_admin steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3f798f7f8..8c49f2139 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,9 @@ on: - "assets/**" - "rel/**" - "native/**" + - "dev/**" - "mix.exs" + - "compose.dbs.yml" - "mix.lock" - "Dockerfile" - "run.sh" @@ -35,18 +37,26 @@ jobs: runs-on: blacksmith-8vcpu-ubuntu-2404 env: POSTGRES_IMAGE: ${{ matrix.postgres_image }} + DB_USER: ${{ matrix.db_user }} strategy: fail-fast: false matrix: partition: [1, 2, 3, 4] - postgres: [pg14, pg15, pg17] + postgres: [pg14, pg15, pg15_latest, pg17] include: - postgres: pg14 - postgres_image: supabase/postgres:14.1.0.85 + postgres_image: supabase/postgres:14.1.0.82 + db_user: supabase_admin + # missing supautils.policy_grants - postgres: pg15 - postgres_image: supabase/postgres:15.14.1.127 + postgres_image: supabase/postgres:15.1.0.1 + db_user: supabase_admin + - postgres: pg15_latest + postgres_image: supabase/postgres:15.14.1.129 + db_user: supabase_realtime_admin - postgres: pg17 postgres_image: supabase/postgres:17.6.1.127 + db_user: supabase_realtime_admin steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/README.md b/README.md index fbc0bc867..855e0112f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,15 @@ Once your environment is up and running, check out the following docs to customi - [ERROR_CODES.md](ERROR_CODES.md) - list of operational codes - [OBSERVABILITY_METRICS.md](OBSERVABILITY_METRICS.md) - monitoring information +## Postgres compatibility + +| `supabase/postgres` version | Role | Status | +| --------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| < 14 | - | Not officially supported. | +| 14.x | `supabase_admin` | Requires superuser: `log_min_messages` can only be set by a superuser; supautils doesn't expose per-parameter delegation on this version. On <= 14.5, `realtime.broadcast_changes(...)` called from a trigger via `PERFORM` is unsupported. | +| 15.x < 15.14.1.018 | `supabase_admin` | Requires superuser: `supautils.policy_grants` on `realtime.subscription` is missing until [supabase/postgres@1b916920](https://github.com/supabase/postgres/commit/1b916920). | +| 15.x >= 15.14.1.018, 16.x, 17.x | `supabase_realtime_admin` | No superuser needed. Role must have `REPLICATION` and policies are managed by supautils. | + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/compose.dbs.yml b/compose.dbs.yml index 4f78a32d9..97cc861bd 100644 --- a/compose.dbs.yml +++ b/compose.dbs.yml @@ -4,7 +4,8 @@ services: ports: - "5432:5432" volumes: - - ./dev/postgres/zz-supabase-schema.sql:/docker-entrypoint-initdb.d/zz-supabase-schema.sql + - ./dev/postgres/za-permit-supabase-admin.sh:/docker-entrypoint-initdb.d/za-permit-supabase-admin.sh + - ./dev/postgres/zb-supabase-schema.sql:/docker-entrypoint-initdb.d/zb-supabase-schema.sql command: postgres -c config_file=/etc/postgresql/postgresql.conf environment: POSTGRES_HOST: /var/run/postgresql @@ -20,7 +21,8 @@ services: ports: - "5433:5432" volumes: - - ./dev/postgres/zz-supabase-schema.sql:/docker-entrypoint-initdb.d/zz-supabase-schema.sql + - ./dev/postgres/za-permit-supabase-admin.sh:/docker-entrypoint-initdb.d/za-permit-supabase-admin.sh + - ./dev/postgres/zb-supabase-schema.sql:/docker-entrypoint-initdb.d/zb-supabase-schema.sql command: postgres -c config_file=/etc/postgresql/postgresql.conf environment: POSTGRES_HOST: /var/run/postgresql diff --git a/compose.tests.yml b/compose.tests.yml index c4b4a5304..0b166486f 100644 --- a/compose.tests.yml +++ b/compose.tests.yml @@ -6,7 +6,8 @@ services: ports: - "5532:5432" volumes: - - ./dev/postgres:/docker-entrypoint-initdb.d/ + - ./dev/postgres/za-permit-supabase-admin.sh:/docker-entrypoint-initdb.d/za-permit-supabase-admin.sh + - ./dev/postgres/zb-supabase-schema.sql:/docker-entrypoint-initdb.d/zb-supabase-schema.sql command: postgres -c config_file=/etc/postgresql/postgresql.conf environment: POSTGRES_HOST: /var/run/postgresql @@ -29,7 +30,7 @@ services: PORT: 4100 DB_HOST: host.docker.internal DB_PORT: 5532 - DB_USER: supabase_admin + DB_USER: ${DB_USER:-supabase_realtime_admin} DB_PASSWORD: postgres DB_NAME: postgres DB_ENC_KEY: 1234567890123456 diff --git a/config/runtime.exs b/config/runtime.exs index 1b3be1a0a..6ac250be5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -28,7 +28,7 @@ db_replica_host = System.get_env("DB_REPLICA_HOST") db_replica_pool_size = Env.get_integer("DB_REPLICA_POOL_SIZE", 5) db_ssl = Env.get_boolean("DB_SSL", false) db_ssl_ca_cert = System.get_env("DB_SSL_CA_CERT") -db_user = System.get_env("DB_USER", "supabase_admin") +db_user = System.get_env("DB_USER", "supabase_realtime_admin") disable_healthcheck_logging = Env.get_boolean("DISABLE_HEALTHCHECK_LOGGING", false) dns_nodes = System.get_env("DNS_NODES") gen_rpc_compress = Env.get_integer("GEN_RPC_COMPRESS", 0) diff --git a/config/test.exs b/config/test.exs index 63bca7cf6..282f548d4 100644 --- a/config/test.exs +++ b/config/test.exs @@ -15,7 +15,7 @@ for repo <- [ Realtime.Repo.Replica.SanJose ] do config :realtime, repo, - username: "supabase_admin", + username: "supabase_realtime_admin", password: "postgres", database: "realtime_test#{partition}", hostname: "127.0.0.1", diff --git a/dev/postgres/za-permit-supabase-admin.sh b/dev/postgres/za-permit-supabase-admin.sh new file mode 100755 index 000000000..0b265e3e6 --- /dev/null +++ b/dev/postgres/za-permit-supabase-admin.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Allow peer auth as supabase_admin so the next init .sql can `\connect -` to it without a password +set -euo pipefail + +echo "supabase_map postgres supabase_admin" >> "${PGDATA}/pg_ident.conf" +printf 'local all supabase_admin peer map=supabase_map\n%s' "$(cat "${PGDATA}/pg_hba.conf")" > "${PGDATA}/pg_hba.conf.new" +mv "${PGDATA}/pg_hba.conf.new" "${PGDATA}/pg_hba.conf" +pg_ctl reload -D "${PGDATA}" >/dev/null diff --git a/dev/postgres/zb-supabase-schema.sql b/dev/postgres/zb-supabase-schema.sql new file mode 100644 index 000000000..6def1d4f9 --- /dev/null +++ b/dev/postgres/zb-supabase-schema.sql @@ -0,0 +1,26 @@ +\connect - supabase_admin + +do $$ +begin + if not exists (select from pg_roles where rolname = 'supabase_realtime_admin') then + create user supabase_realtime_admin noinherit createrole login replication password 'postgres'; + end if; +end$$; + +create schema if not exists _realtime; + +alter user supabase_realtime_admin set search_path = public, extensions, realtime; +grant create on database postgres to supabase_realtime_admin; +do $$ +begin + if current_setting('server_version_num')::int >= 150000 then + execute 'grant set on parameter log_min_messages to supabase_realtime_admin'; + end if; +end$$; +grant anon, authenticated, service_role to supabase_realtime_admin; +grant create, usage on schema public to supabase_realtime_admin; +grant usage on schema extensions to supabase_realtime_admin; +grant usage on schema auth to supabase_realtime_admin; +grant execute on all functions in schema auth to supabase_realtime_admin; +grant all on schema realtime to supabase_realtime_admin with grant option; +grant create, usage on schema _realtime to supabase_realtime_admin; diff --git a/dev/postgres/zz-supabase-schema.sql b/dev/postgres/zz-supabase-schema.sql deleted file mode 100644 index 750357a6f..000000000 --- a/dev/postgres/zz-supabase-schema.sql +++ /dev/null @@ -1,69 +0,0 @@ -do $$ -begin - if not exists (select from pg_roles where rolname = 'postgres') then - create role postgres with login superuser createdb createrole replication bypassrls password 'postgres'; - end if; - if not exists (select from pg_roles where rolname = 'supabase_admin') then - create role supabase_admin with login superuser createdb createrole replication bypassrls password 'postgres'; - end if; - if not exists (select from pg_roles where rolname = 'anon') then - create role anon nologin noinherit; - end if; - if not exists (select from pg_roles where rolname = 'authenticated') then - create role authenticated nologin noinherit; - end if; - if not exists (select from pg_roles where rolname = 'service_role') then - create role service_role nologin noinherit bypassrls; - end if; -end$$; - -create schema if not exists _realtime; -create schema if not exists realtime; - --- auth schema and functions below are only used by tests -do $$ -begin - if not exists (select 1 from pg_namespace where nspname = 'auth') then - create schema auth; - - execute $f$ - create function auth.uid() returns uuid language sql stable as $body$ - select coalesce( - nullif(current_setting('request.jwt.claim.sub', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub') - )::uuid - $body$ - $f$; - - execute $f$ - create function auth.role() returns text language sql stable as $body$ - select coalesce( - nullif(current_setting('request.jwt.claim.role', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'role') - )::text - $body$ - $f$; - - execute $f$ - create function auth.email() returns text language sql stable as $body$ - select coalesce( - nullif(current_setting('request.jwt.claim.email', true), ''), - (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'email') - )::text - $body$ - $f$; - - execute $f$ - create function auth.jwt() returns jsonb language sql stable as $body$ - select coalesce( - nullif(current_setting('request.jwt.claim', true), ''), - nullif(current_setting('request.jwt.claims', true), '') - )::jsonb - $body$ - $f$; - - grant usage on schema auth to anon, authenticated, service_role, supabase_admin; - grant execute on all functions in schema auth to anon, authenticated, service_role, supabase_admin; - alter default privileges in schema auth grant execute on functions to anon, authenticated, service_role, supabase_admin; - end if; -end$$; diff --git a/lib/realtime/tenants.ex b/lib/realtime/tenants.ex index 90f236297..158887570 100644 --- a/lib/realtime/tenants.ex +++ b/lib/realtime/tenants.ex @@ -476,7 +476,14 @@ defmodule Realtime.Tenants do Checks if migrations for a given tenant need to run. """ @spec run_migrations?(Tenant.t() | integer()) :: boolean() - def run_migrations?(%Tenant{} = tenant), do: run_migrations?(tenant.migrations_ran) + def run_migrations?(%Tenant{} = tenant) do + available_migrations = + tenant.external_id + |> Migrations.migrations() + |> Enum.count() + + tenant.migrations_ran < available_migrations + end def run_migrations?(migrations_ran) when is_integer(migrations_ran), do: migrations_ran < Enum.count(Migrations.migrations()) diff --git a/lib/realtime/tenants/migrations.ex b/lib/realtime/tenants/migrations.ex index e9da9ead9..d015a418c 100644 --- a/lib/realtime/tenants/migrations.ex +++ b/lib/realtime/tenants/migrations.ex @@ -7,6 +7,7 @@ defmodule Realtime.Tenants.Migrations do alias Realtime.Tenants alias Realtime.Database + alias Realtime.FeatureFlags alias Realtime.Registry.Unique alias Realtime.Repo alias Realtime.Api.Tenant @@ -85,7 +86,9 @@ defmodule Realtime.Tenants.Migrations do FilterActionPostgresChanges, FixByteaDoubleEncodingInCast, ListChangesWithSlotCount, - AddBinaryPayloadToMessages + SubscriptionCheckFiltersUsePgAttribute, + AddBinaryPayloadToMessages, + SetupSupabaseRealtimeAdmin } @migrations [ @@ -158,7 +161,9 @@ defmodule Realtime.Tenants.Migrations do {20_251_120_215_549, FilterActionPostgresChanges}, {20_260_218_120_000, FixByteaDoubleEncodingInCast}, {20_260_326_120_000, ListChangesWithSlotCount}, - {20_260_514_120_000, AddBinaryPayloadToMessages} + {20_260_506_120_000, SubscriptionCheckFiltersUsePgAttribute}, + {20_260_514_120_000, AddBinaryPayloadToMessages}, + {20_260_515_120_000, SetupSupabaseRealtimeAdmin} ] defstruct [:tenant_external_id, :settings, migrations_ran: 0] @@ -218,7 +223,7 @@ defmodule Realtime.Tenants.Migrations do :ok -> Task.Supervisor.async_nolink(__MODULE__.TaskSupervisor, Api, :update_migrations_ran, [ tenant_external_id, - Enum.count(@migrations) + Enum.count(migrations(tenant_external_id)) ]) :ignore @@ -251,7 +256,7 @@ defmodule Realtime.Tenants.Migrations do try do opts = [all: true, prefix: "realtime", dynamic_repo: repo] - result = Ecto.Migrator.run(Repo, @migrations, :up, opts) + result = Ecto.Migrator.run(Repo, migrations(tenant_external_id), :up, opts) Telemetry.stop(event, start_time, Map.put(metadata, :migrations_executed, length(result))) rescue error -> @@ -294,14 +299,18 @@ defmodule Realtime.Tenants.Migrations do end_timestamp = Date.to_string(Date.add(date, 1)) Database.transaction(db_conn_pid, fn conn -> - query = """ + create = """ CREATE TABLE IF NOT EXISTS realtime.#{partition_name} PARTITION OF realtime.messages - FOR VALUES FROM ('#{start_timestamp}') TO ('#{end_timestamp}'); + FOR VALUES FROM ('#{start_timestamp}') TO ('#{end_timestamp}') """ - case Postgrex.query(conn, query, []) do - {:ok, _} -> Logger.debug("Partition #{partition_name} created") + alter_owner = "ALTER TABLE realtime.#{partition_name} OWNER TO supabase_realtime_admin" + + with {:ok, _} <- Postgrex.query(conn, create, []), + {:ok, _} <- Postgrex.query(conn, alter_owner, []) do + Logger.debug("Partition #{partition_name} created") + else {:error, %Postgrex.Error{postgres: %{code: :duplicate_table}}} -> :ok {:error, error} -> log_error("PartitionCreationFailed", error) end @@ -311,5 +320,21 @@ defmodule Realtime.Tenants.Migrations do :ok end - def migrations(), do: @migrations + @doc """ + Returns the migrations to run. + """ + @spec migrations(String.t() | nil) :: [{pos_integer(), module()}] + def migrations(tenant_external_id \\ nil) do + Enum.filter(@migrations, fn {_version, module} -> migration_enabled?(module, tenant_external_id) end) + end + + defp migration_enabled?(SetupSupabaseRealtimeAdmin, nil = _tenant_external_id) do + FeatureFlags.enabled?("use_supabase_realtime_admin") + end + + defp migration_enabled?(SetupSupabaseRealtimeAdmin, tenant_external_id) when is_binary(tenant_external_id) do + FeatureFlags.enabled?("use_supabase_realtime_admin", tenant_external_id) + end + + defp migration_enabled?(_migration, _tenant_external_id), do: true end diff --git a/lib/realtime/tenants/repo/migrations/20240401105812_create_realtime_admin_and_move_ownership.ex b/lib/realtime/tenants/repo/migrations/20240401105812_create_realtime_admin_and_move_ownership.ex index bc0672472..3104d86c6 100644 --- a/lib/realtime/tenants/repo/migrations/20240401105812_create_realtime_admin_and_move_ownership.ex +++ b/lib/realtime/tenants/repo/migrations/20240401105812_create_realtime_admin_and_move_ownership.ex @@ -30,6 +30,13 @@ defmodule Realtime.Tenants.Migrations.CreateRealtimeAdminAndMoveOwnership do execute("ALTER table realtime.presences OWNER TO supabase_realtime_admin") execute("ALTER function realtime.channel_name() owner to supabase_realtime_admin") - execute("GRANT supabase_realtime_admin TO postgres") + execute(""" + DO $$ + BEGIN + IF (SELECT rolsuper FROM pg_roles WHERE rolname = current_user) THEN + GRANT supabase_realtime_admin TO postgres; + END IF; + END $$; + """) end end diff --git a/lib/realtime/tenants/repo/migrations/20260506120000_subscription_check_filters_use_pg_attribute.ex b/lib/realtime/tenants/repo/migrations/20260506120000_subscription_check_filters_use_pg_attribute.ex new file mode 100644 index 000000000..dab5d06bc --- /dev/null +++ b/lib/realtime/tenants/repo/migrations/20260506120000_subscription_check_filters_use_pg_attribute.ex @@ -0,0 +1,69 @@ +defmodule Realtime.Tenants.Migrations.SubscriptionCheckFiltersUsePgAttribute do + @moduledoc false + + use Ecto.Migration + + def change do + execute(""" + create or replace function realtime.subscription_check_filters() + returns trigger + language plpgsql + as $$ + declare + col_names text[] = coalesce( + array_agg(a.attname order by a.attnum), + '{}'::text[] + ) + from + pg_catalog.pg_attribute a + where + a.attrelid = new.entity + and a.attnum > 0 + and not a.attisdropped + and pg_catalog.has_column_privilege( + (new.claims ->> 'role'), + a.attrelid, + a.attnum, + 'SELECT' + ); + filter realtime.user_defined_filter; + col_type regtype; + + in_val jsonb; + begin + for filter in select * from unnest(new.filters) loop + if not filter.column_name = any(col_names) then + raise exception 'invalid column for filter %', filter.column_name; + end if; + + col_type = ( + select atttypid::regtype + from pg_catalog.pg_attribute + where attrelid = new.entity + and attname = filter.column_name + ); + if col_type is null then + raise exception 'failed to lookup type for column %', filter.column_name; + end if; + + if filter.op = 'in'::realtime.equality_op then + in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype); + if coalesce(jsonb_array_length(in_val), 0) > 100 then + raise exception 'too many values for `in` filter. Maximum 100'; + end if; + else + perform realtime.cast(filter.value, col_type); + end if; + end loop; + + new.filters = coalesce( + array_agg(f order by f.column_name, f.op, f.value), + '{}' + ) from unnest(new.filters) f; + + return new; + end; + $$; + """) + end +end diff --git a/lib/realtime/tenants/repo/migrations/20260515120000_setup_supabase_realtime_admin.ex b/lib/realtime/tenants/repo/migrations/20260515120000_setup_supabase_realtime_admin.ex new file mode 100644 index 000000000..e986b357f --- /dev/null +++ b/lib/realtime/tenants/repo/migrations/20260515120000_setup_supabase_realtime_admin.ex @@ -0,0 +1,82 @@ +defmodule Realtime.Tenants.Migrations.SetupSupabaseRealtimeAdmin do + @moduledoc false + + use Ecto.Migration + + def change do + execute(""" + DO $$ + BEGIN + IF (SELECT rolsuper FROM pg_roles WHERE rolname = current_user) THEN + ALTER ROLE supabase_realtime_admin WITH NOINHERIT CREATEROLE LOGIN REPLICATION; + ALTER ROLE supabase_realtime_admin SET search_path = public, extensions, realtime; + GRANT CREATE ON DATABASE postgres TO supabase_realtime_admin; + IF current_setting('server_version_num')::int >= 150000 THEN + EXECUTE 'GRANT SET ON PARAMETER log_min_messages TO supabase_realtime_admin'; + END IF; + GRANT anon, authenticated, service_role TO supabase_realtime_admin; + GRANT CREATE, USAGE ON SCHEMA public TO supabase_realtime_admin; + GRANT USAGE ON SCHEMA extensions TO supabase_realtime_admin; + GRANT USAGE ON SCHEMA auth TO supabase_realtime_admin; + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA auth TO supabase_realtime_admin; + GRANT USAGE ON SCHEMA realtime TO postgres, anon, authenticated, service_role; + GRANT ALL ON SCHEMA realtime TO supabase_realtime_admin WITH GRANT OPTION; + END IF; + END $$; + """) + + execute("ALTER TABLE realtime.messages OWNER TO supabase_realtime_admin") + execute("ALTER TABLE realtime.subscription OWNER TO supabase_realtime_admin") + execute("ALTER TYPE realtime.action OWNER TO supabase_realtime_admin") + execute("ALTER TYPE realtime.equality_op OWNER TO supabase_realtime_admin") + execute("ALTER TYPE realtime.user_defined_filter OWNER TO supabase_realtime_admin") + execute("ALTER TYPE realtime.wal_column OWNER TO supabase_realtime_admin") + execute("ALTER TYPE realtime.wal_rls OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.apply_rls(jsonb, integer) OWNER TO supabase_realtime_admin") + + execute( + "ALTER FUNCTION realtime.broadcast_changes(text, text, text, text, text, record, record, text) OWNER TO supabase_realtime_admin" + ) + + execute( + "ALTER FUNCTION realtime.build_prepared_statement_sql(text, regclass, realtime.wal_column[]) OWNER TO supabase_realtime_admin" + ) + + execute("ALTER FUNCTION realtime.cast(text, regtype) OWNER TO supabase_realtime_admin") + + execute( + "ALTER FUNCTION realtime.check_equality_op(realtime.equality_op, regtype, text, text) OWNER TO supabase_realtime_admin" + ) + + execute( + "ALTER FUNCTION realtime.is_visible_through_filters(realtime.wal_column[], realtime.user_defined_filter[]) OWNER TO supabase_realtime_admin" + ) + + execute("ALTER FUNCTION realtime.list_changes(name, name, integer, integer) OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.quote_wal2json(regclass) OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.send(jsonb, text, text, boolean) OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.send(bytea, text, text, boolean) OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.subscription_check_filters() OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.to_regrole(text) OWNER TO supabase_realtime_admin") + execute("ALTER FUNCTION realtime.topic() OWNER TO supabase_realtime_admin") + + # Revoke supabase_realtime_admin from postgres when supautils.policy_grants includes realtime.subscription (supabase/postgres 15.14.1.018 or higher), + # otherwise keep the membership so postgres can manage policies via inheritance. + execute(""" + DO $$ + BEGIN + IF (SELECT rolsuper FROM pg_roles WHERE rolname = current_user) THEN + IF current_setting('supautils.policy_grants', true) LIKE '%realtime.subscription%' THEN + REVOKE supabase_realtime_admin FROM postgres; + ELSE + GRANT supabase_realtime_admin TO postgres; + END IF; + END IF; + END $$; + """) + + execute("REVOKE CREATE ON SCHEMA realtime FROM postgres") + execute("REVOKE ALL ON realtime.schema_migrations FROM anon, authenticated, service_role, postgres") + execute("GRANT USAGE ON SCHEMA realtime TO postgres WITH GRANT OPTION") + end +end diff --git a/lib/realtime_web/dashboard/tenant_migrations.ex b/lib/realtime_web/dashboard/tenant_migrations.ex index a1d8e1448..0fc510e02 100644 --- a/lib/realtime_web/dashboard/tenant_migrations.ex +++ b/lib/realtime_web/dashboard/tenant_migrations.ex @@ -306,6 +306,10 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do ]) end + @doc false + # Used for debugging + def pg_delta_filter, do: @pg_delta_filter + defp run_pg_delta(%Database{} = settings) do case System.find_executable("pgdelta") do nil -> @@ -322,7 +326,7 @@ defmodule RealtimeWeb.Dashboard.TenantMigrations do "--target", baseline, "--filter", - @pg_delta_filter, + pg_delta_filter(), "--format", "sql" ] diff --git a/priv/repo/dev_seeds.exs b/priv/repo/dev_seeds.exs index 1cdd34d92..2138c79b3 100644 --- a/priv/repo/dev_seeds.exs +++ b/priv/repo/dev_seeds.exs @@ -5,6 +5,7 @@ alias Realtime.Tenants tenant_name = "realtime-dev" default_db_host = "127.0.0.1" +publication = "supabase_realtime" {:ok, tenant} = Repo.transaction(fn -> @@ -26,7 +27,7 @@ default_db_host = "127.0.0.1" "settings" => %{ "db_name" => System.get_env("DB_NAME", "postgres"), "db_host" => System.get_env("DB_HOST", default_db_host), - "db_user" => System.get_env("DB_USER", "supabase_admin"), + "db_user" => System.get_env("DB_USER", "supabase_realtime_admin"), "db_password" => System.get_env("DB_PASSWORD", "postgres"), "db_port" => System.get_env("DB_PORT", "5433"), "region" => "us-east-1", @@ -43,19 +44,17 @@ default_db_host = "127.0.0.1" end) # Reset Tenant DB -{:ok, settings} = Database.from_tenant(tenant, "realtime_migrations", :stop) -settings = %{settings | max_restarts: 0, ssl: false} -{:ok, tenant_conn} = Database.connect_db(settings) -publication = "supabase_realtime" +{:ok, settings} = Database.from_tenant(tenant, "realtime_seeds", :stop) +{:ok, admin_conn} = Database.connect_db(%{settings | username: "supabase_admin", max_restarts: 0, ssl: false}) -Postgrex.transaction(tenant_conn, fn db_conn -> +Postgrex.transaction(admin_conn, fn db_conn -> [ + "grant usage on schema realtime to postgres, anon, authenticated, service_role", + "grant all on schema realtime to supabase_realtime_admin with grant option", "drop publication if exists #{publication}", - "drop table if exists public.test_tenant;", - "create table public.test_tenant ( id SERIAL PRIMARY KEY, details text );", - "grant all on table public.test_tenant to anon;", - "grant all on table public.test_tenant to supabase_admin;", - "grant all on table public.test_tenant to authenticated;", + "drop table if exists public.test_tenant", + "create table public.test_tenant ( id SERIAL PRIMARY KEY, details text )", + "grant all on table public.test_tenant to anon, authenticated, supabase_realtime_admin", "create publication #{publication} for table public.test_tenant" ] |> Enum.each(&Postgrex.query!(db_conn, &1)) @@ -66,5 +65,3 @@ case Tenants.Migrations.run_migrations(tenant) do :noop -> :ok _ -> raise "Running Migrations failed" end - -Tenants.Migrations.run_migrations(tenant) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 95715f77b..3acc8976f 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -28,7 +28,7 @@ default_db_host = "host.docker.internal" "settings" => %{ "db_name" => System.get_env("DB_NAME", "postgres"), "db_host" => System.get_env("DB_HOST", default_db_host), - "db_user" => System.get_env("DB_USER", "supabase_admin"), + "db_user" => System.get_env("DB_USER", "supabase_realtime_admin"), "db_password" => System.get_env("DB_PASSWORD", "postgres"), "db_port" => System.get_env("DB_PORT", "5433"), "region" => "us-east-1", diff --git a/priv/repo/tenant_db_baseline.json b/priv/repo/tenant_db_baseline.json index a4610c26a..562c0537d 100644 --- a/priv/repo/tenant_db_baseline.json +++ b/priv/repo/tenant_db_baseline.json @@ -18,7 +18,7 @@ "is_partition": false, "options": null, "partition_bound": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "columns": [ { @@ -83,7 +83,7 @@ "grantable": false }, { - "grantee": "supabase_admin", + "grantee": "supabase_realtime_admin", "privilege": "USAGE", "grantable": false } @@ -104,7 +104,7 @@ "is_partition": false, "options": null, "partition_bound": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "columns": [ { @@ -223,7 +223,7 @@ "grantable": false }, { - "grantee": "supabase_admin", + "grantee": "supabase_realtime_admin", "privilege": "USAGE", "grantable": false } @@ -244,7 +244,7 @@ "is_partition": false, "options": null, "partition_bound": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "columns": [ { @@ -327,7 +327,7 @@ "grantable": false }, { - "grantee": "supabase_admin", + "grantee": "supabase_realtime_admin", "privilege": "USAGE", "grantable": false } @@ -340,7 +340,7 @@ "type:realtime.action": { "schema": "realtime", "name": "action", - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "labels": [ { "sort_order": 1, @@ -371,7 +371,7 @@ "grantable": false }, { - "grantee": "supabase_admin", + "grantee": "supabase_realtime_admin", "privilege": "USAGE", "grantable": false } @@ -381,7 +381,7 @@ "type:realtime.equality_op": { "schema": "realtime", "name": "equality_op", - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "labels": [ { "sort_order": 1, @@ -420,7 +420,7 @@ "grantable": false }, { - "grantee": "supabase_admin", + "grantee": "supabase_realtime_admin", "privilege": "USAGE", "grantable": false } @@ -594,6 +594,11 @@ "grantee": "dashboard_user", "privilege": "EXECUTE", "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "EXECUTE", + "grantable": false } ], "security_labels": [] @@ -642,6 +647,11 @@ "grantee": "dashboard_user", "privilege": "EXECUTE", "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "EXECUTE", + "grantable": false } ], "security_labels": [] @@ -690,6 +700,11 @@ "grantee": "dashboard_user", "privilege": "EXECUTE", "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "EXECUTE", + "grantable": false } ], "security_labels": [] @@ -1147,7 +1162,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.\"cast\"(val text, type_ regtype)\n RETURNS jsonb\n LANGUAGE plpgsql\n IMMUTABLE\nAS $function$\ndeclare\n res jsonb;\nbegin\n if type_::text = 'bytea' then\n return to_jsonb(val);\n end if;\n execute format('select to_jsonb(%L::'|| type_::text || ')', val) into res;\n return res;\nend\n$function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1155,11 +1170,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1180,11 +1190,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1226,7 +1231,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.apply_rls(wal jsonb, max_record_bytes integer DEFAULT (1024 * 1024))\n RETURNS SETOF realtime.wal_rls\n LANGUAGE plpgsql\nAS $function$\ndeclare\n-- Regclass of the table e.g. public.notes\nentity_ regclass = (quote_ident(wal ->> 'schema') || '.' || quote_ident(wal ->> 'table'))::regclass;\n\n-- I, U, D, T: insert, update ...\naction realtime.action = (\n case wal ->> 'action'\n when 'I' then 'INSERT'\n when 'U' then 'UPDATE'\n when 'D' then 'DELETE'\n else 'ERROR'\n end\n);\n\n-- Is row level security enabled for the table\nis_rls_enabled bool = relrowsecurity from pg_class where oid = entity_;\n\nsubscriptions realtime.subscription[] = array_agg(subs)\n from\n realtime.subscription subs\n where\n subs.entity = entity_\n -- Filter by action early - only get subscriptions interested in this action\n -- action_filter column can be: '*' (all), 'INSERT', 'UPDATE', or 'DELETE'\n and (subs.action_filter = '*' or subs.action_filter = action::text);\n\n-- Subscription vars\nroles regrole[] = array_agg(distinct us.claims_role::text)\n from\n unnest(subscriptions) us;\n\nworking_role regrole;\nclaimed_role regrole;\nclaims jsonb;\n\nsubscription_id uuid;\nsubscription_has_access bool;\nvisible_to_subscription_ids uuid[] = '{}';\n\n-- structured info for wal's columns\ncolumns realtime.wal_column[];\n-- previous identity values for update/delete\nold_columns realtime.wal_column[];\n\nerror_record_exceeds_max_size boolean = octet_length(wal::text) > max_record_bytes;\n\n-- Primary jsonb output for record\noutput jsonb;\n\nbegin\nperform set_config('role', null, true);\n\ncolumns =\n array_agg(\n (\n x->>'name',\n x->>'type',\n x->>'typeoid',\n realtime.cast(\n (x->'value') #>> '{}',\n coalesce(\n (x->>'typeoid')::regtype, -- null when wal2json version <= 2.4\n (x->>'type')::regtype\n )\n ),\n (pks ->> 'name') is not null,\n true\n )::realtime.wal_column\n )\n from\n jsonb_array_elements(wal -> 'columns') x\n left join jsonb_array_elements(wal -> 'pk') pks\n on (x ->> 'name') = (pks ->> 'name');\n\nold_columns =\n array_agg(\n (\n x->>'name',\n x->>'type',\n x->>'typeoid',\n realtime.cast(\n (x->'value') #>> '{}',\n coalesce(\n (x->>'typeoid')::regtype, -- null when wal2json version <= 2.4\n (x->>'type')::regtype\n )\n ),\n (pks ->> 'name') is not null,\n true\n )::realtime.wal_column\n )\n from\n jsonb_array_elements(wal -> 'identity') x\n left join jsonb_array_elements(wal -> 'pk') pks\n on (x ->> 'name') = (pks ->> 'name');\n\nfor working_role in select * from unnest(roles) loop\n\n -- Update `is_selectable` for columns and old_columns\n columns =\n array_agg(\n (\n c.name,\n c.type_name,\n c.type_oid,\n c.value,\n c.is_pkey,\n pg_catalog.has_column_privilege(working_role, entity_, c.name, 'SELECT')\n )::realtime.wal_column\n )\n from\n unnest(columns) c;\n\n old_columns =\n array_agg(\n (\n c.name,\n c.type_name,\n c.type_oid,\n c.value,\n c.is_pkey,\n pg_catalog.has_column_privilege(working_role, entity_, c.name, 'SELECT')\n )::realtime.wal_column\n )\n from\n unnest(old_columns) c;\n\n if action <> 'DELETE' and count(1) = 0 from unnest(columns) c where c.is_pkey then\n return next (\n jsonb_build_object(\n 'schema', wal ->> 'schema',\n 'table', wal ->> 'table',\n 'type', action\n ),\n is_rls_enabled,\n -- subscriptions is already filtered by entity\n (select array_agg(s.subscription_id) from unnest(subscriptions) as s where claims_role = working_role),\n array['Error 400: Bad Request, no primary key']\n )::realtime.wal_rls;\n\n -- The claims role does not have SELECT permission to the primary key of entity\n elsif action <> 'DELETE' and sum(c.is_selectable::int) <> count(1) from unnest(columns) c where c.is_pkey then\n return next (\n jsonb_build_object(\n 'schema', wal ->> 'schema',\n 'table', wal ->> 'table',\n 'type', action\n ),\n is_rls_enabled,\n (select array_agg(s.subscription_id) from unnest(subscriptions) as s where claims_role = working_role),\n array['Error 401: Unauthorized']\n )::realtime.wal_rls;\n\n else\n output = jsonb_build_object(\n 'schema', wal ->> 'schema',\n 'table', wal ->> 'table',\n 'type', action,\n 'commit_timestamp', to_char(\n ((wal ->> 'timestamp')::timestamptz at time zone 'utc'),\n 'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'\n ),\n 'columns', (\n select\n jsonb_agg(\n jsonb_build_object(\n 'name', pa.attname,\n 'type', pt.typname\n )\n order by pa.attnum asc\n )\n from\n pg_attribute pa\n join pg_type pt\n on pa.atttypid = pt.oid\n where\n attrelid = entity_\n and attnum > 0\n and pg_catalog.has_column_privilege(working_role, entity_, pa.attname, 'SELECT')\n )\n )\n -- Add \"record\" key for insert and update\n || case\n when action in ('INSERT', 'UPDATE') then\n jsonb_build_object(\n 'record',\n (\n select\n jsonb_object_agg(\n -- if unchanged toast, get column name and value from old record\n coalesce((c).name, (oc).name),\n case\n when (c).name is null then (oc).value\n else (c).value\n end\n )\n from\n unnest(columns) c\n full outer join unnest(old_columns) oc\n on (c).name = (oc).name\n where\n coalesce((c).is_selectable, (oc).is_selectable)\n and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))\n )\n )\n else '{}'::jsonb\n end\n -- Add \"old_record\" key for update and delete\n || case\n when action = 'UPDATE' then\n jsonb_build_object(\n 'old_record',\n (\n select jsonb_object_agg((c).name, (c).value)\n from unnest(old_columns) c\n where\n (c).is_selectable\n and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))\n )\n )\n when action = 'DELETE' then\n jsonb_build_object(\n 'old_record',\n (\n select jsonb_object_agg((c).name, (c).value)\n from unnest(old_columns) c\n where\n (c).is_selectable\n and ( not error_record_exceeds_max_size or (octet_length((c).value::text) <= 64))\n and ( not is_rls_enabled or (c).is_pkey ) -- if RLS enabled, we can't secure deletes so filter to pkey\n )\n )\n else '{}'::jsonb\n end;\n\n -- Create the prepared statement\n if is_rls_enabled and action <> 'DELETE' then\n if (select 1 from pg_prepared_statements where name = 'walrus_rls_stmt' limit 1) > 0 then\n deallocate walrus_rls_stmt;\n end if;\n execute realtime.build_prepared_statement_sql('walrus_rls_stmt', entity_, columns);\n end if;\n\n visible_to_subscription_ids = '{}';\n\n for subscription_id, claims in (\n select\n subs.subscription_id,\n subs.claims\n from\n unnest(subscriptions) subs\n where\n subs.entity = entity_\n and subs.claims_role = working_role\n and (\n realtime.is_visible_through_filters(columns, subs.filters)\n or (\n action = 'DELETE'\n and realtime.is_visible_through_filters(old_columns, subs.filters)\n )\n )\n ) loop\n\n if not is_rls_enabled or action = 'DELETE' then\n visible_to_subscription_ids = visible_to_subscription_ids || subscription_id;\n else\n -- Check if RLS allows the role to see the record\n perform\n -- Trim leading and trailing quotes from working_role because set_config\n -- doesn't recognize the role as valid if they are included\n set_config('role', trim(both '\"' from working_role::text), true),\n set_config('request.jwt.claims', claims::text, true);\n\n execute 'execute walrus_rls_stmt' into subscription_has_access;\n\n if subscription_has_access then\n visible_to_subscription_ids = visible_to_subscription_ids || subscription_id;\n end if;\n end if;\n end loop;\n\n perform set_config('role', null, true);\n\n return next (\n output,\n is_rls_enabled,\n visible_to_subscription_ids,\n case\n when error_record_exceeds_max_size then array['Error 413: Payload Too Large']\n else '{}'\n end\n )::realtime.wal_rls;\n\n end if;\nend loop;\n\nperform set_config('role', null, true);\nend;\n$function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1234,11 +1239,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1259,11 +1259,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1317,7 +1312,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.broadcast_changes(topic_name text, event_name text, operation text, table_name text, table_schema text, new record, old record, level text DEFAULT 'ROW'::text)\n RETURNS void\n LANGUAGE plpgsql\nAS $function$\nDECLARE\n -- Declare a variable to hold the JSONB representation of the row\n row_data jsonb := '{}'::jsonb;\nBEGIN\n IF level = 'STATEMENT' THEN\n RAISE EXCEPTION 'function can only be triggered for each row, not for each statement';\n END IF;\n -- Check the operation type and handle accordingly\n IF operation = 'INSERT' OR operation = 'UPDATE' OR operation = 'DELETE' THEN\n row_data := jsonb_build_object('old_record', OLD, 'record', NEW, 'operation', operation, 'table', table_name, 'schema', table_schema);\n PERFORM realtime.send (row_data, event_name, topic_name);\n ELSE\n RAISE EXCEPTION 'Unexpected operation type: %', operation;\n END IF;\nEXCEPTION\n WHEN OTHERS THEN\n RAISE EXCEPTION 'Failed to process the row: %', SQLERRM;\nEND;\n\n$function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1326,17 +1321,7 @@ "grantable": false }, { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "postgres", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "dashboard_user", + "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", "grantable": false } @@ -1378,7 +1363,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.build_prepared_statement_sql(prepared_statement_name text, entity regclass, columns realtime.wal_column[])\n RETURNS text\n LANGUAGE sql\nAS $function$\n /*\n Builds a sql string that, if executed, creates a prepared statement to\n tests retrive a row from *entity* by its primary key columns.\n Example\n select realtime.build_prepared_statement_sql('public.notes', '{\"id\"}'::text[], '{\"bigint\"}'::text[])\n */\n select\n 'prepare ' || prepared_statement_name || ' as\n select\n exists(\n select\n 1\n from\n ' || entity || '\n where\n ' || string_agg(quote_ident(pkc.name) || '=' || quote_nullable(pkc.value #>> '{}') , ' and ') || '\n )'\n from\n unnest(columns) pkc\n where\n pkc.is_pkey\n group by\n entity\n $function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1386,11 +1371,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1411,11 +1391,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1461,7 +1436,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.check_equality_op(op realtime.equality_op, type_ regtype, val_1 text, val_2 text)\n RETURNS boolean\n LANGUAGE plpgsql\n IMMUTABLE\nAS $function$\n /*\n Casts *val_1* and *val_2* as type *type_* and check the *op* condition for truthiness\n */\n declare\n op_symbol text = (\n case\n when op = 'eq' then '='\n when op = 'neq' then '!='\n when op = 'lt' then '<'\n when op = 'lte' then '<='\n when op = 'gt' then '>'\n when op = 'gte' then '>='\n when op = 'in' then '= any'\n else 'UNKNOWN OP'\n end\n );\n res boolean;\n begin\n execute format(\n 'select %L::'|| type_::text || ' ' || op_symbol\n || ' ( %L::'\n || (\n case\n when op = 'in' then type_::text || '[]'\n else type_::text end\n )\n || ')', val_1, val_2) into res;\n return res;\n end;\n $function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1469,11 +1444,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1494,11 +1464,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1540,7 +1505,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.is_visible_through_filters(columns realtime.wal_column[], filters realtime.user_defined_filter[])\n RETURNS boolean\n LANGUAGE sql\n IMMUTABLE\nAS $function$\n /*\n Should the record be visible (true) or filtered out (false) after *filters* are applied\n */\n select\n -- Default to allowed when no filters present\n $2 is null -- no filters. this should not happen because subscriptions has a default\n or array_length($2, 1) is null -- array length of an empty array is null\n or bool_and(\n coalesce(\n realtime.check_equality_op(\n op:=f.op,\n type_:=coalesce(\n col.type_oid::regtype, -- null when wal2json version <= 2.4\n col.type_name::regtype\n ),\n -- cast jsonb to text\n val_1:=col.value #>> '{}',\n val_2:=f.value\n ),\n false -- if null, filter does not match\n )\n )\n from\n unnest(filters) f\n join unnest(columns) col\n on f.column_name = col.name;\n $function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1548,11 +1513,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1573,11 +1533,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1650,7 +1605,7 @@ "config": [ "log_min_messages=fatal" ], - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1659,17 +1614,7 @@ "grantable": false }, { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "postgres", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "dashboard_user", + "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", "grantable": false } @@ -1707,7 +1652,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.quote_wal2json(entity regclass)\n RETURNS text\n LANGUAGE sql\n IMMUTABLE STRICT\nAS $function$\n select\n (\n select string_agg('' || ch,'')\n from unnest(string_to_array(nsp.nspname::text, null)) with ordinality x(ch, idx)\n where\n not (x.idx = 1 and x.ch = '\"')\n and not (\n x.idx = array_length(string_to_array(nsp.nspname::text, null), 1)\n and x.ch = '\"'\n )\n )\n || '.'\n || (\n select string_agg('' || ch,'')\n from unnest(string_to_array(pc.relname::text, null)) with ordinality x(ch, idx)\n where\n not (x.idx = 1 and x.ch = '\"')\n and not (\n x.idx = array_length(string_to_array(nsp.nspname::text, null), 1)\n and x.ch = '\"'\n )\n )\n from\n pg_class pc\n join pg_namespace nsp\n on pc.relnamespace = nsp.oid\n where\n pc.oid = entity\n $function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1715,11 +1660,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1740,11 +1680,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1790,7 +1725,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.send(payload jsonb, event text, topic text, private boolean DEFAULT true)\n RETURNS void\n LANGUAGE plpgsql\nAS $function$\nDECLARE\n generated_id uuid;\n final_payload jsonb;\nBEGIN\n BEGIN\n -- Generate a new UUID for the id\n generated_id := gen_random_uuid();\n\n -- Check if payload has an 'id' key, if not, add the generated UUID\n IF payload ? 'id' THEN\n final_payload := payload;\n ELSE\n final_payload := jsonb_set(payload, '{id}', to_jsonb(generated_id));\n END IF;\n\n -- Set the topic configuration\n EXECUTE format('SET LOCAL realtime.topic TO %L', topic);\n\n -- Attempt to insert the message\n INSERT INTO realtime.messages (id, payload, event, topic, private, extension)\n VALUES (generated_id, final_payload, event, topic, private, 'broadcast');\n EXCEPTION\n WHEN OTHERS THEN\n -- Capture and notify the error\n RAISE WARNING 'ErrorSendingBroadcastMessage: %', SQLERRM;\n END;\nEND;\n$function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1799,17 +1734,7 @@ "grantable": false }, { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "postgres", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "dashboard_user", + "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", "grantable": false } @@ -1853,7 +1778,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.send(payload bytea, event text, topic text, private boolean DEFAULT true)\n RETURNS void\n LANGUAGE plpgsql\nAS $function$\nDECLARE\n generated_id uuid;\nBEGIN\n BEGIN\n generated_id := gen_random_uuid();\n\n EXECUTE format('SET LOCAL realtime.topic TO %L', topic);\n\n INSERT INTO realtime.messages (id, binary_payload, event, topic, private, extension)\n VALUES (generated_id, payload, event, topic, private, 'broadcast');\n EXCEPTION\n WHEN OTHERS THEN\n RAISE WARNING 'ErrorSendingBroadcastMessage: %', SQLERRM;\n END;\nEND;\n$function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1862,17 +1787,7 @@ "grantable": false }, { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "postgres", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "dashboard_user", + "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", "grantable": false } @@ -1901,12 +1816,12 @@ "all_argument_types": [], "argument_modes": null, "argument_defaults": null, - "source_code": "\n /*\n Validates that the user defined filters for a subscription:\n - refer to valid columns that the claimed role may access\n - values are coercable to the correct column type\n */\n declare\n col_names text[] = coalesce(\n array_agg(c.column_name order by c.ordinal_position),\n '{}'::text[]\n )\n from\n information_schema.columns c\n where\n format('%I.%I', c.table_schema, c.table_name)::regclass = new.entity\n and pg_catalog.has_column_privilege(\n (new.claims ->> 'role'),\n format('%I.%I', c.table_schema, c.table_name)::regclass,\n c.column_name,\n 'SELECT'\n );\n filter realtime.user_defined_filter;\n col_type regtype;\n\n in_val jsonb;\n begin\n for filter in select * from unnest(new.filters) loop\n -- Filtered column is valid\n if not filter.column_name = any(col_names) then\n raise exception 'invalid column for filter %', filter.column_name;\n end if;\n\n -- Type is sanitized and safe for string interpolation\n col_type = (\n select atttypid::regtype\n from pg_catalog.pg_attribute\n where attrelid = new.entity\n and attname = filter.column_name\n );\n if col_type is null then\n raise exception 'failed to lookup type for column %', filter.column_name;\n end if;\n\n -- Set maximum number of entries for in filter\n if filter.op = 'in'::realtime.equality_op then\n in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);\n if coalesce(jsonb_array_length(in_val), 0) > 100 then\n raise exception 'too many values for `in` filter. Maximum 100';\n end if;\n else\n -- raises an exception if value is not coercable to type\n perform realtime.cast(filter.value, col_type);\n end if;\n\n end loop;\n\n -- Apply consistent order to filters so the unique constraint on\n -- (subscription_id, entity, filters) can't be tricked by a different filter order\n new.filters = coalesce(\n array_agg(f order by f.column_name, f.op, f.value),\n '{}'\n ) from unnest(new.filters) f;\n\n return new;\n end;\n ", + "source_code": "\ndeclare\n col_names text[] = coalesce(\n array_agg(a.attname order by a.attnum),\n '{}'::text[]\n )\n from\n pg_catalog.pg_attribute a\n where\n a.attrelid = new.entity\n and a.attnum > 0\n and not a.attisdropped\n and pg_catalog.has_column_privilege(\n (new.claims ->> 'role'),\n a.attrelid,\n a.attnum,\n 'SELECT'\n );\n filter realtime.user_defined_filter;\n col_type regtype;\n\n in_val jsonb;\nbegin\n for filter in select * from unnest(new.filters) loop\n if not filter.column_name = any(col_names) then\n raise exception 'invalid column for filter %', filter.column_name;\n end if;\n\n col_type = (\n select atttypid::regtype\n from pg_catalog.pg_attribute\n where attrelid = new.entity\n and attname = filter.column_name\n );\n if col_type is null then\n raise exception 'failed to lookup type for column %', filter.column_name;\n end if;\n\n if filter.op = 'in'::realtime.equality_op then\n in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);\n if coalesce(jsonb_array_length(in_val), 0) > 100 then\n raise exception 'too many values for `in` filter. Maximum 100';\n end if;\n else\n perform realtime.cast(filter.value, col_type);\n end if;\n end loop;\n\n new.filters = coalesce(\n array_agg(f order by f.column_name, f.op, f.value),\n '{}'\n ) from unnest(new.filters) f;\n\n return new;\nend;\n", "binary_path": null, "sql_body": null, - "definition": "CREATE OR REPLACE FUNCTION realtime.subscription_check_filters()\n RETURNS trigger\n LANGUAGE plpgsql\nAS $function$\n /*\n Validates that the user defined filters for a subscription:\n - refer to valid columns that the claimed role may access\n - values are coercable to the correct column type\n */\n declare\n col_names text[] = coalesce(\n array_agg(c.column_name order by c.ordinal_position),\n '{}'::text[]\n )\n from\n information_schema.columns c\n where\n format('%I.%I', c.table_schema, c.table_name)::regclass = new.entity\n and pg_catalog.has_column_privilege(\n (new.claims ->> 'role'),\n format('%I.%I', c.table_schema, c.table_name)::regclass,\n c.column_name,\n 'SELECT'\n );\n filter realtime.user_defined_filter;\n col_type regtype;\n\n in_val jsonb;\n begin\n for filter in select * from unnest(new.filters) loop\n -- Filtered column is valid\n if not filter.column_name = any(col_names) then\n raise exception 'invalid column for filter %', filter.column_name;\n end if;\n\n -- Type is sanitized and safe for string interpolation\n col_type = (\n select atttypid::regtype\n from pg_catalog.pg_attribute\n where attrelid = new.entity\n and attname = filter.column_name\n );\n if col_type is null then\n raise exception 'failed to lookup type for column %', filter.column_name;\n end if;\n\n -- Set maximum number of entries for in filter\n if filter.op = 'in'::realtime.equality_op then\n in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);\n if coalesce(jsonb_array_length(in_val), 0) > 100 then\n raise exception 'too many values for `in` filter. Maximum 100';\n end if;\n else\n -- raises an exception if value is not coercable to type\n perform realtime.cast(filter.value, col_type);\n end if;\n\n end loop;\n\n -- Apply consistent order to filters so the unique constraint on\n -- (subscription_id, entity, filters) can't be tricked by a different filter order\n new.filters = coalesce(\n array_agg(f order by f.column_name, f.op, f.value),\n '{}'\n ) from unnest(new.filters) f;\n\n return new;\n end;\n $function$\n", + "definition": "CREATE OR REPLACE FUNCTION realtime.subscription_check_filters()\n RETURNS trigger\n LANGUAGE plpgsql\nAS $function$\ndeclare\n col_names text[] = coalesce(\n array_agg(a.attname order by a.attnum),\n '{}'::text[]\n )\n from\n pg_catalog.pg_attribute a\n where\n a.attrelid = new.entity\n and a.attnum > 0\n and not a.attisdropped\n and pg_catalog.has_column_privilege(\n (new.claims ->> 'role'),\n a.attrelid,\n a.attnum,\n 'SELECT'\n );\n filter realtime.user_defined_filter;\n col_type regtype;\n\n in_val jsonb;\nbegin\n for filter in select * from unnest(new.filters) loop\n if not filter.column_name = any(col_names) then\n raise exception 'invalid column for filter %', filter.column_name;\n end if;\n\n col_type = (\n select atttypid::regtype\n from pg_catalog.pg_attribute\n where attrelid = new.entity\n and attname = filter.column_name\n );\n if col_type is null then\n raise exception 'failed to lookup type for column %', filter.column_name;\n end if;\n\n if filter.op = 'in'::realtime.equality_op then\n in_val = realtime.cast(filter.value, (col_type::text || '[]')::regtype);\n if coalesce(jsonb_array_length(in_val), 0) > 100 then\n raise exception 'too many values for `in` filter. Maximum 100';\n end if;\n else\n perform realtime.cast(filter.value, col_type);\n end if;\n end loop;\n\n new.filters = coalesce(\n array_agg(f order by f.column_name, f.op, f.value),\n '{}'\n ) from unnest(new.filters) f;\n\n return new;\nend;\n$function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1914,11 +1829,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -1939,11 +1849,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -1983,7 +1888,7 @@ "sql_body": null, "definition": "CREATE OR REPLACE FUNCTION realtime.to_regrole(role_name text)\n RETURNS regrole\n LANGUAGE sql\n IMMUTABLE\nAS $function$ select role_name::regrole $function$\n", "config": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "privileges": [ { @@ -1991,11 +1896,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "supabase_admin", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "postgres", "privilege": "EXECUTE", @@ -2016,11 +1916,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -2064,16 +1959,6 @@ "privilege": "EXECUTE", "grantable": false }, - { - "grantee": "postgres", - "privilege": "EXECUTE", - "grantable": false - }, - { - "grantee": "dashboard_user", - "privilege": "EXECUTE", - "grantable": false - }, { "grantee": "supabase_realtime_admin", "privilege": "EXECUTE", @@ -2742,7 +2627,7 @@ "parent_index_name": null, "definition": "CREATE UNIQUE INDEX schema_migrations_pkey ON realtime.schema_migrations USING btree (version)", "comment": null, - "owner": "supabase_admin" + "owner": "supabase_realtime_admin" }, "index:realtime.subscription.pk_subscription": { "schema": "realtime", @@ -2782,7 +2667,7 @@ "parent_index_name": null, "definition": "CREATE UNIQUE INDEX pk_subscription ON realtime.subscription USING btree (id)", "comment": null, - "owner": "supabase_admin" + "owner": "supabase_realtime_admin" }, "index:realtime.subscription.ix_realtime_subscription_entity": { "schema": "realtime", @@ -2822,7 +2707,7 @@ "parent_index_name": null, "definition": "CREATE INDEX ix_realtime_subscription_entity ON realtime.subscription USING btree (entity)", "comment": null, - "owner": "supabase_admin" + "owner": "supabase_realtime_admin" }, "index:realtime.subscription.subscription_subscription_id_entity_filters_action_filter_key": { "schema": "realtime", @@ -2877,7 +2762,7 @@ "parent_index_name": null, "definition": "CREATE UNIQUE INDEX subscription_subscription_id_entity_filters_action_filter_key ON realtime.subscription USING btree (subscription_id, entity, filters, action_filter)", "comment": null, - "owner": "supabase_admin" + "owner": "supabase_realtime_admin" } }, "materializedViews": {}, @@ -2922,6 +2807,13 @@ ], "comment": null, "members": [ + { + "member": "supabase_realtime_admin", + "grantor": "supabase_admin", + "admin_option": false, + "inherit_option": false, + "set_option": true + }, { "member": "authenticator", "grantor": "supabase_admin", @@ -3072,6 +2964,13 @@ ], "comment": null, "members": [ + { + "member": "supabase_realtime_admin", + "grantor": "supabase_admin", + "admin_option": false, + "inherit_option": false, + "set_option": true + }, { "member": "authenticator", "grantor": "supabase_admin", @@ -4349,6 +4248,13 @@ "config": null, "comment": null, "members": [ + { + "member": "supabase_realtime_admin", + "grantor": "supabase_admin", + "admin_option": false, + "inherit_option": false, + "set_option": true + }, { "member": "authenticator", "grantor": "supabase_admin", @@ -6401,23 +6307,17 @@ "name": "supabase_realtime_admin", "is_superuser": false, "can_inherit": false, - "can_create_roles": false, + "can_create_roles": true, "can_create_databases": false, - "can_login": false, - "can_replicate": false, + "can_login": true, + "can_replicate": true, "connection_limit": -1, "can_bypass_rls": false, - "config": null, - "comment": null, - "members": [ - { - "member": "postgres", - "grantor": "supabase_admin", - "admin_option": false, - "inherit_option": true, - "set_option": true - } + "config": [ + "search_path=public, extensions, realtime" ], + "comment": null, + "members": [], "default_privileges": [ { "in_schema": null, @@ -6823,6 +6723,16 @@ "grantee": "supabase_admin", "privilege": "USAGE", "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "CREATE", + "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "USAGE", + "grantable": false } ], "security_labels": [] @@ -6881,10 +6791,15 @@ "grantee": "dashboard_user", "privilege": "USAGE", "grantable": false - } - ], - "security_labels": [] - }, + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "USAGE", + "grantable": false + } + ], + "security_labels": [] + }, "schema:extensions": { "name": "extensions", "owner": "postgres", @@ -6924,6 +6839,11 @@ "grantee": "dashboard_user", "privilege": "USAGE", "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "USAGE", + "grantable": false } ], "security_labels": [] @@ -7061,6 +6981,16 @@ "grantee": "service_role", "privilege": "USAGE", "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "CREATE", + "grantable": false + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "USAGE", + "grantable": false } ], "security_labels": [] @@ -7085,6 +7015,16 @@ "privilege": "USAGE", "grantable": false }, + { + "grantee": "postgres", + "privilege": "USAGE", + "grantable": false + }, + { + "grantee": "anon", + "privilege": "USAGE", + "grantable": false + }, { "grantee": "anon", "privilege": "USAGE", @@ -7095,6 +7035,16 @@ "privilege": "USAGE", "grantable": false }, + { + "grantee": "authenticated", + "privilege": "USAGE", + "grantable": false + }, + { + "grantee": "service_role", + "privilege": "USAGE", + "grantable": false + }, { "grantee": "service_role", "privilege": "USAGE", @@ -7105,6 +7055,16 @@ "privilege": "CREATE", "grantable": false }, + { + "grantee": "supabase_realtime_admin", + "privilege": "CREATE", + "grantable": true + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "USAGE", + "grantable": true + }, { "grantee": "supabase_realtime_admin", "privilege": "USAGE", @@ -9388,6 +9348,54 @@ "privilege": "UPDATE", "grantable": false, "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "DELETE", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "INSERT", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "MAINTAIN", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "REFERENCES", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "SELECT", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "TRIGGER", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "TRUNCATE", + "grantable": false, + "columns": null + }, + { + "grantee": "supabase_realtime_admin", + "privilege": "UPDATE", + "grantable": false, + "columns": null } ], "security_labels": [] @@ -9657,48 +9665,18 @@ } ], "privileges": [ - { - "grantee": "postgres", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, { "grantee": "postgres", "privilege": "INSERT", "grantable": false, "columns": null }, - { - "grantee": "postgres", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, { "grantee": "postgres", "privilege": "SELECT", "grantable": false, "columns": null }, - { - "grantee": "postgres", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, { "grantee": "postgres", "privilege": "UPDATE", @@ -9759,54 +9737,6 @@ "grantable": false, "columns": null }, - { - "grantee": "dashboard_user", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "UPDATE", - "grantable": false, - "columns": null - }, { "grantee": "supabase_realtime_admin", "privilege": "DELETE", @@ -9875,7 +9805,7 @@ "options": null, "partition_bound": null, "partition_by": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "parent_schema": null, "parent_name": null, @@ -9949,218 +9879,80 @@ "on_delete": null, "match_type": null, "check_expression": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "definition": "PRIMARY KEY (version)", "comment": null } ], "privileges": [ { - "grantee": "supabase_admin", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", + "grantee": "postgres", "privilege": "SELECT", "grantable": false, "columns": null }, { - "grantee": "supabase_admin", - "privilege": "TRIGGER", + "grantee": "anon", + "privilege": "SELECT", "grantable": false, "columns": null }, { - "grantee": "supabase_admin", - "privilege": "TRUNCATE", + "grantee": "authenticated", + "privilege": "SELECT", "grantable": false, "columns": null }, { - "grantee": "supabase_admin", - "privilege": "UPDATE", + "grantee": "service_role", + "privilege": "SELECT", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "DELETE", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "INSERT", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "MAINTAIN", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "REFERENCES", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "SELECT", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "TRIGGER", "grantable": false, "columns": null }, { - "grantee": "postgres", + "grantee": "supabase_realtime_admin", "privilege": "TRUNCATE", "grantable": false, "columns": null }, { - "grantee": "postgres", - "privilege": "UPDATE", - "grantable": false, - "columns": null - }, - { - "grantee": "anon", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "authenticated", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "service_role", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "UPDATE", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_realtime_admin", + "grantee": "supabase_realtime_admin", "privilege": "UPDATE", "grantable": false, "columns": null @@ -10185,7 +9977,7 @@ "options": null, "partition_bound": null, "partition_by": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "comment": null, "parent_schema": null, "parent_name": null, @@ -10373,7 +10165,7 @@ "on_delete": null, "match_type": null, "check_expression": null, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "definition": "PRIMARY KEY (id)", "comment": null }, @@ -10406,108 +10198,18 @@ "on_delete": null, "match_type": null, "check_expression": "(action_filter = ANY (ARRAY['*'::text, 'INSERT'::text, 'UPDATE'::text, 'DELETE'::text]))", - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "definition": "CHECK (action_filter = ANY (ARRAY['*'::text, 'INSERT'::text, 'UPDATE'::text, 'DELETE'::text]))", "comment": null } ], "privileges": [ - { - "grantee": "supabase_admin", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, - { - "grantee": "supabase_admin", - "privilege": "UPDATE", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, { "grantee": "postgres", "privilege": "SELECT", "grantable": false, "columns": null }, - { - "grantee": "postgres", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, - { - "grantee": "postgres", - "privilege": "UPDATE", - "grantable": false, - "columns": null - }, { "grantee": "anon", "privilege": "SELECT", @@ -10526,54 +10228,6 @@ "grantable": false, "columns": null }, - { - "grantee": "dashboard_user", - "privilege": "DELETE", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "INSERT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "MAINTAIN", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "REFERENCES", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "SELECT", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "TRIGGER", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "TRUNCATE", - "grantable": false, - "columns": null - }, - { - "grantee": "dashboard_user", - "privilege": "UPDATE", - "grantable": false, - "columns": null - }, { "grantee": "supabase_realtime_admin", "privilege": "DELETE", @@ -10650,7 +10304,7 @@ "parent_table_schema": null, "parent_table_name": null, "is_on_partitioned_table": false, - "owner": "supabase_admin", + "owner": "supabase_realtime_admin", "definition": "CREATE TRIGGER tr_check_filters BEFORE INSERT OR UPDATE ON realtime.subscription FOR EACH ROW EXECUTE FUNCTION realtime.subscription_check_filters()", "comment": null } @@ -10769,6 +10423,16 @@ "referenced_stable_id": "role:supabase_auth_admin", "deptype": "n" }, + { + "dependent_stable_id": "acl:procedure:auth.email()::grantee:supabase_realtime_admin", + "referenced_stable_id": "procedure:auth.email()", + "deptype": "n" + }, + { + "dependent_stable_id": "acl:procedure:auth.email()::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, { "dependent_stable_id": "acl:procedure:auth.role()::grantee:dashboard_user", "referenced_stable_id": "procedure:auth.role()", @@ -10799,6 +10463,16 @@ "referenced_stable_id": "role:supabase_auth_admin", "deptype": "n" }, + { + "dependent_stable_id": "acl:procedure:auth.role()::grantee:supabase_realtime_admin", + "referenced_stable_id": "procedure:auth.role()", + "deptype": "n" + }, + { + "dependent_stable_id": "acl:procedure:auth.role()::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, { "dependent_stable_id": "acl:procedure:auth.uid()::grantee:dashboard_user", "referenced_stable_id": "procedure:auth.uid()", @@ -10829,6 +10503,16 @@ "referenced_stable_id": "role:supabase_auth_admin", "deptype": "n" }, + { + "dependent_stable_id": "acl:procedure:auth.uid()::grantee:supabase_realtime_admin", + "referenced_stable_id": "procedure:auth.uid()", + "deptype": "n" + }, + { + "dependent_stable_id": "acl:procedure:auth.uid()::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, { "dependent_stable_id": "acl:procedure:extensions.grant_pg_cron_access()::grantee:dashboard_user", "referenced_stable_id": "procedure:extensions.grant_pg_cron_access()", @@ -11109,16 +10793,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.\"cast\"(text,regtype)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.\"cast\"(text,regtype)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.\"cast\"(text,regtype)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.\"cast\"(text,regtype)::grantee:postgres", "referenced_stable_id": "procedure:realtime.\"cast\"(text,regtype)", @@ -11149,16 +10823,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.\"cast\"(text,regtype)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.\"cast\"(text,regtype)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.\"cast\"(text,regtype)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.\"cast\"(text,regtype)::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.\"cast\"(text,regtype)", @@ -11189,16 +10853,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.apply_rls(jsonb,integer)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.apply_rls(jsonb,integer)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.apply_rls(jsonb,integer)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.apply_rls(jsonb,integer)::grantee:postgres", "referenced_stable_id": "procedure:realtime.apply_rls(jsonb,integer)", @@ -11229,16 +10883,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.apply_rls(jsonb,integer)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.apply_rls(jsonb,integer)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.apply_rls(jsonb,integer)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.apply_rls(jsonb,integer)::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.apply_rls(jsonb,integer)", @@ -11249,46 +10893,6 @@ "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:postgres", - "referenced_stable_id": "procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:postgres", - "referenced_stable_id": "role:postgres", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:PUBLIC", - "referenced_stable_id": "procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:PUBLIC", - "referenced_stable_id": "role:PUBLIC", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:anon", "referenced_stable_id": "procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])", @@ -11309,16 +10913,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:postgres", "referenced_stable_id": "procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])", @@ -11349,16 +10943,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])", @@ -11389,16 +10973,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)::grantee:postgres", "referenced_stable_id": "procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)", @@ -11429,16 +11003,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)", @@ -11469,16 +11033,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])::grantee:postgres", "referenced_stable_id": "procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])", @@ -11509,16 +11063,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])", @@ -11529,46 +11073,6 @@ "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.list_changes(name,name,integer,integer)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:postgres", - "referenced_stable_id": "procedure:realtime.list_changes(name,name,integer,integer)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:postgres", - "referenced_stable_id": "role:postgres", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:PUBLIC", - "referenced_stable_id": "procedure:realtime.list_changes(name,name,integer,integer)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:PUBLIC", - "referenced_stable_id": "role:PUBLIC", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.list_changes(name,name,integer,integer)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.list_changes(name,name,integer,integer)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:anon", "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", @@ -11585,148 +11089,48 @@ "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:authenticated", - "referenced_stable_id": "role:authenticated", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:postgres", - "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:postgres", - "referenced_stable_id": "role:postgres", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:PUBLIC", - "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:PUBLIC", - "referenced_stable_id": "role:PUBLIC", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:service_role", - "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:service_role", - "referenced_stable_id": "role:service_role", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:supabase_realtime_admin", - "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:supabase_realtime_admin", - "referenced_stable_id": "role:supabase_realtime_admin", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.send(bytea,text,text,boolean)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:postgres", - "referenced_stable_id": "procedure:realtime.send(bytea,text,text,boolean)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:postgres", - "referenced_stable_id": "role:postgres", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:PUBLIC", - "referenced_stable_id": "procedure:realtime.send(bytea,text,text,boolean)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:PUBLIC", - "referenced_stable_id": "role:PUBLIC", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.send(bytea,text,text,boolean)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.send(bytea,text,text,boolean)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:authenticated", + "referenced_stable_id": "role:authenticated", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.send(jsonb,text,text,boolean)", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:postgres", + "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:postgres", + "referenced_stable_id": "role:postgres", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:postgres", - "referenced_stable_id": "procedure:realtime.send(jsonb,text,text,boolean)", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:PUBLIC", + "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:postgres", - "referenced_stable_id": "role:postgres", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:PUBLIC", + "referenced_stable_id": "role:PUBLIC", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:PUBLIC", - "referenced_stable_id": "procedure:realtime.send(jsonb,text,text,boolean)", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:service_role", + "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:PUBLIC", - "referenced_stable_id": "role:PUBLIC", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:service_role", + "referenced_stable_id": "role:service_role", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.send(jsonb,text,text,boolean)", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:supabase_realtime_admin", + "referenced_stable_id": "procedure:realtime.quote_wal2json(regclass)", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.send(jsonb,text,text,boolean)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", + "dependent_stable_id": "acl:procedure:realtime.quote_wal2json(regclass)::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -11749,16 +11153,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.subscription_check_filters()::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.subscription_check_filters()", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.subscription_check_filters()::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.subscription_check_filters()::grantee:postgres", "referenced_stable_id": "procedure:realtime.subscription_check_filters()", @@ -11789,16 +11183,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.subscription_check_filters()::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.subscription_check_filters()", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.subscription_check_filters()::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.subscription_check_filters()::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.subscription_check_filters()", @@ -11829,16 +11213,6 @@ "referenced_stable_id": "role:authenticated", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.to_regrole(text)::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.to_regrole(text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.to_regrole(text)::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.to_regrole(text)::grantee:postgres", "referenced_stable_id": "procedure:realtime.to_regrole(text)", @@ -11869,16 +11243,6 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, - { - "dependent_stable_id": "acl:procedure:realtime.to_regrole(text)::grantee:supabase_admin", - "referenced_stable_id": "procedure:realtime.to_regrole(text)", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.to_regrole(text)::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, { "dependent_stable_id": "acl:procedure:realtime.to_regrole(text)::grantee:supabase_realtime_admin", "referenced_stable_id": "procedure:realtime.to_regrole(text)", @@ -11890,43 +11254,23 @@ "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:dashboard_user", - "referenced_stable_id": "procedure:realtime.topic()", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:postgres", - "referenced_stable_id": "procedure:realtime.topic()", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:postgres", - "referenced_stable_id": "role:postgres", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:PUBLIC", - "referenced_stable_id": "procedure:realtime.topic()", + "dependent_stable_id": "acl:schema:_realtime::grantee:supabase_admin", + "referenced_stable_id": "role:supabase_admin", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:PUBLIC", - "referenced_stable_id": "role:PUBLIC", + "dependent_stable_id": "acl:schema:_realtime::grantee:supabase_admin", + "referenced_stable_id": "schema:_realtime", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:supabase_realtime_admin", - "referenced_stable_id": "procedure:realtime.topic()", + "dependent_stable_id": "acl:schema:_realtime::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { - "dependent_stable_id": "acl:procedure:realtime.topic()::grantee:supabase_realtime_admin", - "referenced_stable_id": "role:supabase_realtime_admin", + "dependent_stable_id": "acl:schema:_realtime::grantee:supabase_realtime_admin", + "referenced_stable_id": "schema:_realtime", "deptype": "n" }, { @@ -11999,6 +11343,16 @@ "referenced_stable_id": "schema:auth", "deptype": "n" }, + { + "dependent_stable_id": "acl:schema:auth::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, + { + "dependent_stable_id": "acl:schema:auth::grantee:supabase_realtime_admin", + "referenced_stable_id": "schema:auth", + "deptype": "n" + }, { "dependent_stable_id": "acl:schema:extensions::grantee:anon", "referenced_stable_id": "role:anon", @@ -12049,6 +11403,16 @@ "referenced_stable_id": "schema:extensions", "deptype": "n" }, + { + "dependent_stable_id": "acl:schema:extensions::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, + { + "dependent_stable_id": "acl:schema:extensions::grantee:supabase_realtime_admin", + "referenced_stable_id": "schema:extensions", + "deptype": "n" + }, { "dependent_stable_id": "acl:schema:graphql_public::grantee:anon", "referenced_stable_id": "role:anon", @@ -12209,6 +11573,16 @@ "referenced_stable_id": "schema:public", "deptype": "n" }, + { + "dependent_stable_id": "acl:schema:public::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, + { + "dependent_stable_id": "acl:schema:public::grantee:supabase_realtime_admin", + "referenced_stable_id": "schema:public", + "deptype": "n" + }, { "dependent_stable_id": "acl:schema:realtime::grantee:anon", "referenced_stable_id": "role:anon", @@ -12469,16 +11843,6 @@ "referenced_stable_id": "sequence:realtime.subscription_id_seq", "deptype": "n" }, - { - "dependent_stable_id": "acl:sequence:realtime.subscription_id_seq::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:sequence:realtime.subscription_id_seq::grantee:dashboard_user", - "referenced_stable_id": "sequence:realtime.subscription_id_seq", - "deptype": "n" - }, { "dependent_stable_id": "acl:sequence:realtime.subscription_id_seq::grantee:postgres", "referenced_stable_id": "role:postgres", @@ -12499,16 +11863,6 @@ "referenced_stable_id": "sequence:realtime.subscription_id_seq", "deptype": "n" }, - { - "dependent_stable_id": "acl:sequence:realtime.subscription_id_seq::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:sequence:realtime.subscription_id_seq::grantee:supabase_admin", - "referenced_stable_id": "sequence:realtime.subscription_id_seq", - "deptype": "n" - }, { "dependent_stable_id": "acl:sequence:realtime.subscription_id_seq::grantee:supabase_realtime_admin", "referenced_stable_id": "role:supabase_realtime_admin", @@ -12700,32 +12054,32 @@ "deptype": "n" }, { - "dependent_stable_id": "acl:table:realtime.messages::grantee:anon", - "referenced_stable_id": "role:anon", + "dependent_stable_id": "acl:table:public.test_tenant::grantee:supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { - "dependent_stable_id": "acl:table:realtime.messages::grantee:anon", - "referenced_stable_id": "table:realtime.messages", + "dependent_stable_id": "acl:table:public.test_tenant::grantee:supabase_realtime_admin", + "referenced_stable_id": "table:public.test_tenant", "deptype": "n" }, { - "dependent_stable_id": "acl:table:realtime.messages::grantee:authenticated", - "referenced_stable_id": "role:authenticated", + "dependent_stable_id": "acl:table:realtime.messages::grantee:anon", + "referenced_stable_id": "role:anon", "deptype": "n" }, { - "dependent_stable_id": "acl:table:realtime.messages::grantee:authenticated", + "dependent_stable_id": "acl:table:realtime.messages::grantee:anon", "referenced_stable_id": "table:realtime.messages", "deptype": "n" }, { - "dependent_stable_id": "acl:table:realtime.messages::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", + "dependent_stable_id": "acl:table:realtime.messages::grantee:authenticated", + "referenced_stable_id": "role:authenticated", "deptype": "n" }, { - "dependent_stable_id": "acl:table:realtime.messages::grantee:dashboard_user", + "dependent_stable_id": "acl:table:realtime.messages::grantee:authenticated", "referenced_stable_id": "table:realtime.messages", "deptype": "n" }, @@ -12779,16 +12133,6 @@ "referenced_stable_id": "table:realtime.schema_migrations", "deptype": "n" }, - { - "dependent_stable_id": "acl:table:realtime.schema_migrations::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:table:realtime.schema_migrations::grantee:dashboard_user", - "referenced_stable_id": "table:realtime.schema_migrations", - "deptype": "n" - }, { "dependent_stable_id": "acl:table:realtime.schema_migrations::grantee:postgres", "referenced_stable_id": "role:postgres", @@ -12809,16 +12153,6 @@ "referenced_stable_id": "table:realtime.schema_migrations", "deptype": "n" }, - { - "dependent_stable_id": "acl:table:realtime.schema_migrations::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:table:realtime.schema_migrations::grantee:supabase_admin", - "referenced_stable_id": "table:realtime.schema_migrations", - "deptype": "n" - }, { "dependent_stable_id": "acl:table:realtime.schema_migrations::grantee:supabase_realtime_admin", "referenced_stable_id": "role:supabase_realtime_admin", @@ -12849,16 +12183,6 @@ "referenced_stable_id": "table:realtime.subscription", "deptype": "n" }, - { - "dependent_stable_id": "acl:table:realtime.subscription::grantee:dashboard_user", - "referenced_stable_id": "role:dashboard_user", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:table:realtime.subscription::grantee:dashboard_user", - "referenced_stable_id": "table:realtime.subscription", - "deptype": "n" - }, { "dependent_stable_id": "acl:table:realtime.subscription::grantee:postgres", "referenced_stable_id": "role:postgres", @@ -12879,16 +12203,6 @@ "referenced_stable_id": "table:realtime.subscription", "deptype": "n" }, - { - "dependent_stable_id": "acl:table:realtime.subscription::grantee:supabase_admin", - "referenced_stable_id": "role:supabase_admin", - "deptype": "n" - }, - { - "dependent_stable_id": "acl:table:realtime.subscription::grantee:supabase_admin", - "referenced_stable_id": "table:realtime.subscription", - "deptype": "n" - }, { "dependent_stable_id": "acl:table:realtime.subscription::grantee:supabase_realtime_admin", "referenced_stable_id": "role:supabase_realtime_admin", @@ -13067,12 +12381,12 @@ { "dependent_stable_id": "constraint:realtime.messages.messages_payload_exclusive", "referenced_stable_id": "column:realtime.messages.payload", - "deptype": "n" + "deptype": "a" }, { "dependent_stable_id": "constraint:realtime.messages.messages_payload_exclusive", "referenced_stable_id": "column:realtime.messages.payload", - "deptype": "a" + "deptype": "n" }, { "dependent_stable_id": "constraint:realtime.messages.messages_pkey", @@ -14709,6 +14023,16 @@ "referenced_stable_id": "role:postgres", "deptype": "n" }, + { + "dependent_stable_id": "membership:anon->supabase_realtime_admin", + "referenced_stable_id": "role:anon", + "deptype": "n" + }, + { + "dependent_stable_id": "membership:anon->supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, { "dependent_stable_id": "membership:authenticated->authenticator", "referenced_stable_id": "role:authenticated", @@ -14729,6 +14053,16 @@ "referenced_stable_id": "role:postgres", "deptype": "n" }, + { + "dependent_stable_id": "membership:authenticated->supabase_realtime_admin", + "referenced_stable_id": "role:authenticated", + "deptype": "n" + }, + { + "dependent_stable_id": "membership:authenticated->supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, { "dependent_stable_id": "membership:authenticator->postgres", "referenced_stable_id": "role:authenticator", @@ -14879,6 +14213,16 @@ "referenced_stable_id": "role:service_role", "deptype": "n" }, + { + "dependent_stable_id": "membership:service_role->supabase_realtime_admin", + "referenced_stable_id": "role:service_role", + "deptype": "n" + }, + { + "dependent_stable_id": "membership:service_role->supabase_realtime_admin", + "referenced_stable_id": "role:supabase_realtime_admin", + "deptype": "n" + }, { "dependent_stable_id": "membership:supabase_privileged_role->postgres", "referenced_stable_id": "role:postgres", @@ -14899,16 +14243,6 @@ "referenced_stable_id": "role:supabase_privileged_role", "deptype": "n" }, - { - "dependent_stable_id": "membership:supabase_realtime_admin->postgres", - "referenced_stable_id": "role:postgres", - "deptype": "n" - }, - { - "dependent_stable_id": "membership:supabase_realtime_admin->postgres", - "referenced_stable_id": "role:supabase_realtime_admin", - "deptype": "n" - }, { "dependent_stable_id": "procedure:auth.email()", "referenced_stable_id": "role:supabase_auth_admin", @@ -15556,7 +14890,7 @@ }, { "dependent_stable_id": "procedure:realtime.\"cast\"(text,regtype)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15571,7 +14905,7 @@ }, { "dependent_stable_id": "procedure:realtime.apply_rls(jsonb,integer)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15591,7 +14925,7 @@ }, { "dependent_stable_id": "procedure:realtime.broadcast_changes(text,text,text,text,text,record,record,text)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15601,7 +14935,7 @@ }, { "dependent_stable_id": "procedure:realtime.build_prepared_statement_sql(text,regclass,realtime.wal_column[])", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15621,7 +14955,7 @@ }, { "dependent_stable_id": "procedure:realtime.check_equality_op(realtime.equality_op,regtype,text,text)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15636,7 +14970,7 @@ }, { "dependent_stable_id": "procedure:realtime.is_visible_through_filters(realtime.wal_column[],realtime.user_defined_filter[])", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15656,7 +14990,7 @@ }, { "dependent_stable_id": "procedure:realtime.list_changes(name,name,integer,integer)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15666,7 +15000,7 @@ }, { "dependent_stable_id": "procedure:realtime.quote_wal2json(regclass)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15681,7 +15015,7 @@ }, { "dependent_stable_id": "procedure:realtime.send(bytea,text,text,boolean)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15696,7 +15030,7 @@ }, { "dependent_stable_id": "procedure:realtime.send(jsonb,text,text,boolean)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15711,7 +15045,7 @@ }, { "dependent_stable_id": "procedure:realtime.subscription_check_filters()", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15721,7 +15055,7 @@ }, { "dependent_stable_id": "procedure:realtime.to_regrole(text)", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -15807,12 +15141,12 @@ { "dependent_stable_id": "publication:supabase_realtime", "referenced_stable_id": "table:public.test_tenant", - "deptype": "a" + "deptype": "n" }, { "dependent_stable_id": "publication:supabase_realtime", "referenced_stable_id": "table:public.test_tenant", - "deptype": "n" + "deptype": "a" }, { "dependent_stable_id": "rule:extensions.pg_stat_statements_info.\"_RETURN\"", @@ -15951,7 +15285,7 @@ }, { "dependent_stable_id": "sequence:realtime.subscription_id_seq", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16031,7 +15365,7 @@ }, { "dependent_stable_id": "table:realtime.schema_migrations", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16041,7 +15375,7 @@ }, { "dependent_stable_id": "table:realtime.subscription", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16066,7 +15400,7 @@ }, { "dependent_stable_id": "trigger:realtime.subscription.tr_check_filters", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16116,12 +15450,12 @@ }, { "dependent_stable_id": "type:realtime._action", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { "dependent_stable_id": "type:realtime._equality_op", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16131,32 +15465,32 @@ }, { "dependent_stable_id": "type:realtime._schema_migrations", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { "dependent_stable_id": "type:realtime._subscription", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { "dependent_stable_id": "type:realtime._user_defined_filter", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { "dependent_stable_id": "type:realtime._wal_column", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { "dependent_stable_id": "type:realtime._wal_rls", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { "dependent_stable_id": "type:realtime.action", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16166,7 +15500,7 @@ }, { "dependent_stable_id": "type:realtime.equality_op", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16176,7 +15510,7 @@ }, { "dependent_stable_id": "type:realtime.user_defined_filter", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16191,7 +15525,7 @@ }, { "dependent_stable_id": "type:realtime.wal_column", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16201,7 +15535,7 @@ }, { "dependent_stable_id": "type:realtime.wal_rls", - "referenced_stable_id": "role:supabase_admin", + "referenced_stable_id": "role:supabase_realtime_admin", "deptype": "n" }, { @@ -16220,22 +15554,22 @@ "deptype": "n" }, { - "dependent_stable_id": "unknown:pg_class.16853", + "dependent_stable_id": "unknown:pg_class.16857", "referenced_stable_id": "column:realtime.messages.extension", "deptype": "a" }, { - "dependent_stable_id": "unknown:pg_class.16853", + "dependent_stable_id": "unknown:pg_class.16857", "referenced_stable_id": "column:realtime.messages.inserted_at", "deptype": "a" }, { - "dependent_stable_id": "unknown:pg_class.16853", + "dependent_stable_id": "unknown:pg_class.16857", "referenced_stable_id": "column:realtime.messages.private", "deptype": "a" }, { - "dependent_stable_id": "unknown:pg_class.16853", + "dependent_stable_id": "unknown:pg_class.16857", "referenced_stable_id": "column:realtime.messages.topic", "deptype": "a" }, diff --git a/test/integration/region_aware_migrations_test.exs b/test/integration/region_aware_migrations_test.exs index cc3f38051..8f18629ba 100644 --- a/test/integration/region_aware_migrations_test.exs +++ b/test/integration/region_aware_migrations_test.exs @@ -15,7 +15,7 @@ defmodule Realtime.Integration.RegionAwareMigrationsTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "#{port}", "poll_interval" => 100, diff --git a/test/integration/rt_channel/broadcast_test.exs b/test/integration/rt_channel/broadcast_test.exs index f3aaba7a8..1e85a0ed4 100644 --- a/test/integration/rt_channel/broadcast_test.exs +++ b/test/integration/rt_channel/broadcast_test.exs @@ -297,7 +297,7 @@ defmodule Realtime.Integration.RtChannel.BroadcastTest do @tag policies: [:authenticated_read_broadcast_and_presence, :authenticated_write_broadcast_and_presence], requires_data: true, - requires_pg14_6_plus: true + requires_pg_140006: true test "broadcast update event changes on update in table with trigger", %{ tenant: tenant, topic: topic, @@ -341,7 +341,7 @@ defmodule Realtime.Integration.RtChannel.BroadcastTest do end @tag policies: [:authenticated_read_broadcast_and_presence, :authenticated_write_broadcast_and_presence], - requires_pg14_6_plus: true + requires_pg_140006: true test "broadcast delete event changes on delete in table with trigger", %{ tenant: tenant, topic: topic, diff --git a/test/realtime/api_test.exs b/test/realtime/api_test.exs index e0705c2b3..ce0d101a3 100644 --- a/test/realtime/api_test.exs +++ b/test/realtime/api_test.exs @@ -246,7 +246,7 @@ defmodule Realtime.ApiTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "5432", "poll_interval" => 100, @@ -360,7 +360,7 @@ defmodule Realtime.ApiTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "1234", "poll_interval" => 100, diff --git a/test/realtime/database_test.exs b/test/realtime/database_test.exs index dd6275f26..69cda4bc2 100644 --- a/test/realtime/database_test.exs +++ b/test/realtime/database_test.exs @@ -24,7 +24,7 @@ defmodule Realtime.DatabaseTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "region" => "us-east-1", "ssl_enforced" => false, @@ -111,7 +111,7 @@ defmodule Realtime.DatabaseTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "region" => "us-east-1", "ssl_enforced" => false, @@ -283,6 +283,7 @@ defmodule Realtime.DatabaseTest do {:ok, ip_version} = Database.detect_ip_version("127.0.0.1") socket_options = [ip_version] settings = Realtime.PostgresCdc.filter_settings("postgres_cdc_rls", tenant.extensions) + username = System.get_env("DB_USER", "supabase_realtime_admin") {:ok, settings} = Database.from_settings(settings, application_name, backoff) port = settings.port @@ -293,7 +294,7 @@ defmodule Realtime.DatabaseTest do hostname: "127.0.0.1", port: ^port, database: "postgres", - username: "supabase_admin", + username: ^username, password: "postgres", pool_size: 1, queue_target: 5000, diff --git a/test/realtime/extensions/cdc_rls/cdc_rls_test.exs b/test/realtime/extensions/cdc_rls/cdc_rls_test.exs index 1f4698918..372358c55 100644 --- a/test/realtime/extensions/cdc_rls/cdc_rls_test.exs +++ b/test/realtime/extensions/cdc_rls/cdc_rls_test.exs @@ -100,7 +100,8 @@ defmodule Realtime.Extensions.CdcRlsTest do %{oids: oids2} = :sys.get_state(subscriber_manager_pid) assert !Map.equal?(oids, oids2) - Postgrex.query!(conn, "create publication supabase_realtime_test for all tables", []) + # `for all tables` requires superuser + Postgrex.query!(conn, "create publication supabase_realtime_test for table public.test", []) send(subscriber_manager_pid, :check_oids) %{oids: oids3} = :sys.get_state(subscriber_manager_pid) assert !Map.equal?(oids2, oids3) diff --git a/test/realtime/extensions/cdc_rls/subscriptions_test.exs b/test/realtime/extensions/cdc_rls/subscriptions_test.exs index 0db9c67e7..0e8270bf5 100644 --- a/test/realtime/extensions/cdc_rls/subscriptions_test.exs +++ b/test/realtime/extensions/cdc_rls/subscriptions_test.exs @@ -38,6 +38,63 @@ defmodule Realtime.Extensions.PostgresCdcRls.SubscriptionsTest do Postgrex.query!(conn, "select filters, action_filter from realtime.subscription", []) end + test "create with filter on valid column succeeds", %{conn: conn} do + {:ok, subscription_params} = + Subscriptions.parse_subscription_params(%{ + "schema" => "public", + "table" => "test", + "filter" => "id=eq.123" + }) + + params_list = [%{claims: %{"role" => "anon"}, id: UUID.uuid1(), subscription_params: subscription_params}] + + assert {:ok, [%Postgrex.Result{}]} = + Subscriptions.create(conn, "supabase_realtime_test", params_list, self(), self()) + + assert %Postgrex.Result{ + rows: [ + [ + "test", + [{"id", "eq", "123"}], + "*" + ] + ] + } = + Postgrex.query!( + conn, + "select entity::text, filters, action_filter from realtime.subscription", + [] + ) + end + + test "subscription works when role lacks usage permission", %{conn: conn, tenant: tenant} do + {:ok, admin_settings} = Database.from_tenant(tenant, "realtime_test", :stop) + + {:ok, admin_conn} = + Postgrex.start_link( + hostname: admin_settings.hostname, + port: admin_settings.port, + database: admin_settings.database, + username: "supabase_admin", + password: admin_settings.password + ) + + Postgrex.query!(admin_conn, "CREATE SCHEMA IF NOT EXISTS vault", []) + Postgrex.query!(admin_conn, "REVOKE USAGE ON SCHEMA vault FROM supabase_realtime_admin", []) + + {:ok, subscription_params} = + Subscriptions.parse_subscription_params(%{ + "schema" => "public", + "table" => "test", + "filter" => "id=eq.1" + }) + + params_list = [%{claims: %{"role" => "anon"}, id: UUID.uuid1(), subscription_params: subscription_params}] + + assert {:ok, [%Postgrex.Result{}]} = + Subscriptions.create(conn, "supabase_realtime_test", params_list, self(), self()) + end + test "create all tables & all events on INSERT", %{conn: conn} do {:ok, subscription_params} = Subscriptions.parse_subscription_params(%{"event" => "INSERT", "schema" => "public"}) params_list = [%{claims: %{"role" => "anon"}, id: UUID.uuid1(), subscription_params: subscription_params}] diff --git a/test/realtime/tenants/connect_test.exs b/test/realtime/tenants/connect_test.exs index 92125b9e1..7a1413fe4 100644 --- a/test/realtime/tenants/connect_test.exs +++ b/test/realtime/tenants/connect_test.exs @@ -150,7 +150,7 @@ defmodule Realtime.Tenants.ConnectTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "poll_interval" => 100, "poll_max_changes" => 100, @@ -359,7 +359,7 @@ defmodule Realtime.Tenants.ConnectTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "poll_interval" => 100, "poll_max_changes" => 100, @@ -385,7 +385,7 @@ defmodule Realtime.Tenants.ConnectTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "poll_interval" => 100, "poll_max_changes" => 100, @@ -545,7 +545,7 @@ defmodule Realtime.Tenants.ConnectTest do replication_slot_opts = %PostgresReplication{ connection_opts: opts, - table: :all, + table: "test", output_plugin: "pgoutput", output_plugin_options: [proto_version: "1", publication_names: "test_#{i}_publication"], handler_module: Replication.TestHandler, @@ -592,7 +592,7 @@ defmodule Realtime.Tenants.ConnectTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "poll_interval" => 100, "poll_max_changes" => 100, @@ -640,7 +640,7 @@ defmodule Realtime.Tenants.ConnectTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "poll_interval" => 100, "poll_max_changes" => 100, diff --git a/test/realtime/tenants/janitor/maintenance_task_test.exs b/test/realtime/tenants/janitor/maintenance_task_test.exs index 5d4aea474..f61b9b90a 100644 --- a/test/realtime/tenants/janitor/maintenance_task_test.exs +++ b/test/realtime/tenants/janitor/maintenance_task_test.exs @@ -54,7 +54,7 @@ defmodule Realtime.Tenants.Janitor.MaintenanceTaskTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "11111", "poll_interval" => 100, diff --git a/test/realtime/tenants/janitor_test.exs b/test/realtime/tenants/janitor_test.exs index aa32b86f8..7ead28e97 100644 --- a/test/realtime/tenants/janitor_test.exs +++ b/test/realtime/tenants/janitor_test.exs @@ -142,7 +142,7 @@ defmodule Realtime.Tenants.JanitorTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "1111", "poll_interval" => 100, diff --git a/test/realtime/tenants/migrations_test.exs b/test/realtime/tenants/migrations_test.exs index 8bb887476..2e4cde465 100644 --- a/test/realtime/tenants/migrations_test.exs +++ b/test/realtime/tenants/migrations_test.exs @@ -1,11 +1,18 @@ defmodule Realtime.Tenants.MigrationsTest do - alias Realtime.Tenants.Cache # Can't use async: true because Cachex does not work well with Ecto Sandbox use Realtime.DataCase, async: false use Mimic + alias Realtime.Api + alias Realtime.Database + alias Realtime.Tenants.Cache alias Realtime.Tenants.Migrations + setup do + Cachex.clear(Realtime.FeatureFlags.Cache) + :ok + end + describe "run_migrations/1" do test "migrations for a given tenant only run once" do tenant = Containers.checkout_tenant() @@ -35,6 +42,89 @@ defmodule Realtime.Tenants.MigrationsTest do end end + describe "migrations/1" do + test "excludes SetupSupabaseRealtimeAdmin when the feature flag is disabled" do + {:ok, _} = Api.upsert_feature_flag(%{name: "use_supabase_realtime_admin", enabled: false}) + + modules = Enum.map(Migrations.migrations(), fn {_v, m} -> m end) + refute Migrations.SetupSupabaseRealtimeAdmin in modules + end + + test "excludes SetupSupabaseRealtimeAdmin when the tenant override is disabled" do + tenant = Containers.checkout_tenant() + {:ok, _} = Api.upsert_feature_flag(%{name: "use_supabase_realtime_admin", enabled: true}) + {:ok, _} = Realtime.FeatureFlags.set_tenant_flag("use_supabase_realtime_admin", tenant.external_id, false) + + Process.sleep(100) + Cache.invalidate_tenant_cache(tenant.external_id) + + modules = Enum.map(Migrations.migrations(tenant.external_id), fn {_v, m} -> m end) + refute Migrations.SetupSupabaseRealtimeAdmin in modules + end + + test "includes SetupSupabaseRealtimeAdmin when the feature flag is enabled" do + {:ok, _} = Api.upsert_feature_flag(%{name: "use_supabase_realtime_admin", enabled: true}) + + modules = Enum.map(Migrations.migrations(), fn {_v, m} -> m end) + assert Migrations.SetupSupabaseRealtimeAdmin in modules + end + + test "includes SetupSupabaseRealtimeAdmin when the tenant override is enabled" do + tenant = Containers.checkout_tenant() + {:ok, _} = Api.upsert_feature_flag(%{name: "use_supabase_realtime_admin", enabled: false}) + {:ok, _} = Realtime.FeatureFlags.set_tenant_flag("use_supabase_realtime_admin", tenant.external_id, true) + + Process.sleep(100) + Cache.invalidate_tenant_cache(tenant.external_id) + + modules = Enum.map(Migrations.migrations(tenant.external_id), fn {_v, m} -> m end) + assert Migrations.SetupSupabaseRealtimeAdmin in modules + end + end + + describe "create_partitions/1" do + test "reassigns ownership of existing partitions to supabase_realtime_admin" do + tenant = Containers.checkout_tenant(run_migrations: true) + {:ok, settings} = Database.from_tenant(tenant, "realtime_test", :stop) + {:ok, conn} = Database.connect_db(%{settings | username: "supabase_admin", max_restarts: 0, ssl: false}) + + # Pick a date inside the window create_partitions iterates over (today-1 .. today+3). + date = Date.utc_today() + partition_name = "messages_#{date |> Date.to_iso8601() |> String.replace("-", "_")}" + next_day = Date.to_string(Date.add(date, 1)) + start_day = Date.to_string(date) + + Postgrex.query!(conn, "DROP TABLE IF EXISTS realtime.#{partition_name}", []) + + Postgrex.query!( + conn, + """ + CREATE TABLE realtime.#{partition_name} + PARTITION OF realtime.messages + FOR VALUES FROM ('#{start_day}') TO ('#{next_day}') + """, + [] + ) + + assert {:ok, %{rows: [["supabase_admin"]]}} = partition_owner(conn, partition_name) + assert :ok = Migrations.create_partitions(conn) + assert {:ok, %{rows: [["supabase_realtime_admin"]]}} = partition_owner(conn, partition_name) + end + end + + defp partition_owner(conn, name) do + Postgrex.query( + conn, + """ + SELECT r.rolname FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_roles r ON r.oid = c.relowner + WHERE n.nspname = 'realtime' AND c.relname = $1 + """, + [name] + ) + end + describe "telemetry" do setup :set_mimic_global diff --git a/test/realtime/tenants/replication_connection_test.exs b/test/realtime/tenants/replication_connection_test.exs index 1e525c508..8e46d0168 100644 --- a/test/realtime/tenants/replication_connection_test.exs +++ b/test/realtime/tenants/replication_connection_test.exs @@ -86,7 +86,7 @@ defmodule Realtime.Tenants.ReplicationConnectionTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "9001", "poll_interval" => 100, @@ -634,7 +634,7 @@ defmodule Realtime.Tenants.ReplicationConnectionTest do replication_slot_opts = %PostgresReplication{ connection_opts: opts, - table: :all, + table: "test", output_plugin: "pgoutput", output_plugin_options: [proto_version: "1", publication_names: "test_#{i}_publication"], handler_module: Replication.TestHandler, diff --git a/test/realtime/tenants/schema_test.exs b/test/realtime/tenants/schema_test.exs new file mode 100644 index 000000000..10b72c48f --- /dev/null +++ b/test/realtime/tenants/schema_test.exs @@ -0,0 +1,337 @@ +defmodule Realtime.Tenants.SchemaTest do + @moduledoc false + + use Realtime.DataCase, async: false + alias Realtime.Database + + setup do + tenant = Containers.checkout_tenant(run_migrations: true) + {:ok, settings} = Database.from_tenant(tenant, "realtime_test", :stop) + + opts = settings |> Map.from_struct() |> Keyword.new() + + # simulate postgres dashboard role + {:ok, conn} = opts |> Keyword.put(:username, "postgres") |> Postgrex.start_link() + {:ok, realtime_conn} = opts |> Keyword.put(:username, "supabase_realtime_admin") |> Postgrex.start_link() + + %{conn: conn, realtime_conn: realtime_conn, settings: settings} + end + + describe "restrictions" do + @describetag :requires_supautils_policy_grants + + test "deny create trigger on realtime.messages", %{conn: conn} do + Postgrex.query!( + conn, + "CREATE OR REPLACE FUNCTION public.dummy_function() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql", + [] + ) + + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query( + conn, + "CREATE TRIGGER messages_trigger BEFORE INSERT ON realtime.messages FOR EACH ROW EXECUTE FUNCTION public.dummy_function()", + [] + ) + end + + test "deny create trigger on realtime.schema_migrations", %{conn: conn} do + Postgrex.query!( + conn, + "CREATE OR REPLACE FUNCTION public.dummy_function() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql", + [] + ) + + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query( + conn, + "CREATE TRIGGER schema_migrations_trigger BEFORE INSERT ON realtime.schema_migrations FOR EACH ROW EXECUTE FUNCTION public.dummy_function()", + [] + ) + end + + test "deny create trigger on realtime.subscription", %{conn: conn} do + Postgrex.query!( + conn, + """ + CREATE OR REPLACE FUNCTION public.test_function() RETURNS trigger + LANGUAGE plpgsql SECURITY INVOKER AS $$ + BEGIN + RETURN NEW; + END $$ + """, + [] + ) + + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query( + conn, + "CREATE TRIGGER test_trigger AFTER INSERT OR UPDATE OR DELETE ON realtime.subscription FOR EACH ROW EXECUTE FUNCTION public.test_function()", + [] + ) + end + + test "supabase_realtime_admin cannot grant super to postgres", %{realtime_conn: realtime_conn} do + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query(realtime_conn, "ALTER ROLE postgres WITH SUPERUSER", []) + end + + test "deny alter function owner to postgres", %{conn: conn} do + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query( + conn, + "ALTER FUNCTION realtime.send(jsonb, text, text, boolean) OWNER TO postgres", + [] + ) + end + + test "deny create on realtime schema", %{conn: conn} do + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query(conn, "CREATE TABLE realtime.new_table (id int)", []) + end + + test "postgres is not a member of supabase_realtime_admin", %{conn: conn} do + assert %Postgrex.Result{rows: [[false]]} = + Postgrex.query!(conn, "SELECT pg_has_role('postgres', 'supabase_realtime_admin', 'MEMBER')", []) + end + + test "postgres cannot modify realtime.schema_migrations", %{conn: conn} do + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query( + conn, + "INSERT INTO realtime.schema_migrations (version, inserted_at) VALUES (0, now())", + [] + ) + + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query(conn, "DELETE FROM realtime.schema_migrations", []) + end + + test "postgres cannot create policy on realtime.schema_migrations", %{conn: conn} do + assert {:error, %Postgrex.Error{postgres: %{code: :insufficient_privilege}}} = + Postgrex.query( + conn, + "CREATE POLICY sm_policy ON realtime.schema_migrations FOR SELECT TO authenticated USING (true)", + [] + ) + end + end + + describe "privileges" do + test "postgres can grant USAGE on schema realtime to a custom role", %{conn: conn} do + Postgrex.query!(conn, "CREATE ROLE role_test", []) + + assert {:ok, _} = Postgrex.query(conn, "GRANT USAGE ON SCHEMA realtime TO role_test", []) + + assert %Postgrex.Result{rows: [[true]]} = + Postgrex.query!(conn, "SELECT has_schema_privilege('role_test', 'realtime', 'USAGE')", []) + + Postgrex.query!(conn, "REVOKE USAGE ON SCHEMA realtime FROM role_test", []) + Postgrex.query!(conn, "DROP ROLE role_test", []) + end + + test "supabase_realtime_admin can create a role", %{realtime_conn: realtime_conn} do + role = "role_realtime_admin_create_#{System.unique_integer([:positive])}" + + assert {:ok, _} = Postgrex.query(realtime_conn, "CREATE ROLE #{role}", []) + + assert %Postgrex.Result{rows: [[true]]} = + Postgrex.query!(realtime_conn, "SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = $1)", [role]) + end + + test "supabase_realtime_admin has NOINHERIT", %{realtime_conn: realtime_conn} do + assert %Postgrex.Result{rows: [[false]]} = + Postgrex.query!( + realtime_conn, + "SELECT rolinherit FROM pg_roles WHERE rolname = 'supabase_realtime_admin'", + [] + ) + end + + test "supabase_realtime_admin can SET ROLE to granted roles", %{realtime_conn: realtime_conn} do + for role <- ~w(anon authenticated service_role) do + assert {:ok, _} = Postgrex.query(realtime_conn, "SET ROLE #{role}", []) + Postgrex.query!(realtime_conn, "RESET ROLE", []) + end + end + + test "supabase_realtime_admin can drop a role", %{realtime_conn: realtime_conn} do + role = "role_realtime_admin_drop_#{System.unique_integer([:positive])}" + Postgrex.query!(realtime_conn, "CREATE ROLE #{role}", []) + + assert {:ok, _} = Postgrex.query(realtime_conn, "DROP ROLE #{role}", []) + + assert %Postgrex.Result{rows: [[false]]} = + Postgrex.query!(realtime_conn, "SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = $1)", [role]) + end + + test "insert into realtime.messages", %{conn: conn} do + assert {:ok, %Postgrex.Result{num_rows: 1}} = + Postgrex.query( + conn, + "INSERT INTO realtime.messages (payload, event, topic, private, extension) VALUES ($1, $2, $3, $4, $5)", + [%{"hello" => "world"}, "test_event", "test_topic", false, "broadcast"] + ) + end + end + + describe "ownership" do + test "all objects in realtime schema are owned by supabase_realtime_admin", %{conn: conn} do + query = """ + SELECT r.rolname FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_roles r ON r.oid = c.relowner + WHERE n.nspname = 'realtime' AND c.relkind IN ('r', 'p', 'v', 'm', 'S', 'f') + AND c.relname <> 'schema_migrations' + UNION + SELECT r.rolname FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + JOIN pg_roles r ON r.oid = p.proowner + WHERE n.nspname = 'realtime' + UNION + SELECT r.rolname FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + JOIN pg_roles r ON r.oid = t.typowner + WHERE n.nspname = 'realtime' AND t.typtype IN ('b', 'd', 'e', 'r', 'm') + AND t.typname <> '_schema_migrations' + """ + + assert %Postgrex.Result{rows: [["supabase_realtime_admin"]]} = Postgrex.query!(conn, query, []) + end + + test "realtime schema is owned by supabase_admin", %{conn: conn} do + assert %Postgrex.Result{rows: [["supabase_admin"]]} = + Postgrex.query!( + conn, + "SELECT r.rolname FROM pg_namespace n JOIN pg_roles r ON r.oid = n.nspowner WHERE n.nspname = 'realtime'", + [] + ) + end + end + + describe "realtime.messages policy grants" do + test "create and drop SELECT policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY messages_policy_select_test ON realtime.messages FOR SELECT TO authenticated USING (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY messages_policy_select_test ON realtime.messages", []) + end + + test "create and drop INSERT policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY messages_policy_insert_test ON realtime.messages FOR INSERT TO authenticated WITH CHECK (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY messages_policy_insert_test ON realtime.messages", []) + end + + test "create and drop FOR ALL policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY messages_policy ON realtime.messages FOR ALL TO authenticated USING (true) WITH CHECK (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY messages_policy ON realtime.messages", []) + end + + test "alter existing policy", %{conn: conn} do + Postgrex.query!( + conn, + "CREATE POLICY messages_policy_alter_test ON realtime.messages FOR SELECT TO authenticated USING (true)", + [] + ) + + assert {:ok, _} = + Postgrex.query( + conn, + "ALTER POLICY messages_policy_alter_test ON realtime.messages USING (auth.role() = 'authenticated')", + [] + ) + + Postgrex.query!(conn, "DROP POLICY messages_policy_alter_test ON realtime.messages", []) + end + end + + describe "realtime.subscription policy grants" do + test "create and drop SELECT policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY subscription_policy_select ON realtime.subscription FOR SELECT TO authenticated USING (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY subscription_policy_select ON realtime.subscription", []) + end + + test "create and drop INSERT policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY subscription_policy_insert ON realtime.subscription FOR INSERT TO authenticated WITH CHECK (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY subscription_policy_insert ON realtime.subscription", []) + end + + test "create and drop UPDATE policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY subscription_policy_update ON realtime.subscription FOR UPDATE TO authenticated USING (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY subscription_policy_update ON realtime.subscription", []) + end + + test "create and drop DELETE policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY subscription_policy_delete ON realtime.subscription FOR DELETE TO authenticated USING (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY subscription_policy_delete ON realtime.subscription", []) + end + + test "create and drop FOR ALL policy", %{conn: conn} do + assert {:ok, _} = + Postgrex.query( + conn, + "CREATE POLICY subscription_policy_all ON realtime.subscription FOR ALL TO authenticated USING (true) WITH CHECK (true)", + [] + ) + + assert {:ok, _} = Postgrex.query(conn, "DROP POLICY subscription_policy_all ON realtime.subscription", []) + end + + test "alter existing policy", %{conn: conn} do + Postgrex.query!( + conn, + "CREATE POLICY subscription_policy_alter_test ON realtime.subscription FOR SELECT TO authenticated USING (true)", + [] + ) + + assert {:ok, _} = + Postgrex.query( + conn, + "ALTER POLICY subscription_policy_alter_test ON realtime.subscription USING (auth.role() = 'authenticated')", + [] + ) + + Postgrex.query!(conn, "DROP POLICY subscription_policy_alter_test ON realtime.subscription", []) + end + end +end diff --git a/test/realtime/tenants_test.exs b/test/realtime/tenants_test.exs index 35f2fd137..7c585a979 100644 --- a/test/realtime/tenants_test.exs +++ b/test/realtime/tenants_test.exs @@ -37,7 +37,8 @@ defmodule Realtime.TenantsTest do tenant = tenant_fixture(%{migrations_ran: 0}) assert Tenants.run_migrations?(tenant) - tenant = tenant_fixture(%{migrations_ran: Enum.count(Migrations.migrations()) - 1}) + migrations = Enum.count(Migrations.migrations(tenant.external_id)) + tenant = tenant_fixture(%{migrations_ran: migrations - 1}) assert Tenants.run_migrations?(tenant) end @@ -58,7 +59,7 @@ defmodule Realtime.TenantsTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "#{port()}", "poll_interval" => 100, diff --git a/test/realtime_web/channels/realtime_channel_test.exs b/test/realtime_web/channels/realtime_channel_test.exs index 667f60078..37dbbcc70 100644 --- a/test/realtime_web/channels/realtime_channel_test.exs +++ b/test/realtime_web/channels/realtime_channel_test.exs @@ -1488,7 +1488,7 @@ defmodule RealtimeWeb.RealtimeChannelTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "poll_interval" => 100, "poll_max_changes" => 100, diff --git a/test/realtime_web/controllers/tenant_controller_test.exs b/test/realtime_web/controllers/tenant_controller_test.exs index 89fe010f7..49fa73996 100644 --- a/test/realtime_web/controllers/tenant_controller_test.exs +++ b/test/realtime_web/controllers/tenant_controller_test.exs @@ -90,7 +90,7 @@ defmodule RealtimeWeb.TenantControllerTest do assert Crypto.encrypt!("127.0.0.1") == settings["db_host"] assert Crypto.encrypt!("postgres") == settings["db_name"] - assert Crypto.encrypt!("supabase_admin") == settings["db_user"] + assert Crypto.encrypt!("supabase_realtime_admin") == settings["db_user"] refute settings["db_password"] Process.sleep(100) @@ -119,7 +119,7 @@ defmodule RealtimeWeb.TenantControllerTest do assert Crypto.encrypt!("127.0.0.1") == settings["db_host"] assert Crypto.encrypt!("postgres") == settings["db_name"] - assert Crypto.encrypt!("supabase_admin") == settings["db_user"] + assert Crypto.encrypt!("supabase_realtime_admin") == settings["db_user"] refute settings["db_password"] Process.sleep(100) %{extensions: [%{settings: settings}]} = tenant = Tenants.get_tenant_by_external_id(external_id) @@ -660,7 +660,7 @@ defmodule RealtimeWeb.TenantControllerTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "#{port}", "poll_interval" => 100, diff --git a/test/realtime_web/plugs/assign_tenant_test.exs b/test/realtime_web/plugs/assign_tenant_test.exs index 536d7a548..102c8c6a1 100644 --- a/test/realtime_web/plugs/assign_tenant_test.exs +++ b/test/realtime_web/plugs/assign_tenant_test.exs @@ -15,7 +15,7 @@ defmodule RealtimeWeb.Plugs.AssignTenantTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "6432", "poll_interval" => 100, diff --git a/test/realtime_web/plugs/rate_limiter_test.exs b/test/realtime_web/plugs/rate_limiter_test.exs index d79aef969..7fd270a66 100644 --- a/test/realtime_web/plugs/rate_limiter_test.exs +++ b/test/realtime_web/plugs/rate_limiter_test.exs @@ -13,7 +13,7 @@ defmodule RealtimeWeb.Plugs.RateLimiterTest do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => "supabase_realtime_admin", "db_password" => "postgres", "db_port" => "6432", "poll_interval" => 100, diff --git a/test/support/cleanup.ex b/test/support/cleanup.ex index 12954698c..161eb58de 100644 --- a/test/support/cleanup.ex +++ b/test/support/cleanup.ex @@ -10,7 +10,7 @@ defmodule Cleanup do hostname: "localhost", port: 5433, database: "postgres", - username: "supabase_admin", + username: "supabase_realtime_admin", password: "postgres" ) diff --git a/test/support/containers.ex b/test/support/containers.ex index 94445ae40..57e1c1836 100644 --- a/test/support/containers.ex +++ b/test/support/containers.ex @@ -146,6 +146,12 @@ defmodule Containers do port <- Container.port(container) do tenant = repo_run(mode, fn -> Generators.tenant_fixture(%{port: port, migrations_ran: 0}) end) + # TODO: REAL-818 - remove when Project Migrations v2 is done + Realtime.FeatureFlags.Cache.update_cache(%Realtime.Api.FeatureFlag{ + name: "use_supabase_realtime_admin", + enabled: true + }) + run_migrations? = Keyword.get(opts, :run_migrations, false) {:ok, settings} = Database.from_tenant(tenant, "realtime_test", :stop) @@ -153,11 +159,7 @@ defmodule Containers do {:ok, conn} = Database.connect_db(settings) try do - Postgrex.transaction(conn, fn db_conn -> - Postgrex.query!(db_conn, "DROP SCHEMA IF EXISTS realtime CASCADE", []) - Postgrex.query!(db_conn, "CREATE SCHEMA IF NOT EXISTS realtime", []) - end) - + reset_realtime_schema!(settings) storage_up!(tenant) RateCounterHelper.stop(tenant.external_id) @@ -213,11 +215,27 @@ defmodule Containers do defp repo_run(:unboxed, fun), do: Ecto.Adapters.SQL.Sandbox.unboxed_run(Realtime.Repo, fun) defp repo_run(:sandbox, fun), do: fun.() + defp reset_realtime_schema!(settings) do + {:ok, admin_conn} = + Postgrex.start_link( + hostname: settings.hostname, + port: settings.port, + database: settings.database, + username: "supabase_admin", + password: settings.password + ) + + Postgrex.query!(admin_conn, "DROP PUBLICATION IF EXISTS supabase_realtime_test", []) + Postgrex.query!(admin_conn, "DROP SCHEMA IF EXISTS realtime CASCADE", []) + Postgrex.query!(admin_conn, "CREATE SCHEMA realtime", []) + Postgrex.query!(admin_conn, "GRANT USAGE ON SCHEMA realtime TO postgres, anon, authenticated, service_role", []) + Postgrex.query!(admin_conn, "GRANT ALL ON SCHEMA realtime TO supabase_realtime_admin WITH GRANT OPTION", []) + end + def stop_containers() do {list, 0} = System.cmd("docker", ["ps", "-a", "--format", "{{.Names}}", "--filter", "name=realtime-test-*"]) - names = list |> String.trim() |> String.split("\n") - for name <- names do + for name <- String.split(list, "\n", trim: true) do System.cmd("docker", ["rm", "-f", name]) end end @@ -292,7 +310,8 @@ defmodule Containers do end defp docker_run!(name, port) do - initdb = Path.expand("../../dev/postgres/zz-supabase-schema.sql", __DIR__) + initdb_sh = Path.expand("../../dev/postgres/za-permit-supabase-admin.sh", __DIR__) + initdb_sql = Path.expand("../../dev/postgres/zb-supabase-schema.sql", __DIR__) {_, 0} = System.cmd("docker", [ @@ -306,7 +325,9 @@ defmodule Containers do "-e", "POSTGRES_PASSWORD=postgres", "-v", - "#{initdb}:/docker-entrypoint-initdb.d/zz-supabase-schema.sql", + "#{initdb_sh}:/docker-entrypoint-initdb.d/za-permit-supabase-admin.sh", + "-v", + "#{initdb_sql}:/docker-entrypoint-initdb.d/zb-supabase-schema.sql", "-p", "#{port}:5432", image(), diff --git a/test/support/generators.ex b/test/support/generators.ex index df73b90a2..03a09c23e 100644 --- a/test/support/generators.ex +++ b/test/support/generators.ex @@ -20,7 +20,7 @@ defmodule Generators do "settings" => %{ "db_host" => "127.0.0.1", "db_name" => "postgres", - "db_user" => "supabase_admin", + "db_user" => System.get_env("DB_USER", "supabase_realtime_admin"), "db_password" => "postgres", "db_port" => "#{override[:port] || port()}", "poll_interval_ms" => 10, diff --git a/test/support/integrations.ex b/test/support/integrations.ex index b8150497f..8ed2bc06d 100644 --- a/test/support/integrations.ex +++ b/test/support/integrations.ex @@ -79,9 +79,10 @@ defmodule Integrations do primary key ("id")); """, "grant all on table public.test to anon;", - "grant all on table public.test to supabase_admin;", + "grant all on table public.test to supabase_realtime_admin;", "grant all on table public.test to authenticated;", - "create publication #{publication} for all tables", + # `for all tables` requires superuser + "create publication #{publication} for table public.test", """ DO $$ DECLARE diff --git a/test/test_helper.exs b/test/test_helper.exs index 50a8b4183..8f5e3e3c4 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -16,8 +16,16 @@ repo_config = Application.fetch_env!(:realtime, Realtime.Repo) %{rows: [[pg_version_num]]} = Postgrex.query!(pg_conn, "SELECT current_setting('server_version_num')::int") +%{rows: [[has_supautils_subscription_grants]]} = + Postgrex.query!(pg_conn, "SELECT current_setting('supautils.policy_grants', true) LIKE '%realtime.subscription%'") + # `realtime.broadcast_changes(..., NEW record, OLD record, ...)` (introduced in commit 2922658c) called from a trigger via `PERFORM` fails on PG <= 14.5 -exclude = [:failing] ++ if pg_version_num < 140_006, do: [:requires_pg14_6_plus], else: [] +requires_pg_140006 = if pg_version_num < 140_006, do: :requires_pg_140006 + +# Restriction assertions on the postgres role only hold on builds where supautils.policy_grants includes realtime.subscription (supabase/postgres 15.14.1.018 or higher) +requires_supautils_policy_grants = if !has_supautils_subscription_grants, do: :requires_supautils_policy_grants + +exclude = [:failing, requires_pg_140006, requires_supautils_policy_grants] ExUnit.start(exclude: exclude, max_cases: max_cases, capture_log: true)