updated backend

This commit is contained in:
yesoreyeram
2025-07-16 10:38:26 +01:00
parent 6518ae1398
commit 97b8e98a2e
7 changed files with 262 additions and 115 deletions
@@ -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
}
@@ -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)
})
}
}
@@ -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 {
@@ -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)
@@ -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
}
@@ -134,7 +134,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P
<RadioButtonGroup<PostgresConnectionType>
value={jsonData.connectionType || 'default'}
options={[
{ value: 'default', label: 'Default' },
{ value: 'default', label: 'Connection Parameters' },
{ value: 'connectionString', label: 'Connection String' },
]}
onChange={onConnectionTypeChanged}
@@ -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;