mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #6692 from grafana/feature/tls-client-auth
Feature/tls client auth
This commit is contained in:
commit
42f522fe97
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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-----`
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
24
pkg/components/securejsondata/securejsondata.go
Normal file
24
pkg/components/securejsondata/securejsondata.go
Normal 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
|
||||||
|
}
|
@ -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:"-"`
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user