PostgreSQL: Support PGPASSFILE by making password optional (#108856)

* 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 <zoltan.bedi@gmail.com>

* run prettier - docs

---------

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Taras
2025-09-26 16:23:40 +02:00
committed by GitHub
co-authored by Zoltán Bedi
parent 00fb2e9537
commit e7f57446c1
5 changed files with 109 additions and 32 deletions
@@ -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:**
@@ -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)
}
@@ -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)
})
}
@@ -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
}
@@ -159,7 +159,7 @@ export const PostgresConfigEditor = (props: DataSourcePluginOptionsEditorProps<P
/>
</Field>
<Field label="Password" required>
<Field label="Password">
<SecretInput
width={WIDTH_LONG}
placeholder="Password"