From 3f99edfc6fb32971d053d4395b5f2d11da1d64b9 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:38:47 -0400 Subject: [PATCH] =?UTF-8?q?feat(libs/go):=20OpenPool=20=E2=80=94=20one=20a?= =?UTF-8?q?uthenticated=20pool,=20for=20a=20service=20that=20owns=20its=20?= =?UTF-8?q?own=20tenancy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every constructor in this package was opinionated. Open splits a service across a reader and a writer and installs the scoped RLS factory; OpenMaintenance selects an application role and refuses a privileged login. A service with one runtime principal and its own authority over whose tenant a call concerns fits neither — so it called pgxpool.New itself, and in doing so re-decided the transport, the trust anchor and the credential, each of which this package already decides. Four services in this fleet did exactly that. That is not four call sites; it is four copies of three decisions, which is why a managed-database or passwordless posture could not be rolled out by changing one thing. OpenPool is the primitive underneath the other two, exposed: the profile decides the transport and the pinned authority, the options decide the credential, the caller gets a pool. It grants no role, installs no session policy and judges no privilege — a caller wanting those wants Open or OpenMaintenance, and this does not become a relaxed route to them. It pings before returning. pgxpool connects lazily, so a pool handed back before its first connection succeeded moves a wrong credential, an unverifiable server or an unreachable address from boot to the first query, where it reads as a runtime fault rather than a deployment one. Every refusal the opinionated constructors make about a capability's transport and credential holds here too — two credentials for one pool, a password beside a provider, a pinned transport with no anchor, an anchor on a transport that verifies nothing, an undeclared issuer, verify-full on a pinned transport — so this is the same decision by a shorter route, not a weaker one. The legacy zero profile stays accepted, so a service adopts the library in one call and changes its TLS posture as a separate, visible step. Not verified against a live server: the success path needs a reachable Postgres and is exercised only by the refusals and the eager-ping behaviour here. Co-Authored-By: Claude Opus 5 --- libs/go/README.md | 22 +++++++++ libs/go/pool.go | 70 +++++++++++++++++++++++++++ libs/go/pool_test.go | 109 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 libs/go/pool.go create mode 100644 libs/go/pool_test.go diff --git a/libs/go/README.md b/libs/go/README.md index 473a0d4..5f1549c 100644 --- a/libs/go/README.md +++ b/libs/go/README.md @@ -216,6 +216,28 @@ factory, close, err := postgres.Open(ctx, readerURL, writerURL, authenticator, postgres.WithExternalIdentity(declaredIssuer)) ``` +## One authenticated pool, for a service that owns its own tenancy + +`OpenPool(ctx, connection, profile, options...)` opens one pool for one +capability and nothing else: the profile decides the transport and the pinned +authority, the options decide the credential, and the caller gets a pool whose +first connection has already succeeded. It grants no role, installs no session +policy and judges no privilege. + +It exists because every other constructor here is opinionated. `Open` splits a +service across a reader and a writer and installs the scoped RLS factory; +`OpenMaintenance` selects an application role and refuses a privileged login. A +service with one runtime principal and its own authority over whose tenant a +call concerns fits neither, and so called `pgxpool.New` itself — which means it +also re-decided the transport, the trust anchor and the credential, each of +which this package already decides. This is the primitive underneath the other +two, exposed, so adopting the library is one call rather than a redesign. + +`pgxpool` connects lazily, so `OpenPool` pings before returning: a wrong +credential, an unverifiable server or an unreachable address fails at boot, +where it is a deployment fault, rather than at the first query, where it reads +as a runtime one. + ## External identity: the consuming service names no cloud `WithExternalIdentity(declaration)` authenticates every pooled connection as the diff --git a/libs/go/pool.go b/libs/go/pool.go new file mode 100644 index 0000000..cf4fdc0 --- /dev/null +++ b/libs/go/pool.go @@ -0,0 +1,70 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// OpenPool opens one authenticated pool for one capability, and nothing else. +// +// It exists because every other constructor here is opinionated: Open splits a +// service across a reader and a writer and installs the scoped RLS factory, +// OpenMaintenance selects an application role and refuses a privileged login. +// A service that owns its own tenancy model — one runtime principal, its own +// authority on whose tenant a call is about — fits neither, and so wrote +// pgxpool.New itself. Four services in this fleet did, which is four copies of +// the transport and credential decisions this package already makes. +// +// So this is the primitive under the others, exposed: the profile decides the +// transport and the pinned authority, the options decide the credential, and the +// caller gets a pool whose first connection has already succeeded. It grants no +// role, installs no session policy and judges no privilege — a caller wanting +// those wants Open or OpenMaintenance. +// +// The returned closer is safe to call more than once. +func OpenPool(ctx context.Context, connection string, profile ConnectionProfile, options ...Option) (*pgxpool.Pool, func(), error) { + if ctx == nil { + return nil, nil, errors.New("Postgres context is required") + } + if connection == "" { + return nil, nil, errors.New("Postgres connection is required") + } + configuration, err := configured(options...) + if err != nil { + return nil, nil, err + } + // Resolved before the configuration is parsed, because whether a credential + // provider is installed decides whether a password in the URL is legal. + if err := resolveExternalIdentity(ctx, &configuration); err != nil { + return nil, nil, err + } + if configuration.operationTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, configuration.operationTimeout) + defer cancel() + } + poolConfig, err := ParseConnection(connection, profile, configuration.accessTokenProvider != nil) + if err != nil { + return nil, nil, fmt.Errorf("parse Postgres capability: %w", err) + } + if configuration.accessTokenProvider != nil { + installAccessTokenProvider(poolConfig, configuration.accessTokenProvider) + } + pool, err := pgxpool.NewWithConfig(ctx, poolConfig) + if err != nil { + return nil, nil, profileConnectionError(profile, fmt.Errorf("open Postgres capability: %w", err)) + } + // pgxpool connects lazily, so without this the first failure of a wrong + // credential, an unverifiable server or an unreachable address would surface + // at the first query rather than at boot, where it belongs. + if err := pool.Ping(ctx); err != nil { + pool.Close() + return nil, nil, profileConnectionError(profile, fmt.Errorf("ping Postgres capability: %w", err)) + } + var once sync.Once + return pool, func() { once.Do(pool.Close) }, nil +} diff --git a/libs/go/pool_test.go b/libs/go/pool_test.go new file mode 100644 index 0000000..30462a3 --- /dev/null +++ b/libs/go/pool_test.go @@ -0,0 +1,109 @@ +package postgres + +import ( + "context" + "errors" + "strings" + "testing" + "time" +) + +const poolTestHost = "store.identity.internal.example.com" + +func TestOpenPoolRefusesAnIncompleteCall(t *testing.T) { + cleanProfileEnvironment(t) + dsn := "postgresql://writer@" + poolTestHost + "/records?sslmode=verify-full" + //nolint:staticcheck // a nil context is exactly what this asserts is refused + if _, _, err := OpenPool(nil, dsn, ConnectionProfile{Transport: VerifiedTLS}); err == nil { + t.Fatal("a nil context was accepted") + } + if _, _, err := OpenPool(t.Context(), "", ConnectionProfile{Transport: VerifiedTLS}); err == nil { + t.Fatal("an empty connection was accepted") + } +} + +// Every refusal the opinionated constructors make about a capability's +// transport and credential must hold here too: this is the same decision, not a +// relaxed version of it reached by a shorter route. +func TestOpenPoolKeepsEveryTransportAndCredentialRefusal(t *testing.T) { + cleanProfileEnvironment(t) + anchorPEM, _, _ := authority(t, "Cloud SQL Server CA", time.Now().Add(time.Hour)) + provider := func(context.Context, string) (string, error) { return "token", nil } + + for name, call := range map[string]func() error{ + "two credentials for one pool": func() error { + _, _, err := OpenPool(t.Context(), "postgresql://writer@"+poolTestHost+"/records?sslmode=verify-ca", + ConnectionProfile{Transport: PinnedAuthorityTLS, ServerTrustAnchorPEM: anchorPEM}, + WithExternalIdentity(IssuerGCPCloudSQL), WithAccessTokenProvider(provider)) + return err + }, + "a password beside a credential provider": func() error { + _, _, err := OpenPool(t.Context(), "postgresql://writer:seeded@"+poolTestHost+"/records?sslmode=verify-full", + ConnectionProfile{Transport: VerifiedTLS}, WithAccessTokenProvider(provider)) + return err + }, + "a pinned transport with no anchor": func() error { + _, _, err := OpenPool(t.Context(), "postgresql://writer@"+poolTestHost+"/records?sslmode=verify-ca", + ConnectionProfile{Transport: PinnedAuthorityTLS}) + return err + }, + "an anchor on a transport that verifies nothing": func() error { + _, _, err := OpenPool(t.Context(), "postgresql://writer@"+poolTestHost+"/records?sslmode=disable", + ConnectionProfile{Transport: MeshProtected, ServerTrustAnchorPEM: anchorPEM}) + return err + }, + "an undeclared external identity": func() error { + _, _, err := OpenPool(t.Context(), "postgresql://writer@"+poolTestHost+"/records?sslmode=verify-full", + ConnectionProfile{Transport: VerifiedTLS}, WithExternalIdentity("")) + return err + }, + "verify-full on a pinned transport": func() error { + _, _, err := OpenPool(t.Context(), "postgresql://writer@"+poolTestHost+"/records?sslmode=verify-full", + ConnectionProfile{Transport: PinnedAuthorityTLS, ServerTrustAnchorPEM: anchorPEM}) + return err + }, + } { + t.Run(name, func(t *testing.T) { + if err := call(); err == nil { + t.Fatal("accepted") + } + }) + } +} + +// pgxpool connects lazily. A pool handed back before its first connection +// succeeded moves a wrong credential, an unverifiable server or an unreachable +// address from boot to the first query, where it reads as a runtime fault. +func TestOpenPoolFailsAtBootRatherThanAtFirstQuery(t *testing.T) { + cleanProfileEnvironment(t) + pool, close, err := OpenPool(t.Context(), + "postgresql://writer@unresolvable.invalid/records?sslmode=verify-full", + ConnectionProfile{Transport: VerifiedTLS}, + WithOperationTimeout(3*time.Second)) + if err == nil { + close() + t.Fatal("a pool whose server does not resolve was returned as usable") + } + if pool != nil || close != nil { + t.Fatal("a failed open returned a pool or a closer") + } + // An explicit profile reports the capability as unavailable rather than + // handing back the driver's own text, which carries the binding. + if !errors.Is(err, ErrConnectionUnavailable) && !strings.Contains(err.Error(), "ping") { + t.Fatalf("unexpected error shape: %v", err) + } +} + +// The legacy zero profile stays available for a caller not yet declaring a +// transport, so adoption is one call rather than one call plus a posture change. +func TestOpenPoolAcceptsTheLegacyZeroProfile(t *testing.T) { + cleanProfileEnvironment(t) + _, _, err := OpenPool(t.Context(), "postgresql://writer:local@127.0.0.1:1/records?sslmode=disable", + ConnectionProfile{}, WithOperationTimeout(3*time.Second)) + if err == nil { + t.Fatal("a pool to a closed port was returned as usable") + } + if errors.Is(err, ErrConnectionProfile) { + t.Fatalf("the zero profile was rejected as a profile error: %v", err) + } +}