mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-13 01:54:20 +08:00
🐛 fix(connection): 隔离保存连接中的扩展密钥
- 将 JVM 凭据与敏感连接参数拆入密钥存储 - 公共和编辑连接视图仅返回脱敏值与存在标记 - 完善连接包迁移、显式清除和密钥往返测试
This commit is contained in:
@@ -155,6 +155,26 @@ func encryptSecretBundle(appKey []byte, bundle connectionSecretBundle, connectio
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
encrypted.JVMJMXPassword, err = encryptSecretField(appKey, bundle.JVMJMXPassword, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
encrypted.JVMEndpointAPIKey, err = encryptSecretField(appKey, bundle.JVMEndpointAPIKey, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
encrypted.JVMAgentAPIKey, err = encryptSecretField(appKey, bundle.JVMAgentAPIKey, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
encrypted.JVMDiagnosticAPIKey, err = encryptSecretField(appKey, bundle.JVMDiagnosticAPIKey, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
encrypted.SensitiveParams, err = encryptSecretField(appKey, bundle.SensitiveParams, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
@@ -199,6 +219,26 @@ func decryptSecretBundle(appKey []byte, bundle connectionSecretBundle, connectio
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
decrypted.JVMJMXPassword, err = decryptSecretField(appKey, bundle.JVMJMXPassword, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
decrypted.JVMEndpointAPIKey, err = decryptSecretField(appKey, bundle.JVMEndpointAPIKey, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
decrypted.JVMAgentAPIKey, err = decryptSecretField(appKey, bundle.JVMAgentAPIKey, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
decrypted.JVMDiagnosticAPIKey, err = decryptSecretField(appKey, bundle.JVMDiagnosticAPIKey, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
decrypted.SensitiveParams, err = decryptSecretField(appKey, bundle.SensitiveParams, connectionID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, err
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
@@ -98,6 +98,11 @@ func TestEncryptSecretBundleRoundTripAndAADBinding(t *testing.T) {
|
||||
RedisSentinelPassword: "sentinel-secret",
|
||||
OpaqueURI: "postgres://user:pass@db.local/app",
|
||||
OpaqueDSN: "server=db.local;password=secret",
|
||||
JVMJMXPassword: "jmx-secret",
|
||||
JVMEndpointAPIKey: "endpoint-key",
|
||||
JVMAgentAPIKey: "agent-key",
|
||||
JVMDiagnosticAPIKey: "diagnostic-key",
|
||||
SensitiveParams: "accessToken=param-secret",
|
||||
}
|
||||
|
||||
encrypted, err := encryptSecretBundle(appKey, plain, "conn-1")
|
||||
@@ -115,6 +120,11 @@ func TestEncryptSecretBundleRoundTripAndAADBinding(t *testing.T) {
|
||||
"redisSentinelPassword": encrypted.RedisSentinelPassword,
|
||||
"opaqueURI": encrypted.OpaqueURI,
|
||||
"opaqueDSN": encrypted.OpaqueDSN,
|
||||
"jvmJMXPassword": encrypted.JVMJMXPassword,
|
||||
"jvmEndpointAPIKey": encrypted.JVMEndpointAPIKey,
|
||||
"jvmAgentAPIKey": encrypted.JVMAgentAPIKey,
|
||||
"jvmDiagnosticAPIKey": encrypted.JVMDiagnosticAPIKey,
|
||||
"sensitiveParams": encrypted.SensitiveParams,
|
||||
} {
|
||||
if value == "" {
|
||||
t.Fatalf("expected encrypted %s field to be populated", name)
|
||||
@@ -125,7 +135,9 @@ func TestEncryptSecretBundleRoundTripAndAADBinding(t *testing.T) {
|
||||
if value == plain.Password || value == plain.SSHPassword || value == plain.ProxyPassword ||
|
||||
value == plain.HTTPTunnelPassword || value == plain.MySQLReplicaPassword || value == plain.MongoReplicaPassword ||
|
||||
value == plain.RedisSentinelPassword ||
|
||||
value == plain.OpaqueURI || value == plain.OpaqueDSN {
|
||||
value == plain.OpaqueURI || value == plain.OpaqueDSN || value == plain.JVMJMXPassword ||
|
||||
value == plain.JVMEndpointAPIKey || value == plain.JVMAgentAPIKey ||
|
||||
value == plain.JVMDiagnosticAPIKey || value == plain.SensitiveParams {
|
||||
t.Fatalf("expected encrypted %s field to differ from plaintext", name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,11 @@ func newSavedConnectionInputFromPackageItem(item connectionPackageItem) connecti
|
||||
config.RedisSentinelPassword = secrets.RedisSentinelPassword
|
||||
config.URI = secrets.OpaqueURI
|
||||
config.DSN = secrets.OpaqueDSN
|
||||
config.JVM.JMX.Password = secrets.JVMJMXPassword
|
||||
config.JVM.Endpoint.APIKey = secrets.JVMEndpointAPIKey
|
||||
config.JVM.Agent.APIKey = secrets.JVMAgentAPIKey
|
||||
config.JVM.Diagnostic.APIKey = secrets.JVMDiagnosticAPIKey
|
||||
config.ConnectionParams = mergeConnectionParams(config.ConnectionParams, secrets.SensitiveParams)
|
||||
|
||||
return connection.SavedConnectionInput{
|
||||
ID: id,
|
||||
@@ -249,6 +254,11 @@ func newSavedConnectionInputFromPackageItem(item connectionPackageItem) connecti
|
||||
ClearRedisSentinelPassword: strings.TrimSpace(secrets.RedisSentinelPassword) == "",
|
||||
ClearOpaqueURI: strings.TrimSpace(secrets.OpaqueURI) == "",
|
||||
ClearOpaqueDSN: strings.TrimSpace(secrets.OpaqueDSN) == "",
|
||||
ClearJVMJMXPassword: strings.TrimSpace(secrets.JVMJMXPassword) == "",
|
||||
ClearJVMEndpointAPIKey: strings.TrimSpace(secrets.JVMEndpointAPIKey) == "",
|
||||
ClearJVMAgentAPIKey: strings.TrimSpace(secrets.JVMAgentAPIKey) == "",
|
||||
ClearJVMDiagnosticAPIKey: strings.TrimSpace(secrets.JVMDiagnosticAPIKey) == "",
|
||||
ClearSensitiveParams: strings.TrimSpace(secrets.SensitiveParams) == "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
81
internal/app/connection_secret_params.go
Normal file
81
internal/app/connection_secret_params.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// partitionConnectionParams keeps ordinary driver options in connection metadata while
|
||||
// routing credential-like query parameters to the connection secret bundle. If the
|
||||
// value cannot be parsed losslessly as a query string, it is treated as secret in its
|
||||
// entirety: exposing less metadata is safer than accidentally serializing a token.
|
||||
func partitionConnectionParams(raw string) (public string, sensitive string) {
|
||||
normalized := strings.TrimSpace(raw)
|
||||
if normalized == "" {
|
||||
return "", ""
|
||||
}
|
||||
normalized = strings.TrimPrefix(normalized, "?")
|
||||
values, err := url.ParseQuery(normalized)
|
||||
if err != nil {
|
||||
return "", raw
|
||||
}
|
||||
|
||||
publicValues := make(url.Values)
|
||||
sensitiveValues := make(url.Values)
|
||||
for key, items := range values {
|
||||
target := publicValues
|
||||
if isSensitiveConnectionParamKey(key) {
|
||||
target = sensitiveValues
|
||||
}
|
||||
for _, item := range items {
|
||||
target.Add(key, item)
|
||||
}
|
||||
}
|
||||
return publicValues.Encode(), sensitiveValues.Encode()
|
||||
}
|
||||
|
||||
func isSensitiveConnectionParamKey(key string) bool {
|
||||
compact := strings.ToLower(strings.TrimSpace(key))
|
||||
compact = strings.NewReplacer("_", "", "-", "", ".", "", " ", "").Replace(compact)
|
||||
if compact == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
switch compact {
|
||||
case "auth", "authorization", "credential", "credentials", "cookie", "sessioncookie", "signature", "sig":
|
||||
return true
|
||||
}
|
||||
return compact == "pwd" ||
|
||||
strings.HasSuffix(compact, "password") ||
|
||||
strings.HasSuffix(compact, "passwd") ||
|
||||
strings.HasSuffix(compact, "token") ||
|
||||
strings.HasSuffix(compact, "apikey") ||
|
||||
strings.HasSuffix(compact, "secret") ||
|
||||
strings.HasSuffix(compact, "secretkey") ||
|
||||
strings.HasSuffix(compact, "privatekey") ||
|
||||
strings.HasSuffix(compact, "accesskey")
|
||||
}
|
||||
|
||||
func mergeConnectionParams(public string, sensitive string) string {
|
||||
public = strings.TrimSpace(public)
|
||||
sensitive = strings.TrimSpace(sensitive)
|
||||
if public == "" {
|
||||
return sensitive
|
||||
}
|
||||
if sensitive == "" {
|
||||
return public
|
||||
}
|
||||
|
||||
publicValues, publicErr := url.ParseQuery(strings.TrimPrefix(public, "?"))
|
||||
sensitiveValues, sensitiveErr := url.ParseQuery(strings.TrimPrefix(sensitive, "?"))
|
||||
if publicErr != nil || sensitiveErr != nil {
|
||||
return strings.TrimSuffix(public, "&") + "&" + strings.TrimPrefix(sensitive, "&")
|
||||
}
|
||||
for key, items := range sensitiveValues {
|
||||
publicValues.Del(key)
|
||||
for _, item := range items {
|
||||
publicValues.Add(key, item)
|
||||
}
|
||||
}
|
||||
return publicValues.Encode()
|
||||
}
|
||||
@@ -61,7 +61,15 @@ func connectionConfigCarriesInlineSecrets(config connection.ConnectionConfig) bo
|
||||
strings.TrimSpace(config.MongoReplicaPassword) != "" ||
|
||||
strings.TrimSpace(config.RedisSentinelPassword) != "" ||
|
||||
strings.TrimSpace(config.URI) != "" ||
|
||||
strings.TrimSpace(config.DSN) != ""
|
||||
strings.TrimSpace(config.DSN) != "" ||
|
||||
strings.TrimSpace(config.JVM.JMX.Password) != "" ||
|
||||
strings.TrimSpace(config.JVM.Endpoint.APIKey) != "" ||
|
||||
strings.TrimSpace(config.JVM.Agent.APIKey) != "" ||
|
||||
strings.TrimSpace(config.JVM.Diagnostic.APIKey) != "" ||
|
||||
func() bool {
|
||||
_, sensitive := partitionConnectionParams(config.ConnectionParams)
|
||||
return strings.TrimSpace(sensitive) != ""
|
||||
}()
|
||||
}
|
||||
|
||||
func mergeInlineConnectionSecrets(base connection.ConnectionConfig, inline connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
@@ -93,6 +101,24 @@ func mergeInlineConnectionSecrets(base connection.ConnectionConfig, inline conne
|
||||
if strings.TrimSpace(inline.DSN) != "" {
|
||||
merged.DSN = inline.DSN
|
||||
}
|
||||
if strings.TrimSpace(inline.JVM.JMX.Password) != "" {
|
||||
merged.JVM.JMX.Password = inline.JVM.JMX.Password
|
||||
}
|
||||
if strings.TrimSpace(inline.JVM.Endpoint.APIKey) != "" {
|
||||
merged.JVM.Endpoint.APIKey = inline.JVM.Endpoint.APIKey
|
||||
}
|
||||
if strings.TrimSpace(inline.JVM.Agent.APIKey) != "" {
|
||||
merged.JVM.Agent.APIKey = inline.JVM.Agent.APIKey
|
||||
}
|
||||
if strings.TrimSpace(inline.JVM.Diagnostic.APIKey) != "" {
|
||||
merged.JVM.Diagnostic.APIKey = inline.JVM.Diagnostic.APIKey
|
||||
}
|
||||
publicParams, sensitiveParams := partitionConnectionParams(inline.ConnectionParams)
|
||||
if strings.TrimSpace(sensitiveParams) != "" {
|
||||
merged.ConnectionParams = mergeConnectionParams(merged.ConnectionParams, sensitiveParams)
|
||||
} else if strings.TrimSpace(merged.ConnectionParams) == "" {
|
||||
merged.ConnectionParams = publicParams
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -157,5 +183,18 @@ func mergeConnectionSecretBundleIntoConfig(config connection.ConnectionConfig, b
|
||||
if strings.TrimSpace(merged.DSN) == "" {
|
||||
merged.DSN = bundle.OpaqueDSN
|
||||
}
|
||||
if strings.TrimSpace(merged.JVM.JMX.Password) == "" {
|
||||
merged.JVM.JMX.Password = bundle.JVMJMXPassword
|
||||
}
|
||||
if strings.TrimSpace(merged.JVM.Endpoint.APIKey) == "" {
|
||||
merged.JVM.Endpoint.APIKey = bundle.JVMEndpointAPIKey
|
||||
}
|
||||
if strings.TrimSpace(merged.JVM.Agent.APIKey) == "" {
|
||||
merged.JVM.Agent.APIKey = bundle.JVMAgentAPIKey
|
||||
}
|
||||
if strings.TrimSpace(merged.JVM.Diagnostic.APIKey) == "" {
|
||||
merged.JVM.Diagnostic.APIKey = bundle.JVMDiagnosticAPIKey
|
||||
}
|
||||
merged.ConnectionParams = mergeConnectionParams(merged.ConnectionParams, bundle.SensitiveParams)
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -103,35 +103,39 @@ func migrateSavedConnectionSecrets(repo *savedConnectionRepository, legacy legac
|
||||
|
||||
func (r *savedConnectionRepository) resolveMigrationConnectionBundle(view connection.SavedConnectionView, legacy legacyWebKitVisibleConfig) (connectionSecretBundle, bool, error) {
|
||||
inline := extractConnectionSecretBundle(view.Config)
|
||||
if inline.hasAny() {
|
||||
return inline, true, nil
|
||||
}
|
||||
|
||||
stored, ok, err := r.dailySecrets().GetConnection(view.ID)
|
||||
if err != nil {
|
||||
return connectionSecretBundle{}, false, err
|
||||
}
|
||||
if ok {
|
||||
return fromDailyConnectionBundle(stored), true, nil
|
||||
return mergeConnectionSecretBundles(fromDailyConnectionBundle(stored), inline), true, nil
|
||||
}
|
||||
|
||||
legacyBundle := findLegacyConnectionSecretBundle(legacy.Connections, view.ID)
|
||||
if legacyBundle.hasAny() {
|
||||
return legacyBundle, true, nil
|
||||
return mergeConnectionSecretBundles(legacyBundle, inline), true, nil
|
||||
}
|
||||
|
||||
if !shouldReadLegacySecretStoreForDailySecrets() {
|
||||
if inline.hasAny() {
|
||||
return inline, true, nil
|
||||
}
|
||||
return connectionSecretBundle{}, false, nil
|
||||
}
|
||||
|
||||
if strings.TrimSpace(view.SecretRef) == "" {
|
||||
if inline.hasAny() {
|
||||
return inline, true, nil
|
||||
}
|
||||
return connectionSecretBundle{}, false, nil
|
||||
}
|
||||
bundle, err := r.loadSecretBundleFromStore(view)
|
||||
if err == nil {
|
||||
return bundle, true, nil
|
||||
return mergeConnectionSecretBundles(bundle, inline), true, nil
|
||||
}
|
||||
if os.IsNotExist(err) || secretstore.IsUnavailable(err) {
|
||||
if inline.hasAny() {
|
||||
return inline, true, nil
|
||||
}
|
||||
return connectionSecretBundle{}, false, nil
|
||||
}
|
||||
return connectionSecretBundle{}, false, err
|
||||
|
||||
@@ -12,6 +12,7 @@ var runtimeGOOS = func() string {
|
||||
}
|
||||
|
||||
func extractConnectionSecretBundle(config connection.ConnectionConfig) connectionSecretBundle {
|
||||
_, sensitiveParams := partitionConnectionParams(config.ConnectionParams)
|
||||
return connectionSecretBundle{
|
||||
Password: config.Password,
|
||||
SSHPassword: config.SSH.Password,
|
||||
@@ -22,6 +23,11 @@ func extractConnectionSecretBundle(config connection.ConnectionConfig) connectio
|
||||
RedisSentinelPassword: config.RedisSentinelPassword,
|
||||
OpaqueURI: config.URI,
|
||||
OpaqueDSN: config.DSN,
|
||||
JVMJMXPassword: config.JVM.JMX.Password,
|
||||
JVMEndpointAPIKey: config.JVM.Endpoint.APIKey,
|
||||
JVMAgentAPIKey: config.JVM.Agent.APIKey,
|
||||
JVMDiagnosticAPIKey: config.JVM.Diagnostic.APIKey,
|
||||
SensitiveParams: sensitiveParams,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +42,11 @@ func toDailyConnectionBundle(bundle connectionSecretBundle) dailysecret.Connecti
|
||||
RedisSentinelPassword: bundle.RedisSentinelPassword,
|
||||
OpaqueURI: bundle.OpaqueURI,
|
||||
OpaqueDSN: bundle.OpaqueDSN,
|
||||
JVMJMXPassword: bundle.JVMJMXPassword,
|
||||
JVMEndpointAPIKey: bundle.JVMEndpointAPIKey,
|
||||
JVMAgentAPIKey: bundle.JVMAgentAPIKey,
|
||||
JVMDiagnosticAPIKey: bundle.JVMDiagnosticAPIKey,
|
||||
SensitiveParams: bundle.SensitiveParams,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +61,11 @@ func fromDailyConnectionBundle(bundle dailysecret.ConnectionBundle) connectionSe
|
||||
RedisSentinelPassword: bundle.RedisSentinelPassword,
|
||||
OpaqueURI: bundle.OpaqueURI,
|
||||
OpaqueDSN: bundle.OpaqueDSN,
|
||||
JVMJMXPassword: bundle.JVMJMXPassword,
|
||||
JVMEndpointAPIKey: bundle.JVMEndpointAPIKey,
|
||||
JVMAgentAPIKey: bundle.JVMAgentAPIKey,
|
||||
JVMDiagnosticAPIKey: bundle.JVMDiagnosticAPIKey,
|
||||
SensitiveParams: bundle.SensitiveParams,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +80,11 @@ func stripConnectionSecretFields(config connection.ConnectionConfig) connection.
|
||||
stripped.RedisSentinelPassword = ""
|
||||
stripped.URI = ""
|
||||
stripped.DSN = ""
|
||||
stripped.JVM.JMX.Password = ""
|
||||
stripped.JVM.Endpoint.APIKey = ""
|
||||
stripped.JVM.Agent.APIKey = ""
|
||||
stripped.JVM.Diagnostic.APIKey = ""
|
||||
stripped.ConnectionParams, _ = partitionConnectionParams(stripped.ConnectionParams)
|
||||
return stripped
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,9 @@ func (a *App) GetEditableSavedConnection(id string) (connection.SavedConnectionV
|
||||
if err != nil {
|
||||
return connection.SavedConnectionView{}, err
|
||||
}
|
||||
resolvedConfig, err := a.resolveConnectionSecrets(view.Config)
|
||||
if err != nil {
|
||||
return connection.SavedConnectionView{}, err
|
||||
}
|
||||
view.Config = resolvedConfig
|
||||
return view, nil
|
||||
// Editing relies on the Has* flags and explicit clear fields. Returning the
|
||||
// resolved bundle would expose every saved credential to the WebView.
|
||||
return sanitizeSavedConnectionView(view), nil
|
||||
}
|
||||
|
||||
func (a *App) SaveConnection(input connection.SavedConnectionInput) (connection.SavedConnectionView, error) {
|
||||
@@ -74,6 +71,12 @@ func (a *App) ImportLegacyConnections(items []connection.LegacySavedConnection)
|
||||
input.ClearRedisSentinelPassword = strings.TrimSpace(item.Config.RedisSentinelPassword) == ""
|
||||
input.ClearOpaqueURI = strings.TrimSpace(item.Config.URI) == ""
|
||||
input.ClearOpaqueDSN = strings.TrimSpace(item.Config.DSN) == ""
|
||||
input.ClearJVMJMXPassword = strings.TrimSpace(item.Config.JVM.JMX.Password) == ""
|
||||
input.ClearJVMEndpointAPIKey = strings.TrimSpace(item.Config.JVM.Endpoint.APIKey) == ""
|
||||
input.ClearJVMAgentAPIKey = strings.TrimSpace(item.Config.JVM.Agent.APIKey) == ""
|
||||
input.ClearJVMDiagnosticAPIKey = strings.TrimSpace(item.Config.JVM.Diagnostic.APIKey) == ""
|
||||
_, sensitiveParams := partitionConnectionParams(item.Config.ConnectionParams)
|
||||
input.ClearSensitiveParams = strings.TrimSpace(sensitiveParams) == ""
|
||||
inputs = append(inputs, input)
|
||||
}
|
||||
views, err := a.importSavedConnectionsAtomically(inputs)
|
||||
|
||||
@@ -225,7 +225,7 @@ func TestSaveConnectionSanitizesSchemaVisibilityRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEditableSavedConnectionReturnsResolvedSecretsForEdit(t *testing.T) {
|
||||
func TestGetEditableSavedConnectionReturnsSecretlessViewForEdit(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
@@ -255,11 +255,11 @@ func TestGetEditableSavedConnectionReturnsResolvedSecretsForEdit(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if view.Config.Password != "mysql-secret" {
|
||||
t.Fatalf("expected editable primary password, got %q", view.Config.Password)
|
||||
if view.Config.Password != "" {
|
||||
t.Fatalf("editable view must not expose primary password, got %q", view.Config.Password)
|
||||
}
|
||||
if view.Config.SSH.Password != "ssh-secret" {
|
||||
t.Fatalf("expected editable SSH password, got %q", view.Config.SSH.Password)
|
||||
if view.Config.SSH.Password != "" {
|
||||
t.Fatalf("editable view must not expose SSH password, got %q", view.Config.SSH.Password)
|
||||
}
|
||||
if !view.HasPrimaryPassword || !view.HasSSHPassword {
|
||||
t.Fatalf("expected secret flags to stay true, got %#v", view)
|
||||
|
||||
363
internal/app/saved_connection_secret_boundary_test.go
Normal file
363
internal/app/saved_connection_secret_boundary_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
)
|
||||
|
||||
func TestPartitionConnectionParamsSeparatesCredentialKeys(t *testing.T) {
|
||||
public, sensitive := partitionConnectionParams(
|
||||
"application_name=gonavi&connectTimeout=10&accessToken=token-secret&client_secret=client-secret&PASSWORD=db-secret",
|
||||
)
|
||||
publicValues, err := url.ParseQuery(public)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if publicValues.Get("application_name") != "gonavi" || publicValues.Get("connectTimeout") != "10" {
|
||||
t.Fatalf("ordinary parameters were not preserved: %q", public)
|
||||
}
|
||||
if strings.Contains(public, "token-secret") || strings.Contains(public, "client-secret") || strings.Contains(public, "db-secret") {
|
||||
t.Fatalf("public parameters contain credentials: %q", public)
|
||||
}
|
||||
|
||||
sensitiveValues, err := url.ParseQuery(sensitive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sensitiveValues.Get("accessToken") != "token-secret" ||
|
||||
sensitiveValues.Get("client_secret") != "client-secret" ||
|
||||
sensitiveValues.Get("PASSWORD") != "db-secret" {
|
||||
t.Fatalf("credential parameters were not retained in the secret partition: %q", sensitive)
|
||||
}
|
||||
|
||||
merged, err := url.ParseQuery(mergeConnectionParams(public, sensitive))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if merged.Get("application_name") != "gonavi" || merged.Get("accessToken") != "token-secret" {
|
||||
t.Fatalf("merged parameters do not reconstruct runtime configuration: %#v", merged)
|
||||
}
|
||||
|
||||
malformedPublic, malformedSecret := partitionConnectionParams("token=secret;broken")
|
||||
if malformedPublic != "" || malformedSecret != "token=secret;broken" {
|
||||
t.Fatalf("malformed parameters must fail closed, public=%q secret=%q", malformedPublic, malformedSecret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedConnectionJVMSecretsAndSensitiveParamsStayOutOfPublicMetadata(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
|
||||
input := connection.SavedConnectionInput{
|
||||
ID: "conn-secret-boundary",
|
||||
Name: "Secret boundary",
|
||||
Config: connection.ConnectionConfig{
|
||||
ID: "conn-secret-boundary",
|
||||
Type: "jvm",
|
||||
Host: "jvm.local",
|
||||
Password: "primary-secret",
|
||||
URI: "https://user:uri-secret@jvm.local",
|
||||
DSN: "password=dsn-secret",
|
||||
ConnectionParams: "application_name=gonavi&accessToken=param-token&client_secret=param-secret",
|
||||
JVM: connection.JVMConfig{
|
||||
JMX: connection.JVMJMXConfig{
|
||||
Enabled: true,
|
||||
Username: "monitor",
|
||||
Password: "jmx-secret",
|
||||
},
|
||||
Endpoint: connection.JVMEndpointConfig{Enabled: true, BaseURL: "https://endpoint.local", APIKey: "endpoint-key"},
|
||||
Agent: connection.JVMAgentConfig{Enabled: true, BaseURL: "https://agent.local", APIKey: "agent-key"},
|
||||
Diagnostic: connection.JVMDiagnosticConfig{
|
||||
Enabled: true,
|
||||
BaseURL: "https://diagnostic.local",
|
||||
APIKey: "diagnostic-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
saved, err := app.SaveConnection(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertPublicSavedConnectionSecretless(t, saved)
|
||||
if !saved.HasPrimaryPassword || !saved.HasOpaqueURI || !saved.HasOpaqueDSN ||
|
||||
!saved.HasJVMJMXPassword || !saved.HasJVMEndpointAPIKey || !saved.HasJVMAgentAPIKey ||
|
||||
!saved.HasJVMDiagnosticAPIKey || !saved.HasSensitiveParams {
|
||||
t.Fatalf("expected all saved secret flags, got %#v", saved)
|
||||
}
|
||||
|
||||
rawView, err := app.savedConnectionRepository().Find(input.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertPublicSavedConnectionSecretless(t, rawView)
|
||||
metadata, err := os.ReadFile(app.savedConnectionRepository().connectionsPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertJSONOmitsSecretLiterals(t, metadata)
|
||||
|
||||
bundle, ok, err := app.dailySecretStore().GetConnection(input.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("expected secret bundle to be persisted")
|
||||
}
|
||||
if bundle.JVMJMXPassword != "jmx-secret" || bundle.JVMEndpointAPIKey != "endpoint-key" ||
|
||||
bundle.JVMAgentAPIKey != "agent-key" || bundle.JVMDiagnosticAPIKey != "diagnostic-key" {
|
||||
t.Fatalf("JVM credentials were not persisted in the secret store: %#v", bundle)
|
||||
}
|
||||
sensitiveValues, err := url.ParseQuery(bundle.SensitiveParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sensitiveValues.Get("accessToken") != "param-token" || sensitiveValues.Get("client_secret") != "param-secret" {
|
||||
t.Fatalf("sensitive connection params were not persisted in the secret store: %q", bundle.SensitiveParams)
|
||||
}
|
||||
|
||||
resolved, err := app.resolveConnectionSecrets(rawView.Config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolved.Password != "primary-secret" || resolved.URI == "" || resolved.DSN == "" ||
|
||||
resolved.JVM.JMX.Password != "jmx-secret" || resolved.JVM.Endpoint.APIKey != "endpoint-key" ||
|
||||
resolved.JVM.Agent.APIKey != "agent-key" || resolved.JVM.Diagnostic.APIKey != "diagnostic-key" {
|
||||
t.Fatalf("runtime secret resolution did not restore credentials: %#v", resolved)
|
||||
}
|
||||
resolvedParams, err := url.ParseQuery(resolved.ConnectionParams)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolvedParams.Get("application_name") != "gonavi" || resolvedParams.Get("accessToken") != "param-token" {
|
||||
t.Fatalf("runtime connection params were not reconstructed: %#v", resolvedParams)
|
||||
}
|
||||
|
||||
listed, err := app.GetSavedConnections()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(listed) != 1 {
|
||||
t.Fatalf("expected one saved connection, got %d", len(listed))
|
||||
}
|
||||
assertPublicSavedConnectionSecretless(t, listed[0])
|
||||
listedJSON, err := json.Marshal(listed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertJSONOmitsSecretLiterals(t, listedJSON)
|
||||
|
||||
editable, err := app.GetEditableSavedConnection(input.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertPublicSavedConnectionSecretless(t, editable)
|
||||
editableJSON, err := json.Marshal(editable)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertJSONOmitsSecretLiterals(t, editableJSON)
|
||||
|
||||
// A metadata-only edit must keep the existing bundle even though no secret is
|
||||
// returned to the WebView and therefore none is echoed back.
|
||||
update := connection.SavedConnectionInput{ID: saved.ID, Name: "Renamed", Config: saved.Config}
|
||||
updated, err := app.SaveConnection(update)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !updated.HasJVMJMXPassword || !updated.HasSensitiveParams {
|
||||
t.Fatalf("metadata-only save dropped secret flags: %#v", updated)
|
||||
}
|
||||
resolvedAfterUpdate, err := app.resolveConnectionSecrets(updated.Config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolvedAfterUpdate.JVM.JMX.Password != "jmx-secret" ||
|
||||
!strings.Contains(resolvedAfterUpdate.ConnectionParams, "param-token") {
|
||||
t.Fatalf("metadata-only save dropped stored secrets: %#v", resolvedAfterUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveConnectionExplicitlyClearsNewSecretFields(t *testing.T) {
|
||||
app := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
app.configDir = t.TempDir()
|
||||
initial, err := app.SaveConnection(connection.SavedConnectionInput{
|
||||
ID: "conn-clear-new-secrets",
|
||||
Config: connection.ConnectionConfig{
|
||||
ID: "conn-clear-new-secrets",
|
||||
Type: "jvm",
|
||||
ConnectionParams: "application_name=gonavi&token=secret",
|
||||
JVM: connection.JVMConfig{
|
||||
JMX: connection.JVMJMXConfig{Password: "jmx-secret"},
|
||||
Endpoint: connection.JVMEndpointConfig{APIKey: "endpoint-key"},
|
||||
Agent: connection.JVMAgentConfig{APIKey: "agent-key"},
|
||||
Diagnostic: connection.JVMDiagnosticConfig{APIKey: "diagnostic-key"},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleared, err := app.SaveConnection(connection.SavedConnectionInput{
|
||||
ID: initial.ID,
|
||||
Config: initial.Config,
|
||||
ClearJVMJMXPassword: true,
|
||||
ClearJVMEndpointAPIKey: true,
|
||||
ClearJVMAgentAPIKey: true,
|
||||
ClearJVMDiagnosticAPIKey: true,
|
||||
ClearSensitiveParams: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cleared.HasJVMJMXPassword || cleared.HasJVMEndpointAPIKey || cleared.HasJVMAgentAPIKey ||
|
||||
cleared.HasJVMDiagnosticAPIKey || cleared.HasSensitiveParams {
|
||||
t.Fatalf("explicit clear did not reset secret flags: %#v", cleared)
|
||||
}
|
||||
resolved, err := app.resolveConnectionSecrets(cleared.Config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolved.JVM.JMX.Password != "" || resolved.JVM.Endpoint.APIKey != "" ||
|
||||
resolved.JVM.Agent.APIKey != "" || resolved.JVM.Diagnostic.APIKey != "" ||
|
||||
strings.Contains(resolved.ConnectionParams, "secret") {
|
||||
t.Fatalf("explicit clear did not remove secret material: %#v", resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionPackageEncryptsAndRestoresNewSecretFields(t *testing.T) {
|
||||
source := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
source.configDir = t.TempDir()
|
||||
if _, err := source.SaveConnection(connection.SavedConnectionInput{
|
||||
ID: "conn-package-secrets",
|
||||
Name: "Package secrets",
|
||||
Config: connection.ConnectionConfig{
|
||||
ID: "conn-package-secrets",
|
||||
Type: "jvm",
|
||||
ConnectionParams: "application_name=gonavi&token=package-token",
|
||||
JVM: connection.JVMConfig{
|
||||
JMX: connection.JVMJMXConfig{Password: "package-jmx-secret"},
|
||||
Endpoint: connection.JVMEndpointConfig{APIKey: "package-endpoint-key"},
|
||||
Agent: connection.JVMAgentConfig{APIKey: "package-agent-key"},
|
||||
Diagnostic: connection.JVMDiagnosticConfig{APIKey: "package-diagnostic-key"},
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exported, err := source.buildExportedConnectionPackage(ConnectionExportOptions{IncludeSecrets: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, secret := range []string{
|
||||
"package-token", "package-jmx-secret", "package-endpoint-key", "package-agent-key", "package-diagnostic-key",
|
||||
} {
|
||||
if strings.Contains(string(exported), secret) {
|
||||
t.Fatalf("encrypted connection package contains plaintext %q", secret)
|
||||
}
|
||||
}
|
||||
|
||||
destination := NewAppWithSecretStore(newFakeAppSecretStore())
|
||||
destination.configDir = t.TempDir()
|
||||
result, err := destination.ImportConnectionsPayload(string(exported), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Connections) != 1 {
|
||||
t.Fatalf("expected one imported connection, got %d", len(result.Connections))
|
||||
}
|
||||
assertPublicSavedConnectionSecretless(t, result.Connections[0])
|
||||
resolved, err := destination.resolveConnectionSecrets(result.Connections[0].Config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resolved.JVM.JMX.Password != "package-jmx-secret" ||
|
||||
resolved.JVM.Endpoint.APIKey != "package-endpoint-key" ||
|
||||
resolved.JVM.Agent.APIKey != "package-agent-key" ||
|
||||
resolved.JVM.Diagnostic.APIKey != "package-diagnostic-key" ||
|
||||
!strings.Contains(resolved.ConnectionParams, "package-token") {
|
||||
t.Fatalf("import did not restore encrypted secret fields: %#v", resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailySecretMigrationMergesInlineJVMSecretsWithExistingBundle(t *testing.T) {
|
||||
repo := newSavedConnectionRepository(t.TempDir(), newFakeAppSecretStore())
|
||||
if err := repo.saveAll([]connection.SavedConnectionView{{
|
||||
ID: "conn-migrate-new-secrets",
|
||||
Name: "Migration",
|
||||
HasPrimaryPassword: true,
|
||||
Config: connection.ConnectionConfig{
|
||||
ID: "conn-migrate-new-secrets",
|
||||
Type: "jvm",
|
||||
ConnectionParams: "application_name=gonavi&accessToken=migrated-token",
|
||||
JVM: connection.JVMConfig{
|
||||
JMX: connection.JVMJMXConfig{Password: "migrated-jmx-secret"},
|
||||
Endpoint: connection.JVMEndpointConfig{APIKey: "migrated-endpoint-key"},
|
||||
},
|
||||
},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.dailySecrets().PutConnection("conn-migrate-new-secrets", toDailyConnectionBundle(connectionSecretBundle{
|
||||
Password: "existing-primary-secret",
|
||||
})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := migrateSavedConnectionSecrets(repo, legacyWebKitVisibleConfig{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
view, err := repo.Find("conn-migrate-new-secrets")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertPublicSavedConnectionSecretless(t, view)
|
||||
if !view.HasPrimaryPassword || !view.HasJVMJMXPassword || !view.HasJVMEndpointAPIKey || !view.HasSensitiveParams {
|
||||
t.Fatalf("migration did not preserve all secret flags: %#v", view)
|
||||
}
|
||||
bundle, ok, err := repo.dailySecrets().GetConnection(view.ID)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("expected migrated bundle, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if bundle.Password != "existing-primary-secret" || bundle.JVMJMXPassword != "migrated-jmx-secret" ||
|
||||
bundle.JVMEndpointAPIKey != "migrated-endpoint-key" || !strings.Contains(bundle.SensitiveParams, "migrated-token") {
|
||||
t.Fatalf("migration dropped old or new secret material: %#v", bundle)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPublicSavedConnectionSecretless(t *testing.T, view connection.SavedConnectionView) {
|
||||
t.Helper()
|
||||
config := view.Config
|
||||
if config.Password != "" || config.SSH.Password != "" || config.Proxy.Password != "" ||
|
||||
config.HTTPTunnel.Password != "" || config.MySQLReplicaPassword != "" ||
|
||||
config.MongoReplicaPassword != "" || config.RedisSentinelPassword != "" ||
|
||||
config.URI != "" || config.DSN != "" || config.JVM.JMX.Password != "" ||
|
||||
config.JVM.Endpoint.APIKey != "" || config.JVM.Agent.APIKey != "" ||
|
||||
config.JVM.Diagnostic.APIKey != "" {
|
||||
t.Fatalf("public saved connection contains a secret field: %#v", config)
|
||||
}
|
||||
_, sensitive := partitionConnectionParams(config.ConnectionParams)
|
||||
if sensitive != "" {
|
||||
t.Fatalf("public saved connection contains sensitive connection params: %q", config.ConnectionParams)
|
||||
}
|
||||
}
|
||||
|
||||
func assertJSONOmitsSecretLiterals(t *testing.T, payload []byte) {
|
||||
t.Helper()
|
||||
for _, secret := range []string{
|
||||
"primary-secret", "uri-secret", "dsn-secret", "param-token", "param-secret",
|
||||
"jmx-secret", "endpoint-key", "agent-key", "diagnostic-key",
|
||||
} {
|
||||
if strings.Contains(string(payload), secret) {
|
||||
t.Fatalf("public/metadata JSON contains secret literal %q: %s", secret, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,11 @@ type connectionSecretBundle struct {
|
||||
RedisSentinelPassword string `json:"redisSentinelPassword,omitempty"`
|
||||
OpaqueURI string `json:"opaqueURI,omitempty"`
|
||||
OpaqueDSN string `json:"opaqueDSN,omitempty"`
|
||||
JVMJMXPassword string `json:"jvmJMXPassword,omitempty"`
|
||||
JVMEndpointAPIKey string `json:"jvmEndpointAPIKey,omitempty"`
|
||||
JVMAgentAPIKey string `json:"jvmAgentAPIKey,omitempty"`
|
||||
JVMDiagnosticAPIKey string `json:"jvmDiagnosticAPIKey,omitempty"`
|
||||
SensitiveParams string `json:"sensitiveConnectionParams,omitempty"`
|
||||
}
|
||||
|
||||
type savedConnectionsFile struct {
|
||||
@@ -94,7 +99,12 @@ func (b connectionSecretBundle) hasAny() bool {
|
||||
strings.TrimSpace(b.MongoReplicaPassword) != "" ||
|
||||
strings.TrimSpace(b.RedisSentinelPassword) != "" ||
|
||||
strings.TrimSpace(b.OpaqueURI) != "" ||
|
||||
strings.TrimSpace(b.OpaqueDSN) != ""
|
||||
strings.TrimSpace(b.OpaqueDSN) != "" ||
|
||||
strings.TrimSpace(b.JVMJMXPassword) != "" ||
|
||||
strings.TrimSpace(b.JVMEndpointAPIKey) != "" ||
|
||||
strings.TrimSpace(b.JVMAgentAPIKey) != "" ||
|
||||
strings.TrimSpace(b.JVMDiagnosticAPIKey) != "" ||
|
||||
strings.TrimSpace(b.SensitiveParams) != ""
|
||||
}
|
||||
|
||||
func mergeConnectionSecretBundles(base, overlay connectionSecretBundle) connectionSecretBundle {
|
||||
@@ -126,6 +136,21 @@ func mergeConnectionSecretBundles(base, overlay connectionSecretBundle) connecti
|
||||
if strings.TrimSpace(overlay.OpaqueDSN) != "" {
|
||||
merged.OpaqueDSN = overlay.OpaqueDSN
|
||||
}
|
||||
if strings.TrimSpace(overlay.JVMJMXPassword) != "" {
|
||||
merged.JVMJMXPassword = overlay.JVMJMXPassword
|
||||
}
|
||||
if strings.TrimSpace(overlay.JVMEndpointAPIKey) != "" {
|
||||
merged.JVMEndpointAPIKey = overlay.JVMEndpointAPIKey
|
||||
}
|
||||
if strings.TrimSpace(overlay.JVMAgentAPIKey) != "" {
|
||||
merged.JVMAgentAPIKey = overlay.JVMAgentAPIKey
|
||||
}
|
||||
if strings.TrimSpace(overlay.JVMDiagnosticAPIKey) != "" {
|
||||
merged.JVMDiagnosticAPIKey = overlay.JVMDiagnosticAPIKey
|
||||
}
|
||||
if strings.TrimSpace(overlay.SensitiveParams) != "" {
|
||||
merged.SensitiveParams = overlay.SensitiveParams
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -158,6 +183,21 @@ func applyConnectionSecretClears(bundle connectionSecretBundle, input connection
|
||||
if input.ClearOpaqueDSN {
|
||||
cleared.OpaqueDSN = ""
|
||||
}
|
||||
if input.ClearJVMJMXPassword {
|
||||
cleared.JVMJMXPassword = ""
|
||||
}
|
||||
if input.ClearJVMEndpointAPIKey {
|
||||
cleared.JVMEndpointAPIKey = ""
|
||||
}
|
||||
if input.ClearJVMAgentAPIKey {
|
||||
cleared.JVMAgentAPIKey = ""
|
||||
}
|
||||
if input.ClearJVMDiagnosticAPIKey {
|
||||
cleared.JVMDiagnosticAPIKey = ""
|
||||
}
|
||||
if input.ClearSensitiveParams {
|
||||
cleared.SensitiveParams = ""
|
||||
}
|
||||
return cleared
|
||||
}
|
||||
|
||||
@@ -332,6 +372,11 @@ func splitConnectionSecrets(input connection.SavedConnectionInput) (connection.S
|
||||
HasRedisSentinelPassword: strings.TrimSpace(bundle.RedisSentinelPassword) != "",
|
||||
HasOpaqueURI: strings.TrimSpace(bundle.OpaqueURI) != "",
|
||||
HasOpaqueDSN: strings.TrimSpace(bundle.OpaqueDSN) != "",
|
||||
HasJVMJMXPassword: strings.TrimSpace(bundle.JVMJMXPassword) != "",
|
||||
HasJVMEndpointAPIKey: strings.TrimSpace(bundle.JVMEndpointAPIKey) != "",
|
||||
HasJVMAgentAPIKey: strings.TrimSpace(bundle.JVMAgentAPIKey) != "",
|
||||
HasJVMDiagnosticAPIKey: strings.TrimSpace(bundle.JVMDiagnosticAPIKey) != "",
|
||||
HasSensitiveParams: strings.TrimSpace(bundle.SensitiveParams) != "",
|
||||
}
|
||||
return view, bundle
|
||||
}
|
||||
@@ -589,7 +634,8 @@ func (r *savedConnectionRepository) loadSecretBundleFromStore(view connection.Sa
|
||||
|
||||
func savedConnectionViewHasSecrets(view connection.SavedConnectionView) bool {
|
||||
return view.HasPrimaryPassword || view.HasSSHPassword || view.HasProxyPassword || view.HasHTTPTunnelPassword ||
|
||||
view.HasMySQLReplicaPassword || view.HasMongoReplicaPassword || view.HasRedisSentinelPassword || view.HasOpaqueURI || view.HasOpaqueDSN
|
||||
view.HasMySQLReplicaPassword || view.HasMongoReplicaPassword || view.HasRedisSentinelPassword || view.HasOpaqueURI || view.HasOpaqueDSN ||
|
||||
view.HasJVMJMXPassword || view.HasJVMEndpointAPIKey || view.HasJVMAgentAPIKey || view.HasJVMDiagnosticAPIKey || view.HasSensitiveParams
|
||||
}
|
||||
|
||||
func applyConnectionBundleFlags(view *connection.SavedConnectionView, bundle connectionSecretBundle) {
|
||||
@@ -602,6 +648,11 @@ func applyConnectionBundleFlags(view *connection.SavedConnectionView, bundle con
|
||||
view.HasRedisSentinelPassword = strings.TrimSpace(bundle.RedisSentinelPassword) != ""
|
||||
view.HasOpaqueURI = strings.TrimSpace(bundle.OpaqueURI) != ""
|
||||
view.HasOpaqueDSN = strings.TrimSpace(bundle.OpaqueDSN) != ""
|
||||
view.HasJVMJMXPassword = strings.TrimSpace(bundle.JVMJMXPassword) != ""
|
||||
view.HasJVMEndpointAPIKey = strings.TrimSpace(bundle.JVMEndpointAPIKey) != ""
|
||||
view.HasJVMAgentAPIKey = strings.TrimSpace(bundle.JVMAgentAPIKey) != ""
|
||||
view.HasJVMDiagnosticAPIKey = strings.TrimSpace(bundle.JVMDiagnosticAPIKey) != ""
|
||||
view.HasSensitiveParams = strings.TrimSpace(bundle.SensitiveParams) != ""
|
||||
}
|
||||
|
||||
func buildDuplicateConnectionName(baseName string, existing []connection.SavedConnectionView, unnamedName string, copySuffix string) string {
|
||||
|
||||
@@ -29,6 +29,11 @@ type SavedConnectionInput struct {
|
||||
ClearRedisSentinelPassword bool `json:"clearRedisSentinelPassword,omitempty"`
|
||||
ClearOpaqueURI bool `json:"clearOpaqueURI,omitempty"`
|
||||
ClearOpaqueDSN bool `json:"clearOpaqueDSN,omitempty"`
|
||||
ClearJVMJMXPassword bool `json:"clearJVMJMXPassword,omitempty"`
|
||||
ClearJVMEndpointAPIKey bool `json:"clearJVMEndpointAPIKey,omitempty"`
|
||||
ClearJVMAgentAPIKey bool `json:"clearJVMAgentAPIKey,omitempty"`
|
||||
ClearJVMDiagnosticAPIKey bool `json:"clearJVMDiagnosticAPIKey,omitempty"`
|
||||
ClearSensitiveParams bool `json:"clearSensitiveConnectionParams,omitempty"`
|
||||
}
|
||||
|
||||
type SavedConnectionView struct {
|
||||
@@ -53,6 +58,11 @@ type SavedConnectionView struct {
|
||||
HasRedisSentinelPassword bool `json:"hasRedisSentinelPassword,omitempty"`
|
||||
HasOpaqueURI bool `json:"hasOpaqueURI,omitempty"`
|
||||
HasOpaqueDSN bool `json:"hasOpaqueDSN,omitempty"`
|
||||
HasJVMJMXPassword bool `json:"hasJVMJMXPassword,omitempty"`
|
||||
HasJVMEndpointAPIKey bool `json:"hasJVMEndpointAPIKey,omitempty"`
|
||||
HasJVMAgentAPIKey bool `json:"hasJVMAgentAPIKey,omitempty"`
|
||||
HasJVMDiagnosticAPIKey bool `json:"hasJVMDiagnosticAPIKey,omitempty"`
|
||||
HasSensitiveParams bool `json:"hasSensitiveConnectionParams,omitempty"`
|
||||
}
|
||||
|
||||
type LegacySavedConnection = SavedConnectionInput
|
||||
|
||||
@@ -22,6 +22,11 @@ type ConnectionBundle struct {
|
||||
RedisSentinelPassword string `json:"redisSentinelPassword,omitempty"`
|
||||
OpaqueURI string `json:"opaqueURI,omitempty"`
|
||||
OpaqueDSN string `json:"opaqueDSN,omitempty"`
|
||||
JVMJMXPassword string `json:"jvmJMXPassword,omitempty"`
|
||||
JVMEndpointAPIKey string `json:"jvmEndpointAPIKey,omitempty"`
|
||||
JVMAgentAPIKey string `json:"jvmAgentAPIKey,omitempty"`
|
||||
JVMDiagnosticAPIKey string `json:"jvmDiagnosticAPIKey,omitempty"`
|
||||
SensitiveParams string `json:"sensitiveConnectionParams,omitempty"`
|
||||
}
|
||||
|
||||
func (b ConnectionBundle) HasAny() bool {
|
||||
@@ -33,7 +38,12 @@ func (b ConnectionBundle) HasAny() bool {
|
||||
strings.TrimSpace(b.MongoReplicaPassword) != "" ||
|
||||
strings.TrimSpace(b.RedisSentinelPassword) != "" ||
|
||||
strings.TrimSpace(b.OpaqueURI) != "" ||
|
||||
strings.TrimSpace(b.OpaqueDSN) != ""
|
||||
strings.TrimSpace(b.OpaqueDSN) != "" ||
|
||||
strings.TrimSpace(b.JVMJMXPassword) != "" ||
|
||||
strings.TrimSpace(b.JVMEndpointAPIKey) != "" ||
|
||||
strings.TrimSpace(b.JVMAgentAPIKey) != "" ||
|
||||
strings.TrimSpace(b.JVMDiagnosticAPIKey) != "" ||
|
||||
strings.TrimSpace(b.SensitiveParams) != ""
|
||||
}
|
||||
|
||||
type GlobalProxyBundle struct {
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package dailysecret
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStorePutGetDeleteConnectionSecret(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
store := NewStore(root)
|
||||
|
||||
bundle := ConnectionBundle{
|
||||
Password: "postgres-secret",
|
||||
OpaqueDSN: "postgres://user:pass@db.local/app",
|
||||
SSHPassword: "ssh-secret",
|
||||
Password: "postgres-secret",
|
||||
OpaqueDSN: "postgres://user:pass@db.local/app",
|
||||
SSHPassword: "ssh-secret",
|
||||
JVMJMXPassword: "jmx-secret",
|
||||
JVMEndpointAPIKey: "endpoint-key",
|
||||
JVMAgentAPIKey: "agent-key",
|
||||
JVMDiagnosticAPIKey: "diagnostic-key",
|
||||
SensitiveParams: "accessToken=param-secret",
|
||||
}
|
||||
if err := store.PutConnection("conn-1", bundle); err != nil {
|
||||
t.Fatalf("PutConnection returned error: %v", err)
|
||||
@@ -22,7 +30,7 @@ func TestStorePutGetDeleteConnectionSecret(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("expected connection bundle to exist")
|
||||
}
|
||||
if got.Password != "postgres-secret" || got.OpaqueDSN != bundle.OpaqueDSN || got.SSHPassword != "ssh-secret" {
|
||||
if !reflect.DeepEqual(got, bundle) {
|
||||
t.Fatalf("unexpected bundle: %#v", got)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user