Second section of #23, a quick fix listed in its change. Only start and config validate resolved db-dsn-file, so migrate, config list|set|clear and seed-demo, run in a container that mounts the connection string as a file, found no string and let the driver connect with its defaults. They now resolve the database's secret pair before opening it, and openDB refuses an empty db-dsn ("db-dsn is not set (MC_DB_DSN or MC_DB_DSN_FILE)") before it opens anything. connectSettings returns its error, so a test can call it; the config commands print it as before. TestDatabaseCommandsReadTheFileKeys runs migrate up, down and status, connectSettings and seed-demo with only db-dsn-file set, naming a closed port, and expects the connection failure; with neither key set it expects the refusal. It blanks MC_DB_DSN and MC_DB_DSN_FILE, so the rows, migrate down among them, never reach a real database. Full suite on reset databases: 4207 pass, 15 skip. make lint passes. Deliberate breaks, scripts/mutation-check -side new, all CAUGHT (6): migrate up, migrate down, connectSettings and seed-demo each without resolveDatabaseSecretFiles openDB without the empty db-dsn check resolveDatabaseSecretFiles resolves an empty list
60 KiB
Tests follow docs/testing.md: database tests open the database through dbtest, a test whose subject is a migration or the migrator gets a dbtest.Scratch database, and tests meet the same complexity limit as production code, so a table runs its rows through a named function. A test that proves a privilege by running a statement drops to db.RuntimeGroupRole with SET LOCAL ROLE inside a transaction it rolls back, and asserts the SQLSTATE (42501 for a privilege, 22023 for a refused argument), never only a word of the message. A test that creates a role does it inside a transaction it rolls back, or drops the role in its cleanup. A refusal's test compares the whole sentence with its entry in the design's Copy table.
Sections 1 to 6 are one commit each, and their items are that commit's parts; section 7 is not a commit. Every commit passes make test with the test stack up, which runs make lint first, including TestConsoleReaderColumns, TestEnvironmentReferenceDocumentsEveryConfigKey, filler-copy and em-dash-copy. No golden file and no screen changes in any section, and no existing test's result changes except where a section's items name the test; any other change is a stop. The copy is the design's Copy table, approved by the maintainer before section 1 starts; a string that differs from its approved entry is a stop. Sections 1 to 3 prove the roles by SET LOCAL ROLE console_runtime on the superuser TEST_DATABASE_URL, so they need no runtime login and pass on any worktree's stack as it stands; section 4 does not (its preamble says so). Paths under migrations/ are in the stream the item names: internal/db/migrations/ (core), internal/domains/migrations/, and internal/integrations/<key>/store/migrations/.
Breaks. Each section's list is written to a file outside the repository and run with go run ./scripts/mutation-check -list <file> -side new on the section's commit, after its green control run; a new test has no old side. The enclosure tests that section 1 moves from core_writer or applicationRole to console_runtime are converted checks: their breaks also run with -side old on a checkout of the parent commit, and a break the old side misses is named in the commit message as the strength the move adds. A break in a migration file reaches the shared test database only after it is migrated again, so a list with such a break carries "prepare": "cd test && ./reset-test-db.sh"; a break in a role membership, which is cluster-wide and survives a reset, reaches its test without one (design D16). Every record must be CAUGHT. An old text below is the code as the section's tasks write it; when the committed code words it differently, the record is corrected to the committed text before the run, never dropped. The commit message lists every break with its result.
1. The runtime group role, the writers' privileges and the baselines' logins
- 1.1 Core
migrations/00029_runtime_group_role.sql(design D5, D6, D7), oneStatementBeginblock per section. Up: aDOblock that createsconsole_runtime NOLOGINwhenpg_roleshas no such role, and revokescore_writerfrommember_consolewhenpg_roleshas that role andpg_auth_membersrecords the membership; thenGRANT core_writer TO console_runtimeandGRANT SELECT ON goose_db_version_core TO console_runtime(unqualified, as goose created it). Down:REVOKE SELECT ON goose_db_version_core FROM console_runtime, andDROP ROLE IF EXISTS console_runtimeinside a block that catchesdependent_objects_still_existand raises a notice, as the baselines' Down sections do. The Down neither revokescore_writerfromconsole_runtimenor grants it back tomember_console. The file's header comment says why the role exists, why the grant is unqualified, and why the Down leaves both memberships. - 1.2 The same for the other four streams: domains
migrations/00003_runtime_group_role.sql, FedWikimigrations/00005_runtime_group_role.sql, Stripemigrations/00006_runtime_group_role.sql, Discoursemigrations/00004_runtime_group_role.sql, each with its own writer and its own ledger, the guarded revoke frommember_console(the role must exist and hold the membership, because revoking from a role that does not exist is an error), and a Down that revokes only the ledger grant. None creates or dropsconsole_runtime. - 1.3 The five baselines name no login (design D6): delete
GRANT core_writer TO member_consoleand its DownREVOKEfrom coreinternal/db/migrations/00001_init.sql(:1173and its Down), and the same statement and DownREVOKEfordomains_writer(internal/domains/migrations/00001_init.sql:201),fedwiki_writer(internal/integrations/fedwiki/store/migrations/00001_init.sql:100),stripe_writer(internal/integrations/stripe/store/migrations/00001_init.sql:165) anddiscourse_writer(internal/integrations/discourse/store/migrations/00001_init.sql:122). The comment above core's statement, which says the role keeps its privileges explicitly, is rewritten to say that the runtime user reaches the writers throughconsole_runtime(migrations00029and its siblings). Nothing else in the baselines changes in this section. - 1.4 The writers keep the four DML privileges (design D13):
migrations/00030_writer_dml_only.sqlin core,00004_writer_dml_only.sqlin domains,00006_writer_dml_only.sqlin FedWiki,00007_writer_dml_only.sqlin Stripe and00005_writer_dml_only.sqlin Discourse. Each Up is one statement,REVOKE TRUNCATE, REFERENCES, TRIGGER, MAINTAIN ON ALL TABLES IN SCHEMA <schema> FROM <schema>_writer; each Down does nothing, and the header comment says why (re-granting would also undo enclosures that revokedTRUNCATEearlier, and no code path used the privileges) and that a table added later must be grantedSELECT, INSERT, UPDATE, DELETEby name, neverALL. The earlierGRANT ALLlines are not edited. - 1.5
internal/db/runtime_user.go(new): the constantRuntimeGroupRole = "console_runtime", with a doc comment naming the migrations that grant to it. - 1.6 The enclosure tests move to the group role (design D15). Delete
applicationRolefrominternal/db/integration_delivery_fixtures_test.goand update the file's header comment; every caller (acts_test.go,integration_delivery_enclosure_test.go,plan_act_obligations_test.go) passesdb.RuntimeGroupRoletoasRole.TestConferralEnclosure_WriterRole(internal/entitlements/conferral_enclosure_test.go) andTestRuleTableDMLRevoked(internal/entitlements/enclosure_test.go) runSET LOCAL ROLE console_runtimethrough the constant, and their comments say that the test login is a superuser and that the group role is what the runtime user holds.TestConferralEnclosure_WriterRolegains three probes on each of the three position tables, each in a savepoint expecting SQLSTATE42501:TRUNCATE <table>;CREATE TRIGGERnaming a trigger function the test created inpg_tempfirst, so the error cannot be a missing function; andLOCK TABLE <table> IN ACCESS EXCLUSIVE MODE. Its header comment namesTRUNCATE,TRIGGERandMAINTAINwith the other three writes. The tests that drop tocore_reader,core_owner,core_writer(plan_act_obligations_test.go:848) or a bare role keep doing so. The other tests' results do not change. - 1.7
TestStreamsGrantTheirWriterToTheRuntimeRole(internal/db/runtime_role_test.go), on adbtest.SimpleProtocolconnection to the test database, in one transaction it rolls back: revoke the five writers and the five ledger grants fromconsole_runtime; grant the writers tomember_consolewhen that role exists, which simulates a database an earlier release migrated; run the Up section of each of the five group-role migrations (dbtest.UpSQL), core first; then assert thatconsole_runtimeinherits each writer (pg_has_role(..., 'USAGE')), thatmember_consoleis a direct member of none, and thatconsole_runtimeholdsSELECTon each stream's ledger. A last row runs each of the five Up sections again withmember_consolereplaced by a name no role has (dbtest.Name("mc_absent_")) and expects no error: on the test stackmember_consolealways exists, so no other row notices a missing guard.TestBaselinesGrantNoLogin, on a scratch database: revoke the five writers frommember_consolefirst, when the role exists; migrate core to version 28, the one before00029, withdbtest.UpTo, then apply version 1 of domains, FedWiki, Stripe and Discourse; assert thatmember_consoleis a direct member of no writer role; revoke again in cleanup, because the membership would be cluster-wide. Core goes first and goes that far because a real run migrates core to its end before any other stream, and domains' baseline inserts akindcolumn that core00008adds, so version 1 of core alone fails the test on correct code; the run stops before the group-role migrations because they would revoke a restored grant and hide it.TestRuntimeRoleOutlivesARollbackElsewheremigrates a scratch database withmigrate.Sources(), rolls back each stream's group-role migration withdbtest.ApplyVersion(..., false), integrations first, then core00029, and asserts thatconsole_runtimestill exists, because the shared test database still grants it the ledgers, that it still inheritscore_writer, and thatmember_consoleis still a direct member of no writer role. - 1.8
TestRuntimeRolePrivileges(internal/db/runtime_role_privileges_test.go, design D16), on the shared test database: the locked list as a table in the test, each entry naming the privileges it holds (the three position tables holdSELECT); for every relation of the five schemas, partitions and views included,console_runtimeholds none ofTRUNCATE,REFERENCES,TRIGGERandMAINTAIN. Every failure names the relation and the privilege. It also asserts that no role butconsole_runtimeis a direct member of a writer role, apart fromfedwiki_writer, which the FedWiki stream makes a member ofdomains_writer. - 1.9 Breaks:
- core
00029withoutGRANT core_writer TO console_runtime: caught byTestStreamsGrantTheirWriterToTheRuntimeRole(no reset); - FedWiki
00005grantsdomains_writerin place offedwiki_writer: caught by the same test; - Stripe
00006without its revoke frommember_console: caught by the same test; - Discourse
00004without its ledger grant: caught by the same test and, with the reset, byTestRuntimeRolePrivileges; - the core baseline with
GRANT core_writer TO member_consolerestored: caught byTestBaselinesGrantNoLogin; - the Discourse baseline with
GRANT discourse_writer TO member_consolerestored: caught byTestBaselinesGrantNoLogin; - core
00029's Down revokescore_writerfromconsole_runtime: caught byTestRuntimeRoleOutlivesARollbackElsewhere; - core
00029's Down dropsconsole_runtimewithout thedependent_objects_still_existhandler: caught by the same test, whose rollback then fails; - core
00029's Down grantscore_writerback tomember_console: caught by the same test; - core
00030emptied, so the baseline'sGRANT ALLstands (with the reset): caught byTestRuntimeRolePrivileges, naming a core table andTRIGGER, and by the three probes ofTestConferralEnclosure_WriterRole; - core
00030revokesTRUNCATE,REFERENCESandMAINTAINand leavesTRIGGER(with the reset): caught byTestRuntimeRolePrivilegesnaming the table andTRIGGER, and by theCREATE TRIGGERprobe; - core
00030revokes from two position tables only, by listing the tables in place ofALL TABLES(with the reset): caught by the probes on the third table; - domains
00004, FedWiki00006, Stripe00007and Discourse00005, each emptied in turn (with the reset): caught byTestRuntimeRolePrivileges, naming a table of that stream's schema andTRIGGER; GRANT ALL ON core.integration_config_overrides TO core_writerappended after the revoke in core00030(with the reset): caught byTestRuntimeRolePrivileges, naming that table andTRIGGER;00009'sGRANT ALL ON core.integration_config_overrides TO core_owner, core_writerlosescore_writer(with the reset): caught byTestRuntimeRolePrivileges, naming the table andINSERT;00027'sGRANT SELECT ON core.acts, core.plan_act_details TO core_writerbecomesGRANT SELECT, INSERT(with the reset): caught byTestRuntimeRolePrivilegesand byTestActRecordIsEnclosed;- a locked entry for a table that does not exist added to the list: caught by
TestRuntimeRolePrivileges(a test-side record; it proves the stale-entry check); - the membership assertion of
TestRuntimeRolePrivilegeswithout itsfedwiki_writerexception: caught, namingfedwiki_writer(a test-side record; a production break would leave a cluster-wide membership behind); - domains
00003's revoke without its guard on the role existing: caught by the absent-role row ofTestStreamsGrantTheirWriterToTheRuntimeRole(no reset); - as converted checks, on both sides (with the reset):
00005'sREVOKE INSERT, UPDATE, DELETE ON core.pool_provisions FROM core_writerlosesINSERT: caught byTestConferralEnclosure_WriterRoleon both sides;00024'sGRANT SELECT ON core.person_cut_offs TO core_writerbecomesGRANT SELECT, INSERT: caught byTestPersonCutOffEnclosureon both sides; - on the new side only, since
console_runtimedoes not exist on the old one (with the reset): core00029also grantsINSERToncore.pool_provisionstoconsole_runtime: caught byTestConferralEnclosure_WriterRoleandTestRuntimeRolePrivileges, a grant that a test run ascore_writercannot see; the same grant ofTRIGGER: caught by theCREATE TRIGGERprobe andTestRuntimeRolePrivileges.
- core
- 1.10
docs/building-an-integration.md, section 7: the "member_consolemembership" bullet becomes "console_runtimemembership": a baseline grants{key}_writerandSELECTongoose_db_version_<key>toconsole_runtime, never to a login, and core creates the role first; a table is grantedSELECT, INSERT, UPDATE, DELETEby name and neverALL, and the stream'swriter_dml_onlymigration revokes the other four; the Down bullet says the Down revokes the ledger grant and leaves the membership (design D7). Section 9's FedWiki paragraph says "its role triple and its grants toconsole_runtime".docs/database-management.md, "Roles": the paragraph aboutmember_consoledescribesconsole_runtime, the five memberships, the ledger grants, that the writers hold the four DML privileges and no other, that the console grants the role to the runtime user at start, and that owner roles are granted to no login; "Owner roles are reserved for running migrations" goes. The ownership paragraph lands in section 3. - 1.11 No golden moves;
internal/migrate/testdata/console-reader-columns.txtdoes not change.
2. Quick fix: every command that opens the database reads the -file keys
- 2.1
cmd/start.go(design D12): split the core secret pairs intodatabaseSecretPairs(db-dsn/db-dsn-file) and the rest; the loop ofresolveSecretFilesbecomesresolveSecretPairs(pairs);resolveSecretFilesresolves the core pairs, the database pairs and the integrations' pairs as today; a newresolveDatabaseSecretFilesresolvesdatabaseSecretPairsalone. Doc comments say which commands call which and why a command resolves once. - 2.2
migrate up,migrate down,migrate status(cmd/migrate.go),connectSettings(cmd/config.go, the one path ofconfig list,setandclear) andseed-demo(cmd/seed_demo.go) callresolveDatabaseSecretFilesbeforeopenDB.connectSettingsreturns its error, and the threeRunfunctions print it and exit as they do today, so a test can call it. - 2.3
openDBrefuses an emptydb-dsnwith copy 13 before it opens anything. - 2.4
TestDatabaseCommandsReadTheFileKeys(cmd/database_files_test.go), one row per path through a named function:migrate upandmigrate status(theirRunE),connectSettings, andseed-demo(itsRunE). Each row sets onlydb-dsn-file, naming a file that holdspostgres://u:p@127.0.0.1:<a closed port>/x?connect_timeout=1, and asserts that the error is a connection failure that names that port, not copy 13. A second table sets neither key and asserts copy 13 for each path.TestResolveSecretFiles_DBDSNpasses unchanged. - 2.5 Breaks:
migrate upwithout itsresolveDatabaseSecretFilescall: caught by its row (copy 13 in place of the connection failure);connectSettingswithout the call: caught by its row;seed-demowithout the call: caught by its row;openDBwithout the empty check: caught by the second table;resolveDatabaseSecretFilesresolves an empty list: caught by every row of the first table.
3. Runtime paths through functions owned by core_owner: webhook partitions and the plan-act obligation lock
The plan-act obligation lock (items 3.10 to 3.14) lives in this section, not in a section of its own, because it is the same kind of change as the partition function (a core migration that gives the runtime user a path through the enclosure that its role alone does not have), it needs only console_runtime from section 1, it passes make test alone, and it lands before section 4, which is the first section that serves anything as the runtime user. A section of its own would renumber sections 4 to 7 and every reference to them.
-
3.1 Core
migrations/00031_webhook_partition_function.sql(design D3, D4): Up movescore.webhook_eventstocore_owner, then each of its partitions through aDOloop overpg_inherits; createscore.ensure_webhook_event_partitionsas the design writes it; sets its owner, revokesEXECUTEfromPUBLICand grants it toconsole_runtime. Down drops the function, gives the parent and each partition toCURRENT_USER, and then, in the same loop, runsGRANT ALL ON core.webhook_events TO core_ownerand the same for each partition, so the database is back where the baseline'sGRANT ALL ON ALL TABLES IN SCHEMA core TO core_ownerleft it (the Up'sOWNER TO core_ownermerges that explicit entry into the owner's, and without the re-grant the Down leavescore_ownernone). The header comment says whycore_ownerowns the table, why the Down re-grants, and that the bounds must match the baseline'sDOblock and the Go names. -
3.2 Core
internal/db/migrations/00001_init.sql, the initial-partitionDOblock (:1048-1064; design D4):m_startbecomes aTIMESTAMPcomputed asDATE_TRUNC('month', NOW() AT TIME ZONE 'UTC') + make_interval(months => i), the upper bound ism_start + INTERVAL '1 month', and both bounds are rendered withAT TIME ZONE 'UTC', so the block names and bounds the partitions as the function does in any session time zone. The block's comment says so. Nothing else in the baseline changes in this section. -
3.3
internal/db/partitions.go:EnsureWebhookEventPartitions(ctx, ex, now, monthsAhead)takes an executor (ExecContext) and runsSELECT core.ensure_webhook_event_partitions($1, $2). The doc comment says that the function, owned bycore_owner, creates the partitions, and that the naming and bounds parity is between the function and the baseline'sDOblock; the pgx placeholder paragraph goes.cmd/bootstate.goandinternal/workflows/maintenance/activities.gocompile unchanged. -
3.4
internal/db/partitions_test.go, rewritten.TestEnsureWebhookEventPartitions_ClosesTheGapAndIsIdempotentruns in one transaction it rolls back, underSET LOCAL ROLE console_runtimeandSET LOCAL TimeZone = 'Pacific/Kiritimati', with the synthetic month of today's test: an insert into the target month fails with23514in a savepoint; the ensure creates the four partitions; inserts at the target month's first hour and last hour (UTC) each land in the target month's partition; a second ensure creates nothing; every created partition is owned bycore_owner. Nothing is dropped, since the transaction rolls back.TestWebhookPartitionsBelongToCoreOwnerasserts the owner ofcore.webhook_eventsand of every partition, and that the function isSECURITY DEFINER, owned bycore_owner, configuredsearch_path=core, pg_temp, and not executable byPUBLIC.TestRuntimeRoleCannotCreatePartitionsDirectly, asconsole_runtime, runs theCREATE TABLE ... PARTITION OFthe Go code ran before and expects42501.TestEnsureWebhookEventPartitionsRefusesAWindow, one row each for 13 months, -1 months and a null time, expects22023and no new partition. The file's header comment says what runs as whom. -
3.5
schemaSnapshot(internal/db/integration_delivery_migration_test.go:152-153) renders every ACL order-insensitively, forrelacl,attacl,nspaclandproacl: for example(SELECT string_agg(x::text, ',' ORDER BY x::text) FROM unnest(c.relacl) x)in place ofc.relacl::text. Sorting both sides keeps equal sets equal and unequal sets unequal, so the three existing up/down/up tests keep their results, and a Down that re-grants an entry appended at the end of the ACL compares equal.TestWebhookOwnershipMoveRoundTrips(internal/db), on a scratch database migrated withdbtest.UpToto core00030: recordschemaSnapshot(which recordsrelownerand the ACLs); apply00031up and record again; apply it down and assert that the snapshot equals the first, so the parent and every partition are back with the user that ran the Down; apply it up again and assert that the snapshot equals the second. -
3.6
TestBaselinePartitionBoundsAreUTC(internal/db), on a scratch database: apply core version 1 on a connection whoseTimeZoneisPacific/Kiritimati(a runtime parameter of the connection string), then assert that each of the three initial partitions' bounds, read astimestamptz, is midnight UTC on the first of the current UTC month and of the two months after it, and that the names carry those months. -
3.7
TestRuntimeRolePrivilegesassertsEXECUTEoncore.ensure_webhook_event_partitionsand oncore.lock_plan_act_obligationforconsole_runtime. -
3.8 Breaks (the migrations' with the reset):
- the function computes the month in the session's time zone (
date_trunc('month', p_now)withoutAT TIME ZONE 'UTC'): caught by the last-hour insert; - no
GRANT EXECUTE ... TO console_runtime: caught by the gap test andTestRuntimeRolePrivileges; SECURITY INVOKERin place ofSECURITY DEFINER: caught by the gap test (42501on the create) and the ownership test;- no
SET search_path: caught by the ownership test; - no
REVOKE EXECUTE ... FROM PUBLIC: caught by the ownership test andTestConsoleReaderColumns; - the partition loop in the Up removed: caught by the ownership test, on a partition the baseline created;
- the window check allows 13: caught by the refusal test;
EnsureWebhookEventPartitionspassesmonthsAhead - 1: caught by the gap test;- the baseline block takes
DATE_TRUNC('month', NOW())in the session's time zone, as it did: caught byTestBaselinePartitionBoundsAreUTC(a scratch database; no reset); 00031's Down gives the parent tocore_ownerin place ofCURRENT_USER: caught byTestWebhookOwnershipMoveRoundTrips;00031's Down leaves the partitions withcore_owner: caught by the same test;00031's Down without thecore_ownerre-grant: caught byTestWebhookOwnershipMoveRoundTrips;schemaSnapshotwith the ACLs in stored order, as the test code reads it (a test-side record): caught byTestWebhookOwnershipMoveRoundTripson a correct Down, whose re-grant appends the entry at the end;TestRuntimeRoleCannotCreatePartitionsDirectlywithout itsSET LOCAL ROLE(a test-side record): caught, because the statement succeeds as the superuser;
- the function computes the month in the session's time zone (
-
3.9
docs/database-management.md, "Roles": a paragraph on ownership (design D3): objects belong to the migration user,core_ownerowns the definer functions andcore.webhook_eventswith its partitions, and the runtime user owns nothing, neither in the schemas nor the database. -
3.10 Core
migrations/00032_lock_plan_act_obligation.sql(design D19): Up createscore.lock_plan_act_obligation(p_act_id UUID, p_pool_id UUID)as the design writes it, sets its owner tocore_owner, revokesEXECUTEfromPUBLICand grants it tocore_writer, in the order and shape of00028's other definer functions; Down drops the function. The header comment says that a locking read needsUPDATE, which the enclosure withholds from the writer, why the function reads only the obligation row, and that the lock, its mode and its order are those of the query it replaces. -
3.11
internal/entitlements/queries/plan_acts.sql:LockPlanActObligationbecomes the query below (design D19); its comment keeps the account of why only the obligation row is read, adds that the function takes the lock for the runtime role, and says that sqlc cannot see a function's output columns, so the alias list names them and the casts give them their non-null types. Regenerate withmake sqlc-generate, which restores the SPDX header that sqlc drops, sointernal/entitlements/plan_acts.sql.gois regenerated; the row type,LockPlanActObligationRow, andplan_act_move.godo not change.SELECT l.act_id::uuid AS act_id, l.pool_id::uuid AS pool_id, l.org_type::varchar AS org_type, l.org_id::uuid AS org_id, l.status::varchar AS status, l.owned::boolean AS owned, l.motion_act_id::uuid AS motion_act_id FROM core.lock_plan_act_obligation(sqlc.arg(act_id)::uuid, sqlc.arg(pool_id)::uuid) AS l(act_id, pool_id, org_type, org_id, status, owned, motion_act_id); -
3.12
TestLockPlanActObligationRunsAsTheRuntimeRole(internal/entitlements/plan_act_queries_db_test.go, with the helpers ofTestLockPlanActObligationReadsTheMove), in one transaction it rolls back: write the act and its obligation as the test login,SET LOCAL ROLE console_runtime, callLockPlanActObligationand assert the row ofTestLockPlanActObligationReadsTheMove; requeue it and assert the Retry as the motion; assertsql.ErrNoRowsfor a pool with no obligation; and in a savepoint runSELECT ... FROM core.plan_act_obligations WHERE false FOR UPDATEand assert SQLSTATE42501, which names the reason for the function.TestPlanActWritersAreEnclosedInTheCatalog(internal/db/plan_act_obligations_test.go) gainslock_plan_act_obligationin its list of functions and in itsPUBLICandcore_readerrefusal loop, which asserts that the function isSECURITY DEFINER, owned bycore_owner, configuredsearch_path=core, pg_temp, executable bycore_writerandconsole_runtime, and not byPUBLICorcore_reader; no new test.TestLockPlanActObligationSurvivesARequeueItWaitedOnand the plan-act drain tests run unchanged. -
3.13
docs/database-locks.md: in the plan act obligation row's "Taken by" cell and in the move's paragraph, theLockPlanActObligationquery is described as callingcore.lock_plan_act_obligation, which takes the sameFOR UPDATEafter the pool row; the lock, its mode and the order are unchanged. Nothing else in the file changes. -
3.14 Breaks for 3.10 to 3.13 (the migration's with the reset):
- the sqlc query returned to the bare
SELECT ... FROM core.plan_act_obligations ... FOR UPDATE OF ob(the generated text edited the same way): caught byTestLockPlanActObligationRunsAsTheRuntimeRole(42501at the lock); - the function without
FOR UPDATE: caught byTestLockPlanActObligationSurvivesARequeueItWaitedOn(the move never waits); SECURITY INVOKERin place ofSECURITY DEFINER: caught byTestLockPlanActObligationRunsAsTheRuntimeRoleandTestPlanActWritersAreEnclosedInTheCatalog;- no
SET search_path: caught byTestPlanActWritersAreEnclosedInTheCatalog; - no
REVOKE EXECUTE ... FROM PUBLIC: caught byTestPlanActWritersAreEnclosedInTheCatalogandTestConsoleReaderColumns; - no
GRANT EXECUTE ... TO core_writer: caught byTestLockPlanActObligationRunsAsTheRuntimeRoleandTestRuntimeRolePrivileges; - the function written to return a single composite row in place of
TABLE(a missing pair would return a row of nulls): caught by thesql.ErrNoRowsassertion ofTestLockPlanActObligationRunsAsTheRuntimeRole; UPDATEgranted oncore.plan_act_obligationstocore_writerin place of the function (with the reset): caught byTestRuntimeRolePrivileges(the locked list holdsSELECTonly).
- the sqlc query returned to the bare
4. Two connection strings, the boot grant, and the test stack on two users
This section's make test passes only after the documented stack respin of item 4.9, which the implementer performs in the worktree before the section's suite runs: the guard refuses a test/.env without MC_DB_MIGRATE_DSN, and postgres-seed creates the runtime login only on docker compose up. "Every commit passes make test" therefore holds from a stack so respun, not from a fresh checkout beside an old test/.env.
- 4.1 Keys (design D1):
startCmdregistersdb-migrate-dsnanddb-migrate-dsn-file(copies 23, 24), and the help ofdb-dsnanddb-dsn-filechanges (copies 21, 22);databaseSecretPairsgainsdb-migrate-dsn/db-migrate-dsn-file;ValidateStartrefuses a malformeddb-migrate-dsn(copy 14) and treats it as optional. - 4.2
internal/db:PendingMigrations(ctx, q, sources)(design D10) inmigrations.go, which begins its transaction withSET LOCAL search_path = pg_catalog, pg_tempand reads each ledger aspublic.goose_db_version_<stream>by qualified name and callspg_catalog.to_regclassandpg_catalog.has_table_privilege(design D2, the qualification principle), with a doc comment that says why it does not use goose's provider and why the names are qualified;ConnectMigration(ctx, logger, config), which opens the migration pool with the startup parameterssearch_path=publicandrole=noneas runtime parameters of the connection string, replacing anysearch_pathorrolethe string carries (ensureSearchPath,internal/db/database.go:43-60, keeps an operator's value and is not reused, so adb-migrate-dsncopied from the runtime string cannot open withcore,public); the runtime pool's open gainsrole=none, always replacing the string's value, beside itssearch_path=core,public(ConnectPlainstays fordbConfig);CurrentUserandGrantRuntimeRole(ctx, tx, name)(design D8, which begins withSET LOCAL search_path = pg_catalog, pg_temp, and whose statement callspg_catalog.set_config,pg_catalog.formatandpg_catalog.current_setting; it takes a transaction because the setting lasts until the transaction ends, andcmdruns it throughdb.InTx) inruntime_user.go, and the refusal typeRuntimeUserRefusal. Every SQL string of these functions is a constant whose name beginsquery(item 5.8 scans them).ConnectAndMigrateis deleted. - 4.3
cmd/database.go(design D11): modesdbRuntime,dbMigrationanddbConfig.dbMigrationopensdb-migrate-dsnwithsearch_path=publicandrole=none(design D2, rule 2) and refuses without it (copy 12).dbConfigis today'sdbPlainondb-dsn.dbRuntimeopens the runtime pool withrole=nonebeside itssearch_path, and runs the boot sequence's steps 2, 3, 6 and 7, step 5's migrations, and the pending check in both modes (copies 4, 4b to 4d, 10, 18, 20); the other checks, and steps 4 and 8 in full, are section 5's, with copy 4a, which belongs to check 5.startandseed-demoopendbRuntime;startlogs a refusal with copy 17; themigratecommands opendbMigration; theconfigcommands opendbConfig. The comment onopenDBsays which mode each command uses and why. - 4.4
internal/embeds/mc-config.yaml: copies 27 and 27a. - 4.5 Test stack (design D14):
test/compose.yamlgivespostgresa health check and addspostgres-seed;test/seed/postgres/seed-runtime-user.sh(new, SPDX header, copies 30 and 31);test/bootstrap-stack.shwritesMC_DB_DSNandMC_DB_MIGRATE_DSN, prints copy 29, refuses a stale.envon a re-run with copy 28, and its header says what it writes;test/reset-test-db.shrefuses a stale.env(copy 28) after its existing guard and runsMC_DB_MIGRATE_DSN="$dsn" go run . migrate up;test/reset-app-db.shrefuses a stale.envand usesMC_DB_MIGRATE_DSNformigrate up, the pin,pg_dumpand the snapshot's address, and its header says so;test/seed-demo.shrefuses a stale.env;test/mc-config.yamlcarries both strings;test/mockshot-nuke.sql's comment (:111-114) says that the stack's migration user is a superuser, whichSET ROLE core_ownerrelies on;test/seed/discourse/seed-discourse.sh's header says the runtime user needsconsole_runtime, which the firststartorreset-app-db.shgrants. - 4.6
test/e2e/browsertest/session.go:AppAdminDBopensMC_DB_MIGRATE_DSNand skips with copy 32;AppDB's comment says it is the runtime user.test/e2e/screens/screens_test.go'spinTimestampsusesAppAdminDB. - 4.7 Docs:
docs/environment-reference.md(rows fordb-migrate-dsnanddb-migrate-dsn-file;db-dsnanddb-dsn-filename the runtime user);docs/hosting.md(five secrets);docs/database-management.md("Migrations run automatically on startup" and theConnectAndMigrateparagraph, design D17; the migration connection opens withsearch_path=public, and theConnectPlainreason is restated for the application'score,publicpath);docs/testing.md,test/AGENTS.md(the.envkeys block, the databases table's user column, the regeneration command at:210-211) andtest/README.md(the Postgres row names both users). - 4.8 Tests:
TestPendingMigrations(internal/db), on scratch databases: migrated in full, nothing pending; a stream stopped short withdbtest.UpTo, its missing versions; a database with no Discourse ledger, Discourse with every version; a ledger whoseSELECTgrant toconsole_runtimeis revoked, the unreadable detail; a temporary table named like a ledger, created on the checking connection, changes no result; each read asconsole_runtimeon a connection withsearch_path=core,public.TestMigrationPoolIgnoresADatabaseSearchPath(internal/db, in adb_testfile such asinternal/db/migration_pool_test.go, because it exercisesdb.ConnectMigrationandschemaSnapshotlives in packagedb_test, whichcmdcannot import), on scratch databases the test creates: withALTER DATABASEsettingsearch_pathto a schema the test created and thenpublic, the migrations run through the migration pool,SHOW search_pathon it returnspublic, and every ledger is inpublicand read byPendingMigrations; a row whose database setsroleto a role the test creates, expectingSELECT current_useron the pool to be the migration login; and two fresh scratch databases with no extra schema (schemaSnapshotreads every user schema, so a database that carries the first row's extra schema never compares equal), one migrated through the migration pool and one by a plain replay (dbtest), with equal snapshots, so a dropped object fails the test.TestGrantRuntimeRole(internal/db), in a transaction it rolls back, whichGrantRuntimeRoletakes because its setting is local to a transaction: a role namedMc-Runtime-<n>, createdLOGIN NOINHERIT, inheritsconsole_runtimeafter the grant; a second grant succeeds and changes nothing.TestOpenRuntimeDatabase(cmd/database_db_test.go), on scratch databases with login roles the test creates with a password and drops in its cleanup: with both strings on a fresh scratch database, the runtime login inheritsconsole_runtime,SELECT current_useron the returned pool is the runtime login, the ledgers read through the returned pool are at every embedded version, andpg_stat_activity, read from the shared test database, shows no session of the migration user on the scratch database; withoutdb-migrate-dsnon a scratch database stopped short, copy 4 with 4d, and the ledgers unchanged; without it on a current database, the pool is returned and nothing ran;migrate upwithoutdb-migrate-dsn, copy 12; a runtime login that is a member ofconsole_runtimeand has runALTER ROLE CURRENT_USER SET role = 'console_runtime'(its later sessions would reportcurrent_useras the group role), a normal boot with the login ascurrent_useron the returned pool.TestValidateStartgains a row for copy 14;TestResolveSecretFiles_DBDSNgains thedb-migrate-dsnpair's two rows;TestDatabaseCommandsReadTheFileKeys'smigraterows setdb-migrate-dsn-fileand expect copy 12 when neither key is set.TestScriptsRefuseAnEnvWithoutTheMigrationDSN(test/scripts_env_test.go), with the shims ofTestScriptsRefuseUnknownArguments, runs each of the four scripts beside a.envwith every other key it reads and asserts copy 28 on stderr, exit status 1, and that no shim ran.TestBootstrapChecksTheFarmName's fixture.envgainsMC_DB_MIGRATE_DSN; its result does not change.
- 4.9 Before this section's
make test, regenerate this worktree'stest/.envwith copy 28's commands;docker compose logs postgres-seedshows copy 30. Thencd test && ./reset-app-db.sh --rebuild, start the application, and check that it boots, that the boot log shows copy 20, and that the operator and member pages load. - 4.10 Walkthroughs from the demo snapshot before and after (
test/AGENTS.md, "Browser walkthroughs"), withgo test -count=1 ./test/e2e/operator-walkthroughs/on the parent commit with thetest/.envit writes, and on this one with the regenerated file: the test names, results and skip messages match.make screenslists no changed screen. - 4.11 Breaks (the migration-free ones need no reset):
dbRuntimeruns the migrations on the runtime pool: caught byTestOpenRuntimeDatabase's first row (the runtime login cannot create a schema);dbRuntimeskips the boot grant: caught by the first row's membership assertion;dbRuntimeleaves the migration pool open: caught by the first row'spg_stat_activityassertion;dbRuntimeskips the pending check whendb-migrate-dsnis unset: caught by the second row;dbRuntimemigrates ondb-dsnwhendb-migrate-dsnis unset: caught by the second row (the ledgers change);GrantRuntimeRolewithoutWITH INHERIT TRUE: caught byTestGrantRuntimeRole;GrantRuntimeRolewith%sin place of%I: caught byTestGrantRuntimeRole(the mixed-case name);PendingMigrationstakes a missing ledger as current: caught byTestPendingMigrations;PendingMigrationsresolves the ledger without thepublic.qualifier: caught by the temporary-table row ofTestPendingMigrations;- the migration pool opened without
search_path=public: caught byTestMigrationPoolIgnoresADatabaseSearchPath(the ledgers land in the test's schema); - the migration pool opened without
role=none: caught by therolerow ofTestMigrationPoolIgnoresADatabaseSearchPath(current_useris the test's role); - the runtime pool opened without
role=none: caught by the self-set-role row ofTestOpenRuntimeDatabase(the boot grant namesconsole_runtimeand fails, copy 10); ConnectMigrationkeeps thesearch_pathorrolethat the string carries, asensureSearchPathdoes: caught byTestMigrationPoolIgnoresADatabaseSearchPath, whose scratch connection string carriessearch_path=core,public&role=<the test's role>;dbMigrationfalls back todb-dsn: caught by themigrate uprow;- the bootstrap guard removed: caught by
TestScriptsRefuseAnEnvWithoutTheMigrationDSN's bootstrap row; reset-app-db.sh's guard after its firstpsql: caught by its row (a shim ran).
5. The start-up check refuses a runtime user that can bypass the grants
- 5.1
internal/db/runtime_user.go:CheckBeforeMigrating(ctx, q)runs the membership half of check 3 and the runtime half of check 4 of design D9 in one read-onlyREPEATABLE READtransaction and returns a*RuntimeUserRefusalwith copy 3a, 3b or 3c, or copy 11 on a failed query; it scans the direct role as nullable.CheckMigrationDatabaseOwner(ctx, q)runs the owner half of check 4 on the migration connection, with qualified names only, and returns a*DatabaseOwnerRefusalwith copy 3d, or copy 11 on a failed query. The identity read on the migration connection isSELECT current_user, pg_catalog.current_database()and nothing else; the superuser bit and the quoted name come from the runtime connection.CheckSchemasBeforeMigrating(ctx, q, login, schemas)runs check 12 (design D9): theCREATEquery overpublicand the five schemas, then check 9's query restricted topublicwith the login as a parameter, and returns a*RuntimeUserRefusalwith copy 3e, 7 or 8, or copy 11;cmdcalls it on the migration connection afterCheckMigrationDatabaseOwner, and on the runtime connection in the separate-step mode.CheckRuntimeUser(ctx, q, check)runs checks 5 to 11 in that order in one such transaction and returns a*RuntimeUserRefusalwith copies 4 to 9, 8a and 9a, or copy 11.RuntimeCheckcarries the stream names and the owner role names;cmdbuilds both frommigrate.Sources(). Every function and catalog relation of every query is written withpg_catalog.(design D2, the qualification principle), and every query is a constant whose name beginsquery. Every transaction these functions open, on either connection (the identity reads, the owner half of check 4, check 12,CheckRuntimeUser's), begins withSET LOCAL search_path = pg_catalog, pg_temp, a constant of its own, before its first query (design D2, the search-path pin). The second query of checks 9 and 12 looks the login up as(SELECT r.oid FROM pg_catalog.pg_roles r WHERE r.rolname = $2::name), never withregrole. The queries of checks 3, 4, 8 and 9 selectpg_catalog.quote_identof the direct role; check 9's first query also selectspg_catalog.pg_get_userbyid(owner), which copy 8 prints; and check 11's two reads return the grantee (PUBLIC, or a quoted role name) frompg_catalog.aclexplodeof the relation's ACL, restricted to entries that reach the user (a grant toPUBLICor to a role the user inherits), so copy 9 prints the oneREVOKEthat removes the privilege; when the grantee is null, a third query names the predefined role (pg_write_all_dataforINSERT,pg_maintainforMAINTAIN) and the role it is granted to, and the refusal is copy 9a. Check 11 also reads the first relation of the five schemas on which the user holdsTRUNCATE,REFERENCES,TRIGGERorMAINTAIN. The pending check of section 4 moves intoCheckRuntimeUseras check 7. - 5.2
cmd/database.go:dbRuntimereads both identities after it opens the pools (the migration connection's with the one qualified query) and refuses checks 1 to 3 (copies 1, 2, 3), callsCheckBeforeMigrating(copies 3a, 3b, 3c), thenCheckMigrationDatabaseOwner(copy 3d), thenCheckSchemasBeforeMigratingon the migration connection (copies 3e, 7, 8), before it migrates and before the boot grant (in the separate-step mode it callsCheckSchemasBeforeMigratingon the runtime connection beforeCheckRuntimeUser), and callsCheckRuntimeUserafter it closes the migration pool; a passing check logs copy 19.dbMigrationcallsCheckMigrationDatabaseOwnerbefore everymigratecommand (up,downandstatus) runs goose, because goose'sStatuscreates any missing ledger inpublic. Nothing readsenv. - 5.3
TestCheckBeforeMigrating(internal/db/runtime_user_test.go), one row per case through a named function, each dropping to the row's role withSET LOCAL ROLEinside a transaction it rolls back that creates the roles the row needs (the database rows run on a scratch database that a role the test creates owns, dropped in cleanup after the database): a member ofconsole_runtimethat owns nothing passes; one that is also a member of a superuser role created in the transaction, copy 3a; the owner of the scratch database, copy 3b naming the database; a member of the role that owns the scratch database, through an intermediate role namedMc-Via-<n>, copy 3c naming the owner and the direct role, quoted; a role that owns another database and not this one passes; and the check run on a closed database (dbtest.Closed), copy 11. (A superuser that does not own the database reaches copy 3a from this function, because the membership half of check 3 runs first and a superuser is a member of the bootstrap superuser; the superuser row ofTestOpenRuntimeDatabasecovers check 4's order.) The same test runsCheckSchemasBeforeMigrating(check 12) with a login role created in the transaction: a login that owns nothing and holds noCREATEpasses; afterGRANT CREATE ON SCHEMA public TO PUBLICin the transaction, copy 3e namingpublicand the granteePUBLIC;GRANT CREATE ON SCHEMA publicto a role the test creates (Mc-Creator, mixed case so the quoting shows) and of which the login is a member withINHERIT FALSE, SET TRUE, copy 3e naming that role quoted, which proves both the grantee and theMEMBERtest; afterGRANT CREATE ON SCHEMA core TOthe login, copy 3e namingcore; a login that owns a function inpublic, copy 7 with the function's signature, and one that is a member of a role that owns such a function throughMc-Via-<n>, copy 8 naming the owner by name and the direct role, quoted, with the login itself namedMc-Rt-<n>so that its name needs quoting (copy 8 and not copy 11, because the second query of check 9 looks the login up by name); a login holdingCREATEonpublicthrough a grant toPUBLICafter a role the test creates has plantedpublic.=(oid, integer), whose function writes a marker row recordingcurrent_userinto a table the test created, copy 3e and no marker row; and a closed database, copy 11.TestCheckMigrationDatabaseOwneradds rows: a scratch database owned by the migration user passes, one owned by a superuser role passes, one owned by a role the test creates, copy 3d naming the owner and the quoted database name, and a closed database, copy 11. Every row but the passing ones asserts the whole sentence. - 5.4
TestCheckRuntimeUser(internal/db/runtime_user_test.go), one row per case through a named function, each in a transaction it rolls back that creates the roles the row needs and drops to the row's role withSET LOCAL ROLE: a member ofconsole_runtimepasses; a role namedMc-Rt-<n>that is no member, copy 5 with the name quoted; a member ofconsole_runtimegrantedWITH INHERIT FALSE, copy 5; a direct member ofcore_owner, copy 6 namingcore_ownerin both places, which also shows that the owner role is reported before check 10; a member ofcore_ownerthrough another role namedMc-Via-<n>, copy 6 naming that role, quoted, in theREVOKE; a member ofcore_ownergrantedWITH INHERIT FALSE, SET TRUE, copy 6; the owner of a table incorecreated in the transaction, copy 7; the owner of a function incore, copy 7 with its schema-qualified signature (the pin of design D2 makesregprocedure::textqualify it); the owner of a function inpublic, copy 7; a member of a role that owns a table, copy 8; a holder ofTRIGGERon a table ofcorethrough a grant to the user, copy 9 namingTRIGGER, the table and the user as grantee; a holder ofTRUNCATEon a table ofcorethrough a grant tocore_reader, whichcore_writerholds, copy 9 namingcore_readeras grantee; a holder ofMAINTAINthroughpg_maintaingranted toconsole_runtime, copy 9a namingpg_maintainandconsole_runtime; a direct member of an unrelated role namedMc-Extra-<n>, copy 8a with both names quoted; a direct member ofpg_execute_server_program, ofpg_read_server_filesand ofpg_write_all_data, copy 8a for each; a holder ofINSERToncore.pool_provisionsthrough a grant to the user, and through a grant toPUBLIC, copy 9 naming the user andPUBLICas grantee; and the check run on a closed database (dbtest.Closed), copy 11. Every row but the passing one asserts the whole sentence. - 5.5
TestOpenRuntimeDatabase(rows of section 4 stay) gains rows: one user for both strings, copy 1, and the ledgers untouched; a superuser runtime login the test creates (LOGIN SUPERUSER), copy 3, the ledgers untouched and no membership granted; a runtime login that owns the scratch database, copy 3b, the ledgers untouched and no membership granted; the same login having createdpublic.quote_ident(name), which writes a marker row (recordingcurrent_userandis_superuser) into a table the test created and the login may insert into, copy 3b, the ledgers untouched and no marker row, because neither connection runs the function; a runtime login that holdsCREATEonpublicand has createdpublic.format(text, text)with such a marker, copy 3e, the ledgers untouched and no marker row; the same login having createdpublic.=(oid, integer)with such a marker, copy 3e, the ledgers untouched and no marker row; a runtime login that owns a function inpublicand holds noCREATE, copy 7, the ledgers untouched;migrate up,migrate downandmigrate statusagainst a scratch database owned by a role that is neither the migration user nor a superuser, copy 3d and the ledgers untouched (forstatus, no ledger created); the same against a scratch database owned by a third role that has runALTER DATABASE ... SET role = '<itself>', copy 3d frommigrate upand the ledgers untouched; a runtime login that holdspg_read_server_files, copy 8a after the migrations ran; two scratch databases, copy 2; and every row runs withenvset todevelopment. Each row's cleanup drops the scratch database before the login that owns it, because Go runst.Cleanupfunctions in reverse order,dbtest.Scratchregisters its drop first, andDROP ROLEfails while the role owns the database. Section 4's rows pass unchanged. - 5.6 Start this worktree's application on the stack and check that the boot log shows copy 19 naming
member_console_runtimeandmember_console. SetMC_DB_DSNto the migration user's string in one shell and check that the start refuses with copy 1. - 5.7 Breaks:
- check 4 before check 3: caught by the superuser row of
TestOpenRuntimeDatabase(a superuser is a member of every role and matches check 4's membership case, so copy 3c with an empty direct role would print in place of copy 3); - checks 1 to 4 after the migrations: caught by the same-user, superuser and database-owner rows' ledger assertions;
- the boot grant before the superuser check: caught by the superuser row's membership assertion;
- the database-ownership check removed: caught by the copy 3b row of
TestCheckBeforeMigratingand the database-owner row ofTestOpenRuntimeDatabase; - check 4 tests
datdba = current_userin place ofpg_has_role(..., 'MEMBER'): caught by the copy 3c row; - check 4 reads the first database in
pg_databasein place ofcurrent_database(): caught by the copy 3b row; - check 4 without its
datname = current_database()filter: caught by the row whose role owns another database; - the identity query run on the migration pool (unqualified
quote_ident,pg_roles): caught by the planted-function row ofTestOpenRuntimeDatabase(the marker row is written); - the runtime identity read with an unqualified
quote_ident: caught by the planted-function row (a marker row is written); - the migration pool without
role=none: caught by the owner-set-role row ofTestOpenRuntimeDatabase(the owner half passes and goose runs as the owner); - check 12's query reads
has_schema_privilegein place of the ACL entries (so aSET-only membership is not counted and the grantee is lost): caught by theMc-Creatorrow ofTestCheckBeforeMigrating; - the
CREATEcheck removed: caught by theCREATErow ofTestCheckBeforeMigratingand thepublic.formatrow ofTestOpenRuntimeDatabase; - check 12's
publichalf removed: caught by the owned-function rows of both tests; - check 12 run after the migrations: caught by the
public.formatand owned-function rows' ledger assertions; - the boot grant's
pg_catalog.formatwrittenformat: caught by the scan of item 5.8; - a check transaction (check 12's, on the migration connection) without its
SET LOCAL search_path = pg_catalog, pg_temp: caught by the planted-operator rows ofTestCheckBeforeMigratingandTestOpenRuntimeDatabase(a marker row is written); CheckRuntimeUser's transaction without the pin: caught by the function rows ofTestCheckRuntimeUser, whose copy 7 asserts the schema-qualified signature (core.<name>(core.<type>), not<name>(<type>));- the second query of check 9 or 12 casting the login with
regrole: caught by the mixed-caseMc-Rt-<n>indirect-owner row ofTestCheckBeforeMigrating(copy 11 in place of copy 8); CheckMigrationDatabaseOwnerremoved fromdbMigration: caught by themigrate uprow ofTestOpenRuntimeDatabase;dbMigrationchecks beforeuponly: caught by themigrate downandmigrate statusrows ofTestOpenRuntimeDatabase(thestatusrow finds a ledger created);CheckMigrationDatabaseOwnerremoved fromdbRuntime: caught bystart's copy 3d row;- the owner half refuses a superuser-owned database: caught by
TestCheckMigrationDatabaseOwner's superuser-owner row; - the refusal of copy 3d prints the name unquoted: caught by the copy 3d row;
- check 11 without the four-privilege read: caught by the
TRIGGERrow ofTestCheckRuntimeUser; - copy 9 prints the user in place of the grantee: caught by the
core_readerrow; - check 11's grantee filter dropped, so every ACL entry counts: caught by the
core_readerrow (a grantee the user does not reach is named in place ofcore_reader); - check 11's predefined-role query removed: caught by the
pg_maintainrow (copy 9 prints with an empty grantee); - copy 3c or copy 6 prints the direct role unquoted: caught by the
Mc-Via-<n>rows; - check 6 tests
MEMBERin place ofUSAGE: caught by a row whose role holds the membership withINHERIT FALSE; - check 8 tests
USAGEin place ofMEMBER: caught by a row whose role holdscore_ownerwithINHERIT FALSE, SET TRUE; - check 9 without functions: caught by both function rows;
- check 9 without
public: caught by thepublicfunction row; - check 9 tests
owner = current_userin place ofpg_has_role(..., 'MEMBER'): caught by the copy 8 row; - the direct-membership check removed: caught by the unrelated-role row, the three predefined-role rows and the
pg_read_server_filesrow ofTestOpenRuntimeDatabase; - the direct-membership check counts
console_runtime: caught by the passing rows; - the direct-membership check before check 8: caught by the direct
core_ownerrow (copy 8a in place of copy 6); - check 11 removed: caught by the two
INSERTrows; - check 3 without its membership half: caught by the copy 3a row;
- the refusal quotes the name in Go (
%q): caught by the copy 5 row; - an exemption when
envisdevelopment: caught by theenvrows ofTestOpenRuntimeDatabase; - a failed check query passes (the error dropped): caught by the closed-database rows, which expect copy 11;
TestOpenRuntimeDatabase's cleanup dropping the login before the scratch database (a test-side record): caught, becauseDROP ROLEfails while the login owns the database;- an unqualified
quote_ident(in the identity query: caught by the scan, naming the query; - an unqualified
pg_rolesin the owner check: caught by the scan; PendingMigrationscallsto_regclasswithoutpg_catalog.: caught by the scan, naming the query andto_regclass(;- the queries' constants renamed so that none begins
query(a test-side record): caught by the scan's own empty-collection failure.
- check 4 before check 3: caught by the superuser row of
- 5.8
TestCheckQueriesQualifyEveryName(internal/db/qualified_names_test.go, design D2, the qualification principle), in packagedb_test: parseruntime_user.goandmigrations.gowithgo/parser, collect every string constant whose name beginsquery(the check queries, the boot-grant statement and the pending check's queries), remove string literals and--comments, and fail for each function call (an identifier directly followed by(, after optional spaces, that nopg_catalog.precedes) and each catalog relation (an identifier startingpg_that nopg_catalog.precedes and that is notpg_catalogitself), naming the query's constant and the token. The exceptions are a table in the test, each with its reason:ANY,ALL,IN,EXISTS,VALUES,CAST,COALESCE,CASE,AND,OR,ON,FROM,JOIN,SELECT,WHERE,WHEN,THEN,ELSEare SQL syntax that precedes a parenthesis, not calls; an identifier directly afterASis an alias with a column list (AS p(priv)). The scan does not look at operators; the search-path pin covers them (design D2), and the planted-operator rows of items 5.3 and 5.5 show it. The pin's own constant (SET LOCAL search_path = pg_catalog, pg_temp) is an exception listed with its reason:pg_tempis a schema name, not a catalog relation. The test also fails when it finds noqueryconstant in either file, so a rename cannot empty it. The breaks in item 5.7 name what the scan must catch.
6. Deployment docs and the procedure for an existing deployment
- 6.1
docs/production-deployment.md, step 5 and step 7, and the procedure for an existing deployment, as design D17 writes them, with placeholders only, including that the runtime login owns neither the database nor any object and holds no role butconsole_runtime, and that the verification readspg_db_role_settingand the functions ofpublic(design D17 step 5). "Retry a failed desired-state delivery by hand" says "the runtime user". - 6.2
docs/database-management.md, "Running the script" and "Logging in": the script runs as the migration user (POSTGRES_USERwith the official image, a superuser); on a managed database, as a user withCREATEROLEthat owns the console's tables. - 6.3 No code changes;
make testpasses.
7. Landing
- 7.1 With the test stack up,
make testpasses (it runsmake lintfirst), andopenspec validate runtime-database-user --strictpasses. - 7.2 Post a comment on #23 under CONTRIBUTING.md's rules for forge text, and close it. The comment states that the console serves from a runtime user that is not a superuser and owns nothing, and migrates on a second connection string that it closes before serving; that the migrations' revokes now bind the console; that the console refuses to start on a configuration that would reopen the bypass, with no exemption; that a host can run migrations as a separate step and keep the migration password out of the running process; that the writer roles now hold only
SELECT,INSERT,UPDATEandDELETE(noTRUNCATE,TRIGGER,MAINTAINorREFERENCES), that the plan-act obligation move takes its row lock through a function owned bycore_owner, because a locking read of an enclosed table needsUPDATE, that the core baseline's initial partitions are computed in UTC, and that the-filekeys of the other commands are fixed in the same change; and that a deployment changes its configuration to upgrade, following the procedure indocs/production-deployment.md. It names the commits of sections 1 to 6. - 7.3 Post a comment on #252 under the same rules:
core.webhook_eventsand its partitions now belong tocore_owner, and partitions are created by aSECURITY DEFINERfunction it owns; the runtime user is refused when it owns or can act as the owner of the database or any object, whoever owns it, so moving the rest is not needed for the fence; moving them needs a per-objectALTER ... OWNER TOloop, because Postgres refusesREASSIGN OWNED BYfor the bootstrap superuser, andSETmembership in each owner role for the migration user; andscripts/db-reader-role.sqlis run by the schemas' owner, which that move would change. - 7.4 Hand the host procedure of
docs/production-deployment.mdto the maintainer, with the values his deployment needs filled in outside the repository. No agent runs it.