From 97b8e98a2ec072c95e9908a65beee7159de1beca Mon Sep 17 00:00:00 2001 From: yesoreyeram <153843+yesoreyeram@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:38:26 +0100 Subject: [PATCH] updated backend --- .../connection_string.go | 142 ++++++++++++++++++ .../connection_string_test.go | 91 +++++++++++ .../grafana-postgresql-datasource/postgres.go | 107 +------------ .../postgres_test.go | 5 +- .../tlsmanager.go | 28 +++- .../configuration/ConfigurationEditor.tsx | 2 +- .../grafana-postgresql-datasource/types.ts | 2 +- 7 files changed, 262 insertions(+), 115 deletions(-) create mode 100644 pkg/tsdb/grafana-postgresql-datasource/connection_string.go create mode 100644 pkg/tsdb/grafana-postgresql-datasource/connection_string_test.go diff --git a/pkg/tsdb/grafana-postgresql-datasource/connection_string.go b/pkg/tsdb/grafana-postgresql-datasource/connection_string.go new file mode 100644 index 00000000000..e7ce9876940 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/connection_string.go @@ -0,0 +1,142 @@ +package postgres + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" + "github.com/lib/pq" +) + +func GenerateConnectionString(dsInfo sqleng.DataSourceInfo, tlsManager tlsSettingsProvider, logger log.Logger) (string, error) { + connStr, err := getInitialConnectionString(dsInfo, logger) + if err != nil { + return connStr, err + } + return getTLSIncludedConnectionString(connStr, tlsManager, dsInfo, logger) +} + +func getInitialConnectionString(dsInfo sqleng.DataSourceInfo, logger log.Logger) (string, error) { + if dsInfo.JsonData.ConnectionType == sqleng.ConnectionTypeConnectionString { + return dsInfo.DecryptedSecureJSONData["connectionString"], validateConnectionString(dsInfo) + } + var host string + var port int + if strings.HasPrefix(dsInfo.URL, "/") { + host = dsInfo.URL + logger.Debug("Generating connection string with Unix socket specifier", "address", dsInfo.URL) + } else { + index := strings.LastIndex(dsInfo.URL, ":") + v6Index := strings.Index(dsInfo.URL, "]") + sp := strings.SplitN(dsInfo.URL, ":", 2) + host = sp[0] + if v6Index == -1 { + if len(sp) > 1 { + var err error + port, err = strconv.Atoi(sp[1]) + if err != nil { + logger.Debug("Error parsing the IPv4 address", "address", dsInfo.URL) + return "", sqleng.ErrParsingPostgresURL + } + logger.Debug("Generating IPv4 connection string with network host/port pair", "host", host, "port", port, "address", dsInfo.URL) + } else { + logger.Debug("Generating IPv4 connection string with network host", "host", host, "address", dsInfo.URL) + } + } else { + if index == v6Index+1 { + host = dsInfo.URL[1 : index-1] + var err error + port, err = strconv.Atoi(dsInfo.URL[index+1:]) + if err != nil { + logger.Debug("Error parsing the IPv6 address", "address", dsInfo.URL) + return "", sqleng.ErrParsingPostgresURL + } + logger.Debug("Generating IPv6 connection string with network host/port pair", "host", host, "port", port, "address", dsInfo.URL) + } else { + host = dsInfo.URL[1 : len(dsInfo.URL)-1] + logger.Debug("Generating IPv6 connection string with network host", "host", host, "address", dsInfo.URL) + } + } + } + + connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", + escape(dsInfo.User), escape(dsInfo.DecryptedSecureJSONData["password"]), escape(host), escape(dsInfo.Database)) + if port > 0 { + connStr += fmt.Sprintf(" port=%d", port) + } + return connStr, nil +} + +func getTLSIncludedConnectionString(connStr string, tlsManager tlsSettingsProvider, dsInfo sqleng.DataSourceInfo, logger log.Logger) (string, error) { + tlsSettings, err := tlsManager.getTLSSettings(dsInfo) + if err != nil { + return "", err + } + + if dsInfo.JsonData.ConnectionType == sqleng.ConnectionTypeConnectionString { + connStr, err = removeTLSSettingsFromConnectionString(connStr) + if err != nil { + return connStr, err + } + } + + connStr += fmt.Sprintf(" sslmode='%s'", escape(string(tlsSettings.Mode))) + + // there is an issue with the lib/pq module, the `verify-ca` tls mode + // does not work correctly. ( see https://github.com/lib/pq/issues/1106 ) + // to workaround the problem, if the `verify-ca` mode is chosen, + // we disable sslsni. + if tlsSettings.Mode == TLSModeVerifyCA { + connStr += " sslsni=0" + } + + // Attach root certificate if provided + if tlsSettings.RootCertFile != "" { + logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile) + connStr += fmt.Sprintf(" sslrootcert='%s'", escape(tlsSettings.RootCertFile)) + } + + // Attach client certificate and key if both are provided + if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" { + logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile) + connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile)) + } else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" { + return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified") + } + return connStr, nil +} + +func validateConnectionString(dsInfo sqleng.DataSourceInfo) error { + connectionString := strings.ToLower(dsInfo.DecryptedSecureJSONData["connectionString"]) + if dsInfo.JsonData.ConnectionType == sqleng.ConnectionTypeConnectionString && strings.TrimSpace(connectionString) == "" { + return errors.New("invalid / empty connection string") + } + return nil +} + +func removeTLSSettingsFromConnectionString(connStr string) (string, error) { + sslPrefixes := []string{"sslmode", "sslsni", "sslrootcert", "sslcert", "sslkey"} + parsedConnectionString, err := pq.ParseURL(connStr) + if err != nil { + return "", err + } + kv := strings.Split(parsedConnectionString, " ") + newKv := []string{} + for _, v := range kv { + lowerV := strings.ToLower(v) + isSSLParam := false + for _, prefix := range sslPrefixes { + if strings.HasPrefix(lowerV, prefix) { + isSSLParam = true + break + } + } + if !isSSLParam { + newKv = append(newKv, v) + } + } + return strings.Join(newKv, " "), nil +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/connection_string_test.go b/pkg/tsdb/grafana-postgresql-datasource/connection_string_test.go new file mode 100644 index 00000000000..ff22717d3b5 --- /dev/null +++ b/pkg/tsdb/grafana-postgresql-datasource/connection_string_test.go @@ -0,0 +1,91 @@ +package postgres + +import ( + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana/pkg/tsdb/grafana-postgresql-datasource/sqleng" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateConnectionString(t *testing.T) { + tests := []struct { + name string + dsInfo sqleng.DataSourceInfo + tlsSettings *tlsSettings + want string + wantErr error + }{ + { + name: "default settings shouldn't throw error", + want: "user='' password='' host='' dbname='' sslmode=''", + }, + { + name: "default settings with host, port, dbname", + dsInfo: sqleng.DataSourceInfo{URL: "host:1234", User: "user", Database: "db", DecryptedSecureJSONData: map[string]string{"password": "pass"}}, + tlsSettings: &tlsSettings{Mode: "require"}, + want: "user='user' password='pass' host='host' dbname='db' port=1234 sslmode='require'", + }, + { + name: "default settings with host, port, dbname", + dsInfo: sqleng.DataSourceInfo{URL: "host:1234", User: "user", Database: "db", DecryptedSecureJSONData: map[string]string{"password": "pass"}}, + tlsSettings: &tlsSettings{Mode: "verify-ca", ConfigurationMethod: "file-content", RootCertFile: "root", CertFile: "cert", CertKeyFile: "key"}, + want: "user='user' password='pass' host='host' dbname='db' port=1234 sslmode='verify-ca' sslsni=0 sslrootcert='root' sslcert='cert' sslkey='key'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tlssettings := tt.tlsSettings + if tlssettings == nil { + tlssettings = &tlsSettings{} + } + tlsManager := &tlsTestManager{settings: *tlssettings} + got, err := GenerateConnectionString(tt.dsInfo, tlsManager, log.DefaultLogger) + if tt.wantErr != nil { + require.NotNil(t, err) + assert.Equal(t, tt.wantErr, err) + return + } + require.Nil(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_removeTLSSettingsFromConnectionString(t *testing.T) { + tests := []struct { + name string + connStr string + want string + wantErr error + }{ + { + name: "should send original connection string if no ssl settings present", + connStr: "postgres://bob:secret@1.2.3.4:5432/mydb", + want: "dbname='mydb' host='1.2.3.4' password='secret' port='5432' user='bob'", + }, + { + name: "should remove sslmode", + connStr: "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full", + want: "dbname='mydb' host='1.2.3.4' password='secret' port='5432' user='bob'", + }, + { + name: "should respect case sensitive password", + connStr: "postgres://bob:sEcret@1.2.3.4:5432/mydb?sslmode=verify-full", + want: "dbname='mydb' host='1.2.3.4' password='sEcret' port='5432' user='bob'", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := removeTLSSettingsFromConnectionString(tt.connStr) + if tt.wantErr != nil { + require.NotNil(t, err) + assert.Equal(t, tt.wantErr, err) + return + } + require.Nil(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index 675f5a4fc81..89c023d0cad 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "encoding/json" - "errors" "fmt" "reflect" "strconv" @@ -119,7 +118,7 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { MaxIdleConns: sqlCfg.DefaultMaxIdleConns, ConnMaxLifetime: sqlCfg.DefaultMaxConnLifetimeSeconds, Timescaledb: false, - ConfigurationMethod: "file-path", + ConfigurationMethod: string(TLSConfigurationMethodFilePath), SecureDSProxy: false, } @@ -144,7 +143,7 @@ func (s *Service) newInstanceSettings() datasource.InstanceFactoryFunc { DecryptedSecureJSONData: settings.DecryptedSecureJSONData, } - cnnstr, err := s.generateConnectionString(dsInfo) + cnnstr, err := GenerateConnectionString(dsInfo, s.tlsManager, logger) if err != nil { return nil, err } @@ -171,108 +170,6 @@ func escape(input string) string { return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`) } -func (s *Service) generateConnectionString(dsInfo sqleng.DataSourceInfo) (string, error) { - logger := s.logger - connStr, err := getInitialConnectionString(dsInfo, logger) - if err != nil { - return connStr, err - } - connStr, err = getTLSIncludedConnectionString(connStr, s.tlsManager, dsInfo, logger) - if err != nil { - return connStr, err - } - logger.Debug("Generated Postgres connection string successfully") - return connStr, nil -} - -func getInitialConnectionString(dsInfo sqleng.DataSourceInfo, logger log.Logger) (string, error) { - if dsInfo.JsonData.ConnectionType == sqleng.ConnectionTypeConnectionString { - connStr := dsInfo.DecryptedSecureJSONData["connectionString"] - if connStr == "" { - return connStr, errors.New("Invalid/Empty connection string") - } - return connStr, nil - } - var host string - var port int - if strings.HasPrefix(dsInfo.URL, "/") { - host = dsInfo.URL - logger.Debug("Generating connection string with Unix socket specifier", "address", dsInfo.URL) - } else { - index := strings.LastIndex(dsInfo.URL, ":") - v6Index := strings.Index(dsInfo.URL, "]") - sp := strings.SplitN(dsInfo.URL, ":", 2) - host = sp[0] - if v6Index == -1 { - if len(sp) > 1 { - var err error - port, err = strconv.Atoi(sp[1]) - if err != nil { - logger.Debug("Error parsing the IPv4 address", "address", dsInfo.URL) - return "", sqleng.ErrParsingPostgresURL - } - logger.Debug("Generating IPv4 connection string with network host/port pair", "host", host, "port", port, "address", dsInfo.URL) - } else { - logger.Debug("Generating IPv4 connection string with network host", "host", host, "address", dsInfo.URL) - } - } else { - if index == v6Index+1 { - host = dsInfo.URL[1 : index-1] - var err error - port, err = strconv.Atoi(dsInfo.URL[index+1:]) - if err != nil { - logger.Debug("Error parsing the IPv6 address", "address", dsInfo.URL) - return "", sqleng.ErrParsingPostgresURL - } - logger.Debug("Generating IPv6 connection string with network host/port pair", "host", host, "port", port, "address", dsInfo.URL) - } else { - host = dsInfo.URL[1 : len(dsInfo.URL)-1] - logger.Debug("Generating IPv6 connection string with network host", "host", host, "address", dsInfo.URL) - } - } - } - - connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", - escape(dsInfo.User), escape(dsInfo.DecryptedSecureJSONData["password"]), escape(host), escape(dsInfo.Database)) - if port > 0 { - connStr += fmt.Sprintf(" port=%d", port) - } - return connStr, nil -} - -func getTLSIncludedConnectionString(connStr string, tlsManager tlsSettingsProvider, dsInfo sqleng.DataSourceInfo, logger log.Logger) (string, error) { - tlsSettings, err := tlsManager.getTLSSettings(dsInfo) - if err != nil { - return "", err - } - - connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode)) - - // there is an issue with the lib/pq module, the `verify-ca` tls mode - // does not work correctly. ( see https://github.com/lib/pq/issues/1106 ) - // to workaround the problem, if the `verify-ca` mode is chosen, - // we disable sslsni. - if tlsSettings.Mode == "verify-ca" { - connStr += " sslsni=0" - } - - // Attach root certificate if provided - if tlsSettings.RootCertFile != "" { - logger.Debug("Setting server root certificate", "tlsRootCert", tlsSettings.RootCertFile) - connStr += fmt.Sprintf(" sslrootcert='%s'", escape(tlsSettings.RootCertFile)) - } - - // Attach client certificate and key if both are provided - if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" { - logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile) - connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile)) - } else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" { - return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified") - } - - return connStr, nil -} - type postgresQueryResultTransformer struct{} func (t *postgresQueryResultTransformer) TransformQueryError(_ log.Logger, err error) error { diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index 47eee7b1a6b..3063c0b1d7a 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -146,8 +146,9 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { + tlsManager := &tlsTestManager{settings: tt.tlsSettings} svc := Service{ - tlsManager: &tlsTestManager{settings: tt.tlsSettings}, + tlsManager: tlsManager, logger: backend.NewLoggerWith("logger", "tsdb.postgres"), } @@ -159,7 +160,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { UID: tt.uid, } - connStr, err := svc.generateConnectionString(ds) + connStr, err := GenerateConnectionString(ds, tlsManager, svc.logger) if tt.expErr == "" { require.NoError(t, err, tt.desc) diff --git a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go index 306b1f43e81..b2b0599d29c 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go +++ b/pkg/tsdb/grafana-postgresql-datasource/tlsmanager.go @@ -47,9 +47,25 @@ func newTLSManager(logger log.Logger, dataPath string) tlsSettingsProvider { } } +type TLSMode string + +const ( + TLSModeDisable TLSMode = "disable" + TLSModeRequire TLSMode = "require" + TLSModeVerifyCA TLSMode = "verify-ca" + TLSModeVerifyFull TLSMode = "verify-full" +) + +type TLSConfigurationMethod string + +const ( + TLSConfigurationMethodFilePath TLSConfigurationMethod = "file-path" + TLSConfigurationMethodFileContent TLSConfigurationMethod = "file-content" +) + type tlsSettings struct { - Mode string - ConfigurationMethod string + Mode TLSMode + ConfigurationMethod TLSConfigurationMethod RootCertFile string CertFile string CertKeyFile string @@ -57,10 +73,10 @@ type tlsSettings struct { func (m *tlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, error) { tlsconfig := tlsSettings{ - Mode: dsInfo.JsonData.Mode, + Mode: TLSMode(dsInfo.JsonData.Mode), } - isTLSDisabled := (tlsconfig.Mode == "disable") + isTLSDisabled := (tlsconfig.Mode == TLSModeDisable) if isTLSDisabled { m.logger.Debug("Postgres TLS/SSL is disabled") @@ -69,12 +85,12 @@ func (m *tlsManager) getTLSSettings(dsInfo sqleng.DataSourceInfo) (tlsSettings, m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsconfig.Mode) - tlsconfig.ConfigurationMethod = dsInfo.JsonData.ConfigurationMethod + tlsconfig.ConfigurationMethod = TLSConfigurationMethod(dsInfo.JsonData.ConfigurationMethod) tlsconfig.RootCertFile = dsInfo.JsonData.RootCertFile tlsconfig.CertFile = dsInfo.JsonData.CertFile tlsconfig.CertKeyFile = dsInfo.JsonData.CertKeyFile - if tlsconfig.ConfigurationMethod == "file-content" { + if tlsconfig.ConfigurationMethod == TLSConfigurationMethodFileContent { if err := m.writeCertFiles(dsInfo, &tlsconfig); err != nil { return tlsconfig, err } diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx index e3394e858ac..5eae1457d4c 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx @@ -134,7 +134,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

value={jsonData.connectionType || 'default'} options={[ - { value: 'default', label: 'Default' }, + { value: 'default', label: 'Connection Parameters' }, { value: 'connectionString', label: 'Connection String' }, ]} onChange={onConnectionTypeChanged} diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/types.ts b/public/app/plugins/datasource/grafana-postgresql-datasource/types.ts index ed242c8eb2b..7cdfd1dfb85 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/types.ts +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/types.ts @@ -15,7 +15,7 @@ export enum PostgresTLSMethods { export type PostgresConnectionType = 'default' | 'connectionString'; export interface PostgresOptions extends SQLOptions { - connectionType: PostgresConnectionType; + connectionType?: PostgresConnectionType; tlsConfigurationMethod?: PostgresTLSMethods; sslmode?: PostgresTLSModes; sslRootCertFile?: string;