feat(httpd): Add option to authenticate debug/pprof and ping e… (#15222)
feat(httpd): Add option to authenticate debug/pprof and ping endpointspull/15236/head
commit
1618c25566
|
@ -4,6 +4,7 @@ v1.8.0 [unreleased]
|
|||
### Features
|
||||
|
||||
- [#14315](https://github.com/influxdata/influxdb/pull/14315): Update to go 1.12.7
|
||||
- [#15222](https://github.com/influxdata/influxdb/pull/15222): Add options to authenticate pprof and ping endpoints.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
|
|
|
@ -288,10 +288,19 @@
|
|||
# troubleshooting and monitoring.
|
||||
# pprof-enabled = true
|
||||
|
||||
# Enables authentication on pprof endpoints. Users will need admin permissions
|
||||
# to access the pprof endpoints when this setting is enabled. This setting has
|
||||
# no effect if either auth-enabled or pprof-enabled are set to false.
|
||||
# pprof-auth-enabled = false
|
||||
|
||||
# Enables a pprof endpoint that binds to localhost:6060 immediately on startup.
|
||||
# This is only needed to debug startup issues.
|
||||
# debug-pprof-enabled = false
|
||||
|
||||
# Enables authentication on the /ping, /metrics, and deprecated /status
|
||||
# endpoints. This setting has no effect if auth-enabled is set to false.
|
||||
# ping-auth-enabled = false
|
||||
|
||||
# Determines whether HTTPS is enabled.
|
||||
# https-enabled = false
|
||||
|
||||
|
|
|
@ -41,7 +41,9 @@ type Config struct {
|
|||
FluxEnabled bool `toml:"flux-enabled"`
|
||||
FluxLogEnabled bool `toml:"flux-log-enabled"`
|
||||
PprofEnabled bool `toml:"pprof-enabled"`
|
||||
PprofAuthEnabled bool `toml:"pprof-auth-enabled"`
|
||||
DebugPprofEnabled bool `toml:"debug-pprof-enabled"`
|
||||
PingAuthEnabled bool `toml:"ping-auth-enabled"`
|
||||
HTTPSEnabled bool `toml:"https-enabled"`
|
||||
HTTPSCertificate string `toml:"https-certificate"`
|
||||
HTTPSPrivateKey string `toml:"https-private-key"`
|
||||
|
@ -71,7 +73,9 @@ func NewConfig() Config {
|
|||
BindAddress: DefaultBindAddress,
|
||||
LogEnabled: true,
|
||||
PprofEnabled: true,
|
||||
PprofAuthEnabled: false,
|
||||
DebugPprofEnabled: false,
|
||||
PingAuthEnabled: false,
|
||||
HTTPSEnabled: false,
|
||||
HTTPSCertificate: "/etc/ssl/influxdb.pem",
|
||||
MaxRowLimit: 0,
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
httppprof "net/http/pprof"
|
||||
|
||||
"github.com/bmizerany/pat"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
|
@ -154,6 +156,19 @@ func NewHandler(c Config) *Handler {
|
|||
writeLogEnabled = false
|
||||
}
|
||||
|
||||
var authWrapper func(handler func(http.ResponseWriter, *http.Request)) interface{}
|
||||
if h.Config.AuthEnabled && h.Config.PingAuthEnabled {
|
||||
authWrapper = func(handler func(http.ResponseWriter, *http.Request)) interface{} {
|
||||
return func(w http.ResponseWriter, r *http.Request, user meta.User) {
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
authWrapper = func(handler func(http.ResponseWriter, *http.Request)) interface{} {
|
||||
return handler
|
||||
}
|
||||
}
|
||||
|
||||
h.AddRoutes([]Route{
|
||||
Route{
|
||||
"query-options", // Satisfy CORS checks.
|
||||
|
@ -185,26 +200,67 @@ func NewHandler(c Config) *Handler {
|
|||
},
|
||||
Route{ // Ping
|
||||
"ping",
|
||||
"GET", "/ping", false, true, h.servePing,
|
||||
"GET", "/ping", false, true, authWrapper(h.servePing),
|
||||
},
|
||||
Route{ // Ping
|
||||
"ping-head",
|
||||
"HEAD", "/ping", false, true, h.servePing,
|
||||
"HEAD", "/ping", false, true, authWrapper(h.servePing),
|
||||
},
|
||||
Route{ // Ping w/ status
|
||||
"status",
|
||||
"GET", "/status", false, true, h.serveStatus,
|
||||
"GET", "/status", false, true, authWrapper(h.serveStatus),
|
||||
},
|
||||
Route{ // Ping w/ status
|
||||
"status-head",
|
||||
"HEAD", "/status", false, true, h.serveStatus,
|
||||
"HEAD", "/status", false, true, authWrapper(h.serveStatus),
|
||||
},
|
||||
Route{
|
||||
"prometheus-metrics",
|
||||
"GET", "/metrics", false, true, promhttp.Handler().ServeHTTP,
|
||||
"GET", "/metrics", false, true, authWrapper(promhttp.Handler().ServeHTTP),
|
||||
},
|
||||
}...)
|
||||
|
||||
// When PprofAuthEnabled is enabled, create debug/pprof endpoints with the
|
||||
// same authentication handlers as other endpoints.
|
||||
if h.Config.AuthEnabled && h.Config.PprofEnabled && h.Config.PprofAuthEnabled {
|
||||
authWrapper = func(handler func(http.ResponseWriter, *http.Request)) interface{} {
|
||||
return func(w http.ResponseWriter, r *http.Request, user meta.User) {
|
||||
if user == nil || !user.AuthorizeUnrestricted() {
|
||||
h.Logger.Info("Unauthorized request", zap.String("user", user.ID()), zap.String("path", r.URL.Path))
|
||||
h.httpError(w, "error authorizing admin access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
h.AddRoutes([]Route{
|
||||
Route{
|
||||
"pprof-cmdline",
|
||||
"GET", "/debug/pprof/cmdline", true, true, authWrapper(httppprof.Cmdline),
|
||||
},
|
||||
Route{
|
||||
"pprof-profile",
|
||||
"GET", "/debug/pprof/profile", true, true, authWrapper(httppprof.Profile),
|
||||
},
|
||||
Route{
|
||||
"pprof-symbol",
|
||||
"GET", "/debug/pprof/symbol", true, true, authWrapper(httppprof.Symbol),
|
||||
},
|
||||
Route{
|
||||
"pprof-all",
|
||||
"GET", "/debug/pprof/all", true, true, authWrapper(h.archiveProfilesAndQueries),
|
||||
},
|
||||
Route{
|
||||
"debug-expvar",
|
||||
"GET", "/debug/vars", true, true, authWrapper(h.serveExpvar),
|
||||
},
|
||||
Route{
|
||||
"debug-requests",
|
||||
"GET", "/debug/requests", true, true, authWrapper(h.serveDebugRequests),
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
fluxRoute := Route{
|
||||
"flux-read",
|
||||
"POST", "/api/v2/query", true, true, nil,
|
||||
|
@ -376,7 +432,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Add("X-Influxdb-Version", h.Version)
|
||||
w.Header().Add("X-Influxdb-Build", h.BuildType)
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/debug/pprof") && h.Config.PprofEnabled {
|
||||
// Maintain backwards compatibility by using unwrapped pprof/debug handlers
|
||||
// when PprofAuthEnabled is false.
|
||||
if h.Config.AuthEnabled && h.Config.PprofEnabled && h.Config.PprofAuthEnabled {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/debug/pprof") && h.Config.PprofEnabled {
|
||||
h.handleProfiles(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/debug/vars") {
|
||||
h.serveExpvar(w, r)
|
||||
|
|
|
@ -594,6 +594,158 @@ func TestHandler_Query_CloseNotify(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure the handler returns an appropriate 401 status when authentication
|
||||
// fails on ping endpoints.
|
||||
func TestHandler_Ping_ErrAuthorize(t *testing.T) {
|
||||
h := NewHandlerWithConfig(NewHandlerConfig(WithAuthentication(), WithPingAuthEnabled()))
|
||||
h.MetaClient.AdminUserExistsFn = func() bool { return true }
|
||||
h.MetaClient.DatabaseFn = func(name string) *meta.DatabaseInfo {
|
||||
return &meta.DatabaseInfo{}
|
||||
}
|
||||
h.MetaClient.AuthenticateFn = func(u, p string) (meta.User, error) {
|
||||
users := []meta.UserInfo{
|
||||
{
|
||||
Name: "admin",
|
||||
Hash: "admin",
|
||||
Admin: true,
|
||||
},
|
||||
{
|
||||
Name: "user1",
|
||||
Hash: "abcd",
|
||||
Privileges: map[string]influxql.Privilege{
|
||||
"db0": influxql.ReadPrivilege,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if u == user.Name {
|
||||
if p == user.Hash {
|
||||
return &user, nil
|
||||
}
|
||||
return nil, meta.ErrAuthenticate
|
||||
}
|
||||
}
|
||||
return nil, meta.ErrUserNotFound
|
||||
}
|
||||
|
||||
for i, tt := range []struct {
|
||||
user string
|
||||
password string
|
||||
query string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
query: "/ping",
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
user: "user1",
|
||||
password: "abcd",
|
||||
query: "/ping",
|
||||
code: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
user: "user2",
|
||||
password: "abcd",
|
||||
query: "/ping",
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
r := MustNewJSONRequest("GET", tt.query, nil)
|
||||
params := r.URL.Query()
|
||||
if tt.user != "" {
|
||||
params.Set("u", tt.user)
|
||||
}
|
||||
if tt.password != "" {
|
||||
params.Set("p", tt.password)
|
||||
}
|
||||
r.URL.RawQuery = params.Encode()
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != tt.code {
|
||||
t.Errorf("%d. unexpected status: got=%d exp=%d\noutput: %s", i, w.Code, tt.code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the handler returns an appropriate 403 status when authentication or
|
||||
// authorization fails on debug endpoints.
|
||||
func TestHandler_Debug_ErrAuthorize(t *testing.T) {
|
||||
h := NewHandlerWithConfig(NewHandlerConfig(WithAuthentication(), WithPprofAuthEnabled()))
|
||||
h.MetaClient.AdminUserExistsFn = func() bool { return true }
|
||||
h.MetaClient.DatabaseFn = func(name string) *meta.DatabaseInfo {
|
||||
return &meta.DatabaseInfo{}
|
||||
}
|
||||
h.MetaClient.AuthenticateFn = func(u, p string) (meta.User, error) {
|
||||
users := []meta.UserInfo{
|
||||
{
|
||||
Name: "admin",
|
||||
Hash: "admin",
|
||||
Admin: true,
|
||||
},
|
||||
{
|
||||
Name: "user1",
|
||||
Hash: "abcd",
|
||||
Privileges: map[string]influxql.Privilege{
|
||||
"db0": influxql.ReadPrivilege,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if u == user.Name {
|
||||
if p == user.Hash {
|
||||
return &user, nil
|
||||
}
|
||||
return nil, meta.ErrAuthenticate
|
||||
}
|
||||
}
|
||||
return nil, meta.ErrUserNotFound
|
||||
}
|
||||
|
||||
for i, tt := range []struct {
|
||||
user string
|
||||
password string
|
||||
query string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
query: "/debug/vars",
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
user: "user1",
|
||||
password: "abcd",
|
||||
query: "/debug/vars",
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
user: "user2",
|
||||
password: "abcd",
|
||||
query: "/debug/vars",
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
r := MustNewJSONRequest("GET", tt.query, nil)
|
||||
params := r.URL.Query()
|
||||
if tt.user != "" {
|
||||
params.Set("u", tt.user)
|
||||
}
|
||||
if tt.password != "" {
|
||||
params.Set("p", tt.password)
|
||||
}
|
||||
r.URL.RawQuery = params.Encode()
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != tt.code {
|
||||
t.Errorf("%d. unexpected status: got=%d exp=%d\noutput: %s", i, w.Code, tt.code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the prometheus remote write works with valid values.
|
||||
func TestHandler_PromWrite(t *testing.T) {
|
||||
req := &remote.WriteRequest{
|
||||
|
@ -1308,7 +1460,6 @@ func TestHandler_Flux_Auth(t *testing.T) {
|
|||
}
|
||||
|
||||
// Ensure the handler handles ping requests correctly.
|
||||
// TODO: This should be expanded to verify the MetaClient check in servePing is working correctly
|
||||
func TestHandler_Ping(t *testing.T) {
|
||||
h := NewHandler(false)
|
||||
w := httptest.NewRecorder()
|
||||
|
@ -1688,6 +1839,19 @@ func WithAuthentication() configOption {
|
|||
}
|
||||
}
|
||||
|
||||
func WithPprofAuthEnabled() configOption {
|
||||
return func(c *httpd.Config) {
|
||||
c.PprofEnabled = true
|
||||
c.PprofAuthEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithPingAuthEnabled() configOption {
|
||||
return func(c *httpd.Config) {
|
||||
c.PingAuthEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithFlux() configOption {
|
||||
return func(c *httpd.Config) {
|
||||
c.FluxEnabled = true
|
||||
|
|
Loading…
Reference in New Issue