From e4e42cad447c325c54cf152726ead7f8ef509470 Mon Sep 17 00:00:00 2001 From: krushit1307 Date: Fri, 23 Oct 2026 22:48:56 +0530 Subject: [PATCH] fix(cli): resolve unset auto_expose_new_tables to false and revoke all on sequences --- apps/cli-go/internal/db/start/start.go | 2 +- .../internal/testing/helper/privileges.go | 2 +- .../command-internal/db-bootstrap/db-setup.ts | 12 +++++----- .../db-bootstrap/db-setup.unit.test.ts | 8 +++++-- .../db-bootstrap/shadow-cache.ts | 8 +++---- .../db-bootstrap/shadow-cache.unit.test.ts | 6 ++--- .../command-internal/db-config.toml-read.ts | 4 ++-- .../src/shared/init/project-init.templates.ts | 5 +++-- .../init/project-init.templates.unit.test.ts | 22 ++++--------------- apps/docs/public/cli/config.schema.json | 4 ++-- .../public/cli/project-config.schema.json | 2 +- packages/config/src/api.ts | 2 +- 12 files changed, 34 insertions(+), 43 deletions(-) diff --git a/apps/cli-go/internal/db/start/start.go b/apps/cli-go/internal/db/start/start.go index d4c97ba1d5..5058d320ff 100644 --- a/apps/cli-go/internal/db/start/start.go +++ b/apps/cli-go/internal/db/start/start.go @@ -396,7 +396,7 @@ const RevokeDefaultDataApiPrivilegesSql = ` alter default privileges for role postgres in schema public revoke select, insert, update, delete on tables from anon, authenticated, service_role; alter default privileges for role postgres in schema public - revoke usage, select on sequences from anon, authenticated, service_role; + revoke all on sequences from anon, authenticated, service_role; alter default privileges for role postgres in schema public revoke execute on functions from anon, authenticated, service_role; ` diff --git a/apps/cli-go/internal/testing/helper/privileges.go b/apps/cli-go/internal/testing/helper/privileges.go index 4dcca23f58..146c528542 100644 --- a/apps/cli-go/internal/testing/helper/privileges.go +++ b/apps/cli-go/internal/testing/helper/privileges.go @@ -11,7 +11,7 @@ import "github.com/supabase/cli/pkg/pgtest" func MockApiPrivilegesRevoke(conn *pgtest.MockConn) *pgtest.MockConn { conn.Query("alter default privileges for role postgres in schema public\n revoke select, insert, update, delete on tables from anon, authenticated, service_role"). Reply("ALTER DEFAULT PRIVILEGES"). - Query("alter default privileges for role postgres in schema public\n revoke usage, select on sequences from anon, authenticated, service_role"). + Query("alter default privileges for role postgres in schema public\n revoke all on sequences from anon, authenticated, service_role"). Reply("ALTER DEFAULT PRIVILEGES"). Query("alter default privileges for role postgres in schema public\n revoke execute on functions from anon, authenticated, service_role"). Reply("ALTER DEFAULT PRIVILEGES") diff --git a/apps/cli/src/command-internal/db-bootstrap/db-setup.ts b/apps/cli/src/command-internal/db-bootstrap/db-setup.ts index b4f1d11471..e3ce1f5e60 100644 --- a/apps/cli/src/command-internal/db-bootstrap/db-setup.ts +++ b/apps/cli/src/command-internal/db-bootstrap/db-setup.ts @@ -52,7 +52,7 @@ export const START_REVOKE_API_PRIVILEGES_SQL = ` alter default privileges for role postgres in schema public revoke select, insert, update, delete on tables from anon, authenticated, service_role; alter default privileges for role postgres in schema public - revoke usage, select on sequences from anon, authenticated, service_role; + revoke all on sequences from anon, authenticated, service_role; alter default privileges for role postgres in schema public revoke execute on functions from anon, authenticated, service_role; `; @@ -605,10 +605,10 @@ const startInitSchema = Effect.fn("DbSetup.initSchema")(function* ( }); /** - * Applies the `api.auto_expose_new_tables` tri-state: unset and `true` are both a no-op (keeps - * the bundled initial-schema grants); an explicit `false` execs - * {@link START_REVOKE_API_PRIVILEGES_SQL}. Always execs over `session` directly, regardless of PG - * major version — never part of the PG15+ one-shot Docker jobs. + * Applies the `api.auto_expose_new_tables` tri-state: unset and `false` both revoke the default + * Data API privileges by exec'ing {@link START_REVOKE_API_PRIVILEGES_SQL}; an explicit `true` is + * a no-op (keeps the bundled initial-schema grants). Always execs over `session` directly, + * regardless of PG major version — never part of the PG15+ one-shot Docker jobs. * * Takes `session`/`fs`/`path` directly rather than the whole {@link StartSetupLocalDatabaseInput} * because `db reset`'s PG14 path calls it too, without any of `setupDatabase`'s other steps. @@ -620,7 +620,7 @@ export const applyApiPrivileges = Effect.fnUntraced(function* ( tmpDir: string, autoExposeNewTables: Option.Option, ) { - if (Option.getOrElse(autoExposeNewTables, () => true)) return; + if (Option.getOrElse(autoExposeNewTables, () => false)) return; yield* execSqlConstant( session, fs, diff --git a/apps/cli/src/command-internal/db-bootstrap/db-setup.unit.test.ts b/apps/cli/src/command-internal/db-bootstrap/db-setup.unit.test.ts index 3644219133..e127276472 100644 --- a/apps/cli/src/command-internal/db-bootstrap/db-setup.unit.test.ts +++ b/apps/cli/src/command-internal/db-bootstrap/db-setup.unit.test.ts @@ -48,6 +48,8 @@ const SCHEMA_13_FINGERPRINT = const SCHEMA_14_FINGERPRINT_SUFFIX = "CREATE SCHEMA IF NOT EXISTS graphql"; const REVOKE_PRIVILEGES_FINGERPRINT = "revoke execute on functions from anon, authenticated, service_role"; +const REVOKE_SEQUENCES_FINGERPRINT = + "revoke all on sequences from anon, authenticated, service_role"; const PG_NET_CREATE_FINGERPRINT = "create extension if not exists pg_net schema extensions"; function fakeSession() { @@ -584,12 +586,13 @@ describe("startSetupLocalDatabase", () => { yield* run(baseInput(workdir, session, { majorVersion: 14 }), out, docker); const execSql = calls.filter((c) => c.kind === "exec").map((c) => c.sql); expect(execSql.some((sql) => sql.includes(REVOKE_PRIVILEGES_FINGERPRINT))).toBe(true); + expect(execSql.some((sql) => sql.includes(REVOKE_SEQUENCES_FINGERPRINT))).toBe(true); yield* fs.remove(workdir, { recursive: true, force: true }); }).pipe(Effect.provide(BunServices.layer)); }); it.effect( - "unset (no config.toml at all) matches the true behavior — no revoke SQL exec'd", + "unset (no config.toml at all) matches the false behavior — execs the revoke SQL", () => { return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; @@ -599,7 +602,8 @@ describe("startSetupLocalDatabase", () => { const docker = mockDockerRun(); yield* run(baseInput(workdir, session, { majorVersion: 14 }), out, docker); const execSql = calls.filter((c) => c.kind === "exec").map((c) => c.sql); - expect(execSql.some((sql) => sql.includes(REVOKE_PRIVILEGES_FINGERPRINT))).toBe(false); + expect(execSql.some((sql) => sql.includes(REVOKE_PRIVILEGES_FINGERPRINT))).toBe(true); + expect(execSql.some((sql) => sql.includes(REVOKE_SEQUENCES_FINGERPRINT))).toBe(true); yield* fs.remove(workdir, { recursive: true, force: true }); }).pipe(Effect.provide(BunServices.layer)); }, diff --git a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts index 8ecf4f3542..2d62673571 100644 --- a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts +++ b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts @@ -184,12 +184,12 @@ function canonicalJson(value: unknown): string { /** * The two-state behavior `applyApiPrivileges` derives from the tri-state - * `api.auto_expose_new_tables`: unset and explicit `true` both keep the grants; only `false` - * revokes them. Hashing the raw tri-state would split unset from `true` into different keys for a - * config edit that changes nothing on disk. + * `api.auto_expose_new_tables`: unset and explicit `false` both revoke the grants; only explicit + * `true` keeps them. Hashing the raw tri-state would split unset from `false` into different keys + * for a config edit that changes nothing on disk. */ const effectiveShadowApiGrantsKept = (value: Option.Option): boolean => - Option.getOrElse(value, () => true); + Option.getOrElse(value, () => false); /** * The cache key: a 16-hex-char (64-bit) scrypt prefix over a fixed field order. 64 bits is ample diff --git a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.unit.test.ts b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.unit.test.ts index 27fc8f0a6f..df3e29fa75 100644 --- a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.unit.test.ts +++ b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.unit.test.ts @@ -77,7 +77,7 @@ describe("shadowCacheKey", () => { { label: "db settings", inputs: { ...base, dbSettings: { max_connections: 200 } } }, { label: "auto expose new tables", - inputs: { ...base, autoExposeNewTables: Option.some(false) }, + inputs: { ...base, autoExposeNewTables: Option.some(true) }, }, { label: "effective webhooks / pg_net", inputs: { ...base, webhooksEnabled: false } }, { label: "roles.sql", inputs: { ...base, rolesSql: "" } }, @@ -168,8 +168,8 @@ describe("shadowCacheKey", () => { ...base, autoExposeNewTables: Option.some(false), }); - expect(explicitTrue).toBe(unset); - expect(explicitFalse).not.toBe(unset); + expect(explicitFalse).toBe(unset); + expect(explicitTrue).not.toBe(unset); }); it("excludes a disabled service's image tag entirely", () => { diff --git a/apps/cli/src/command-internal/db-config.toml-read.ts b/apps/cli/src/command-internal/db-config.toml-read.ts index 5134fbd1b2..82d7647b3b 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.ts @@ -149,8 +149,8 @@ interface BaselineTomlConfig { readonly realtimeEnabled: boolean; /** * `[api] auto_expose_new_tables`, tri-state (`None` when unset). The cache - * key folds in the effective bool — unset and `true` both mean grants are - * kept. + * key folds in the effective bool — unset and `false` both mean grants are + * revoked. */ readonly apiAutoExposeNewTables: Option.Option; /** `[db.vault]` secret names (sorted), created during setup. */ diff --git a/apps/cli/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index 2d4e0ef25b..6a4740447e 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -21,8 +21,9 @@ extra_search_path = ["public", "extensions"] max_rows = 1000 # Controls whether new tables, views, sequences and functions created in the \`public\` schema by # \`postgres\` are reachable through the Data API roles (\`anon\`, \`authenticated\`, \`service_role\`) -# without explicit GRANTs, matching the cloud default. Set to \`false\` to require explicit GRANTs -# instead. Left unset, a fresh project falls back to \`true\`. +# without explicit GRANTs. When unset, new entities are NOT auto-exposed, matching the new cloud +# default. Set to \`true\` to keep the legacy behaviour of auto-exposing new entities; this is +# deprecated and the field is removed on 2026-10-30 once the always-revoked behaviour is permanent. # auto_expose_new_tables = true [api.tls] diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index fcb288038d..80c2144d4e 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -53,26 +53,12 @@ const renderExpectedGoEject = readVendoredTemplate("config.toml").pipe( ), ); -// The Go scaffold still describes `auto_expose_new_tables` as unset-means- -// revoked and deprecated; the native template documents unset-means-exposed -// instead, since platform projects never stopped auto-exposing new entities. -const GO_AUTO_EXPOSE_COMMENT = `# without explicit GRANTs. When unset, new entities are NOT auto-exposed, matching the new cloud -# default. Set to \`true\` to keep the legacy behaviour of auto-exposing new entities; this is -# deprecated and the field is removed on 2026-10-30 once the always-revoked behaviour is permanent. -# auto_expose_new_tables = true`; - -const NATIVE_AUTO_EXPOSE_COMMENT = `# without explicit GRANTs, matching the cloud default. Set to \`false\` to require explicit GRANTs -# instead. Left unset, a fresh project falls back to \`true\`. -# auto_expose_new_tables = true`; - const renderExpectedNativeEject = renderExpectedGoEject.pipe( Effect.map((eject) => - eject - .replace( - '# content_path = "./templates/password_changed_notification.html"', - '# content_path = "./supabase/templates/password_changed_notification.html"', - ) - .replace(GO_AUTO_EXPOSE_COMMENT, NATIVE_AUTO_EXPOSE_COMMENT), + eject.replace( + '# content_path = "./templates/password_changed_notification.html"', + '# content_path = "./supabase/templates/password_changed_notification.html"', + ), ), ); diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 526ce97a0c..23ec39d1be 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -83,7 +83,7 @@ }, "auto_expose_new_tables": { "type": "boolean", - "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off." + "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are not auto-exposed, matching the cloud default. Set to `true` to keep the legacy behaviour of auto-exposing new entities without explicit GRANTs." }, "tls": { "type": "object", @@ -2772,7 +2772,7 @@ }, "auto_expose_new_tables": { "type": "boolean", - "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off." + "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are not auto-exposed, matching the cloud default. Set to `true` to keep the legacy behaviour of auto-exposing new entities without explicit GRANTs." }, "tls": { "type": "object", diff --git a/apps/docs/public/cli/project-config.schema.json b/apps/docs/public/cli/project-config.schema.json index 7cff1018b1..3a7a16a265 100644 --- a/apps/docs/public/cli/project-config.schema.json +++ b/apps/docs/public/cli/project-config.schema.json @@ -36,7 +36,7 @@ }, "auto_expose_new_tables": { "type": "boolean", - "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off." + "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are not auto-exposed, matching the cloud default. Set to `true` to keep the legacy behaviour of auto-exposing new entities without explicit GRANTs." } }, "additionalProperties": true diff --git a/packages/config/src/api.ts b/packages/config/src/api.ts index 082a8a7a31..8c2c1d46f8 100644 --- a/packages/config/src/api.ts +++ b/packages/config/src/api.ts @@ -59,7 +59,7 @@ export const api = Schema.Struct({ auto_expose_new_tables: Schema.optionalKey( Schema.Boolean.annotate({ description: - 'Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the "Default privileges for new entities" toggle turned off.', + "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are not auto-exposed, matching the cloud default. Set to `true` to keep the legacy behaviour of auto-exposing new entities without explicit GRANTs.", tags, links, }),