feat(maven): maven remote credential store (S14 Step 7)

What

Step 7 of the Maven remote plan: the remote.CredentialRepo implementation for Maven remote repositories, over the interim tmp_plaintext_username / tmp_plaintext_password columns on maven_remote_repositories (S14 Credentials and health).

  • RemoteCredentialStore in internal/format/maven/remote_credentials.go, on its own store type per the plan's sibling-parallel split from Step 6's finder/health store. A compile-time assertion anchors it against remote.CredentialRepo.
  • Two-tier transaction shape per the S13 seam constraint: the exported SetCredentials / ClearCredentials own their transaction through datastore.RunInTx and delegate to unexported qrm.DB-accepting internals, so S17's update endpoint can later compose the url write and the credential clear in one transaction. Because unexported internals cannot cross the package boundary, S17 lands an exported composer in this package (or exports the internals) when it ships.
  • No policy at the store: S13's remote.CredentialManager owns redaction, the 2048-character cap, the all-or-none invariant, and the audit event. The store persists and reads; credentials flow through labkitsecret.Secret so no value can reach a log. S14 lands no HTTP surface for credentials; S17 owns the endpoints.
  • Store hardening from review: every method rejects a nil ctx, a zero-UUID repositoryID, or a nil qrm.DB handle with an argument-guard sentinel before touching the database; a CHECK refusal (SQLSTATE 23514) maps to the exported ErrMavenRemoteRepositoryCheckViolation carrying only the constraint name, so the pgconn Detail (which echoes the failing row) never travels the chain; and a non-empty AuthToken is rejected with a sentinel — data-integrity rejections of shapes the columns cannot represent, not policy.

Tests

  • remote_credentials_integration_test.go — real-Postgres floor: set/read round-trip, clear-to-NULL with anonymous read-back, rollback discipline of both qrm.DB internals, per-repository isolation, ErrNotFound on a missing remote row, and the CredentialManager-driven cases: URL change with no new pair clears and reports cleared, URL change with a new pair replaces and reports not-cleared, presence-only readback surfaces no value, and over-length / half-set pairs are rejected before persistence, argument guards on all five methods, CHECK-violation sentinel mapping with no credential value leaking into the error, auth-token rejection leaving the stored pair intact, canceled-context read and write never misreported as ErrNotFound, URL-change clearing from an anonymous prior, and an out-of-band ''/'' stored state reading back as not-present. Covers S14 Credentials and health criteria 3 (credential redaction) and 4 (credential clearing on URL change).
  • remote_credentials_internal_test.go — DB-free pins on the SQL-shaping helpers' NULL rendering (NULL::text, no bound argument for the empty value).

E2E scenario impact

None. No Maven e2e scenario catalog exists (docs/testing/e2e/ holds README.md, docker.md, and oci.md only), so this MR adds, changes, and invalidates no catalog scenario. Step 18's hermetic harness is the planned automated coverage.

Notes for reviewers

  • The step landed at ~745 reviewable LOC at open (~1,150 after the review-hardening commits), over the development model's 500 ceiling. Most of the overage is the integration suite's table-driven seed/assert boilerplate, disclosed in the plan's amended Why column for Step 7.
  • The tmp_plaintext_* credential columns are the documented interim divergence from ADR-007 (encrypted_* bytea), recorded in S13 and S14 pending an ADR-007 amendment in the handbook repo; the schema carrying the columns merged in !1120 (merged), and encryption at rest remains GA-blocking under #68.

Database Review Evidence

Queries

Note

Plans are from EXPLAIN (ANALYZE, BUFFERS) against an ephemeral PostgreSQL 17 container (matching GL_PG_CURR_VERSION from .gitlab-ci-other-versions.yml), with synthesized seed data rolled back per query and the container torn down at the end of the run. Numbers reflect moderate cardinality and do not capture production-scale effects. See Database review evidence for seed sizing, methodology, and the anomalies the skill flags. Expand each row's details for the seed shape, rendered SQL, bound args, and raw plan.

