Cluster:
# The cluster uuid prefix
zzzzz:
+ ManagementToken: xyzzy
NodeProfile:
# For each node, the profile name corresponds to a
# locally-resolvable hostname, and describes which Arvados
# services are available on that machine.
api:
arvados-controller:
- Listen: 8000
+ Listen: :8000
arvados-api-server:
- Listen: 8001
+ Listen: :8001
manage:
arvados-node-manager:
- Listen: 8002
+ Listen: :8002
workbench:
arvados-workbench:
- Listen: 8003
+ Listen: :8003
arvados-ws:
- Listen: 8004
+ Listen: :8004
keep:
keep-web:
- Listen: 8005
+ Listen: :8005
keepproxy:
- Listen: 8006
+ Listen: :8006
keep0:
keepstore:
- Listen: 25701
+ Listen: :25107
keep1:
keepstore:
- Listen: 25701
+ Listen: :25107
</pre>
AuthToken: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
KeepServiceTypes:
- disk
+Listen: :9005
+ManagementToken: <span class="userinput">xyzzy</span>
RunPeriod: 10m
CollectionBatchSize: 100000
CollectionBuffers: 1000
# Format of request/response and error logs: "json" or "text".
LogFormat: json
-# The secret key that must be provided by monitoring services
-# wishing to access the health check endpoint (/_health).
-ManagementToken: ""
+# The secret key that must be provided by monitoring services when
+# using the health check and metrics endpoints (/_health, /metrics).
+ManagementToken: xyzzy
# Maximum RAM to use for data buffers, given in multiples of block
# size (64 MiB). When this limit is reached, HTTP requests requiring
return &Credentials{Tokens: []string{}}
}
-func NewCredentialsFromHTTPRequest(r *http.Request) *Credentials {
+func CredentialsFromRequest(r *http.Request) *Credentials {
+ if c, ok := r.Context().Value(contextKeyCredentials).(*Credentials); ok {
+ // preloaded by middleware
+ return c
+ }
c := NewCredentials()
c.LoadTokensFromHTTPRequest(r)
return c
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package auth
+
+import (
+ "context"
+ "net/http"
+)
+
+type contextKey string
+
+var contextKeyCredentials contextKey = "credentials"
+
+// LoadToken wraps the next handler, adding credentials to the request
+// context so subsequent handlers can access them efficiently via
+// CredentialsFromRequest.
+func LoadToken(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), contextKeyCredentials, CredentialsFromRequest(r))))
+ })
+}
+
+// RequireLiteralToken wraps the next handler, rejecting any request
+// that doesn't supply the given token. If the given token is empty,
+// RequireLiteralToken returns next (i.e., no auth checks are
+// performed).
+func RequireLiteralToken(token string, next http.Handler) http.Handler {
+ if token == "" {
+ return next
+ }
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c := CredentialsFromRequest(r)
+ if len(c.Tokens) == 0 {
+ http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ return
+ }
+ for _, t := range c.Tokens {
+ if t == token {
+ next.ServeHTTP(w, r)
+ return
+ }
+ }
+ http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+ })
+}
}
func (agg *Aggregator) checkAuth(req *http.Request, cluster *arvados.Cluster) bool {
- creds := auth.NewCredentialsFromHTTPRequest(req)
+ creds := auth.CredentialsFromRequest(req)
for _, token := range creds.Tokens {
if token != "" && token == cluster.ManagementToken {
return true
"strings"
"time"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/stats"
"github.com/Sirupsen/logrus"
"github.com/gogo/protobuf/jsonpb"
// Returns an http.Handler that serves the Handler's metrics
// data at /metrics and /metrics.json, and passes other
// requests through to next.
- ServeAPI(next http.Handler) http.Handler
+ ServeAPI(token string, next http.Handler) http.Handler
}
type metrics struct {
// metrics API endpoints (currently "GET /metrics(.json)?") and passes
// other requests through to next.
//
+// If the given token is not empty, that token must be supplied by a
+// client in order to access the metrics endpoints.
+//
// Typical example:
//
// m := Instrument(...)
-// srv := http.Server{Handler: m.ServeAPI(m)}
-func (m *metrics) ServeAPI(next http.Handler) http.Handler {
+// srv := http.Server{Handler: m.ServeAPI("secrettoken", m)}
+func (m *metrics) ServeAPI(token string, next http.Handler) http.Handler {
+ jsonMetrics := auth.RequireLiteralToken(token, http.HandlerFunc(m.exportJSON))
+ plainMetrics := auth.RequireLiteralToken(token, m.exportProm)
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch {
case req.Method != "GET" && req.Method != "HEAD":
next.ServeHTTP(w, req)
case req.URL.Path == "/metrics.json":
- m.exportJSON(w, req)
+ jsonMetrics.ServeHTTP(w, req)
case req.URL.Path == "/metrics":
- m.exportProm.ServeHTTP(w, req)
+ plainMetrics.ServeHTTP(w, req)
default:
next.ServeHTTP(w, req)
}
httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)
}()
- creds := auth.NewCredentialsFromHTTPRequest(r)
+ creds := auth.CredentialsFromRequest(r)
if len(creds.Tokens) == 0 {
statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
func (s *runSuite) TestCommit(c *check.C) {
s.config.Listen = ":"
+ s.config.ManagementToken = "xyzzy"
opts := RunOptions{
CommitPulls: true,
CommitTrash: true,
func (s *runSuite) TestRunForever(c *check.C) {
s.config.Listen = ":"
+ s.config.ManagementToken = "xyzzy"
opts := RunOptions{
CommitPulls: true,
CommitTrash: true,
func (s *runSuite) getMetrics(c *check.C, srv *Server) string {
resp, err := http.Get("http://" + srv.listening + "/metrics")
c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
+
+ resp, err = http.Get("http://" + srv.listening + "/metrics?api_token=xyzzy")
+ c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusOK)
buf, err := ioutil.ReadAll(resp.Body)
c.Check(err, check.IsNil)
return string(buf)
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"github.com/Sirupsen/logrus"
)
// address, address:port, or :port for management interface
Listen string
+ // token for management APIs
+ ManagementToken string
+
// How often to check
RunPeriod arvados.Duration
}
server := &httpserver.Server{
Server: http.Server{
- Handler: httpserver.LogRequests(srv.Logger, srv.metrics.Handler(srv.Logger)),
+ Handler: httpserver.LogRequests(srv.Logger,
+ auth.RequireLiteralToken(srv.config.ManagementToken,
+ srv.metrics.Handler(srv.Logger))),
},
Addr: srv.config.Listen,
}
KeepServiceTypes:
- disk
Listen: ":9005"
+ManagementToken: xyzzy
RunPeriod: 600s
CollectionBatchSize: 100000
CollectionBuffers: 1000
if useSiteFS {
if tokens == nil {
- tokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+ tokens = auth.CredentialsFromRequest(r).Tokens
}
h.serveSiteFS(w, r, tokens, credentialsOK, attachment)
return
if tokens == nil {
if credentialsOK {
- reqTokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+ reqTokens = auth.CredentialsFromRequest(r).Tokens
}
tokens = append(reqTokens, h.Config.AnonymousTokens...)
}
reg := prometheus.NewRegistry()
h.Config.Cache.registry = reg
mh := httpserver.Instrument(reg, nil, httpserver.AddRequestIDs(httpserver.LogRequests(nil, h)))
- h.MetricsAPI = mh.ServeAPI(http.NotFoundHandler())
+ h.MetricsAPI = mh.ServeAPI(h.Config.ManagementToken, http.NotFoundHandler())
srv.Handler = mh
srv.Addr = srv.Config.Listen
return srv.Server.Start()
req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
resp, err = http.DefaultClient.Do(req)
c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
+
+ req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
+ req.Header.Set("Authorization", "Bearer badtoken")
+ resp, err = http.DefaultClient.Do(req)
+ c.Assert(err, check.IsNil)
+ c.Check(resp.StatusCode, check.Equals, http.StatusForbidden)
+
+ req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
+ req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
+ resp, err = http.DefaultClient.Do(req)
+ c.Assert(err, check.IsNil)
c.Check(resp.StatusCode, check.Equals, http.StatusOK)
type summary struct {
SampleCount string `json:"sample_count"`
Insecure: true,
}
cfg.Listen = "127.0.0.1:0"
+ cfg.ManagementToken = arvadostest.ManagementToken
s.testServer = &server{Config: cfg}
err := s.testServer.Start()
c.Assert(err, check.Equals, nil)
systemAuthToken string
debugLogf func(string, ...interface{})
- ManagementToken string `doc: The secret key that must be provided by monitoring services
-wishing to access the health check endpoint (/_health).`
+ ManagementToken string
}
var (
rtr.limiter = httpserver.NewRequestLimiter(theConfig.MaxRequests, rtr)
- stack := httpserver.Instrument(nil, nil,
+ instrumented := httpserver.Instrument(nil, nil,
httpserver.AddRequestIDs(httpserver.LogRequests(nil, rtr.limiter)))
- return stack.ServeAPI(stack)
+ return instrumented.ServeAPI(theConfig.ManagementToken, instrumented)
}
// BadRequestHandler is a HandleFunc to address bad requests.
KeepVM = s.vm
theConfig = DefaultConfig()
theConfig.systemAuthToken = arvadostest.DataManagerToken
+ theConfig.ManagementToken = arvadostest.ManagementToken
theConfig.Start()
s.rtr = MakeRESTRouter(testCluster)
}
s.call("PUT", "/"+TestHash, "", TestBlock)
s.call("PUT", "/"+TestHash2, "", TestBlock2)
resp := s.call("GET", "/metrics.json", "", nil)
+ c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+ resp = s.call("GET", "/metrics.json", "foobar", nil)
+ c.Check(resp.Code, check.Equals, http.StatusForbidden)
+ resp = s.call("GET", "/metrics.json", arvadostest.ManagementToken, nil)
c.Check(resp.Code, check.Equals, http.StatusOK)
var j []struct {
Name string
resp := httptest.NewRecorder()
req, _ := http.NewRequest(method, path, bytes.NewReader(body))
if tok != "" {
- req.Header.Set("Authorization", "OAuth2 "+tok)
+ req.Header.Set("Authorization", "Bearer "+tok)
}
s.rtr.ServeHTTP(resp, req)
return resp