S04-A per-format columns Step 4: Move Maven's credential reads and writes behind RowEncryptor
Part of #417 (closed), S04-A per-format credential columns. This plan moves the three remote-repository tables off their interim tmp_plaintext_* columns onto the encrypted shape, puts reads and writes behind crypto.RowEncryptor, and registers each table with the namespace-key rotation sweep.
References: S04-A spec · S04-A per-format columns plan (see "Step 4").
Type: feat | Depends on: Step 3, and #513 — merging this step deploys it, and without the encryption block configured its fail-closed arms take every Maven remote credential path down in that environment. See Dependencies. Steps 7 and 10 inherit the same gate for their formats.
Scope
the store reads and writes ciphertext under Step 3's
crypto.TableName; nothing touches tmp_plaintext_*; every arm of S13's
fail-closed table behaves as specified, the health probe included; and the
boot-time warn log names each Maven remote whose plaintext columns are still
populated, in the bounded count-plus-sample shape
Approach specifies.
Files
internal/datastore/maven_remote_credentials.go(Modify): the store, and the twin of npm'sPgError.Detailcomment at:93-95and:116, both of which assert the row carries the plaintext pairinternal/datastore/maven_remote_repositories_errors.md(Modify): the sidecar:13and:33carry the same factinternal/datastore/maven_remote_repositories_create.go(Modify)internal/datastore/maven_remote_repositories_update.go(Modify)internal/datastore/maven_remote_repositories_details.go(Modify):has_credentialsbecomes "the encrypted unit is populated", which iswrapped_dek IS NOT NULL— not the non-NULL-and-non-empty pair the projection carries today (:266-271). The non-empty half cannot survive: the ciphertext of""is a non-NULLbytea, so no predicate over the encrypted columns can tell it from a real value. The write path is what keeps the old meaning — an empty component is rejected before it is encrypted, so the unit is either absent or genuinely populated — andmavenRemoteCredentialIsUsableretires with the SQL half rather than drifting from it, per the comment at:259-263that ties the two spellings together. Steps 7 and 10 take the same definitioninternal/format/maven/remote_health.go(Modify): the probe resolves credentials before composing its request and must fail rather than probe anonymouslyinternal/format/maven/remote_upstream.go(Modify): reads the pair back withSecret.Value()to build the Authorization header, so it moves with the store or every authenticated Maven remote fetch 401s once Step 5 drops the columns — the anonymous-fetch symptom its own comment at:300-304describes, where "an anonymous fallback would instead pull without credentials on a remote that has them configured", not the colon-validation 401 at:305-309cmd/artifact-registry/wire_remote.go(Modify): pass the encryptor and emit the leftover-plaintext log. Maven's credential store is composed here, not inwire_maven.go, which names no credential type
Shared seams
cmd/artifact-registry/wire_remote.go holds the encryptor
hand-off shape that Steps 7 and 10 reuse. This step owns it and is the first
to open the file, so both later steps carry a merge-order edge to this one
and then touch only their own format's constructor call.
Acceptance
with the encryption block present, a credential set and a
subsequent upstream fetch succeed and no plaintext appears in the row; with
the block absent, a credential write is refused and a read of a
credential-bearing row fails rather than fetching anonymously.
Tests
internal/datastore/maven_remote_credentials_test.go and
_integration_test.go (Modify), plus a no-encryptor subtest per fail-closed
arm in the S13 table, and one asserting the leftover-plaintext log carries
a count, at most ten ids, and never a value, and one asserting a clean table
emits a zero count at info rather than nothing, one asserting a failed
count query emits its error line and lets boot proceed, plus one pinning the
new has_credentials meaning: a row
whose encrypted unit is populated reads true and a row with the unit absent
reads false, with the write-side rejection of an empty component asserted in
the same suite, so the retired predicate's guarantee has a new home. Add
internal/datastore/maven_remote_details_integration_test.go (Modify),
whose helper at :491 has seven uses pinning the flag. Also
internal/datastore/maven_remote_repositories_test.go
(Modify), which pins the generated SQL ten times including
tmp_plaintext_username = $2::text (:67, :272-291, :306-359) — this
step changes that SQL — and the seeding suites that write rows through these
columns: internal/managementapi's remote integration tests and
cmd/artifact-registry/wire_connection_test_sources_integration_test.go.
Diff the resulting suite against the pre-change one for dropped subtests,
not only added ones.