Method Plan node Index Rows (plan / actual) Cost Time Buffers (hit / read) Partitions
datastore.findMavenRemoteCredentialsStmt Limit (Nested Loop) maven_remote_repositories_p53_namespace_id_repository_id_idx, repositories_p53_pkey 1 / 1 16.62 0.011ms 6 / 0 1
datastore.updateMavenRemoteCredentialsStmt Update (Nested Loop Semi Join) maven_remote_repositories_p53_namespace_id_repository_id_idx, repositories_p53_pkey 1 / 1 20.60 0.181ms 27 / 2 1
datastore.findMavenRemoteCredentialsStmt

Summary: Plan matches the method's intent — a keyed single-row read joining maven_remote_repositories to repositories through the activeParentPredicate gate (format = maven, kind = remote, not soft-deleted). Both tables prune to one partition via the namespace_id hash literal, and the Nested Loop drives Index Scans on both sides: the (namespace_id, repository_id) unique index on maven_remote_repositories and the pkey on repositories with a filter for soft_deleted_at IS NULL, format = 1, kind = 2. Actual rows match the estimate (1 / 1) and execution stays under 0.03ms at 5000 seeded rows. No anomalies.

Seed shape: namespaces=1, repositories=5000, maven_remote_repositories=5000

Rendered SQL:

SELECT maven_remote_repositories.repository_id AS "maven_remote_repositories.repository_id",
     maven_remote_repositories.tmp_plaintext_username AS "maven_remote_repositories.tmp_plaintext_username",
     maven_remote_repositories.tmp_plaintext_password AS "maven_remote_repositories.tmp_plaintext_password"
FROM public.maven_remote_repositories
     INNER JOIN public.repositories ON ((repositories.id = maven_remote_repositories.repository_id) AND (repositories.namespace_id = maven_remote_repositories.namespace_id))
WHERE ((maven_remote_repositories.namespace_id = $1::uuid) AND (maven_remote_repositories.repository_id = $2::uuid)) AND ((((repositories.namespace_id = $3::uuid) AND (repositories.format = $4)) AND (repositories.kind = $5)) AND (repositories.soft_deleted_at IS NULL))
LIMIT $6;

Bound args: [<namespace_id>, <repository_id>, <namespace_id>, 1, 2, 1]

Plan (EXPLAIN (ANALYZE, BUFFERS) output):

 Limit  (cost=0.56..16.62 rows=1 width=80) (actual time=0.011..0.011 rows=1 loops=1)
   Buffers: shared hit=6
   ->  Nested Loop  (cost=0.56..16.62 rows=1 width=80) (actual time=0.010..0.010 rows=1 loops=1)
         Buffers: shared hit=6
         ->  Index Scan using maven_remote_repositories_p53_namespace_id_repository_id_idx on maven_remote_repositories_p53 maven_remote_repositories  (cost=0.28..8.30 rows=1 width=96) (actual time=0.005..0.005 rows=1 loops=1)
               Index Cond: ((namespace_id = '<ns_id>'::uuid) AND (repository_id = '<repo_id>'::uuid))
               Buffers: shared hit=3
         ->  Index Scan using repositories_p53_pkey on repositories_p53 repositories  (cost=0.28..8.31 rows=1 width=32) (actual time=0.005..0.005 rows=1 loops=1)
               Index Cond: ((id = '<repo_id>'::uuid) AND (namespace_id = '<ns_id>'::uuid))
               Filter: ((soft_deleted_at IS NULL) AND (format = '1'::smallint) AND (kind = '2'::smallint))
               Buffers: shared hit=3
 Planning:
   Buffers: shared hit=670
 Planning Time: 0.915 ms
 Execution Time: 0.021 ms

Timings: planning 0.915ms, execution 0.021ms, total 0.936ms.

datastore.updateMavenRemoteCredentialsStmt

Summary: Plan matches the method's intent — a keyed single-row UPDATE gated by an EXISTS subquery against repositories through the activeParentPredicate gate (format = maven, kind = remote, not soft-deleted). Both tables prune to one partition via the namespace_id hash literal. The Semi Join drives Bitmap Index Scans on both sides: the (namespace_id, repository_id) unique index on maven_remote_repositories and the pkey on repositories with a filter for soft_deleted_at IS NULL, format = 1, kind = 2. The UPDATE fires FK trigger checks for namespace_id and repository_id. Actual rows match the estimate (1 / 1) and execution stays under 0.5ms. No anomalies.

Seed shape: namespaces=1, repositories=50, maven_remote_repositories=50

Rendered SQL:

UPDATE public.maven_remote_repositories
SET (tmp_plaintext_username, tmp_plaintext_password) = ($1::text, $2::text)
WHERE ((maven_remote_repositories.namespace_id = $3::uuid) AND (maven_remote_repositories.repository_id = $4::uuid)) AND (EXISTS (
           SELECT repositories.id AS "repositories.id"
           FROM public.repositories
           WHERE ((repositories.id = maven_remote_repositories.repository_id) AND (repositories.namespace_id = maven_remote_repositories.namespace_id)) AND ((((repositories.namespace_id = $5::uuid) AND (repositories.format = $6)) AND (repositories.kind = $7)) AND (repositories.soft_deleted_at IS NULL))
      ));

Bound args: ['review-user', 'review-pass', <namespace_id>, <repository_id>, <namespace_id>, 1, 2]

Plan (EXPLAIN (ANALYZE, BUFFERS) output):

 Update on maven_remote_repositories  (cost=12.55..20.60 rows=0 width=0) (actual time=0.181..0.182 rows=0 loops=1)
   Update on maven_remote_repositories_p53 maven_remote_repositories_1
   Buffers: shared hit=27 read=2
   ->  Nested Loop Semi Join  (cost=12.55..20.60 rows=1 width=84) (actual time=0.012..0.013 rows=1 loops=1)
         Buffers: shared hit=6
         ->  Bitmap Heap Scan on maven_remote_repositories_p53 maven_remote_repositories_1  (cost=8.28..12.29 rows=1 width=42) (actual time=0.006..0.007 rows=1 loops=1)
               Recheck Cond: ((namespace_id = '<ns_id>'::uuid) AND (repository_id = '<repo_id>'::uuid))
               Heap Blocks: exact=1
               Buffers: shared hit=3
               ->  Bitmap Index Scan on maven_remote_repositories_p53_namespace_id_repository_id_idx  (cost=0.00..8.28 rows=1 width=0) (actual time=0.003..0.004 rows=1 loops=1)
                     Index Cond: ((namespace_id = '<ns_id>'::uuid) AND (repository_id = '<repo_id>'::uuid))
                     Buffers: shared hit=2
         ->  Bitmap Heap Scan on repositories_p53 repositories  (cost=4.28..8.30 rows=1 width=42) (actual time=0.005..0.005 rows=1 loops=1)
               Recheck Cond: ((id = '<repo_id>'::uuid) AND (namespace_id = '<ns_id>'::uuid))
               Filter: ((soft_deleted_at IS NULL) AND (format = '1'::smallint) AND (kind = '2'::smallint))
               Heap Blocks: exact=1
               Buffers: shared hit=3
               ->  Bitmap Index Scan on repositories_p53_pkey  (cost=0.00..4.27 rows=1 width=0) (actual time=0.003..0.003 rows=1 loops=1)
                     Index Cond: ((id = '<repo_id>'::uuid) AND (namespace_id = '<ns_id>'::uuid))
                     Buffers: shared hit=2
 Planning:
   Buffers: shared hit=286
 Planning Time: 0.715 ms
 Trigger for constraint fk_maven_remote_repositories_namespace_id_namespaces on maven_remote_repositories_p53: time=0.021 calls=1
 Trigger for constraint fk_maven_remote_repositories_repository_id_repositories on maven_remote_repositories_p53: time=0.095 calls=1
 Execution Time: 0.424 ms

Timings: planning 0.715ms, execution 0.424ms, total 1.139ms.

Related to #286 (closed)

Edited by Moaz Khalifa

Merge request reports

Loading
Loading