// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package cmd // Tests of the runtime database mode (openspec change runtime-database-user, // design D1, D8 and D11, task 4.8). Every row works on a scratch database and // on login roles the test creates with a password and drops in its cleanup: // the migration user is the superuser TEST_DATABASE_URL names, the runtime // user is a login that owns nothing. import ( "bytes" "context" "database/sql" "errors" "fmt" "io/fs" "log/slog" "net/url" "regexp" "strconv" "strings" "testing" "time" "github.com/spf13/cobra" "github.com/spf13/viper" "git.coopcloud.tech/wiki-cafe/member-console/internal/db" "git.coopcloud.tech/wiki-cafe/member-console/internal/migrate" "git.coopcloud.tech/wiki-cafe/member-console/internal/testkit/dbtest" ) const runtimeLoginPassword = "runtime-login-password" var migrationFileVersion = regexp.MustCompile(`^(\d+)_`) // highestVersions maps each stream to the highest version it embeds. func highestVersions(t *testing.T) map[string]int64 { t.Helper() out := map[string]int64{} for _, src := range migrate.Sources() { entries, err := fs.ReadDir(src.Migrations, src.Dir) if err != nil { t.Fatalf("read %s migrations: %v", src.Name, err) } for _, e := range entries { if m := migrationFileVersion.FindStringSubmatch(e.Name()); m != nil { v, err := strconv.ParseInt(m[1], 10, 64) if err != nil { t.Fatalf("migration %s: %v", e.Name(), err) } out[src.Name] = max(out[src.Name], v) } } } return out } // bootBox is one scratch database with its two logins' addresses. type bootBox struct { t *testing.T scratch string // the migration user's address: the superuser name string // the scratch database's name login string // the runtime login runtime string // the runtime login's address admin *sql.DB } // newBootBox creates a scratch database and a runtime login that owns nothing. func newBootBox(t *testing.T) *bootBox { t.Helper() scratch := dbtest.Scratch(t) u, err := url.Parse(scratch) if err != nil { t.Fatalf("parse the scratch address: %v", err) } box := &bootBox{t: t, scratch: scratch, name: strings.TrimPrefix(u.Path, "/"), login: dbtest.Name("mc_rt_"), admin: dbtest.Connect(t, dbtest.DSN(t))} dbtest.On(t, box.admin).Exec(fmt.Sprintf("CREATE ROLE %s LOGIN PASSWORD '%s'", box.login, runtimeLoginPassword)) t.Cleanup(func() { box.dropDatabase() if _, err := box.admin.ExecContext(context.Background(), "DROP ROLE IF EXISTS "+box.login); err != nil { t.Errorf("drop role %s: %v", box.login, err) } }) u.User = url.UserPassword(box.login, runtimeLoginPassword) box.runtime = u.String() return box } // configure points the settings at the box: db-dsn always, db-migrate-dsn when // withMigration is true. func (b *bootBox) configure(withMigration bool) { b.t.Helper() viper.Reset() b.t.Cleanup(viper.Reset) viper.Set("env", "development") viper.Set("db-dsn", b.runtime) if withMigration { viper.Set("db-migrate-dsn", b.scratch) } } // migrateTo migrates the scratch database as the migration user, every stream // to its highest version except stripe, which stops one version short when // short is true. func (b *bootBox) migrateTo(short bool) { b.t.Helper() scratch := dbtest.Connect(b.t, b.scratch) highest := highestVersions(b.t) for _, src := range migrate.Sources() { version := highest[src.Name] if short && src.Name == "stripe" { version-- } dbtest.UpTo(b.t, scratch, src, version) } dbtest.On(b.t, scratch).Exec(fmt.Sprintf("GRANT %s TO %s WITH INHERIT TRUE, SET FALSE", db.RuntimeGroupRole, b.login)) } // open opens the database in runtime mode and returns the pool and the log. func (b *bootBox) open() (*sql.DB, *bytes.Buffer, error) { b.t.Helper() var logs bytes.Buffer logger := slog.New(slog.NewTextHandler(&logs, nil)) database, closeDB, err := openDB(context.Background(), logger, dbRuntime) if err != nil { return nil, &logs, err } b.t.Cleanup(closeDB) return database, &logs, nil } // ledgers lists, per stream, the highest applied version and the number of // applied rows, read as the migration user. func (b *bootBox) ledgers() string { b.t.Helper() scratch := dbtest.Connect(b.t, b.scratch) var parts []string for _, src := range migrate.Sources() { var top, n sql.NullInt64 query := "SELECT max(version_id), count(*) FROM public.goose_db_version_" + src.Name + " WHERE is_applied" if err := scratch.QueryRow(query).Scan(&top, &n); err != nil { parts = append(parts, src.Name+": "+err.Error()) continue } parts = append(parts, fmt.Sprintf("%s:%d:%d", src.Name, top.Int64, n.Int64)) } return strings.Join(parts, " ") } // migrationSessions counts the sessions of the migration user on the scratch // database, waiting briefly for a closed pool's sessions to end. func (b *bootBox) migrationSessions() int64 { b.t.Helper() q := dbtest.On(b.t, b.admin) var n int64 for range 30 { n = q.Int(`SELECT count(*) FROM pg_stat_activity WHERE datname = $1 AND usename = (SELECT current_user::text)`, b.name) if n == 0 { return 0 } time.Sleep(100 * time.Millisecond) } return n } // scratchAdmin opens the scratch database as the migration user, which is the // superuser the test stack's TEST_DATABASE_URL names. func (b *bootBox) scratchAdmin() *dbtest.Q { b.t.Helper() return dbtest.On(b.t, dbtest.Connect(b.t, b.scratch)) } // ownedBy gives the scratch database to role. func (b *bootBox) ownedBy(role string) { b.t.Helper() dbtest.On(b.t, b.admin).Exec(fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", b.name, role)) } // dropDatabase drops the scratch database ahead of the roles that own it or // hold privileges in it. Go runs t.Cleanup functions in reverse order and // dbtest.Scratch registers its drop first, while DROP ROLE fails as long as // the role owns the database or an object in it, so each cleanup that drops a // role calls this first. func (b *bootBox) dropDatabase() { if _, err := b.admin.ExecContext(context.Background(), fmt.Sprintf("DROP DATABASE IF EXISTS %s WITH (FORCE)", b.name)); err != nil { b.t.Errorf("drop database %s: %v", b.name, err) } } // thirdRole creates a login-less role, neither the migration user nor the // runtime login, which the test drops. func (b *bootBox) thirdRole() string { b.t.Helper() role := dbtest.Name("mc_third_") dbtest.On(b.t, b.admin).Exec("CREATE ROLE " + role) b.t.Cleanup(func() { b.dropDatabase() if _, err := b.admin.ExecContext(context.Background(), "DROP ROLE IF EXISTS "+role); err != nil { b.t.Errorf("drop role %s: %v", role, err) } }) return role } // plant runs a statement as the runtime login, which creates what the test // plants. func (b *bootBox) plant(statement string) { b.t.Helper() dbtest.On(b.t, dbtest.Connect(b.t, b.runtime)).Exec(statement) } // wantRefusal opens the database in runtime mode and asserts the refusal. func (b *bootBox) wantRefusal(want string) { b.t.Helper() _, _, err := b.open() var refusal *db.RuntimeUserRefusal if !errors.As(err, &refusal) || err.Error() != want { b.t.Errorf("openDB error = %v, want the refusal %q", err, want) } } // wantLedgers asserts that the ledgers read as before. func (b *bootBox) wantLedgers(before string) { b.t.Helper() if after := b.ledgers(); after != before { b.t.Errorf("the ledgers changed: %s, want %s", after, before) } } // wantNoMembership asserts that the runtime login holds no membership in the // group role. func (b *bootBox) wantNoMembership() { b.t.Helper() n := dbtest.On(b.t, b.admin).Int(`SELECT count(*) FROM pg_auth_members m JOIN pg_roles g ON g.oid = m.roleid JOIN pg_roles u ON u.oid = m.member WHERE g.rolname = $1 AND u.rolname = $2`, db.RuntimeGroupRole, b.login) if n != 0 { b.t.Errorf("the runtime login %q was granted %s, want no membership", b.login, db.RuntimeGroupRole) } } // plantedBody is the body of every function the rows plant. A planted function // that writes a marker row is blind in a read-only transaction (the write fails) // and in a savepoint that rolls back (the row goes), so it raises instead: an // exception shows in every path, turns the check into copy 11 and puts this text // in the sentence the row compares. const plantedBody = `BEGIN RAISE EXCEPTION 'planted function ran as % (superuser %)', current_user, current_setting('is_superuser'); END` // grantWithSet makes the box's login a member of the group role as the boot // grant does not leave it: with SET, which lets the login SET ROLE to the group // role and to any role the group role holds with SET. func (b *bootBox) grantWithSet() { b.t.Helper() b.scratchAdmin().Exec(fmt.Sprintf("GRANT %s TO %s WITH INHERIT TRUE, SET TRUE", db.RuntimeGroupRole, b.login)) } // setRole opens a connection as the runtime login and runs SET ROLE to role on // it, the way a session of the login would; the membership's SET option is // checked against the session user, which a SET LOCAL ROLE inside the test's // superuser session would not exercise. func (b *bootBox) setRole(role string) error { b.t.Helper() _, err := dbtest.Connect(b.t, b.runtime).ExecContext(context.Background(), "SET ROLE "+role) return err } // createInPublic is the sentence of copy 3e for a login that holds CREATE on // public through a grant to itself. func (b *bootBox) createInPublic() string { return fmt.Sprintf("db-dsn logs in as %s, which can create objects in schema public through a grant to %s; "+ "as the migration user, run: REVOKE CREATE ON SCHEMA public FROM %s;", b.login, b.login, b.login) } // openRuntimeDatabaseRows are the rows of TestOpenRuntimeDatabase, each run // by its own named function. var openRuntimeDatabaseRows = []struct { name string run func(t *testing.T) }{ {"both strings on a fresh database", bootWithBothStrings}, {"without db-migrate-dsn on a database behind", bootBehindWithoutMigrationString}, {"without db-migrate-dsn on a current database", bootCurrentWithoutMigrationString}, {"migrate up without db-migrate-dsn", migrateUpWithoutMigrationString}, {"a runtime login that set its own role", bootWithSelfSetRole}, {"one user for both strings", bootWithOneUser}, {"a superuser runtime login", bootWithSuperuser}, {"a runtime login that owns the database", bootWithDatabaseOwner}, {"a database owner that planted quote_ident", bootWithPlantedQuoteIdent}, {"a runtime login that can create in public and planted format", bootWithPlantedFormat}, {"a runtime login that can create in public and planted an operator", bootWithPlantedOperator}, {"a runtime login that owns a function in public", bootWithOwnedPublicFunction}, {"start on a database owned by another role", bootOnAForeignOwner}, {"migrate commands on a database owned by another role", migrateCommandsRefuseAForeignOwner}, {"migrate up on a database whose owner set its own role", migrateUpRefusesAnOwnerThatSetItsRole}, {"a runtime login that holds pg_read_server_files", bootWithPredefinedRole}, {"two databases", bootWithTwoDatabases}, {"a runtime login that can create schemas in the database", bootWithDatabaseCreate}, {"a runtime login that owns a table in core before migrating", bootWithOwnedStreamTable}, {"without db-migrate-dsn on a membership that can SET ROLE", bootSeparateStepWithSet}, {"the boot grant closes a SET ROLE chain through the group role", bootClosesASetRoleChain}, } // TestOpenRuntimeDatabase covers tasks 4.8 and 5.5 (design D1, D2, D8, D9, // D11): the runtime mode refuses a configuration that would reopen the bypass // before it migrates, migrates and grants on the migration connection, closes // it, checks the runtime user, and serves as the runtime login; without // db-migrate-dsn it runs nothing and refuses a schema that is behind. Every // row runs with env set to development, because nothing reads env. func TestOpenRuntimeDatabase(t *testing.T) { // viper.Reset drops the environment binding; blanking the variables as // well keeps the rows off a database that make test exports. t.Setenv("MC_DB_DSN", "") t.Setenv("MC_DB_DSN_FILE", "") t.Setenv("MC_DB_MIGRATE_DSN", "") t.Setenv("MC_DB_MIGRATE_DSN_FILE", "") for _, row := range openRuntimeDatabaseRows { t.Run(row.name, row.run) } } func bootWithBothStrings(t *testing.T) { box := newBootBox(t) box.configure(true) database, _, err := box.open() if err != nil { t.Fatalf("openDB: %v", err) } q := dbtest.On(t, database) if got := q.String("SELECT current_user::text"); got != box.login { t.Errorf("SELECT current_user on the returned pool = %q, want the runtime login %q", got, box.login) } if q.String(fmt.Sprintf("SELECT pg_has_role(current_user, '%s', 'USAGE')::text", db.RuntimeGroupRole)) != "true" { t.Errorf("the runtime login %q does not inherit %s", box.login, db.RuntimeGroupRole) } for stream, want := range highestVersions(t) { got := q.Int("SELECT max(version_id) FROM public.goose_db_version_" + stream + " WHERE is_applied") if got != want { t.Errorf("ledger %s through the returned pool is at %d, want %d", stream, got, want) } } if n := box.migrationSessions(); n != 0 { t.Errorf("%d sessions of the migration user remain open on the scratch database, want 0", n) } } func bootBehindWithoutMigrationString(t *testing.T) { box := newBootBox(t) box.migrateTo(true) before := box.ledgers() box.configure(false) _, _, err := box.open() want := fmt.Sprintf("the schema is behind this release (pending migrations: stripe %d); "+ "run member-console migrate up with db-migrate-dsn set, then start again", highestVersions(t)["stripe"]) var refusal *db.RuntimeUserRefusal if !errors.As(err, &refusal) || err.Error() != want { t.Errorf("openDB error = %v, want the refusal %q", err, want) } if after := box.ledgers(); after != before { t.Errorf("the ledgers changed: %s, want %s", after, before) } } func bootCurrentWithoutMigrationString(t *testing.T) { box := newBootBox(t) box.migrateTo(false) before := box.ledgers() box.configure(false) database, logs, err := box.open() if err != nil { t.Fatalf("openDB: %v", err) } if got := dbtest.On(t, database).String("SELECT current_user::text"); got != box.login { t.Errorf("SELECT current_user on the returned pool = %q, want the runtime login %q", got, box.login) } if !strings.Contains(logs.String(), "migrations not run: db-migrate-dsn is not set") { t.Errorf("the log lacks the separate-step line:\n%s", logs) } if strings.Contains(logs.String(), "database migrations applied") { t.Errorf("migrations ran without db-migrate-dsn:\n%s", logs) } if after := box.ledgers(); after != before { t.Errorf("the ledgers changed: %s, want %s", after, before) } } func migrateUpWithoutMigrationString(t *testing.T) { viper.Reset() t.Cleanup(viper.Reset) viper.Set("env", "development") viper.Set("db-dsn", "postgres://runtime:pw@127.0.0.1:1/x?connect_timeout=1") const want = "member-console migrate needs db-migrate-dsn (MC_DB_MIGRATE_DSN or MC_DB_MIGRATE_DSN_FILE)" if err := migrateUpCmd.RunE(migrateUpCmd, nil); err == nil || err.Error() != want { t.Errorf("migrate up error = %v, want %q", err, want) } } func bootWithSelfSetRole(t *testing.T) { box := newBootBox(t) admin := dbtest.On(t, box.admin) admin.Exec(fmt.Sprintf("GRANT %s TO %s WITH INHERIT TRUE", db.RuntimeGroupRole, box.login)) admin.Exec(fmt.Sprintf("ALTER ROLE %s SET role = '%s'", box.login, db.RuntimeGroupRole)) box.configure(true) database, _, err := box.open() if err != nil { t.Fatalf("openDB: %v", err) } if got := dbtest.On(t, database).String("SELECT current_user::text"); got != box.login { t.Errorf("SELECT current_user on the returned pool = %q, want the login %q, not the role it set", got, box.login) } } func bootWithOneUser(t *testing.T) { box := newBootBox(t) before := box.ledgers() box.configure(true) viper.Set("db-dsn", box.scratch) user := dbtest.On(t, box.admin).String("SELECT current_user::text") box.wantRefusal(fmt.Sprintf("db-dsn and db-migrate-dsn both log in as %s; "+ "db-dsn needs a user of its own that is not a superuser and owns nothing", user)) box.wantLedgers(before) } func bootWithSuperuser(t *testing.T) { box := newBootBox(t) dbtest.On(t, box.admin).Exec("ALTER ROLE " + box.login + " SUPERUSER") before := box.ledgers() box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, a superuser; set db-dsn to a user without SUPERUSER", box.login)) box.wantLedgers(before) box.wantNoMembership() } func bootWithDatabaseOwner(t *testing.T) { box := newBootBox(t) box.ownedBy(box.login) before := box.ledgers() box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, which owns the database %s; "+ "as the migration user, run: REASSIGN OWNED BY %s TO CURRENT_USER;", box.login, box.name, box.login)) box.wantLedgers(before) box.wantNoMembership() } // bootWithPlantedQuoteIdent plants public.quote_ident(name), which beats the // catalog's for a name argument whatever the search path, as the database's // owner. Neither connection may run it before the refusal: a run raises, and // the refusal that would show is copy 11 with the function's text. func bootWithPlantedQuoteIdent(t *testing.T) { box := newBootBox(t) box.ownedBy(box.login) box.plant(`CREATE FUNCTION public.quote_ident(name) RETURNS text LANGUAGE plpgsql AS $$ ` + plantedBody + ` $$`) before := box.ledgers() box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, which owns the database %s; "+ "as the migration user, run: REASSIGN OWNED BY %s TO CURRENT_USER;", box.login, box.name, box.login)) box.wantLedgers(before) } func bootWithPlantedFormat(t *testing.T) { box := newBootBox(t) box.scratchAdmin().Exec("GRANT CREATE ON SCHEMA public TO " + box.login) box.plant(`CREATE FUNCTION public.format(text, text) RETURNS text LANGUAGE plpgsql AS $$ ` + plantedBody + ` $$`) before := box.ledgers() box.configure(true) box.wantRefusal(box.createInPublic()) box.wantLedgers(before) } // bootWithPlantedOperator plants public.=(oid, integer), which check 12's own // query would call as the migration user without the search-path pin; a call // raises. func bootWithPlantedOperator(t *testing.T) { box := newBootBox(t) box.scratchAdmin().Exec("GRANT CREATE ON SCHEMA public TO " + box.login) box.plant(`CREATE FUNCTION public.mc_eq(oid, integer) RETURNS boolean LANGUAGE plpgsql AS $$ ` + plantedBody + ` $$`) box.plant("CREATE OPERATOR public.= (LEFTARG = oid, RIGHTARG = integer, FUNCTION = public.mc_eq)") before := box.ledgers() box.configure(true) box.wantRefusal(box.createInPublic()) box.wantLedgers(before) } func bootWithOwnedPublicFunction(t *testing.T) { box := newBootBox(t) q := box.scratchAdmin() q.Exec("CREATE FUNCTION public.mc_f() RETURNS integer LANGUAGE sql AS 'SELECT 1'") q.Exec("ALTER FUNCTION public.mc_f() OWNER TO " + box.login) before := box.ledgers() box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, which owns function public.mc_f(); "+ "as the migration user, run: REASSIGN OWNED BY %s TO CURRENT_USER;", box.login, box.login)) box.wantLedgers(before) } // migrateCommands are the three commands that open the migration pool. var migrateCommands = []*cobra.Command{migrateUpCmd, migrateDownCmd, migrateStatusCmd} // ownerRefusal is the sentence of copy 3d for the scratch database. func (b *bootBox) ownerRefusal(owner string) string { return fmt.Sprintf("database %s is owned by %s, not the migration user; as the migration user, run: ALTER DATABASE %s OWNER TO CURRENT_USER;", b.name, owner, b.name) } // wantMigrateRefusal runs a migrate command and asserts the refusal of copy 3d // and that the ledgers read as before. func (b *bootBox) wantMigrateRefusal(c *cobra.Command, want, before string) { b.t.Helper() err := c.RunE(c, nil) var refusal *db.DatabaseOwnerRefusal if !errors.As(err, &refusal) || err.Error() != want { b.t.Errorf("migrate %s error = %v, want the refusal %q", c.Name(), err, want) } b.wantLedgers(before) } // wantOwnerRefusal opens the database in runtime mode and asserts the refusal // of copy 3d. func (b *bootBox) wantOwnerRefusal(want string) { b.t.Helper() _, _, err := b.open() var refusal *db.DatabaseOwnerRefusal if !errors.As(err, &refusal) || err.Error() != want { b.t.Errorf("openDB error = %v, want the refusal %q", err, want) } } func bootOnAForeignOwner(t *testing.T) { box := newBootBox(t) owner := box.thirdRole() box.ownedBy(owner) before := box.ledgers() box.configure(true) box.wantOwnerRefusal(box.ownerRefusal(owner)) box.wantLedgers(before) box.wantNoMembership() } func migrateCommandsRefuseAForeignOwner(t *testing.T) { box := newBootBox(t) owner := box.thirdRole() box.ownedBy(owner) before := box.ledgers() box.configure(true) for _, c := range migrateCommands { box.wantMigrateRefusal(c, box.ownerRefusal(owner), before) } } func migrateUpRefusesAnOwnerThatSetItsRole(t *testing.T) { box := newBootBox(t) owner := box.thirdRole() box.ownedBy(owner) dbtest.On(t, box.admin).Exec(fmt.Sprintf("ALTER DATABASE %s SET role = '%s'", box.name, owner)) before := box.ledgers() box.configure(true) box.wantMigrateRefusal(migrateUpCmd, box.ownerRefusal(owner), before) } func bootWithPredefinedRole(t *testing.T) { box := newBootBox(t) dbtest.On(t, box.admin).Exec("GRANT pg_read_server_files TO " + box.login) box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, a direct member of pg_read_server_files; "+ "as the migration user, run: REVOKE pg_read_server_files FROM %s;", box.login, box.login)) q := box.scratchAdmin() for stream, want := range highestVersions(t) { if got := q.Int("SELECT max(version_id) FROM public.goose_db_version_" + stream + " WHERE is_applied"); got != want { t.Errorf("ledger %s is at %d, want %d: the check runs after the migrations", stream, got, want) } } } func bootWithTwoDatabases(t *testing.T) { box := newBootBox(t) second := dbtest.Scratch(t) u, err := url.Parse(second) if err != nil { t.Fatalf("parse the second scratch address: %v", err) } secondName := strings.TrimPrefix(u.Path, "/") before := box.ledgers() box.configure(true) viper.Set("db-migrate-dsn", second) box.wantRefusal(fmt.Sprintf("db-dsn connects to database %s and db-migrate-dsn to database %s; both must name the same database", box.name, secondName)) box.wantLedgers(before) } func bootWithDatabaseCreate(t *testing.T) { box := newBootBox(t) dbtest.On(t, box.admin).Exec(fmt.Sprintf("GRANT CREATE ON DATABASE %s TO %s", box.name, box.login)) before := box.ledgers() box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, which can create schemas in database %s through a grant to %s; "+ "as the migration user, run: REVOKE CREATE ON DATABASE %s FROM %s;", box.login, box.name, box.login, box.name, box.login)) box.wantLedgers(before) box.wantNoMembership() } // bootWithOwnedStreamTable shows check 9 reading a stream schema before the // migrations: the database has no migration yet, and the login owns a table in // a core schema that the first migration would find. func bootWithOwnedStreamTable(t *testing.T) { box := newBootBox(t) q := box.scratchAdmin() q.Exec("CREATE SCHEMA core") q.Exec("CREATE TABLE core.mc_t (id integer)") q.Exec("ALTER TABLE core.mc_t OWNER TO " + box.login) before := box.ledgers() box.configure(true) box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, which owns relation core.mc_t; "+ "as the migration user, run: REASSIGN OWNED BY %s TO CURRENT_USER;", box.login, box.login)) box.wantLedgers(before) box.wantNoMembership() } func bootSeparateStepWithSet(t *testing.T) { box := newBootBox(t) box.migrateTo(false) box.grantWithSet() box.configure(false) grantor := box.scratchAdmin().String("SELECT pg_catalog.quote_ident(pg_catalog.pg_get_userbyid(m.grantor)) " + "FROM pg_catalog.pg_auth_members m WHERE m.roleid = 'console_runtime'::pg_catalog.regrole AND m.set_option") box.wantRefusal(fmt.Sprintf("db-dsn logs in as %s, which can SET ROLE to %s through a grant by %s; "+ "as the migration user, run: REVOKE %s FROM %s GRANTED BY %s; GRANT %s TO %s WITH INHERIT TRUE, SET FALSE;", box.login, db.RuntimeGroupRole, grantor, db.RuntimeGroupRole, box.login, grantor, db.RuntimeGroupRole, box.login)) } // bootClosesASetRoleChain is an existing deployment: the login holds the group // role with SET, and the group role holds a role that can insert into an // enclosed table with INHERIT FALSE, SET TRUE, which no privilege check of the // login's counts. The login can SET ROLE to that role before the boot and // cannot after it. func bootClosesASetRoleChain(t *testing.T) { box := newBootBox(t) box.migrateTo(false) chained := box.thirdRole() box.scratchAdmin().Exec("GRANT INSERT ON core.pool_provisions TO " + chained) dbtest.On(t, box.admin).Exec(fmt.Sprintf("GRANT %s TO %s WITH INHERIT FALSE, SET TRUE", chained, db.RuntimeGroupRole)) box.grantWithSet() if err := box.setRole(chained); err != nil { t.Fatalf("before the boot, SET ROLE %s as the runtime login = %v, want it to work", chained, err) } box.configure(true) if _, _, err := box.open(); err != nil { t.Fatalf("openDB: %v", err) } dbtest.WantPgError(t, box.setRole(chained), dbtest.SQLStatePrivilege, "") } // TestLogOpenFailure covers copy 17: start logs a refusal, the owner refusal of // copy 3d among them, as "refused to start", and any other error as a failure // to initialize. func TestLogOpenFailure(t *testing.T) { rows := []struct { name string err error want string }{ {"a runtime user refusal", &db.RuntimeUserRefusal{Message: "refusal"}, `msg="refused to start"`}, {"a database owner refusal", &db.DatabaseOwnerRefusal{Message: "refusal"}, `msg="refused to start"`}, {"a wrapped database owner refusal", fmt.Errorf("opening: %w", &db.DatabaseOwnerRefusal{Message: "refusal"}), `msg="refused to start"`}, {"any other error", errors.New("boom"), `msg="failed to initialize database"`}, } for _, row := range rows { t.Run(row.name, func(t *testing.T) { var logs bytes.Buffer logOpenFailure(slog.New(slog.NewTextHandler(&logs, nil)), row.err) if !strings.Contains(logs.String(), row.want) { t.Errorf("log = %q, want it to contain %s", logs.String(), row.want) } }) } }