mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
Postgres: allow providing TLS/SSL certificates as text in addition to file paths (#30353)
* postgres SSL certification * add back the UI to configure SSL Authentication files by file path * add backend logic * correct unittest * mini changes * Update public/app/plugins/datasource/postgres/config_ctrl.ts Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update public/app/plugins/datasource/postgres/partials/config.html Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * mutex * check file exist before remove * change permission * change default configuremethod to file-path * Update public/app/plugins/datasource/postgres/partials/config.html Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Update public/app/plugins/datasource/postgres/partials/config.html Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Update public/app/plugins/datasource/postgres/partials/config.html Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Update public/app/plugins/datasource/postgres/partials/config.html Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * rename sslconfiguremethod to sslconfigurationmethod * frontend update * solve comments * Postgres: Convert tests to stdlib Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Postgres: Be consistent about TLS/SSL terminology Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * fix init inconsistancy * Fix tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * naming convention * Fix tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Undo change Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix TLS issue Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * change permissions * Fix data source field names Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Clean up HTML Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Improve popover text Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix SSL input bug Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Undo unnecessary change Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Clean up backend code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix build Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * More consistent naming Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Clean up code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Enforce certificate file permissions Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * add settings * Undo changes Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * fix windows file path * PostgresDataSource: Fix mutex usage Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Fix tests Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/postgres/postgres.go Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Apply suggestions from code review Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * fix compilation * fix unittest * Apply suggestions from code review Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Apply suggestions from code review Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * mock function * change kmutex package * add kmutex into middleware * lock connection file per datasource * add unittest regarding concurrency * version should be equal * adding unittest * fix the loop * fix unitest * fix postgres unittst * remove comments * move dataPath from arg to tlsManager struct field * Use DecryptedValues method Use cached decrypted values instead of using secure json data decrypt which will decrypt unchanged values over and over again. * remove unneeded mutex in tests and cleanup tests * fix the lint Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
parent
19c12b5451
commit
c57047a420
@ -55,10 +55,10 @@ export const TLSAuthSettings: React.FC<HttpSettingsBaseProps> = ({ dataSourceCon
|
||||
`
|
||||
)}
|
||||
>
|
||||
<h6>TLS Auth Details</h6>
|
||||
<h6>TLS/SSL Auth Details</h6>
|
||||
<Tooltip
|
||||
placement="right-end"
|
||||
content="TLS Certs are encrypted and stored in the Grafana database."
|
||||
content="TLS/SSL Certs are encrypted and stored in the Grafana database."
|
||||
theme="info"
|
||||
>
|
||||
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
|
||||
|
85
pkg/tsdb/postgres/locker.go
Normal file
85
pkg/tsdb/postgres/locker.go
Normal file
@ -0,0 +1,85 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// locker is a named reader/writer mutual exclusion lock.
|
||||
// The lock for each particular key can be held by an arbitrary number of readers or a single writer.
|
||||
type locker struct {
|
||||
locks map[interface{}]*sync.RWMutex
|
||||
locksRW *sync.RWMutex
|
||||
}
|
||||
|
||||
func newLocker() *locker {
|
||||
return &locker{
|
||||
locks: make(map[interface{}]*sync.RWMutex),
|
||||
locksRW: new(sync.RWMutex),
|
||||
}
|
||||
}
|
||||
|
||||
// Lock locks named rw mutex with specified key for writing.
|
||||
// If the lock with the same key is already locked for reading or writing,
|
||||
// Lock blocks until the lock is available.
|
||||
func (lkr *locker) Lock(key interface{}) {
|
||||
lk, ok := lkr.getLock(key)
|
||||
if !ok {
|
||||
lk = lkr.newLock(key)
|
||||
}
|
||||
lk.Lock()
|
||||
}
|
||||
|
||||
// Unlock unlocks named rw mutex with specified key for writing. It is a run-time error if rw is
|
||||
// not locked for writing on entry to Unlock.
|
||||
func (lkr *locker) Unlock(key interface{}) {
|
||||
lk, ok := lkr.getLock(key)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("lock for key '%s' not initialized", key))
|
||||
}
|
||||
lk.Unlock()
|
||||
}
|
||||
|
||||
// RLock locks named rw mutex with specified key for reading.
|
||||
//
|
||||
// It should not be used for recursive read locking for the same key; a blocked Lock
|
||||
// call excludes new readers from acquiring the lock. See the
|
||||
// documentation on the golang RWMutex type.
|
||||
func (lkr *locker) RLock(key interface{}) {
|
||||
lk, ok := lkr.getLock(key)
|
||||
if !ok {
|
||||
lk = lkr.newLock(key)
|
||||
}
|
||||
lk.RLock()
|
||||
}
|
||||
|
||||
// RUnlock undoes a single RLock call for specified key;
|
||||
// it does not affect other simultaneous readers of locker for specified key.
|
||||
// It is a run-time error if locker for specified key is not locked for reading
|
||||
func (lkr *locker) RUnlock(key interface{}) {
|
||||
lk, ok := lkr.getLock(key)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("lock for key '%s' not initialized", key))
|
||||
}
|
||||
lk.RUnlock()
|
||||
}
|
||||
|
||||
func (lkr *locker) newLock(key interface{}) *sync.RWMutex {
|
||||
lkr.locksRW.Lock()
|
||||
defer lkr.locksRW.Unlock()
|
||||
|
||||
if lk, ok := lkr.locks[key]; ok {
|
||||
return lk
|
||||
}
|
||||
lk := new(sync.RWMutex)
|
||||
lkr.locks[key] = lk
|
||||
return lk
|
||||
}
|
||||
|
||||
func (lkr *locker) getLock(key interface{}) (*sync.RWMutex, bool) {
|
||||
lkr.locksRW.RLock()
|
||||
defer lkr.locksRW.RUnlock()
|
||||
|
||||
lock, ok := lkr.locks[key]
|
||||
return lock, ok
|
||||
}
|
63
pkg/tsdb/postgres/locker_test.go
Normal file
63
pkg/tsdb/postgres/locker_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLocker(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Tests with Sleep")
|
||||
}
|
||||
const notUpdated = "not_updated"
|
||||
const atThread1 = "at_thread_1"
|
||||
const atThread2 = "at_thread_2"
|
||||
t.Run("Should lock for same keys", func(t *testing.T) {
|
||||
updated := notUpdated
|
||||
locker := newLocker()
|
||||
locker.Lock(1)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
defer func() {
|
||||
locker.Unlock(1)
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
locker.RLock(1)
|
||||
defer func() {
|
||||
locker.RUnlock(1)
|
||||
wg.Done()
|
||||
}()
|
||||
require.Equal(t, atThread1, updated, "Value should be updated in different thread")
|
||||
updated = atThread2
|
||||
}()
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
require.Equal(t, notUpdated, updated, "Value should not be updated in different thread")
|
||||
updated = atThread1
|
||||
})
|
||||
|
||||
t.Run("Should not lock for different keys", func(t *testing.T) {
|
||||
updated := notUpdated
|
||||
locker := newLocker()
|
||||
locker.Lock(1)
|
||||
defer locker.Unlock(1)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
locker.RLock(2)
|
||||
defer func() {
|
||||
locker.RUnlock(2)
|
||||
wg.Done()
|
||||
}()
|
||||
require.Equal(t, notUpdated, updated, "Value should not be updated in different thread")
|
||||
updated = atThread2
|
||||
}()
|
||||
wg.Wait()
|
||||
require.Equal(t, atThread2, updated, "Value should be updated in different thread")
|
||||
updated = atThread1
|
||||
})
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
@ -13,24 +14,43 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
|
||||
"xorm.io/core"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tsdb.RegisterTsdbQueryEndpoint("postgres", newPostgresQueryEndpoint)
|
||||
registry.Register(®istry.Descriptor{
|
||||
Name: "PostgresService",
|
||||
InitPriority: registry.Low,
|
||||
Instance: &postgresService{},
|
||||
})
|
||||
}
|
||||
|
||||
func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
logger := log.New("tsdb.postgres")
|
||||
logger.Debug("Creating Postgres query endpoint")
|
||||
type postgresService struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
logger log.Logger
|
||||
tlsManager tlsSettingsProvider
|
||||
}
|
||||
|
||||
cnnstr, err := generateConnectionString(datasource, logger)
|
||||
func (s *postgresService) Init() error {
|
||||
s.logger = log.New("tsdb.postgres")
|
||||
s.tlsManager = newTLSManager(s.logger, s.Cfg.DataPath)
|
||||
tsdb.RegisterTsdbQueryEndpoint("postgres", func(ds *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
return s.newPostgresQueryEndpoint(ds)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *postgresService) newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
s.logger.Debug("Creating Postgres query endpoint")
|
||||
|
||||
cnnstr, err := s.generateConnectionString(datasource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if setting.Env == setting.Dev {
|
||||
logger.Debug("getEngine", "connection", cnnstr)
|
||||
if s.Cfg.Env == setting.Dev {
|
||||
s.logger.Debug("getEngine", "connection", cnnstr)
|
||||
}
|
||||
|
||||
config := sqleng.SqlQueryEndpointConfiguration{
|
||||
@ -41,18 +61,19 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp
|
||||
}
|
||||
|
||||
queryResultTransformer := postgresQueryResultTransformer{
|
||||
log: logger,
|
||||
log: s.logger,
|
||||
}
|
||||
|
||||
timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false)
|
||||
|
||||
endpoint, err := sqleng.NewSqlQueryEndpoint(&config, &queryResultTransformer, newPostgresMacroEngine(timescaledb), logger)
|
||||
endpoint, err := sqleng.NewSqlQueryEndpoint(&config, &queryResultTransformer, newPostgresMacroEngine(timescaledb),
|
||||
s.logger)
|
||||
if err != nil {
|
||||
logger.Debug("Failed connecting to Postgres", "err", err)
|
||||
s.logger.Error("Failed connecting to Postgres", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("Successfully connected to Postgres")
|
||||
s.logger.Debug("Successfully connected to Postgres")
|
||||
return endpoint, err
|
||||
}
|
||||
|
||||
@ -61,15 +82,13 @@ func escape(input string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`)
|
||||
}
|
||||
|
||||
func generateConnectionString(datasource *models.DataSource, logger log.Logger) (string, error) {
|
||||
tlsMode := strings.TrimSpace(strings.ToLower(datasource.JsonData.Get("sslmode").MustString("verify-full")))
|
||||
isTLSDisabled := tlsMode == "disable"
|
||||
|
||||
func (s *postgresService) generateConnectionString(datasource *models.DataSource) (string, error) {
|
||||
var host string
|
||||
var port int
|
||||
var err error
|
||||
if strings.HasPrefix(datasource.Url, "/") {
|
||||
host = datasource.Url
|
||||
logger.Debug("Generating connection string with Unix socket specifier", "socket", host)
|
||||
s.logger.Debug("Generating connection string with Unix socket specifier", "socket", host)
|
||||
} else {
|
||||
sp := strings.SplitN(datasource.Url, ":", 2)
|
||||
host = sp[0]
|
||||
@ -80,41 +99,41 @@ func generateConnectionString(datasource *models.DataSource, logger log.Logger)
|
||||
return "", errutil.Wrapf(err, "invalid port in host specifier %q", sp[1])
|
||||
}
|
||||
|
||||
logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port)
|
||||
s.logger.Debug("Generating connection string with network host/port pair", "host", host, "port", port)
|
||||
} else {
|
||||
logger.Debug("Generating connection string with network host", "host", host)
|
||||
s.logger.Debug("Generating connection string with network host", "host", host)
|
||||
}
|
||||
}
|
||||
|
||||
connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s' sslmode='%s'",
|
||||
escape(datasource.User), escape(datasource.DecryptedPassword()), escape(host), escape(datasource.Database),
|
||||
escape(tlsMode))
|
||||
connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'",
|
||||
escape(datasource.User), escape(datasource.DecryptedPassword()), escape(host), escape(datasource.Database))
|
||||
if port > 0 {
|
||||
connStr += fmt.Sprintf(" port=%d", port)
|
||||
}
|
||||
if isTLSDisabled {
|
||||
logger.Debug("Postgres TLS/SSL is disabled")
|
||||
} else {
|
||||
logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsMode)
|
||||
|
||||
tlsSettings, err := s.tlsManager.getTLSSettings(datasource)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode))
|
||||
|
||||
// Attach root certificate if provided
|
||||
if tlsRootCert := datasource.JsonData.Get("sslRootCertFile").MustString(""); tlsRootCert != "" {
|
||||
logger.Debug("Setting server root certificate", "tlsRootCert", tlsRootCert)
|
||||
connStr += fmt.Sprintf(" sslrootcert='%s'", tlsRootCert)
|
||||
// Attach root certificate if provided
|
||||
if tlsSettings.RootCertFile != "" {
|
||||
s.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
|
||||
tlsCert := datasource.JsonData.Get("sslCertFile").MustString("")
|
||||
tlsKey := datasource.JsonData.Get("sslKeyFile").MustString("")
|
||||
if tlsCert != "" && tlsKey != "" {
|
||||
logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsCert, "tlsKey", tlsKey)
|
||||
connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", tlsCert, tlsKey)
|
||||
} else if tlsCert != "" || tlsKey != "" {
|
||||
if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" {
|
||||
s.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")
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Generated Postgres connection string successfully")
|
||||
s.logger.Debug("Generated Postgres connection string successfully")
|
||||
return connStr, nil
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -27,7 +28,8 @@ import (
|
||||
|
||||
// Test generateConnectionString.
|
||||
func TestGenerateConnectionString(t *testing.T) {
|
||||
logger := log.New("tsdb.postgres")
|
||||
cfg := setting.NewCfg()
|
||||
cfg.DataPath = t.TempDir()
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
@ -35,9 +37,10 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
user string
|
||||
password string
|
||||
database string
|
||||
tlsMode string
|
||||
tlsSettings tlsSettings
|
||||
expConnStr string
|
||||
expErr string
|
||||
uid string
|
||||
}{
|
||||
{
|
||||
desc: "Unix socket host",
|
||||
@ -45,6 +48,7 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
user: "user",
|
||||
password: "password",
|
||||
database: "database",
|
||||
tlsSettings: tlsSettings{Mode: "verify-full"},
|
||||
expConnStr: "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='verify-full'",
|
||||
},
|
||||
{
|
||||
@ -53,6 +57,7 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
user: "user",
|
||||
password: "password",
|
||||
database: "database",
|
||||
tlsSettings: tlsSettings{Mode: "verify-full"},
|
||||
expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full'",
|
||||
},
|
||||
{
|
||||
@ -61,13 +66,15 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
user: "user",
|
||||
password: "password",
|
||||
database: "database",
|
||||
expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' port=1234",
|
||||
tlsSettings: tlsSettings{Mode: "verify-full"},
|
||||
expConnStr: "user='user' password='password' host='host' dbname='database' port=1234 sslmode='verify-full'",
|
||||
},
|
||||
{
|
||||
desc: "Invalid port",
|
||||
host: "host:invalid",
|
||||
user: "user",
|
||||
database: "database",
|
||||
tlsSettings: tlsSettings{},
|
||||
expErr: "invalid port in host specifier",
|
||||
},
|
||||
{
|
||||
@ -76,35 +83,55 @@ func TestGenerateConnectionString(t *testing.T) {
|
||||
user: "user",
|
||||
password: `p'\assword`,
|
||||
database: "database",
|
||||
tlsSettings: tlsSettings{Mode: "verify-full"},
|
||||
expConnStr: `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='verify-full'`,
|
||||
},
|
||||
{
|
||||
desc: "Custom TLS/SSL mode",
|
||||
desc: "Custom TLS mode disabled",
|
||||
host: "host",
|
||||
user: "user",
|
||||
password: "password",
|
||||
database: "database",
|
||||
tlsMode: "disable",
|
||||
tlsSettings: tlsSettings{Mode: "disable"},
|
||||
expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='disable'",
|
||||
},
|
||||
{
|
||||
desc: "Custom TLS mode verify-full with certificate files",
|
||||
host: "host",
|
||||
user: "user",
|
||||
password: "password",
|
||||
database: "database",
|
||||
tlsSettings: tlsSettings{
|
||||
Mode: "verify-full",
|
||||
RootCertFile: "i/am/coding/ca.crt",
|
||||
CertFile: "i/am/coding/client.crt",
|
||||
CertKeyFile: "i/am/coding/client.key",
|
||||
},
|
||||
expConnStr: "user='user' password='password' host='host' dbname='database' sslmode='verify-full' " +
|
||||
"sslrootcert='i/am/coding/ca.crt' sslcert='i/am/coding/client.crt' sslkey='i/am/coding/client.key'",
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
data := map[string]interface{}{}
|
||||
if tt.tlsMode != "" {
|
||||
data["sslmode"] = tt.tlsMode
|
||||
svc := postgresService{
|
||||
Cfg: cfg,
|
||||
logger: log.New("tsdb.postgres"),
|
||||
tlsManager: &tlsTestManager{settings: tt.tlsSettings},
|
||||
}
|
||||
|
||||
ds := &models.DataSource{
|
||||
Url: tt.host,
|
||||
User: tt.user,
|
||||
Password: tt.password,
|
||||
Database: tt.database,
|
||||
JsonData: simplejson.NewFromAny(data),
|
||||
Uid: tt.uid,
|
||||
}
|
||||
connStr, err := generateConnectionString(ds, logger)
|
||||
|
||||
connStr, err := svc.generateConnectionString(ds)
|
||||
|
||||
if tt.expErr == "" {
|
||||
require.NoError(t, err, tt.desc)
|
||||
assert.Equal(t, tt.expConnStr, connStr, tt.desc)
|
||||
assert.Equal(t, tt.expConnStr, connStr)
|
||||
} else {
|
||||
require.Error(t, err, tt.desc)
|
||||
assert.True(t, strings.HasPrefix(err.Error(), tt.expErr),
|
||||
@ -127,7 +154,7 @@ func TestPostgres(t *testing.T) {
|
||||
runPostgresTests := false
|
||||
// runPostgresTests := true
|
||||
|
||||
if !(sqlstore.IsTestDbPostgres() || runPostgresTests) {
|
||||
if !sqlstore.IsTestDbPostgres() && !runPostgresTests {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
@ -146,7 +173,15 @@ func TestPostgres(t *testing.T) {
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
endpoint, err := newPostgresQueryEndpoint(&models.DataSource{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.DataPath = t.TempDir()
|
||||
svc := postgresService{
|
||||
Cfg: cfg,
|
||||
logger: log.New("tsdb.postgres"),
|
||||
tlsManager: &tlsTestManager{settings: tlsSettings{Mode: "disable"}},
|
||||
}
|
||||
|
||||
endpoint, err := svc.newPostgresQueryEndpoint(&models.DataSource{
|
||||
JsonData: simplejson.New(),
|
||||
SecureJsonData: securejsondata.SecureJsonData{},
|
||||
})
|
||||
@ -483,9 +518,9 @@ func TestPostgres(t *testing.T) {
|
||||
ValueTwo int64 `xorm:"integer 'valueTwo'"`
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
|
||||
if exists, err := sess.IsTableExist(metric_values{}); err != nil || exists {
|
||||
require.NoError(t, err)
|
||||
err = sess.DropTable(metric_values{})
|
||||
err := sess.DropTable(metric_values{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
err := sess.CreateTable(metric_values{})
|
||||
@ -1084,9 +1119,7 @@ func InitPostgresTestDB(t *testing.T) *xorm.Engine {
|
||||
testDB := sqlutil.PostgresTestDB()
|
||||
x, err := xorm.NewEngine(testDB.DriverName, strings.Replace(testDB.ConnStr, "dbname=grafanatest",
|
||||
"dbname=grafanadstest", 1))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to init postgres DB %v", err)
|
||||
}
|
||||
require.NoError(t, err, "Failed to init postgres DB")
|
||||
|
||||
x.DatabaseTZ = time.UTC
|
||||
x.TZLocation = time.UTC
|
||||
@ -1108,3 +1141,11 @@ func genTimeRangeByInterval(from time.Time, duration time.Duration, interval tim
|
||||
|
||||
return timeRange
|
||||
}
|
||||
|
||||
type tlsTestManager struct {
|
||||
settings tlsSettings
|
||||
}
|
||||
|
||||
func (m *tlsTestManager) getTLSSettings(datasource *models.DataSource) (tlsSettings, error) {
|
||||
return m.settings, nil
|
||||
}
|
||||
|
228
pkg/tsdb/postgres/tlsmanager.go
Normal file
228
pkg/tsdb/postgres/tlsmanager.go
Normal file
@ -0,0 +1,228 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
var validateCertFunc = validateCertFilePaths
|
||||
var writeCertFileFunc = writeCertFile
|
||||
|
||||
type tlsSettingsProvider interface {
|
||||
getTLSSettings(datasource *models.DataSource) (tlsSettings, error)
|
||||
}
|
||||
|
||||
type datasourceCacheManager struct {
|
||||
locker *locker
|
||||
cache sync.Map
|
||||
}
|
||||
|
||||
type tlsManager struct {
|
||||
logger log.Logger
|
||||
dsCacheInstance datasourceCacheManager
|
||||
dataPath string
|
||||
}
|
||||
|
||||
func newTLSManager(logger log.Logger, dataPath string) tlsSettingsProvider {
|
||||
return &tlsManager{
|
||||
logger: logger,
|
||||
dataPath: dataPath,
|
||||
dsCacheInstance: datasourceCacheManager{locker: newLocker()},
|
||||
}
|
||||
}
|
||||
|
||||
type tlsSettings struct {
|
||||
Mode string
|
||||
ConfigurationMethod string
|
||||
RootCertFile string
|
||||
CertFile string
|
||||
CertKeyFile string
|
||||
}
|
||||
|
||||
func (m *tlsManager) getTLSSettings(datasource *models.DataSource) (tlsSettings, error) {
|
||||
tlsMode := strings.TrimSpace(strings.ToLower(datasource.JsonData.Get("sslmode").MustString("verify-full")))
|
||||
isTLSDisabled := tlsMode == "disable"
|
||||
|
||||
settings := tlsSettings{}
|
||||
settings.Mode = tlsMode
|
||||
|
||||
if isTLSDisabled {
|
||||
m.logger.Debug("Postgres TLS/SSL is disabled")
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
m.logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsMode)
|
||||
|
||||
settings.ConfigurationMethod = strings.TrimSpace(
|
||||
strings.ToLower(datasource.JsonData.Get("tlsConfigurationMethod").MustString("file-path")))
|
||||
|
||||
if settings.ConfigurationMethod == "file-content" {
|
||||
if err := m.writeCertFiles(datasource, &settings); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
} else {
|
||||
settings.RootCertFile = datasource.JsonData.Get("sslRootCertFile").MustString("")
|
||||
settings.CertFile = datasource.JsonData.Get("sslCertFile").MustString("")
|
||||
settings.CertKeyFile = datasource.JsonData.Get("sslKeyFile").MustString("")
|
||||
if err := validateCertFunc(settings.RootCertFile, settings.CertFile, settings.CertKeyFile); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
type certFileType int
|
||||
|
||||
const (
|
||||
rootCert = iota
|
||||
clientCert
|
||||
clientKey
|
||||
)
|
||||
|
||||
func (t certFileType) String() string {
|
||||
switch t {
|
||||
case rootCert:
|
||||
return "root certificate"
|
||||
case clientCert:
|
||||
return "client certificate"
|
||||
case clientKey:
|
||||
return "client key"
|
||||
default:
|
||||
panic(fmt.Sprintf("Unrecognized certFileType %d", t))
|
||||
}
|
||||
}
|
||||
|
||||
func getFileName(dataDir string, fileType certFileType) string {
|
||||
var filename string
|
||||
switch fileType {
|
||||
case rootCert:
|
||||
filename = "root.crt"
|
||||
case clientCert:
|
||||
filename = "client.crt"
|
||||
case clientKey:
|
||||
filename = "client.key"
|
||||
default:
|
||||
panic(fmt.Sprintf("unrecognized certFileType %s", fileType.String()))
|
||||
}
|
||||
generatedFilePath := filepath.Join(dataDir, filename)
|
||||
return generatedFilePath
|
||||
}
|
||||
|
||||
// writeCertFile writes a certificate file.
|
||||
func writeCertFile(
|
||||
ds *models.DataSource, logger log.Logger, fileContent string, generatedFilePath string) error {
|
||||
fileContent = strings.TrimSpace(fileContent)
|
||||
if fileContent != "" {
|
||||
logger.Debug("Writing cert file", "path", generatedFilePath)
|
||||
if err := ioutil.WriteFile(generatedFilePath, []byte(fileContent), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Make sure the file has the permissions expected by the Postgresql driver, otherwise it will bail
|
||||
if err := os.Chmod(generatedFilePath, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug("Deleting cert file since no content is provided", "path", generatedFilePath)
|
||||
exists, err := fs.Exists(generatedFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
if err := os.Remove(generatedFilePath); err != nil {
|
||||
return fmt.Errorf("failed to remove %q: %w", generatedFilePath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *tlsManager) writeCertFiles(ds *models.DataSource, settings *tlsSettings) error {
|
||||
m.logger.Debug("Writing TLS certificate files to disk")
|
||||
decrypted := ds.DecryptedValues()
|
||||
tlsRootCert := decrypted["tlsCACert"]
|
||||
tlsClientCert := decrypted["tlsClientCert"]
|
||||
tlsClientKey := decrypted["tlsClientKey"]
|
||||
|
||||
if tlsRootCert == "" && tlsClientCert == "" && tlsClientKey == "" {
|
||||
m.logger.Debug("No TLS/SSL certificates provided")
|
||||
}
|
||||
|
||||
// Calculate all files path
|
||||
workDir := filepath.Join(m.dataPath, "tls", ds.Uid+"generatedTLSCerts")
|
||||
settings.RootCertFile = getFileName(workDir, rootCert)
|
||||
settings.CertFile = getFileName(workDir, clientCert)
|
||||
settings.CertKeyFile = getFileName(workDir, clientKey)
|
||||
|
||||
// Find datasource in the cache, if found, skip writing files
|
||||
cacheKey := strconv.Itoa(int(ds.Id))
|
||||
m.dsCacheInstance.locker.RLock(cacheKey)
|
||||
item, ok := m.dsCacheInstance.cache.Load(cacheKey)
|
||||
m.dsCacheInstance.locker.RUnlock(cacheKey)
|
||||
if ok {
|
||||
if item.(int) == ds.Version {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
m.dsCacheInstance.locker.Lock(cacheKey)
|
||||
defer m.dsCacheInstance.locker.Unlock(cacheKey)
|
||||
|
||||
item, ok = m.dsCacheInstance.cache.Load(cacheKey)
|
||||
if ok {
|
||||
if item.(int) == ds.Version {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Write certification directory and files
|
||||
exists, err := fs.Exists(workDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err := os.MkdirAll(workDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = writeCertFileFunc(ds, m.logger, tlsRootCert, settings.RootCertFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = writeCertFileFunc(ds, m.logger, tlsClientCert, settings.CertFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = writeCertFileFunc(ds, m.logger, tlsClientKey, settings.CertKeyFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update datasource cache
|
||||
m.dsCacheInstance.cache.Store(cacheKey, ds.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCertFilePaths validates configured certificate file paths.
|
||||
func validateCertFilePaths(rootCert, clientCert, clientKey string) error {
|
||||
for _, fpath := range []string{rootCert, clientCert, clientKey} {
|
||||
if fpath == "" {
|
||||
continue
|
||||
}
|
||||
exists, err := fs.Exists(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("certificate file %q doesn't exist", fpath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
290
pkg/tsdb/postgres/tlsmanager_test.go
Normal file
290
pkg/tsdb/postgres/tlsmanager_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
var writeCertFileCallNum int
|
||||
|
||||
// TestDataSourceCacheManager is to test the Cache manager
|
||||
func TestDataSourceCacheManager(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.DataPath = t.TempDir()
|
||||
mng := tlsManager{
|
||||
logger: log.New("tsdb.postgres"),
|
||||
dsCacheInstance: datasourceCacheManager{locker: newLocker()},
|
||||
dataPath: cfg.DataPath,
|
||||
}
|
||||
|
||||
jsonData := simplejson.NewFromAny(map[string]interface{}{
|
||||
"sslmode": "verify-full",
|
||||
"tlsConfigurationMethod": "file-content",
|
||||
})
|
||||
secureJSONData := securejsondata.GetEncryptedJsonData(map[string]string{
|
||||
"tlsClientCert": "I am client certification",
|
||||
"tlsClientKey": "I am client key",
|
||||
"tlsCACert": "I am CA certification",
|
||||
})
|
||||
|
||||
mockValidateCertFilePaths()
|
||||
t.Cleanup(resetValidateCertFilePaths)
|
||||
|
||||
t.Run("Check datasource cache creation", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(10)
|
||||
for id := int64(1); id <= 10; id++ {
|
||||
go func(id int64) {
|
||||
ds := &models.DataSource{
|
||||
Id: id,
|
||||
Version: 1,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
}
|
||||
s := tlsSettings{}
|
||||
err := mng.writeCertFiles(ds, &s)
|
||||
require.NoError(t, err)
|
||||
wg.Done()
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Run("check cache creation is succeed", func(t *testing.T) {
|
||||
for id := int64(1); id <= 10; id++ {
|
||||
version, ok := mng.dsCacheInstance.cache.Load(strconv.Itoa(int(id)))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int(1), version)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Check datasource cache modification", func(t *testing.T) {
|
||||
t.Run("check when version not changed, cache and files are not updated", func(t *testing.T) {
|
||||
mockWriteCertFile()
|
||||
t.Cleanup(resetWriteCertFile)
|
||||
var wg1 sync.WaitGroup
|
||||
wg1.Add(5)
|
||||
for id := int64(1); id <= 5; id++ {
|
||||
go func(id int64) {
|
||||
ds := &models.DataSource{
|
||||
Id: 1,
|
||||
Version: 2,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
}
|
||||
s := tlsSettings{}
|
||||
err := mng.writeCertFiles(ds, &s)
|
||||
require.NoError(t, err)
|
||||
wg1.Done()
|
||||
}(id)
|
||||
}
|
||||
wg1.Wait()
|
||||
assert.Equal(t, writeCertFileCallNum, 3)
|
||||
})
|
||||
|
||||
t.Run("cache is updated with the last datasource version", func(t *testing.T) {
|
||||
dsV2 := &models.DataSource{
|
||||
Id: 1,
|
||||
Version: 2,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
}
|
||||
dsV3 := &models.DataSource{
|
||||
Id: 1,
|
||||
Version: 3,
|
||||
Database: "database",
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: secureJSONData,
|
||||
Uid: "testData",
|
||||
}
|
||||
s := tlsSettings{}
|
||||
err := mng.writeCertFiles(dsV2, &s)
|
||||
require.NoError(t, err)
|
||||
err = mng.writeCertFiles(dsV3, &s)
|
||||
require.NoError(t, err)
|
||||
version, ok := mng.dsCacheInstance.cache.Load("1")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int(3), version)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Test getFileName
|
||||
|
||||
func TestGetFileName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
datadir string
|
||||
fileType certFileType
|
||||
expErr string
|
||||
expectedGeneratedPath string
|
||||
}{
|
||||
{
|
||||
desc: "Get File Name for root certification",
|
||||
datadir: ".",
|
||||
fileType: rootCert,
|
||||
expectedGeneratedPath: "root.crt",
|
||||
},
|
||||
{
|
||||
desc: "Get File Name for client certification",
|
||||
datadir: ".",
|
||||
fileType: clientCert,
|
||||
expectedGeneratedPath: "client.crt",
|
||||
},
|
||||
{
|
||||
desc: "Get File Name for client certification",
|
||||
datadir: ".",
|
||||
fileType: clientKey,
|
||||
expectedGeneratedPath: "client.key",
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
generatedPath := getFileName(tt.datadir, tt.fileType)
|
||||
assert.Equal(t, tt.expectedGeneratedPath, generatedPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test getTLSSettings.
|
||||
func TestGetTLSSettings(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.DataPath = t.TempDir()
|
||||
|
||||
mockValidateCertFilePaths()
|
||||
t.Cleanup(resetValidateCertFilePaths)
|
||||
testCases := []struct {
|
||||
desc string
|
||||
expErr string
|
||||
jsonData map[string]interface{}
|
||||
secureJSONData map[string]string
|
||||
uid string
|
||||
tlsSettings tlsSettings
|
||||
version int
|
||||
}{
|
||||
{
|
||||
desc: "Custom TLS authentication disabled",
|
||||
version: 1,
|
||||
jsonData: map[string]interface{}{
|
||||
"sslmode": "disable",
|
||||
"sslRootCertFile": "i/am/coding/ca.crt",
|
||||
"sslCertFile": "i/am/coding/client.crt",
|
||||
"sslKeyFile": "i/am/coding/client.key",
|
||||
"tlsConfigurationMethod": "file-path",
|
||||
},
|
||||
tlsSettings: tlsSettings{Mode: "disable"},
|
||||
},
|
||||
{
|
||||
desc: "Custom TLS authentication with file path",
|
||||
version: 2,
|
||||
jsonData: map[string]interface{}{
|
||||
"sslmode": "verify-full",
|
||||
"sslRootCertFile": "i/am/coding/ca.crt",
|
||||
"sslCertFile": "i/am/coding/client.crt",
|
||||
"sslKeyFile": "i/am/coding/client.key",
|
||||
"tlsConfigurationMethod": "file-path",
|
||||
},
|
||||
tlsSettings: tlsSettings{
|
||||
Mode: "verify-full",
|
||||
ConfigurationMethod: "file-path",
|
||||
RootCertFile: "i/am/coding/ca.crt",
|
||||
CertFile: "i/am/coding/client.crt",
|
||||
CertKeyFile: "i/am/coding/client.key",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Custom TLS mode verify-full with certificate files content",
|
||||
version: 3,
|
||||
uid: "xxx",
|
||||
jsonData: map[string]interface{}{
|
||||
"sslmode": "verify-full",
|
||||
"tlsConfigurationMethod": "file-content",
|
||||
},
|
||||
secureJSONData: map[string]string{
|
||||
"tlsCACert": "I am CA certification",
|
||||
"tlsClientCert": "I am client certification",
|
||||
"tlsClientKey": "I am client key",
|
||||
},
|
||||
tlsSettings: tlsSettings{
|
||||
Mode: "verify-full",
|
||||
ConfigurationMethod: "file-content",
|
||||
RootCertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "root.crt"),
|
||||
CertFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.crt"),
|
||||
CertKeyFile: filepath.Join(cfg.DataPath, "tls", "xxxgeneratedTLSCerts", "client.key"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
var settings tlsSettings
|
||||
var err error
|
||||
mng := tlsManager{
|
||||
logger: log.New("tsdb.postgres"),
|
||||
dsCacheInstance: datasourceCacheManager{locker: newLocker()},
|
||||
dataPath: cfg.DataPath,
|
||||
}
|
||||
|
||||
jsonData := simplejson.NewFromAny(tt.jsonData)
|
||||
ds := &models.DataSource{
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: securejsondata.GetEncryptedJsonData(tt.secureJSONData),
|
||||
Uid: tt.uid,
|
||||
Version: tt.version,
|
||||
}
|
||||
|
||||
settings, err = mng.getTLSSettings(ds)
|
||||
|
||||
if tt.expErr == "" {
|
||||
require.NoError(t, err, tt.desc)
|
||||
assert.Equal(t, tt.tlsSettings, settings)
|
||||
} else {
|
||||
require.Error(t, err, tt.desc)
|
||||
assert.True(t, strings.HasPrefix(err.Error(), tt.expErr),
|
||||
fmt.Sprintf("%s: %q doesn't start with %q", tt.desc, err, tt.expErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockValidateCertFilePaths() {
|
||||
validateCertFunc = func(rootCert, clientCert, clientKey string) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func resetValidateCertFilePaths() {
|
||||
validateCertFunc = validateCertFilePaths
|
||||
}
|
||||
|
||||
func mockWriteCertFile() {
|
||||
writeCertFileCallNum = 0
|
||||
writeCertFileFunc = func(ds *models.DataSource, logger log.Logger, fileContent string, generatedFilePath string) error {
|
||||
writeCertFileCallNum++
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func resetWriteCertFile() {
|
||||
writeCertFileCallNum = 0
|
||||
writeCertFileFunc = writeCertFile
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<h6>TLS Auth Details</h6>
|
||||
<info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover>
|
||||
<h6>TLS/SSL Auth Details</h6>
|
||||
<info-popover mode="header">TLS/SSL certificates are encrypted and stored in the Grafana database.</info-popover>
|
||||
</div>
|
||||
<div ng-if="current.jsonData.tlsAuthWithCACert">
|
||||
<div class="gf-form-inline">
|
||||
|
@ -19,11 +19,13 @@ export class PostgresConfigCtrl {
|
||||
constructor($scope: any, datasourceSrv: DatasourceSrv) {
|
||||
this.datasourceSrv = datasourceSrv;
|
||||
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
|
||||
this.current.jsonData.tlsConfigurationMethod = this.current.jsonData.tlsConfigurationMethod || 'file-path';
|
||||
this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
|
||||
this.showTimescaleDBHelp = false;
|
||||
this.autoDetectFeatures();
|
||||
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
|
||||
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
|
||||
this.tlsModeMapping();
|
||||
}
|
||||
|
||||
autoDetectFeatures() {
|
||||
@ -62,6 +64,18 @@ export class PostgresConfigCtrl {
|
||||
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
|
||||
}
|
||||
|
||||
tlsModeMapping() {
|
||||
if (this.current.jsonData.sslmode === 'disable') {
|
||||
this.current.jsonData.tlsAuth = false;
|
||||
this.current.jsonData.tlsAuthWithCACert = false;
|
||||
this.current.jsonData.tlsSkipVerify = true;
|
||||
} else {
|
||||
this.current.jsonData.tlsAuth = true;
|
||||
this.current.jsonData.tlsAuthWithCACert = true;
|
||||
this.current.jsonData.tlsSkipVerify = false;
|
||||
}
|
||||
}
|
||||
|
||||
// the value portion is derived from postgres server_version_num/100
|
||||
postgresVersions = [
|
||||
{ name: '9.3', value: 903 },
|
||||
|
@ -28,27 +28,51 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">TLS/SSL Mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.sslmode"
|
||||
ng-options="mode for mode in ['disable', 'require', 'verify-ca', 'verify-full']"
|
||||
ng-init="ctrl.current.jsonData.sslmode"></select>
|
||||
ng-init="ctrl.current.jsonData.sslmode" ng-change="ctrl.tlsModeMapping()"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-30" ng-if="ctrl.current.jsonData.sslmode != 'disable'">
|
||||
|
||||
<div class="gf-form" ng-if="ctrl.current.jsonData.sslmode != 'disable'">
|
||||
<label class="gf-form-label width-10">TLS/SSL Method</label>
|
||||
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">
|
||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.tlsConfigurationMethod"
|
||||
ng-options="f.id as f.label for f in [{ id: 'file-path', label: 'File system path' }, { id: 'file-content', label: 'Certificate content' }]"
|
||||
ng-init="ctrl.current.jsonData.tlsConfigurationMethod"></select>
|
||||
<info-popover mode="right-absolute">
|
||||
This option determines how TLS/SSL certifications are configured. Selecting <i>File system path</i> will allow
|
||||
you to configure certificates by specifying paths to existing certificates on the local file system where
|
||||
Grafana is running. Be sure that the file is readable by the user executing the Grafana process.<br><br>
|
||||
|
||||
Selecting <i>Certificate content</i> will allow you to configure certificates by specifying its content.
|
||||
The content will be stored encrypted in Grafana's database. When connecting to the database the certificates
|
||||
will be written as files to Grafana's configured data path on the local file system.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-path'">
|
||||
<div class="gf-form">
|
||||
<h6>TLS/SSL Auth Details</h6>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">TLS/SSL Root Certificate</span>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon"
|
||||
ng-model='ctrl.current.jsonData.sslRootCertFile' placeholder="TLS/SSL root cert file"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here.
|
||||
Be sure that the file is readable by the user executing the grafana process.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30" ng-if="ctrl.current.jsonData.sslmode != 'disable'">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">TLS/SSL Client Certificate</span>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslCertFile'
|
||||
placeholder="TLS/SSL client cert file"></input>
|
||||
@ -57,7 +81,7 @@
|
||||
Be sure that the file is readable by the user executing the grafana process.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30" ng-if="ctrl.current.jsonData.sslmode != 'disable'">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-10">TLS/SSL Client Key</span>
|
||||
<input type="text" class="gf-form-input gf-form-input--has-help-icon" ng-model='ctrl.current.jsonData.sslKeyFile'
|
||||
placeholder="TLS/SSL client key file"></input>
|
||||
@ -67,6 +91,9 @@
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<datasource-tls-auth-settings current="ctrl.current"
|
||||
ng-if="ctrl.current.jsonData.sslmode != 'disable' && ctrl.current.jsonData.tlsConfigurationMethod === 'file-content'">
|
||||
</datasource-tls-auth-settings>
|
||||
|
||||
<b>Connection limits</b>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user