diff --git a/internal/outpost/proxyv2/postgresstore/postgresstore.go b/internal/outpost/proxyv2/postgresstore/postgresstore.go index f4bb1c3f84..fcba534e7c 100644 --- a/internal/outpost/proxyv2/postgresstore/postgresstore.go +++ b/internal/outpost/proxyv2/postgresstore/postgresstore.go @@ -187,10 +187,7 @@ func BuildConnConfig(cfg config.PostgreSQLConfig) (*pgx.ConnConfig, error) { if connConfig.RuntimeParams == nil { connConfig.RuntimeParams = make(map[string]string) } - - if cfg.DefaultSchema != "" { - connConfig.RuntimeParams["search_path"] = cfg.DefaultSchema - } + effectiveSearchPath := cfg.DefaultSchema // Parse and apply connection options if specified if cfg.ConnOptions != "" { @@ -198,12 +195,39 @@ func BuildConnConfig(cfg config.PostgreSQLConfig) (*pgx.ConnConfig, error) { if err != nil { return nil, fmt.Errorf("failed to parse connection options: %w", err) } + // search_path from ConnOptions is not supported here; Django controls schema selection. + // Always remove it so it cannot end up in startup RuntimeParams via applyConnOptions. + delete(connOpts, "search_path") if err := applyConnOptions(connConfig, connOpts); err != nil { return nil, fmt.Errorf("failed to apply connection options: %w", err) } } + // search_path may already be present via pgx/libpq inherited defaults (e.g. service files). + // Always remove it from startup RuntimeParams; apply it via AfterConnect instead. + if inheritedSearchPath, hasInheritedSearchPath := connConfig.RuntimeParams["search_path"]; hasInheritedSearchPath { + if effectiveSearchPath == "" { + effectiveSearchPath = inheritedSearchPath + } + delete(connConfig.RuntimeParams, "search_path") + } + + // Set search_path after connection startup to avoid startup-parameter issues with PgBouncer. + if effectiveSearchPath != "" { + connConfig.AfterConnect = func(ctx context.Context, pgConn *pgconn.PgConn) error { + result := pgConn.ExecParams( + ctx, + "select pg_catalog.set_config('search_path', $1, false)", + [][]byte{[]byte(effectiveSearchPath)}, + nil, + nil, + nil, + ).Read() + return result.Err + } + } + return connConfig, nil } diff --git a/internal/outpost/proxyv2/postgresstore/postgresstore_test.go b/internal/outpost/proxyv2/postgresstore/postgresstore_test.go index 30e7160765..0b48d69b19 100644 --- a/internal/outpost/proxyv2/postgresstore/postgresstore_test.go +++ b/internal/outpost/proxyv2/postgresstore/postgresstore_test.go @@ -700,7 +700,7 @@ func TestBuildConnConfig(t *testing.T) { DefaultSchema: "custom_schema", }, validate: func(t *testing.T, cc *pgx.ConnConfig) { - assert.Equal(t, "custom_schema", cc.RuntimeParams["search_path"]) + assert.NotNil(t, cc.AfterConnect) }, }, { @@ -756,7 +756,7 @@ func TestBuildConnConfig(t *testing.T) { assert.Equal(t, "admin", cc.User) assert.Equal(t, "my super secret password!@#", cc.Password) assert.Equal(t, "production", cc.Database) - assert.Equal(t, "app_schema", cc.RuntimeParams["search_path"]) + assert.NotNil(t, cc.AfterConnect) assert.Equal(t, "authentik", cc.RuntimeParams["application_name"]) }, }, @@ -863,7 +863,7 @@ func TestBuildConnConfig_WithSSLCertificates(t *testing.T) { assert.Equal(t, "db.example.com", cc.TLSConfig.ServerName) assert.NotNil(t, cc.TLSConfig.RootCAs) assert.Len(t, cc.TLSConfig.Certificates, 1) - assert.Equal(t, "app_schema", cc.RuntimeParams["search_path"]) + assert.NotNil(t, cc.AfterConnect) assert.Equal(t, "authentik", cc.RuntimeParams["application_name"]) }, }, @@ -1357,6 +1357,83 @@ func TestBuildConnConfig_WithBase64EncodedConnOptions(t *testing.T) { } } +// Verifies DefaultSchema is applied via AfterConnect and never via startup RuntimeParams. +func TestBuildConnConfig_SearchPath_DefaultSchema(t *testing.T) { + cfg := config.PostgreSQLConfig{ + Host: "localhost", + Port: "5432", + User: "authentik", + Name: "authentik", + DefaultSchema: "default_schema", + } + + connConfig, err := BuildConnConfig(cfg) + require.NoError(t, err) + require.NotNil(t, connConfig.AfterConnect) + _, hasSearchPath := connConfig.RuntimeParams["search_path"] + assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams") +} + +// Verifies ConnOptions search_path is ignored and excluded from startup RuntimeParams. +func TestBuildConnConfig_SearchPath_ConnOptions(t *testing.T) { + cfg := config.PostgreSQLConfig{ + Host: "localhost", + Port: "5432", + User: "authentik", + Name: "authentik", + ConnOptions: base64.StdEncoding.EncodeToString([]byte(`{"search_path":"connopt_schema"}`)), + } + + connConfig, err := BuildConnConfig(cfg) + require.NoError(t, err) + assert.Nil(t, connConfig.AfterConnect) + _, hasSearchPath := connConfig.RuntimeParams["search_path"] + assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams") +} + +// Verifies ConnOptions search_path does not override DefaultSchema and other conn options still apply. +func TestBuildConnConfig_SearchPath_ConnOptionsIgnoredWhenDefaultSchemaSet(t *testing.T) { + cfg := config.PostgreSQLConfig{ + Host: "localhost", + Port: "5432", + User: "authentik", + Name: "authentik", + DefaultSchema: "default_schema", + ConnOptions: base64.StdEncoding.EncodeToString([]byte(`{"search_path":"override_schema","application_name":"authentik-proxy"}`)), + } + + connConfig, err := BuildConnConfig(cfg) + require.NoError(t, err) + require.NotNil(t, connConfig.AfterConnect) + assert.Equal(t, "authentik-proxy", connConfig.RuntimeParams["application_name"]) + _, hasSearchPath := connConfig.RuntimeParams["search_path"] + assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams") +} + +// Verifies inherited search_path from pgx/libpq defaults is removed from startup RuntimeParams. +func TestBuildConnConfig_SearchPath_InheritedServiceSetting(t *testing.T) { + serviceFile := filepath.Join(t.TempDir(), "pg_service.conf") + err := os.WriteFile(serviceFile, []byte("[authentik-test]\nsearch_path=service_schema\n"), 0o600) + require.NoError(t, err) + + t.Setenv("PGSERVICE", "authentik-test") + t.Setenv("PGSERVICEFILE", serviceFile) + + cfg := config.PostgreSQLConfig{ + Host: "localhost", + Port: "5432", + User: "authentik", + Name: "authentik", + } + + connConfig, err := BuildConnConfig(cfg) + require.NoError(t, err) + require.NotNil(t, connConfig.AfterConnect) + + _, hasSearchPath := connConfig.RuntimeParams["search_path"] + assert.False(t, hasSearchPath, "search_path should not appear in RuntimeParams") +} + // TestBuildConnConfig_TargetSessionAttrs demonstrates how target_session_attrs // should be properly handled using pgx's ValidateConnect callback func TestBuildConnConfig_TargetSessionAttrs(t *testing.T) {