Merge pull request #6692 from grafana/feature/tls-client-auth

Feature/tls client auth
This commit is contained in:
Carl Bergquist 2016-11-24 13:58:08 +01:00 committed by GitHub
commit 42f522fe97
13 changed files with 423 additions and 121 deletions

View File

@ -1,9 +1,10 @@
# 4.0-stable (unrelased) # 4.0-stable (unreleased)
### Bugfixes ### Bugfixes
* **Server-side rendering**: Fixed address used when rendering panel via phantomjs and using non default http_addr config [#6660](https://github.com/grafana/grafana/issues/6660) * **Server-side rendering**: Fixed address used when rendering panel via phantomjs and using non default http_addr config [#6660](https://github.com/grafana/grafana/issues/6660)
* **Graph panel**: Fixed graph panel tooltip sort order issue [#6648](https://github.com/grafana/grafana/issues/6648) * **Graph panel**: Fixed graph panel tooltip sort order issue [#6648](https://github.com/grafana/grafana/issues/6648)
* **Unsaved changes**: You now navigate to the intended page after saving in the unsaved changes dialog [#6675](https://github.com/grafana/grafana/issues/6675) * **Unsaved changes**: You now navigate to the intended page after saving in the unsaved changes dialog [#6675](https://github.com/grafana/grafana/issues/6675)
* **TLS Client Auth**: Support for TLS client authentication for datasource proxies [#2316](https://github.com/grafana/grafana/issues/2316)
# 4.0-beta2 (2016-11-21) # 4.0-beta2 (2016-11-21)

View File

@ -1,6 +1,11 @@
package api package api
import ( import (
"crypto/tls"
"net"
"net/http"
"time"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/api/pluginproxy"
@ -11,6 +16,16 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
var pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func InitAppPluginRoutes(r *macaron.Macaron) { func InitAppPluginRoutes(r *macaron.Macaron) {
for _, plugin := range plugins.Apps { for _, plugin := range plugins.Apps {
for _, route := range plugin.Routes { for _, route := range plugin.Routes {
@ -40,7 +55,7 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler
path := c.Params("*") path := c.Params("*")
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId) proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)
proxy.Transport = dataProxyTransport proxy.Transport = pluginProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request) proxy.ServeHTTP(c.Resp, c.Req.Request)
} }
} }

View File

@ -2,6 +2,7 @@ package api
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -17,14 +18,45 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
var dataProxyTransport = &http.Transport{ func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) {
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, transport := &http.Transport{
Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{
Dial: (&net.Dialer{ InsecureSkipVerify: true,
Timeout: 30 * time.Second, },
KeepAlive: 30 * time.Second, Proxy: http.ProxyFromEnvironment,
}).Dial, Dial: (&net.Dialer{
TLSHandshakeTimeout: 10 * time.Second, Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
var tlsAuth, tlsAuthWithCACert bool
if ds.JsonData != nil {
tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
}
if tlsAuth {
transport.TLSClientConfig.InsecureSkipVerify = false
decrypted := ds.SecureJsonData.Decrypt()
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
if ok {
transport.TLSClientConfig.RootCAs = caPool
}
}
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
if err != nil {
return nil, err
}
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
}
return transport, nil
} }
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy { func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
@ -128,7 +160,11 @@ func ProxyDataSourceRequest(c *middleware.Context) {
} }
proxy := NewReverseProxy(ds, proxyPath, targetUrl) proxy := NewReverseProxy(ds, proxyPath, targetUrl)
proxy.Transport = dataProxyTransport proxy.Transport, err = DataProxyTransport(ds)
if err != nil {
c.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
proxy.ServeHTTP(c.Resp, c.Req.Request) proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie") c.Resp.Header().Del("Set-Cookie")
} }

View File

@ -7,15 +7,24 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
func TestDataSourceProxy(t *testing.T) { func TestDataSourceProxy(t *testing.T) {
Convey("When getting graphite datasource proxy", t, func() { Convey("When getting graphite datasource proxy", t, func() {
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
targetUrl, _ := url.Parse(ds.Url) targetUrl, err := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "/render", targetUrl) proxy := NewReverseProxy(&ds, "/render", targetUrl)
proxy.Transport, err = DataProxyTransport(&ds)
So(err, ShouldBeNil)
transport, ok := proxy.Transport.(*http.Transport)
So(ok, ShouldBeTrue)
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue)
requestUrl, _ := url.Parse("http://grafana.com/sub") requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl} req := http.Request{URL: requestUrl}
@ -54,7 +63,102 @@ func TestDataSourceProxy(t *testing.T) {
So(queryVals["u"][0], ShouldEqual, "user") So(queryVals["u"][0], ShouldEqual, "user")
So(queryVals["p"][0], ShouldEqual, "password") So(queryVals["p"][0], ShouldEqual, "password")
}) })
})
Convey("When getting kubernetes datasource proxy", t, func() {
setting.SecretKey = "password"
json := simplejson.New()
json.Set("tlsAuth", true)
json.Set("tlsAuthWithCACert", true)
ds := m.DataSource{
Url: "htttp://k8s:8001",
Type: "Kubernetes",
JsonData: json,
SecureJsonData: map[string][]byte{
"tlsCACert": util.Encrypt([]byte(caCert), "password"),
"tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
"tlsClientKey": util.Encrypt([]byte(clientKey), "password"),
},
}
targetUrl, err := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "", targetUrl)
proxy.Transport, err = DataProxyTransport(&ds)
So(err, ShouldBeNil)
transport, ok := proxy.Transport.(*http.Transport)
Convey("Should add cert", func() {
So(ok, ShouldBeTrue)
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
})
}) })
} }
const caCert string = `-----BEGIN CERTIFICATE-----
MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg
QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4
4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d
1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN
iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ
0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx
2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5
VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9
I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB
CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7
adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k
MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh
FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
3lb92xM=
-----END CERTIFICATE-----`
const clientCert string = `-----BEGIN CERTIFICATE-----
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2
FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b
Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo
GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8
SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4
YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP
ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw
AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q
4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe
58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5
llG/Sw5+FquFuChaA6l5KWy7F3bQyA==
-----END CERTIFICATE-----`
const clientKey string = `-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV
u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn
Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega
0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI
LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi
dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs
Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk
CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x
64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM
8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh
WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf
vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz
k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs
DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35
aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ
Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo
jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01
hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0
M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8
v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX
xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL
Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0
Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD
FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD
+VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg=
-----END RSA PRIVATE KEY-----`

View File

@ -104,17 +104,56 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id}) c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
} }
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) { func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id") cmd.Id = c.ParamsInt64(":id")
err := bus.Dispatch(&cmd) err := fillWithSecureJsonData(&cmd)
if err != nil { if err != nil {
c.JsonApiErr(500, "Failed to update datasource", err) return ApiError(500, "Failed to update datasource", err)
return
} }
c.JsonOK("Datasource updated") err = bus.Dispatch(&cmd)
if err != nil {
return ApiError(500, "Failed to update datasource", err)
}
return Json(200, "Datasource updated")
}
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
if len(cmd.SecureJsonData) == 0 {
return nil
}
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
if err != nil {
return err
}
secureJsonData := ds.SecureJsonData.Decrypt()
for k, v := range secureJsonData {
if _, ok := cmd.SecureJsonData[k]; !ok {
cmd.SecureJsonData[k] = v
}
}
return nil
}
func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) {
query := m.GetDataSourceByIdQuery{
Id: id,
OrgId: orgId,
}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
return query.Result, nil
} }
// Get /api/datasources/name/:name // Get /api/datasources/name/:name
@ -152,7 +191,7 @@ func GetDataSourceIdByName(c *middleware.Context) Response {
} }
func convertModelToDtos(ds *m.DataSource) dtos.DataSource { func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
return dtos.DataSource{ dto := dtos.DataSource{
Id: ds.Id, Id: ds.Id,
OrgId: ds.OrgId, OrgId: ds.OrgId,
Name: ds.Name, Name: ds.Name,
@ -169,4 +208,12 @@ func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
IsDefault: ds.IsDefault, IsDefault: ds.IsDefault,
JsonData: ds.JsonData, JsonData: ds.JsonData,
} }
if len(ds.SecureJsonData) > 0 {
dto.TLSAuth.CACertSet = len(ds.SecureJsonData["tlsCACert"]) > 0
dto.TLSAuth.ClientCertSet = len(ds.SecureJsonData["tlsClientCert"]) > 0
dto.TLSAuth.ClientKeySet = len(ds.SecureJsonData["tlsClientKey"]) > 0
}
return dto
} }

View File

@ -64,22 +64,31 @@ type DashboardRedirect struct {
} }
type DataSource struct { type DataSource struct {
Id int64 `json:"id"` Id int64 `json:"id"`
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
TypeLogoUrl string `json:"typeLogoUrl"` TypeLogoUrl string `json:"typeLogoUrl"`
Access m.DsAccess `json:"access"` Access m.DsAccess `json:"access"`
Url string `json:"url"` Url string `json:"url"`
Password string `json:"password"` Password string `json:"password"`
User string `json:"user"` User string `json:"user"`
Database string `json:"database"` Database string `json:"database"`
BasicAuth bool `json:"basicAuth"` BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"` BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"` BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"` WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData,omitempty"` JsonData *simplejson.Json `json:"jsonData,omitempty"`
SecureJsonData map[string]string `json:"secureJsonData,omitempty"`
TLSAuth TLSAuth `json:"tlsAuth,omitempty"`
}
// TLSAuth is used to show if TLS certs have been uploaded already
type TLSAuth struct {
CACertSet bool `json:"tlsCACertSet"`
ClientCertSet bool `json:"tlsClientCertSet"`
ClientKeySet bool `json:"tlsClientKeySet"`
} }
type DataSourceList []DataSource type DataSourceList []DataSource

View File

@ -0,0 +1,24 @@
package securejsondata
import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type SecureJsonData map[string][]byte
func (s SecureJsonData) Decrypt() map[string]string {
decrypted := make(map[string]string)
for key, data := range s {
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
}
return decrypted
}
func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
encrypted := make(SecureJsonData)
for key, data := range sjd {
encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
return encrypted
}

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"time" "time"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
) )
@ -46,6 +47,7 @@ type DataSource struct {
WithCredentials bool WithCredentials bool
IsDefault bool IsDefault bool
JsonData *simplejson.Json JsonData *simplejson.Json
SecureJsonData securejsondata.SecureJsonData
Created time.Time Created time.Time
Updated time.Time Updated time.Time
@ -77,19 +79,20 @@ func IsKnownDataSourcePlugin(dsType string) bool {
// Also acts as api DTO // Also acts as api DTO
type AddDataSourceCommand struct { type AddDataSourceCommand struct {
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"` Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"` Access DsAccess `json:"access" binding:"Required"`
Url string `json:"url"` Url string `json:"url"`
Password string `json:"password"` Password string `json:"password"`
Database string `json:"database"` Database string `json:"database"`
User string `json:"user"` User string `json:"user"`
BasicAuth bool `json:"basicAuth"` BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"` BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"` BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"` WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"` JsonData *simplejson.Json `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
@ -98,19 +101,20 @@ type AddDataSourceCommand struct {
// Also acts as api DTO // Also acts as api DTO
type UpdateDataSourceCommand struct { type UpdateDataSourceCommand struct {
Name string `json:"name" binding:"Required"` Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"` Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"` Access DsAccess `json:"access" binding:"Required"`
Url string `json:"url"` Url string `json:"url"`
Password string `json:"password"` Password string `json:"password"`
User string `json:"user"` User string `json:"user"`
Database string `json:"database"` Database string `json:"database"`
BasicAuth bool `json:"basicAuth"` BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"` BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"` BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"` WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"` IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"` JsonData *simplejson.Json `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
Id int64 `json:"-"` Id int64 `json:"-"`

View File

@ -4,8 +4,7 @@ import (
"errors" "errors"
"time" "time"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/util"
) )
var ( var (
@ -19,23 +18,13 @@ type PluginSetting struct {
Enabled bool Enabled bool
Pinned bool Pinned bool
JsonData map[string]interface{} JsonData map[string]interface{}
SecureJsonData SecureJsonData SecureJsonData securejsondata.SecureJsonData
PluginVersion string PluginVersion string
Created time.Time Created time.Time
Updated time.Time Updated time.Time
} }
type SecureJsonData map[string][]byte
func (s SecureJsonData) Decrypt() map[string]string {
decrypted := make(map[string]string)
for key, data := range s {
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
}
return decrypted
}
// ---------------------- // ----------------------
// COMMANDS // COMMANDS
@ -58,12 +47,8 @@ type UpdatePluginSettingVersionCmd struct {
OrgId int64 `json:"-"` OrgId int64 `json:"-"`
} }
func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData { func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() securejsondata.SecureJsonData {
encrypted := make(SecureJsonData) return securejsondata.GetEncryptedJsonData(cmd.SecureJsonData)
for key, data := range cmd.SecureJsonData {
encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
return encrypted
} }
// --------------------- // ---------------------

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/securejsondata"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
@ -82,6 +83,7 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error {
BasicAuthPassword: cmd.BasicAuthPassword, BasicAuthPassword: cmd.BasicAuthPassword,
WithCredentials: cmd.WithCredentials, WithCredentials: cmd.WithCredentials,
JsonData: cmd.JsonData, JsonData: cmd.JsonData,
SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData),
Created: time.Now(), Created: time.Now(),
Updated: time.Now(), Updated: time.Now(),
} }
@ -128,6 +130,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
BasicAuthPassword: cmd.BasicAuthPassword, BasicAuthPassword: cmd.BasicAuthPassword,
WithCredentials: cmd.WithCredentials, WithCredentials: cmd.WithCredentials,
JsonData: cmd.JsonData, JsonData: cmd.JsonData,
SecureJsonData: securejsondata.GetEncryptedJsonData(cmd.SecureJsonData),
Updated: time.Now(), Updated: time.Now(),
} }

View File

@ -101,4 +101,9 @@ func addDataSourceMigration(mg *Migrator) {
mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{ mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{
Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0", Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0",
})) }))
// add column that can store TLS client auth data
mg.AddMigration("Add secure json data column", NewAddColumnMigration(tableV2, &Column{
Name: "secure_json_data", Type: DB_Text, Nullable: true,
}))
} }

View File

@ -1,56 +1,69 @@
<div class="gf-form-group"> <div class="gf-form-group">
<h3 class="page-heading">Http settings</h3> <h3 class="page-heading">Http settings</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Url</span>
<input class="gf-form-input" type="text"
ng-model='current.url' placeholder="{{suggestUrl}}"
bs-typeahead="getSuggestUrls" min-length="0"
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
<info-popover mode="right-absolute">
<p>Specify a complete HTTP url (for example http://your_server:8080)</p>
<span ng-show="current.access === 'direct'">
Your access method is <em>Direct</em>, this means the url
needs to be accessable from the browser.
</span>
<span ng-show="current.access === 'proxy'">
Your access method is currently <em>Proxy</em>, this means the url
needs to be accessable from the grafana backend.
</span>
</info-popover>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Access</span>
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
<info-popover mode="right-absolute">
Direct = url is used directly from browser<br>
Proxy = Grafana backend will proxy the request
</info-popover>
</div>
</div>
</div>
</div>
<h3 class="page-heading">Http Auth</h3>
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Url</span>
<input class="gf-form-input" type="text"
ng-model='current.url' placeholder="{{suggestUrl}}"
bs-typeahead="getSuggestUrls" min-length="0"
ng-pattern="/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/" required></input>
<info-popover mode="right-absolute">
<p>Specify a complete HTTP url (for example http://your_server:8080)</p>
<span ng-show="current.access === 'direct'">
Your access method is <em>Direct</em>, this means the url
needs to be accessable from the browser.
</span>
<span ng-show="current.access === 'proxy'">
Your access method is currently <em>Proxy</em>, this means the url
needs to be accessable from the grafana backend.
</span>
</info-popover>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Access</span>
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
<info-popover mode="right-absolute">
Direct = url is used directly from browser<br>
Proxy = Grafana backend will proxy the request
</info-popover>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-7">Http Auth</label>
</div>
<gf-form-switch class="gf-form" <gf-form-switch class="gf-form"
label="Basic Auth" label="Basic Auth"
checked="current.basicAuth" switch-class="max-width-6"> checked="current.basicAuth" label-class="width-8" switch-class="max-width-6">
</gf-form-switch> </gf-form-switch>
<gf-form-switch class="gf-form" <gf-form-switch class="gf-form"
label="With Credentials" label="With Credentials" tooltip="Whether credentials such as cookies or auth headers should be sent with cross-site requests."
checked="current.withCredentials" switch-class="max-width-6"> checked="current.withCredentials" label-class="width-11" switch-class="max-width-6">
</gf-form-switch> </gf-form-switch>
</div> </div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
label="TLS Client Auth" label-class="width-8"
checked="current.jsonData.tlsAuth" switch-class="max-width-6">
</gf-form-switch>
<gf-form-switch class="gf-form" ng-if="current.access=='proxy'"
label="With CA Cert" tooltip="Optional. Needed for self-signed TLS Certs."
checked="current.jsonData.tlsAuthWithCACert" label-class="width-11" switch-class="max-width-6">
</gf-form-switch>
</div>
</div>
<div class="gf-form-group" ng-if="current.basicAuth">
<h6>Basic Auth Details</h6>
<div class="gf-form" ng-if="current.basicAuth"> <div class="gf-form" ng-if="current.basicAuth">
<span class="gf-form-label width-7"> <span class="gf-form-label width-7">
User User
@ -58,7 +71,7 @@
<input class="gf-form-input max-width-21" type="text" ng-model='current.basicAuthUser' placeholder="user" required></input> <input class="gf-form-input max-width-21" type="text" ng-model='current.basicAuthUser' placeholder="user" required></input>
</div> </div>
<div class="gf-form" ng-if="current.basicAuth"> <div class="gf-form">
<span class="gf-form-label width-7"> <span class="gf-form-label width-7">
Password Password
</span> </span>
@ -66,3 +79,51 @@
</div> </div>
</div> </div>
<div class="gf-form-group" ng-if="current.jsonData.tlsAuth && current.access=='proxy'">
<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>
</div>
<div ng-if="current.jsonData.tlsAuthWithCACert">
<div class="gf-form-inline">
<div class="gf-form gf-form--v-stretch">
<label class="gf-form-label width-7">CA Cert</label>
</div>
<div class="gf-form gf-form--grow" ng-if="!current.tlsAuth.tlsCACertSet">
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsCACert" placeholder="Begins with -----BEGIN CERTIFICATE-----. The CA Certificate is necessary if you are using self-signed certificates."></textarea>
</div>
<div class="gf-form" ng-if="current.tlsAuth.tlsCACertSet">
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-if="current.tlsAuth.tlsCACertSet" ng-click="current.tlsAuth.tlsCACertSet = false">reset</a>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form gf-form--v-stretch">
<label class="gf-form-label width-7">Client Cert</label>
</div>
<div class="gf-form gf-form--grow" ng-if="!current.tlsAuth.tlsClientCertSet">
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientCert" placeholder="Begins with -----BEGIN CERTIFICATE-----" required></textarea>
</div>
<div class="gf-form" ng-if="current.tlsAuth.tlsClientCertSet">
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-if="current.tlsAuth.tlsClientCertSet" ng-click="current.tlsAuth.tlsClientCertSet = false">reset</a>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form gf-form--v-stretch">
<label class="gf-form-label width-7">Client Key</label>
</div>
<div class="gf-form gf-form--grow" ng-if="!current.tlsAuth.tlsClientKeySet">
<textarea rows="7" class="gf-form-input gf-form-textarea" ng-model="current.secureJsonData.tlsClientKey" placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----" required></textarea>
</div>
<div class="gf-form" ng-if="current.tlsAuth.tlsClientKeySet">
<input type="text" class="gf-form-input max-width-12" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-if="current.tlsAuth.tlsClientKeySet" ng-click="current.tlsAuth.tlsClientKeySet = false">reset</a>
</div>
</div>
</div>

View File

@ -79,6 +79,10 @@ $gf-form-margin: 0.25rem;
} }
} }
.gf-form-textarea {
max-width: 650px;
}
.gf-form-input { .gf-form-input {
display: block; display: block;
width: 100%; width: 100%;
@ -249,6 +253,10 @@ $gf-form-margin: 0.25rem;
&--right-normal { &--right-normal {
float: right; float: right;
} }
&--header {
margin-bottom: $gf-form-margin
}
} }
select.gf-form-input ~ .gf-form-help-icon { select.gf-form-input ~ .gf-form-help-icon {