Skip to content

feat: database grantable + connectionUrl removal (#117)#124

Merged
pedromvgomes merged 3 commits into
mainfrom
feature/grant-database
Jun 16, 2026
Merged

feat: database grantable + connectionUrl removal (#117)#124
pedromvgomes merged 3 commits into
mainfrom
feature/grant-database

Conversation

@pedromvgomes

Copy link
Copy Markdown
Contributor

Part of #117. Second slice of ADR-0025 (the Grant capability) — makes the Database Grantable real and removes the credential-bearing connectionUrl ref surface. Builds on slice A (#123, merged: grant core + schema + validation).

What this slice ships (slice B — Database Grantable)

  • Database.Grant is now real. A service that declares grants: [{resource: database/<name>, permission: ro|rw, outputs: …}] gets a scoped per-service Postgres role (never the database owner/admin credential), and its connection fields are materialized into the service's runtime secrets.
  • New neon:resources:NeonRole plugin resource (providers/neon/cmd/pulumi-resource-neon/resources/neon_role.go): creates the role via the Neon API, connects as the database owner over pgx (pure-Go, CGO-free) to apply the GRANTs, and returns {USER,PASSWORD,HOST,PORT,DBNAME}. Delete reassigns/drops the role's owned objects + privileges before the API drop. Owner connection URI is a secret input, redacted from errors.
  • GRANT semantics (confirmed decision): ro = CONNECT+USAGE+SELECT (+ default privileges); rw = ro + INSERT/UPDATE/DELETE + sequence privileges and CREATE ON SCHEMA public so a rw service owns its own migrations (DDL). Identifiers quoted via pgx.Identifier.Sanitize().

Breaking: connectionUrl removed

DatabaseOutputs.ConnectionURL is gone — a database exposes no referenceable output. ref:database/* is now rejected by both inforge validate and the deploy-time resolveRef with a "use a grants: entry for DB credentials" hint. DB credentials flow only through grants. Migrated all in-repo consumers: resolveRef/validate rejection paths, source.go docs, and the validate testdata fixtures (the ok fixture already had a grant; encrypted-ok/provider-defaults-ok/pki-missing-intermediate/global-bad converted their ref:database env vars to grants:). Downstream consumer repos migrate separately.

Regional → global access is uniform

A regional service granting database/global/<name> resolves through the same global/ redirect that ref: uses (the role-provisioning capability rides on types.DatabaseOutputs.RoleProvisioner threaded through AllOutputs, not adapter-instance memory — the global registry isn't shared with the regional loop). The per-service role is named for the consuming service instance (wardnet-<env>-<consumerSlug>-dbrole-<svc>-<db>), so two regions granting one global database never collide.

Deploy wiring

program.resolveDatabaseGrants mints each database/* grant's role and interpolates its outputs: templates over the value fields, then infisical.ProvisionService(…, grantSecrets) merges them into the same /<svc>/infra batch as the environment.yaml secrets. The provision skip now accounts for grant-only services. Bootstrapper untouched (the value/file field-kind split is the point). pki/* grants are not wired (slice C).

Tests / gates

  • grantSQL table test (exact ro/rw statements, DDL-for-rw, identifier quoting, URI parsing); Database.Grant field mapping; adapter ProvisionRole registers NeonRole; resolveDatabaseGrants (global redirect, consumer-scoped naming, pki-skip, missing target); ref:database rejection in validate + resolveRef; migrated fixtures.
  • go build ./..., go test -race ./..., golangci-lint run ./... clean; CGO_ENABLED=0 go build ./... and goreleaser release --snapshot build all four static binaries with pgx linked.

Docs: ADR-0025 Consequences + Status, internal/CONTEXT.md (Database credential access + Source DSL dialogue), AGENTS.md Grants section.

Out of scope

Slice C (PKI resource Grantable: age-encrypted pki.enc.yaml sidecar, inforge pki generate, file-field projection). PKIResource.Grant stays a stub.

@pedromvgomes pedromvgomes added the pki Service-mesh + daemon PKI work label Jun 15, 2026
- add a {URL} value field (role's already-encoded connection URI) and compose
  DSNs with it; hand-assembling {USER}:{PASSWORD}@... fails to URL-encode a
  password with reserved chars (regression vs the passed-through connectionUrl)
- reject two grants on the same resource target in checkGrants (they would
  collide on one per-service role at deploy)
- NeonRole.Delete: best-effort SQL cleanup, always proceed to the control-plane
  role drop so a suspended endpoint at destroy time does not wedge teardown
- NeonRole.Diff: ignore drift in the transient owner connection URI / API key so
  a non-byte-stable Neon URI never churns every consumer role
- redact url.Parse failures so a role/owner password never lands in deploy logs
- share the global/ redirect via types.ResolveScoped (ref: and grants resolve
  a global resource identically) and interpolate only the fields a template uses
@pedromvgomes pedromvgomes merged commit 06ea2fd into main Jun 16, 2026
3 checks passed
@pedromvgomes pedromvgomes deleted the feature/grant-database branch June 16, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pki Service-mesh + daemon PKI work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant