Commit db8bd829 by Carl Bergquist Committed by GitHub

Merge pull request #14077 from bobmshannon/bs/metrics_endpoint_auth

Add basic authentication support to metrics endpoint
parents b948c9bd c999394b
...@@ -490,6 +490,10 @@ enabled = false ...@@ -490,6 +490,10 @@ enabled = false
enabled = true enabled = true
interval_seconds = 10 interval_seconds = 10
#If both are set, basic auth will be required for the metrics endpoint.
basic_auth_username =
basic_auth_password =
# Send internal Grafana metrics to graphite # Send internal Grafana metrics to graphite
[metrics.graphite] [metrics.graphite]
# Enable by setting the address setting (ex localhost:2003) # Enable by setting the address setting (ex localhost:2003)
......
...@@ -454,6 +454,12 @@ Ex `filters = sqlstore:debug` ...@@ -454,6 +454,12 @@ Ex `filters = sqlstore:debug`
### enabled ### enabled
Enable metrics reporting. defaults true. Available via HTTP API `/metrics`. Enable metrics reporting. defaults true. Available via HTTP API `/metrics`.
### basic_auth_username
If set configures the username to use for basic authentication on the metrics endpoint.
### basic_auth_password
If set configures the password to use for basic authentication on the metrics endpoint.
### interval_seconds ### interval_seconds
Flush/Write interval when sending metrics to external TSDB. Defaults to 10s. Flush/Write interval when sending metrics to external TSDB. Defaults to 10s.
......
package api
import (
"crypto/subtle"
macaron "gopkg.in/macaron.v1"
)
// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials
// and returns true if the provided credentials match the expected username and password.
// Returns false if the request is unauthenticated.
// Uses constant-time comparison in order to mitigate timing attacks.
func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool {
user, pass, ok := req.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 {
return false
}
return true
}
package api
import (
"encoding/base64"
"fmt"
"net/http"
"testing"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
)
func TestBasicAuthenticatedRequest(t *testing.T) {
expectedUser := "prometheus"
expectedPass := "password"
Convey("Given a valid set of basic auth credentials", t, func() {
httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
So(err, ShouldBeNil)
req := macaron.Request{
Request: httpReq,
}
encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
So(authenticated, ShouldBeTrue)
})
Convey("Given an invalid set of basic auth credentials", t, func() {
httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
So(err, ShouldBeNil)
req := macaron.Request{
Request: httpReq,
}
encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
So(authenticated, ShouldBeFalse)
})
}
func encodeBasicAuthCredentials(user, pass string) string {
creds := fmt.Sprintf("%s:%s", user, pass)
return base64.StdEncoding.EncodeToString([]byte(creds))
}
...@@ -245,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { ...@@ -245,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
return return
} }
if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}). promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
ServeHTTP(ctx.Resp, ctx.Req.Request) ServeHTTP(ctx.Resp, ctx.Req.Request)
} }
...@@ -299,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, ...@@ -299,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
}, },
)) ))
} }
func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
}
package api
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestHTTPServer(t *testing.T) {
Convey("Given a HTTPServer", t, func() {
ts := &HTTPServer{
Cfg: setting.NewCfg(),
}
Convey("Given that basic auth on the metrics endpoint is enabled", func() {
ts.Cfg.MetricsEndpointBasicAuthUsername = "foo"
ts.Cfg.MetricsEndpointBasicAuthPassword = "bar"
So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue)
})
Convey("Given that basic auth on the metrics endpoint is disabled", func() {
ts.Cfg.MetricsEndpointBasicAuthUsername = ""
ts.Cfg.MetricsEndpointBasicAuthPassword = ""
So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse)
})
})
}
...@@ -219,6 +219,8 @@ type Cfg struct { ...@@ -219,6 +219,8 @@ type Cfg struct {
DisableBruteForceLoginProtection bool DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration TempDataLifetime time.Duration
MetricsEndpointEnabled bool MetricsEndpointEnabled bool
MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string
EnableAlphaPanels bool EnableAlphaPanels bool
EnterpriseLicensePath string EnterpriseLicensePath string
} }
...@@ -681,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { ...@@ -681,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String()
cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String()
analytics := iniFile.Section("analytics") analytics := iniFile.Section("analytics")
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment