From e7f57446c14ff2f7495ad6f14e99d30d786448c9 Mon Sep 17 00:00:00 2001 From: Taras <9948629+taraspos@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:23:40 +0100 Subject: [PATCH] PostgreSQL: Support PGPASSFILE by making password optional (#108856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * datasource(postgresql): add support of pgpass file * remove `required` label for password field * set `runPostgresTests` back to false * fix after merge conflict * add pgx_test * set `runPostgresTests` back to `false` * Add `no password` test case to the `pgx_test.go` as well * fix `postgres_pgx_test.go` * Update datasource docs * docs wording * docs: `datasource` -> `data source` * Update docs/sources/datasources/postgres/configure/_index.md Co-authored-by: Zoltán Bedi * run prettier - docs --------- Co-authored-by: Zoltán Bedi --- .../datasources/postgres/configure/_index.md | 16 ++--- .../grafana-postgresql-datasource/postgres.go | 9 ++- .../postgres_pgx_test.go | 44 +++++++++--- .../postgres_test.go | 70 ++++++++++++++++--- .../configuration/ConfigurationEditor.tsx | 2 +- 5 files changed, 109 insertions(+), 32 deletions(-) diff --git a/docs/sources/datasources/postgres/configure/_index.md b/docs/sources/datasources/postgres/configure/_index.md index ed229013e9f..f30a1c3ccc1 100644 --- a/docs/sources/datasources/postgres/configure/_index.md +++ b/docs/sources/datasources/postgres/configure/_index.md @@ -102,14 +102,14 @@ Following is a list of PostgreSQL configuration options: **Authentication section:** -| Setting | Description | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Username | Enter the username used to connect to your PostgreSQL database. | -| Password | Enter the password used to connect to the PostgreSQL database. | -| TLS/SSL Mode | Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. When TLS/SSL Mode is disabled, TLS/SSL Method and TLS/SSL Auth Details aren’t visible options. | -| TLS/SSL Method | Determines how TLS/SSL certificates are configured. | -| - File system path | This option allows you to configure certificates by specifying paths to existing certificates on the local file system where Grafana is running. Ensure this file is readable by the user executing the Grafana process. | -| - Certificate content | This option allows you to configure certificate by specifying their content. The content is stored and encrypted in the Grafana database. When connecting to the database, the certificates are saved as files, on the local filesystem, in the Grafana data path. | +| Setting | Description | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Username | Enter the username used to connect to your PostgreSQL database. | +| Password | Enter the password used to connect to the PostgreSQL database. If a password is not specified and your PostgreSQL is configured to request a password, data source will look for a [standard PostgreSQL password file](https://www.postgresql.org/docs/current/static/libpq-pgpass.html). | +| TLS/SSL Mode | Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. When TLS/SSL Mode is disabled, TLS/SSL Method and TLS/SSL Auth Details aren’t visible options. | +| TLS/SSL Method | Determines how TLS/SSL certificates are configured. | +| - File system path | This option allows you to configure certificates by specifying paths to existing certificates on the local file system where Grafana is running. Ensure this file is readable by the user executing the Grafana process. | +| - Certificate content | This option allows you to configure certificate by specifying their content. The content is stored and encrypted in the Grafana database. When connecting to the database, the certificates are saved as files, on the local filesystem, in the Grafana data path. | **TLS/SSL Auth Details:** diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres.go b/pkg/tsdb/grafana-postgresql-datasource/postgres.go index a2ef32bbd76..a889ef662a7 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres.go @@ -329,8 +329,13 @@ func parseNetworkAddress(url string, logger log.Logger) (string, int, error) { } func buildBaseConnectionString(params connectionParams) string { - connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", - escape(params.user), escape(params.password), escape(params.host), escape(params.database)) + connStr := fmt.Sprintf("user='%s' host='%s' dbname='%s'", + escape(params.user), escape(params.host), escape(params.database)) + + if params.password != "" { + connStr += fmt.Sprintf(" password='%s'", escape(params.password)) + } + if params.port > 0 { connStr += fmt.Sprintf(" port=%d", params.port) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go index bb1494717ed..6e14282d824 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_pgx_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "math/rand" + "os" "strings" "testing" "time" @@ -41,7 +42,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='verify-full'", + expConnStr: "user='user' host='/var/run/postgresql' dbname='database' password='password' sslmode='verify-full'", }, { desc: "TCP host", @@ -50,7 +51,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full'", + expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full'", }, { desc: "verify-ca automatically adds disable-sni", @@ -59,7 +60,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-ca"}, - expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-ca' sslsni=0", + expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-ca' sslsni=0", }, { desc: "TCP/port host", @@ -68,7 +69,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-full'", + expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-full'", }, { desc: "Ipv6 host", @@ -77,7 +78,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='::1' dbname='database' sslmode='verify-full'", + expConnStr: "user='user' host='::1' dbname='database' password='password' sslmode='verify-full'", }, { desc: "Ipv6/port host", @@ -86,7 +87,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='::1' dbname='database' port=1234 sslmode='verify-full'", + expConnStr: "user='user' host='::1' dbname='database' password='password' port=1234 sslmode='verify-full'", }, { desc: "Invalid port", @@ -103,7 +104,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: `p'\assword`, database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='verify-full'`, + expConnStr: `user='user' host='host' dbname='database' password='p\'\\assword' sslmode='verify-full'`, }, { desc: "User/DB with single quote and backslash", @@ -112,7 +113,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: `password`, database: `d'\atabase`, tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='u\'\\ser' password='password' host='host' dbname='d\'\\atabase' sslmode='verify-full'`, + expConnStr: `user='u\'\\ser' host='host' dbname='d\'\\atabase' password='password' sslmode='verify-full'`, }, { desc: "Custom TLS mode disabled", @@ -121,7 +122,7 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "disable"}, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='disable'", + expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='disable'", }, { desc: "Custom TLS mode verify-full with certificate files", @@ -135,9 +136,18 @@ func TestIntegrationGenerateConnectionStringPGX(t *testing.T) { CertFile: "i/am/coding/client.crt", CertKeyFile: "i/am/coding/client.key", }, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' " + + expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full' " + "sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'", }, + { + desc: "No password", + host: "host", + user: "user", + password: "", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' host='host' dbname='database' sslmode='verify-full'", + }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { @@ -1555,4 +1565,18 @@ func TestIntegrationPostgresPGX(t *testing.T) { require.Equal(t, "updated", *nameValue) }) }) + + t.Run("Test Postgres connection with pgpass file", func(t *testing.T) { + require.NoError(t, preparePgpassFile(t)) + require.FileExists(t, os.Getenv("PGPASSFILE"), "Make sure that PGPASSFILE is set and file exists") + + cnnstr := postgresTestDBConnString() + require.NotContains(t, cnnstr, "password=", "Make sure that password is not in the connection string") + + pgpassPool, _, err := newPostgresPGX(t.Context(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + require.NoError(t, err) + + _, err = pgpassPool.Query(t.Context(), "SELECT 1") // Test connection + require.NoError(t, err) + }) } diff --git a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go index 37286ffcad6..b71bd23b387 100644 --- a/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go +++ b/pkg/tsdb/grafana-postgresql-datasource/postgres_test.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "os" + "path/filepath" "strings" "testing" "time" @@ -46,7 +47,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='verify-full'", + expConnStr: "user='user' host='/var/run/postgresql' dbname='database' password='password' sslmode='verify-full'", }, { desc: "TCP host", @@ -55,7 +56,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full'", + expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full'", }, { desc: "verify-ca automatically adds disable-sni", @@ -64,7 +65,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-ca"}, - expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-ca' sslsni=0", + expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-ca' sslsni=0", }, { desc: "TCP/port host", @@ -73,7 +74,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-full'", + expConnStr: "user='user' host='host' dbname='database' password='password' port=1234 sslmode='verify-full'", }, { desc: "Ipv6 host", @@ -82,7 +83,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='::1' dbname='database' sslmode='verify-full'", + expConnStr: "user='user' host='::1' dbname='database' password='password' sslmode='verify-full'", }, { desc: "Ipv6/port host", @@ -91,7 +92,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: "user='user' password='password' host='::1' dbname='database' port=1234 sslmode='verify-full'", + expConnStr: "user='user' host='::1' dbname='database' password='password' port=1234 sslmode='verify-full'", }, { desc: "Invalid port", @@ -108,7 +109,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: `p'\assword`, database: "database", tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='verify-full'`, + expConnStr: `user='user' host='host' dbname='database' password='p\'\\assword' sslmode='verify-full'`, }, { desc: "User/DB with single quote and backslash", @@ -117,7 +118,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: `password`, database: `d'\atabase`, tlsSettings: tlsSettings{Mode: "verify-full"}, - expConnStr: `user='u\'\\ser' password='password' host='host' dbname='d\'\\atabase' sslmode='verify-full'`, + expConnStr: `user='u\'\\ser' host='host' dbname='d\'\\atabase' password='password' sslmode='verify-full'`, }, { desc: "Custom TLS mode disabled", @@ -126,7 +127,7 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { password: "password", database: "database", tlsSettings: tlsSettings{Mode: "disable"}, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='disable'", + expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='disable'", }, { desc: "Custom TLS mode verify-full with certificate files", @@ -140,9 +141,18 @@ func TestIntegrationGenerateConnectionString(t *testing.T) { CertFile: "i/am/coding/client.crt", CertKeyFile: "i/am/coding/client.key", }, - expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' " + + expConnStr: "user='user' host='host' dbname='database' password='password' sslmode='verify-full' " + "sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'", }, + { + desc: "No password", + host: "host", + user: "user", + password: "", + database: "database", + tlsSettings: tlsSettings{Mode: "verify-full"}, + expConnStr: "user='user' host='host' dbname='database' sslmode='verify-full'", + }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { @@ -1370,6 +1380,20 @@ func TestIntegrationPostgres(t *testing.T) { require.Empty(t, frames[0].Fields) }) }) + + t.Run("Test Postgres connection with pgpass file", func(t *testing.T) { + require.NoError(t, preparePgpassFile(t)) + require.FileExists(t, os.Getenv("PGPASSFILE"), "Make sure that PGPASSFILE is set and file exists") + + cnnstr := postgresTestDBConnString() + require.NotContains(t, cnnstr, "password=", "Make sure that password is not in the connection string") + + dbPgpass, _, err := newPostgres(context.Background(), "error", 10000, dsInfo, cnnstr, logger, backend.DataSourceInstanceSettings{}) + require.NoError(t, err) + + _, err = dbPgpass.Exec("SELECT 1") // Test connection + require.NoError(t, err) + }) } func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time { @@ -1401,6 +1425,23 @@ func isTestDbPostgres() bool { return false } +func preparePgpassFile(t *testing.T) error { + dir := t.TempDir() + t.Setenv("PGPASSFILE", filepath.Join(dir, ".pgpass")) + + host := os.Getenv("POSTGRES_HOST") + if host == "" { + host = "localhost" + } + port := os.Getenv("POSTGRES_PORT") + if port == "" { + port = "5432" + } + + return os.WriteFile(filepath.Join(dir, ".pgpass"), + []byte(fmt.Sprintf("%s:%s:grafanadstest:grafanatest:grafanatest", host, port)), 0600) +} + func postgresTestDBConnString() string { host := os.Getenv("POSTGRES_HOST") if host == "" { @@ -1410,6 +1451,13 @@ func postgresTestDBConnString() string { if port == "" { port = "5432" } - return fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", + + connStr := fmt.Sprintf("user=grafanatest host=%s port=%s dbname=grafanadstest sslmode=disable", host, port) + + if os.Getenv("PGPASSFILE") == "" { + connStr += " password=grafanatest" + } + + return connStr } 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 273c60df162..8a76ff319f4 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/configuration/ConfigurationEditor.tsx @@ -159,7 +159,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps

- +