mirror of https://github.com/grafana/grafana.git
				
				
				
			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 |         <Tooltip | ||||||
|           placement="right-end" |           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" |           theme="info" | ||||||
|         > |         > | ||||||
|           <div className="gf-form-help-icon gf-form-help-icon--right-normal"> |           <div className="gf-form-help-icon gf-form-help-icon--right-normal"> | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/grafana/grafana/pkg/registry" | ||||||
| 	"github.com/grafana/grafana/pkg/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
| 	"github.com/grafana/grafana/pkg/util/errutil" | 	"github.com/grafana/grafana/pkg/util/errutil" | ||||||
| 
 | 
 | ||||||
|  | @ -13,24 +14,43 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/models" | 	"github.com/grafana/grafana/pkg/models" | ||||||
| 	"github.com/grafana/grafana/pkg/tsdb" | 	"github.com/grafana/grafana/pkg/tsdb" | ||||||
| 	"github.com/grafana/grafana/pkg/tsdb/sqleng" | 	"github.com/grafana/grafana/pkg/tsdb/sqleng" | ||||||
|  | 
 | ||||||
| 	"xorm.io/core" | 	"xorm.io/core" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | 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) { | type postgresService struct { | ||||||
| 	logger := log.New("tsdb.postgres") | 	Cfg        *setting.Cfg `inject:""` | ||||||
| 	logger.Debug("Creating Postgres query endpoint") | 	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 { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if setting.Env == setting.Dev { | 	if s.Cfg.Env == setting.Dev { | ||||||
| 		logger.Debug("getEngine", "connection", cnnstr) | 		s.logger.Debug("getEngine", "connection", cnnstr) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	config := sqleng.SqlQueryEndpointConfiguration{ | 	config := sqleng.SqlQueryEndpointConfiguration{ | ||||||
|  | @ -41,18 +61,19 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	queryResultTransformer := postgresQueryResultTransformer{ | 	queryResultTransformer := postgresQueryResultTransformer{ | ||||||
| 		log: logger, | 		log: s.logger, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false) | 	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 { | 	if err != nil { | ||||||
| 		logger.Debug("Failed connecting to Postgres", "err", err) | 		s.logger.Error("Failed connecting to Postgres", "err", err) | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	logger.Debug("Successfully connected to Postgres") | 	s.logger.Debug("Successfully connected to Postgres") | ||||||
| 	return endpoint, err | 	return endpoint, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -61,15 +82,13 @@ func escape(input string) string { | ||||||
| 	return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`) | 	return strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), "'", `\'`) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func generateConnectionString(datasource *models.DataSource, logger log.Logger) (string, error) { | func (s *postgresService) generateConnectionString(datasource *models.DataSource) (string, error) { | ||||||
| 	tlsMode := strings.TrimSpace(strings.ToLower(datasource.JsonData.Get("sslmode").MustString("verify-full"))) |  | ||||||
| 	isTLSDisabled := tlsMode == "disable" |  | ||||||
| 
 |  | ||||||
| 	var host string | 	var host string | ||||||
| 	var port int | 	var port int | ||||||
|  | 	var err error | ||||||
| 	if strings.HasPrefix(datasource.Url, "/") { | 	if strings.HasPrefix(datasource.Url, "/") { | ||||||
| 		host = 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 { | 	} else { | ||||||
| 		sp := strings.SplitN(datasource.Url, ":", 2) | 		sp := strings.SplitN(datasource.Url, ":", 2) | ||||||
| 		host = sp[0] | 		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]) | 				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 { | 		} 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'", | 	connStr := fmt.Sprintf("user='%s' password='%s' host='%s' dbname='%s'", | ||||||
| 		escape(datasource.User), escape(datasource.DecryptedPassword()), escape(host), escape(datasource.Database), | 		escape(datasource.User), escape(datasource.DecryptedPassword()), escape(host), escape(datasource.Database)) | ||||||
| 		escape(tlsMode)) |  | ||||||
| 	if port > 0 { | 	if port > 0 { | ||||||
| 		connStr += fmt.Sprintf(" port=%d", port) | 		connStr += fmt.Sprintf(" port=%d", port) | ||||||
| 	} | 	} | ||||||
| 	if isTLSDisabled { | 
 | ||||||
| 		logger.Debug("Postgres TLS/SSL is disabled") | 	tlsSettings, err := s.tlsManager.getTLSSettings(datasource) | ||||||
| 	} else { | 	if err != nil { | ||||||
| 		logger.Debug("Postgres TLS/SSL is enabled", "tlsMode", tlsMode) | 		return "", err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	connStr += fmt.Sprintf(" sslmode='%s'", escape(tlsSettings.Mode)) | ||||||
| 
 | 
 | ||||||
| 	// Attach root certificate if provided
 | 	// Attach root certificate if provided
 | ||||||
| 		if tlsRootCert := datasource.JsonData.Get("sslRootCertFile").MustString(""); tlsRootCert != "" { | 	// Attach root certificate if provided
 | ||||||
| 			logger.Debug("Setting server root certificate", "tlsRootCert", tlsRootCert) | 	if tlsSettings.RootCertFile != "" { | ||||||
| 			connStr += fmt.Sprintf(" sslrootcert='%s'", tlsRootCert) | 		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
 | 	// Attach client certificate and key if both are provided
 | ||||||
| 		tlsCert := datasource.JsonData.Get("sslCertFile").MustString("") | 	if tlsSettings.CertFile != "" && tlsSettings.CertKeyFile != "" { | ||||||
| 		tlsKey := datasource.JsonData.Get("sslKeyFile").MustString("") | 		s.logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsSettings.CertFile, "tlsKey", tlsSettings.CertKeyFile) | ||||||
| 		if tlsCert != "" && tlsKey != "" { | 		connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", escape(tlsSettings.CertFile), escape(tlsSettings.CertKeyFile)) | ||||||
| 			logger.Debug("Setting TLS/SSL client auth", "tlsCert", tlsCert, "tlsKey", tlsKey) | 	} else if tlsSettings.CertFile != "" || tlsSettings.CertKeyFile != "" { | ||||||
| 			connStr += fmt.Sprintf(" sslcert='%s' sslkey='%s'", tlsCert, tlsKey) |  | ||||||
| 		} else if tlsCert != "" || tlsKey != "" { |  | ||||||
| 		return "", fmt.Errorf("TLS/SSL client certificate and key must both be specified") | 		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 | 	return connStr, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/models" | 	"github.com/grafana/grafana/pkg/models" | ||||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" | 	"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" | ||||||
| 	"github.com/grafana/grafana/pkg/tsdb/sqleng" | 	"github.com/grafana/grafana/pkg/tsdb/sqleng" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
|  | @ -27,7 +28,8 @@ import ( | ||||||
| 
 | 
 | ||||||
| // Test generateConnectionString.
 | // Test generateConnectionString.
 | ||||||
| func TestGenerateConnectionString(t *testing.T) { | func TestGenerateConnectionString(t *testing.T) { | ||||||
| 	logger := log.New("tsdb.postgres") | 	cfg := setting.NewCfg() | ||||||
|  | 	cfg.DataPath = t.TempDir() | ||||||
| 
 | 
 | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		desc        string | 		desc        string | ||||||
|  | @ -35,9 +37,10 @@ func TestGenerateConnectionString(t *testing.T) { | ||||||
| 		user        string | 		user        string | ||||||
| 		password    string | 		password    string | ||||||
| 		database    string | 		database    string | ||||||
| 		tlsMode    string | 		tlsSettings tlsSettings | ||||||
| 		expConnStr  string | 		expConnStr  string | ||||||
| 		expErr      string | 		expErr      string | ||||||
|  | 		uid         string | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			desc:        "Unix socket host", | 			desc:        "Unix socket host", | ||||||
|  | @ -45,6 +48,7 @@ func TestGenerateConnectionString(t *testing.T) { | ||||||
| 			user:        "user", | 			user:        "user", | ||||||
| 			password:    "password", | 			password:    "password", | ||||||
| 			database:    "database", | 			database:    "database", | ||||||
|  | 			tlsSettings: tlsSettings{Mode: "verify-full"}, | ||||||
| 			expConnStr:  "user='user' password='password' host='/var/run/postgresql' dbname='database' sslmode='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", | 			user:        "user", | ||||||
| 			password:    "password", | 			password:    "password", | ||||||
| 			database:    "database", | 			database:    "database", | ||||||
|  | 			tlsSettings: tlsSettings{Mode: "verify-full"}, | ||||||
| 			expConnStr:  "user='user' password='password' host='host' dbname='database' sslmode='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", | 			user:        "user", | ||||||
| 			password:    "password", | 			password:    "password", | ||||||
| 			database:    "database", | 			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", | 			desc:        "Invalid port", | ||||||
| 			host:        "host:invalid", | 			host:        "host:invalid", | ||||||
| 			user:        "user", | 			user:        "user", | ||||||
| 			database:    "database", | 			database:    "database", | ||||||
|  | 			tlsSettings: tlsSettings{}, | ||||||
| 			expErr:      "invalid port in host specifier", | 			expErr:      "invalid port in host specifier", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | @ -76,35 +83,55 @@ func TestGenerateConnectionString(t *testing.T) { | ||||||
| 			user:        "user", | 			user:        "user", | ||||||
| 			password:    `p'\assword`, | 			password:    `p'\assword`, | ||||||
| 			database:    "database", | 			database:    "database", | ||||||
|  | 			tlsSettings: tlsSettings{Mode: "verify-full"}, | ||||||
| 			expConnStr:  `user='user' password='p\'\\assword' host='host' dbname='database' sslmode='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", | 			host:        "host", | ||||||
| 			user:        "user", | 			user:        "user", | ||||||
| 			password:    "password", | 			password:    "password", | ||||||
| 			database:    "database", | 			database:    "database", | ||||||
| 			tlsMode:    "disable", | 			tlsSettings: tlsSettings{Mode: "disable"}, | ||||||
| 			expConnStr:  "user='user' password='password' host='host' dbname='database' sslmode='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 { | 	for _, tt := range testCases { | ||||||
| 		t.Run(tt.desc, func(t *testing.T) { | 		t.Run(tt.desc, func(t *testing.T) { | ||||||
| 			data := map[string]interface{}{} | 			svc := postgresService{ | ||||||
| 			if tt.tlsMode != "" { | 				Cfg:        cfg, | ||||||
| 				data["sslmode"] = tt.tlsMode | 				logger:     log.New("tsdb.postgres"), | ||||||
|  | 				tlsManager: &tlsTestManager{settings: tt.tlsSettings}, | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
| 			ds := &models.DataSource{ | 			ds := &models.DataSource{ | ||||||
| 				Url:      tt.host, | 				Url:      tt.host, | ||||||
| 				User:     tt.user, | 				User:     tt.user, | ||||||
| 				Password: tt.password, | 				Password: tt.password, | ||||||
| 				Database: tt.database, | 				Database: tt.database, | ||||||
| 				JsonData: simplejson.NewFromAny(data), | 				Uid:      tt.uid, | ||||||
| 			} | 			} | ||||||
| 			connStr, err := generateConnectionString(ds, logger) | 
 | ||||||
|  | 			connStr, err := svc.generateConnectionString(ds) | ||||||
|  | 
 | ||||||
| 			if tt.expErr == "" { | 			if tt.expErr == "" { | ||||||
| 				require.NoError(t, err, tt.desc) | 				require.NoError(t, err, tt.desc) | ||||||
| 				assert.Equal(t, tt.expConnStr, connStr, tt.desc) | 				assert.Equal(t, tt.expConnStr, connStr) | ||||||
| 			} else { | 			} else { | ||||||
| 				require.Error(t, err, tt.desc) | 				require.Error(t, err, tt.desc) | ||||||
| 				assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), | 				assert.True(t, strings.HasPrefix(err.Error(), tt.expErr), | ||||||
|  | @ -127,7 +154,7 @@ func TestPostgres(t *testing.T) { | ||||||
| 	runPostgresTests := false | 	runPostgresTests := false | ||||||
| 	// runPostgresTests := true
 | 	// runPostgresTests := true
 | ||||||
| 
 | 
 | ||||||
| 	if !(sqlstore.IsTestDbPostgres() || runPostgresTests) { | 	if !sqlstore.IsTestDbPostgres() && !runPostgresTests { | ||||||
| 		t.Skip() | 		t.Skip() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -146,7 +173,15 @@ func TestPostgres(t *testing.T) { | ||||||
| 		return sql, nil | 		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(), | 		JsonData:       simplejson.New(), | ||||||
| 		SecureJsonData: securejsondata.SecureJsonData{}, | 		SecureJsonData: securejsondata.SecureJsonData{}, | ||||||
| 	}) | 	}) | ||||||
|  | @ -483,9 +518,9 @@ func TestPostgres(t *testing.T) { | ||||||
| 			ValueTwo            int64 `xorm:"integer 'valueTwo'"` | 			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) | 			require.NoError(t, err) | ||||||
| 			err = sess.DropTable(metric_values{}) | 			err := sess.DropTable(metric_values{}) | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
| 		} | 		} | ||||||
| 		err := sess.CreateTable(metric_values{}) | 		err := sess.CreateTable(metric_values{}) | ||||||
|  | @ -1084,9 +1119,7 @@ func InitPostgresTestDB(t *testing.T) *xorm.Engine { | ||||||
| 	testDB := sqlutil.PostgresTestDB() | 	testDB := sqlutil.PostgresTestDB() | ||||||
| 	x, err := xorm.NewEngine(testDB.DriverName, strings.Replace(testDB.ConnStr, "dbname=grafanatest", | 	x, err := xorm.NewEngine(testDB.DriverName, strings.Replace(testDB.ConnStr, "dbname=grafanatest", | ||||||
| 		"dbname=grafanadstest", 1)) | 		"dbname=grafanadstest", 1)) | ||||||
| 	if err != nil { | 	require.NoError(t, err, "Failed to init postgres DB") | ||||||
| 		t.Fatalf("Failed to init postgres DB %v", err) |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	x.DatabaseTZ = time.UTC | 	x.DatabaseTZ = time.UTC | ||||||
| 	x.TZLocation = time.UTC | 	x.TZLocation = time.UTC | ||||||
|  | @ -1108,3 +1141,11 @@ func genTimeRangeByInterval(from time.Time, duration time.Duration, interval tim | ||||||
| 
 | 
 | ||||||
| 	return timeRange | 	return timeRange | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type tlsTestManager struct { | ||||||
|  | 	settings tlsSettings | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *tlsTestManager) getTLSSettings(datasource *models.DataSource) (tlsSettings, error) { | ||||||
|  | 	return m.settings, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -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-group"> | ||||||
|   <div class="gf-form"> |   <div class="gf-form"> | ||||||
|     <h6>TLS Auth Details</h6> |     <h6>TLS/SSL Auth Details</h6> | ||||||
|     <info-popover mode="header">TLS Certs are encrypted and stored in the Grafana database.</info-popover> |     <info-popover mode="header">TLS/SSL certificates are encrypted and stored in the Grafana database.</info-popover> | ||||||
|   </div> |   </div> | ||||||
|   <div ng-if="current.jsonData.tlsAuthWithCACert"> |   <div ng-if="current.jsonData.tlsAuthWithCACert"> | ||||||
|     <div class="gf-form-inline"> |     <div class="gf-form-inline"> | ||||||
|  |  | ||||||
|  | @ -19,11 +19,13 @@ export class PostgresConfigCtrl { | ||||||
|   constructor($scope: any, datasourceSrv: DatasourceSrv) { |   constructor($scope: any, datasourceSrv: DatasourceSrv) { | ||||||
|     this.datasourceSrv = datasourceSrv; |     this.datasourceSrv = datasourceSrv; | ||||||
|     this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full'; |     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.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903; | ||||||
|     this.showTimescaleDBHelp = false; |     this.showTimescaleDBHelp = false; | ||||||
|     this.autoDetectFeatures(); |     this.autoDetectFeatures(); | ||||||
|     this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); |     this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password); | ||||||
|     this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); |     this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password); | ||||||
|  |     this.tlsModeMapping(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   autoDetectFeatures() { |   autoDetectFeatures() { | ||||||
|  | @ -62,6 +64,18 @@ export class PostgresConfigCtrl { | ||||||
|     this.showTimescaleDBHelp = !this.showTimescaleDBHelp; |     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
 |   // the value portion is derived from postgres server_version_num/100
 | ||||||
|   postgresVersions = [ |   postgresVersions = [ | ||||||
|     { name: '9.3', value: 903 }, |     { name: '9.3', value: 903 }, | ||||||
|  |  | ||||||
|  | @ -28,27 +28,51 @@ | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  | 
 | ||||||
|   <div class="gf-form"> |   <div class="gf-form"> | ||||||
|     <label class="gf-form-label width-10">TLS/SSL Mode</label> |     <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"> |     <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" |       <select class="gf-form-input" ng-model="ctrl.current.jsonData.sslmode" | ||||||
|         ng-options="mode for mode in ['disable', 'require', 'verify-ca', 'verify-full']" |         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"> |       <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. |         This option determines whether or with what priority a secure TLS/SSL TCP/IP connection will be negotiated with the server. | ||||||
|       </info-popover> |       </info-popover> | ||||||
|     </div> |     </div> | ||||||
|   </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> |     <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" |     <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> |       ng-model='ctrl.current.jsonData.sslRootCertFile' placeholder="TLS/SSL root cert file"></input> | ||||||
|     <info-popover mode="right-absolute"> |     <info-popover mode="right-absolute"> | ||||||
|     If the selected TLS/SSL mode requires a server root certificate, provide the path to the file here. |     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> |     </info-popover> | ||||||
|   </div> |   </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> |     <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' |     <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> |       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. |       Be sure that the file is readable by the user executing the grafana process. | ||||||
|     </info-popover> |     </info-popover> | ||||||
|   </div> |   </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> |     <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' |     <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> |       placeholder="TLS/SSL client key file"></input> | ||||||
|  | @ -67,6 +91,9 @@ | ||||||
|     </info-popover> |     </info-popover> | ||||||
|   </div> |   </div> | ||||||
| </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> | <b>Connection limits</b> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue