Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion configresolve/cliflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func RegisterCLIInputsFlags(fs *flag.FlagSet) func() CLIInputs {
configStoreConn := fs.String("config-store", "", "PostgreSQL connection string for config store (env: DUCKGRES_CONFIG_STORE)")
configPollInterval := fs.String("config-poll-interval", "", "How often to poll config store for changes (default: 30s) (env: DUCKGRES_CONFIG_POLL_INTERVAL)")
internalSecret := fs.String("internal-secret", "", "Shared secret for API authentication (env: DUCKGRES_INTERNAL_SECRET)")
sniRoutingMode := fs.String("sni-routing-mode", "", "Hostname-based org routing: 'off' (default), 'passthrough' (validate managed SNI against requested db; use SNI only when db is empty; log legacy), 'enforce' (require managed SNI and same-org db). Multi-tenant only. (env: DUCKGRES_SNI_ROUTING_MODE)")
sniRoutingMode := fs.String("sni-routing-mode", "", "Hostname-based org routing: 'enforce' (default; require a managed SNI hostname that resolves to an org — the database name selects the catalog, not the org), 'passthrough' (warn on legacy hostnames), 'off' (no SNI handling; identity can no longer be resolved). Multi-tenant only. (env: DUCKGRES_SNI_ROUTING_MODE)")
managedHostnameSuffixes := fs.String("managed-hostname-suffixes", "", "Comma-separated DNS suffixes (each starting with '.') for managed tenant hostnames, e.g. '.dw.us.postwh.com'. (env: DUCKGRES_MANAGED_HOSTNAME_SUFFIXES)")
workerBackend := fs.String("worker-backend", "", "Worker backend: process (default) or remote for config-store-backed K8s multitenant mode (env: DUCKGRES_WORKER_BACKEND)")
k8sWorkerImage := fs.String("k8s-worker-image", "", "Container image for K8s worker pods (env: DUCKGRES_K8S_WORKER_IMAGE)")
Expand Down
7 changes: 7 additions & 0 deletions configresolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,13 @@ func ResolveEffective(fileCfg *configloader.FileConfig, cli CLIInputs, getenv fu
if cli.Set["sni-routing-mode"] {
sniRoutingMode = cli.SNIRoutingMode
}
// Identity in multi-tenant mode is derived solely from the managed hostname
// (SNI) + username; the database name no longer routes. Default to enforcing
// managed SNI so unresolvable hostnames are rejected. (Consulted only on the
// configStore-backed multi-tenant path; standalone/process backends ignore it.)
if sniRoutingMode == "" {
sniRoutingMode = "enforce"
}
if cli.Set["managed-hostname-suffixes"] {
managedHostnameSuffixes = splitAndTrim(cli.ManagedHostnameSuffixes, ",")
}
Expand Down
116 changes: 58 additions & 58 deletions controlplane/configstore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,42 @@ type Snapshot struct {
QueryLog QueryLogConfig
}

// Selectable catalog names. The startup `database` param now names the catalog
// a session defaults to rather than identifying the org — these are the only
// non-empty values a client may request.
const (
catalogDuckLake = "ducklake"
catalogIceberg = "iceberg"
)

// PostgresConnectionResolution is the result of resolving and authenticating a
// Postgres startup packet against one immutable config snapshot.
//
// Identity (OrgID) comes solely from the managed hostname (SNI) plus the
// username/password; the startup `database` param is treated as catalog
// selection, not identity.
type PostgresConnectionResolution struct {
EffectiveDatabase string
OrgID string
SNIOrgID string
SNIDatabase string
SNIAliasUsed bool
UsedSNIDatabase bool
RequiresSNIOrgMatch bool
SNIResolved bool
DatabaseExists bool
HostnameMatches bool
Valid bool
Passthrough bool
DefaultCatalog string
// OrgID is the organization the connection belongs to, resolved from the
// managed hostname (SNI). Empty unless SNIResolved.
OrgID string
// SNIOrgID mirrors OrgID; kept distinct for log/observability parity.
SNIOrgID string
// SNIAliasUsed reports whether the hostname matched via hostname_alias.
SNIAliasUsed bool
// SNIResolved is true when the managed hostname resolved to a known org.
SNIResolved bool
// EffectiveCatalog is the catalog the session should default to, selected by
// the startup `database` param: "" (use the per-user/attached default),
// "ducklake", or "iceberg".
EffectiveCatalog string
// CatalogValid is false when the requested `database` is not a selectable
// catalog name (anything other than "", "ducklake", "iceberg").
CatalogValid bool
// Valid is true when (OrgID, username, password) authenticated.
Valid bool
// Passthrough / DefaultCatalog are the per-user flags for the resolved user.
Passthrough bool
DefaultCatalog string
}

// ConfigStore manages configuration stored in a PostgreSQL database.
Expand Down Expand Up @@ -359,9 +379,21 @@ func isDNSLabel(label string) bool {
}

func (cs *ConfigStore) ResolvePostgresConnection(startupDatabase, sniPrefix string, useManagedSNI bool, username, password string) PostgresConnectionResolution {
result := PostgresConnectionResolution{
EffectiveDatabase: startupDatabase,
HostnameMatches: true,
result := PostgresConnectionResolution{}

// The startup `database` param is now pure catalog selection, not identity.
// Valid values: "" (use the per-user/attached default), "ducklake", or
// "iceberg". Anything else fails closed — there is no logical-name masking,
// so an arbitrary name no longer routes anywhere.
switch strings.ToLower(strings.TrimSpace(startupDatabase)) {
case "":
result.CatalogValid = true
case catalogDuckLake:
result.EffectiveCatalog = catalogDuckLake
result.CatalogValid = true
case catalogIceberg:
result.EffectiveCatalog = catalogIceberg
result.CatalogValid = true
}

cs.mu.RLock()
Expand All @@ -370,38 +402,26 @@ func (cs *ConfigStore) ResolvePostgresConnection(startupDatabase, sniPrefix stri
return result
}

if useManagedSNI {
result.RequiresSNIOrgMatch = true
result.SNIOrgID, result.SNIDatabase, result.SNIAliasUsed = resolveSNIPrefixFromSnapshot(cs.snapshot, sniPrefix)
result.SNIResolved = result.SNIOrgID != ""
if result.SNIDatabase == "" {
result.SNIDatabase = sniPrefix
}
if startupDatabase == "" {
result.EffectiveDatabase = result.SNIDatabase
result.UsedSNIDatabase = true
}
}

if result.EffectiveDatabase == "" {
// Identity comes from the managed hostname (SNI) only. Without a managed,
// resolvable hostname there is no org to authenticate against — the database
// name is no longer consulted for routing.
if !useManagedSNI {
return result
}

orgID := cs.snapshot.DatabaseOrg[result.EffectiveDatabase]
orgID, _, aliasUsed := resolveSNIPrefixFromSnapshot(cs.snapshot, sniPrefix)
if orgID == "" {
return result
}
result.DatabaseExists = true
result.SNIResolved = true
result.SNIAliasUsed = aliasUsed
result.SNIOrgID = orgID
result.OrgID = orgID

if result.RequiresSNIOrgMatch && result.SNIOrgID != orgID {
result.HostnameMatches = false
return result
}

// Authenticate the user within the resolved org.
key := OrgUserKey{OrgID: orgID, Username: username}
storedHash, ok := cs.snapshot.OrgUserPassword[key]
if !ok {
// Timing-leak guard: still spend bcrypt time on unknown users.
_ = bcrypt.CompareHashAndPassword([]byte("$2a$10$000000000000000000000000000000000000000000000000000000"), []byte(password))
return result
}
Expand Down Expand Up @@ -498,26 +518,6 @@ func (cs *ConfigStore) ValidateOrgUserAndGetPassthrough(orgID, username, passwor
return true, cs.snapshot.OrgUserPassthrough[key]
}

// FindAndValidateUser scans all orgs to find and authenticate a user by username/password.
// This is used for Flight SQL which doesn't have SNI-based org routing.
func (cs *ConfigStore) FindAndValidateUser(username, password string) (string, bool) {
cs.mu.RLock()
defer cs.mu.RUnlock()
if cs.snapshot == nil {
return "", false
}
for key, storedHash := range cs.snapshot.OrgUserPassword {
if key.Username == username {
if bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password)) == nil {
return key.OrgID, true
}
return "", false
}
}
_ = bcrypt.CompareHashAndPassword([]byte("$2a$10$000000000000000000000000000000000000000000000000000000"), []byte(password))
return "", false
}

// HashPassword hashes a plaintext password using bcrypt.
func HashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
Expand Down
117 changes: 52 additions & 65 deletions controlplane/configstore/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,103 +301,90 @@ func TestResolvePostgresConnection(t *testing.T) {
},
}

t.Run("explicit database must match managed SNI org", func(t *testing.T) {
t.Run("org resolved from SNI; ducklake catalog selected", func(t *testing.T) {
got := cs.ResolvePostgresConnection(
"test_org_smoke_1778167994",
"ducklake",
"test-org-smoke-1778167994",
true,
"root",
"secret",
)
if got.EffectiveDatabase != "test_org_smoke_1778167994" || got.OrgID != "test-org-smoke-1778167994" {
t.Fatalf("effective route = (%q, %q), want explicit db/org", got.EffectiveDatabase, got.OrgID)
if !got.SNIResolved || got.OrgID != "test-org-smoke-1778167994" {
t.Fatalf("org = (resolved=%v, %q), want test org from SNI: %+v", got.SNIResolved, got.OrgID, got)
}
if got.UsedSNIDatabase {
t.Fatalf("explicit database should take priority over SNI fallback")
if !got.CatalogValid || got.EffectiveCatalog != "ducklake" {
t.Fatalf("catalog = (valid=%v, %q), want ducklake: %+v", got.CatalogValid, got.EffectiveCatalog, got)
}
if !got.DatabaseExists || !got.HostnameMatches || !got.Valid || !got.Passthrough {
t.Fatalf("unexpected result: %+v", got)
if !got.Valid || !got.Passthrough {
t.Fatalf("unexpected auth result: %+v", got)
}
})

t.Run("empty database falls back to managed SNI", func(t *testing.T) {
got := cs.ResolvePostgresConnection(
"",
"test-org-smoke-1778167994",
true,
"root",
"secret",
)
if got.EffectiveDatabase != "test_org_smoke_1778167994" || !got.UsedSNIDatabase {
t.Fatalf("SNI fallback result = (%q, used=%v), want test db/used", got.EffectiveDatabase, got.UsedSNIDatabase)
t.Run("iceberg catalog selected", func(t *testing.T) {
got := cs.ResolvePostgresConnection("iceberg", "test-org-smoke-1778167994", true, "root", "secret")
if !got.CatalogValid || got.EffectiveCatalog != "iceberg" {
t.Fatalf("catalog = (valid=%v, %q), want iceberg: %+v", got.CatalogValid, got.EffectiveCatalog, got)
}
if !got.DatabaseExists || !got.HostnameMatches || !got.Valid {
t.Fatalf("unexpected result: %+v", got)
if !got.Valid {
t.Fatalf("expected valid auth: %+v", got)
}
})

t.Run("two existing orgs mismatch is rejected before auth", func(t *testing.T) {
got := cs.ResolvePostgresConnection(
"test_org_smoke_1778167994",
"billing",
true,
"root",
"secret",
)
if !got.DatabaseExists {
t.Fatalf("expected requested database to exist: %+v", got)
t.Run("empty database means use the default catalog", func(t *testing.T) {
got := cs.ResolvePostgresConnection("", "test-org-smoke-1778167994", true, "root", "secret")
if !got.CatalogValid || got.EffectiveCatalog != "" {
t.Fatalf("catalog = (valid=%v, %q), want empty/use-default: %+v", got.CatalogValid, got.EffectiveCatalog, got)
}
if got.HostnameMatches {
t.Fatalf("expected SNI org billing to mismatch requested database org: %+v", got)
}
if got.Valid {
t.Fatalf("mismatched managed hostname must not authenticate: %+v", got)
if !got.Valid {
t.Fatalf("expected valid auth: %+v", got)
}
})

t.Run("unknown managed SNI with explicit database is rejected before auth", func(t *testing.T) {
got := cs.ResolvePostgresConnection(
"test_org_smoke_1778167994",
"ghostorg",
true,
"root",
"secret",
)
if !got.DatabaseExists {
t.Fatalf("expected requested database to exist: %+v", got)
t.Run("legacy database name is no longer a valid catalog", func(t *testing.T) {
// The org's old database_name is not "ducklake"/"iceberg", so it fails the
// catalog check even though SNI+auth would otherwise succeed.
got := cs.ResolvePostgresConnection("test_org_smoke_1778167994", "test-org-smoke-1778167994", true, "root", "secret")
if got.CatalogValid {
t.Fatalf("legacy database name must not be a selectable catalog: %+v", got)
}
if got.HostnameMatches {
t.Fatalf("expected unknown managed SNI to mismatch requested database org: %+v", got)
})

t.Run("unknown managed SNI does not resolve an org", func(t *testing.T) {
got := cs.ResolvePostgresConnection("ducklake", "ghostorg", true, "root", "secret")
if got.SNIResolved || got.OrgID != "" {
t.Fatalf("unknown SNI must not resolve an org: %+v", got)
}
if got.Valid {
t.Fatalf("unknown managed SNI must not authenticate: %+v", got)
t.Fatalf("unknown SNI must not authenticate: %+v", got)
}
})

t.Run("unknown SNI fallback keeps database-style error target", func(t *testing.T) {
got := cs.ResolvePostgresConnection(
"",
"ghostorg",
true,
"root",
"secret",
)
if got.EffectiveDatabase != "ghostorg" || got.DatabaseExists {
t.Fatalf("unknown SNI fallback = (%q, exists=%v), want ghostorg missing", got.EffectiveDatabase, got.DatabaseExists)
t.Run("identity requires managed SNI", func(t *testing.T) {
got := cs.ResolvePostgresConnection("ducklake", "test-org-smoke-1778167994", false, "root", "secret")
if got.SNIResolved || got.Valid {
t.Fatalf("without managed SNI there is no identity: %+v", got)
}
// Catalog validation is independent of identity.
if !got.CatalogValid || got.EffectiveCatalog != "ducklake" {
t.Fatalf("catalog should still validate: %+v", got)
}
})

t.Run("wrong password fails auth but resolves org", func(t *testing.T) {
got := cs.ResolvePostgresConnection("ducklake", "test-org-smoke-1778167994", true, "root", "wrong")
if !got.SNIResolved || got.Valid {
t.Fatalf("expected resolved org but failed auth: %+v", got)
}
})

t.Run("valid user includes configured default catalog", func(t *testing.T) {
got := cs.ResolvePostgresConnection(
"billing_db",
"billing-alias",
true,
"root",
"secret",
)
got := cs.ResolvePostgresConnection("", "billing-alias", true, "root", "secret")
if !got.Valid {
t.Fatalf("expected valid auth: %+v", got)
}
if got.OrgID != "billing" {
t.Fatalf("OrgID = %q, want billing (via hostname alias)", got.OrgID)
}
if got.DefaultCatalog != "iceberg" {
t.Fatalf("DefaultCatalog = %q, want iceberg", got.DefaultCatalog)
}
Expand Down
Loading
Loading