mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
server: Add param for diagnostic address binding
This adds a new config option for the OPA server (along with plumbing from `opa run` downward to the server) to configure separate diagnostic addresses to listen on. These will only be configured to serve the /metrics and /health. This will allow for more secure OPA deployments with the normal "data" or "policies" API's made to be only accessible on localhost. Fixes: #2002 Signed-off-by: Patrick East <east.patrick@gmail.com>
This commit is contained in:
@@ -120,6 +120,7 @@ File paths can be specified as URLs to resolve ambiguity in paths containing col
|
||||
runCommand.Flags().BoolVarP(&cmdParams.serverMode, "server", "s", false, "start the runtime in server mode")
|
||||
runCommand.Flags().StringVarP(&cmdParams.rt.HistoryPath, "history", "H", historyPath(), "set path of history file")
|
||||
cmdParams.rt.Addrs = runCommand.Flags().StringSliceP("addr", "a", []string{defaultAddr}, "set listening address of the server (e.g., [ip]:<port> for TCP, unix://<path> for UNIX domain socket)")
|
||||
cmdParams.rt.DiagnosticAddrs = runCommand.Flags().StringSlice("diagnostic-addr", []string{}, "set read-only diagnostic listening address of the server for /health and /metric APIs (e.g., [ip]:<port> for TCP, unix://<path> for UNIX domain socket)")
|
||||
runCommand.Flags().StringVarP(&cmdParams.rt.InsecureAddr, "insecure-addr", "", "", "set insecure listening address of the server")
|
||||
runCommand.Flags().MarkDeprecated("insecure-addr", "use --addr instead")
|
||||
runCommand.Flags().StringVarP(&cmdParams.rt.OutputFormat, "format", "f", "pretty", "set shell output format, i.e, pretty, json")
|
||||
|
||||
+53
-4
@@ -14,7 +14,8 @@ import (
|
||||
|
||||
func TestRunServerBase(t *testing.T) {
|
||||
params := newRunParams()
|
||||
params.rt = e2e.NewAPIServerTestParams()
|
||||
params.rt.Addrs = &[]string{":0"}
|
||||
params.rt.DiagnosticAddrs = &[]string{}
|
||||
params.serverMode = true
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -22,14 +23,62 @@ func TestRunServerBase(t *testing.T) {
|
||||
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
go startRuntime(ctx, rt, true)
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
startRuntime(ctx, rt, true)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err := testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
err = testRuntime.UploadData(bytes.NewBufferString(`{"x": 1}`))
|
||||
validateBasicServe(t, testRuntime)
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestRunServerWithDiagnosticAddr(t *testing.T) {
|
||||
params := newRunParams()
|
||||
params.rt.Addrs = &[]string{":0"}
|
||||
params.rt.DiagnosticAddrs = &[]string{":0"}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
rt := initRuntime(ctx, params, nil)
|
||||
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
startRuntime(ctx, rt, true)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err := testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
validateBasicServe(t, testRuntime)
|
||||
|
||||
diagURL, err := testRuntime.AddrToURL(rt.DiagnosticAddrs()[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
if err := testRuntime.HealthCheck(diagURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
func validateBasicServe(t *testing.T, runtime *e2e.TestRuntime) {
|
||||
t.Helper()
|
||||
|
||||
err := runtime.UploadData(bytes.NewBufferString(`{"x": 1}`))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
@@ -37,7 +86,7 @@ func TestRunServerBase(t *testing.T) {
|
||||
resp := struct {
|
||||
Result int `json:"result"`
|
||||
}{}
|
||||
err = testRuntime.GetDataWithInputTyped("x", nil, &resp)
|
||||
err = runtime.GetDataWithInputTyped("x", nil, &resp)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
@@ -455,6 +455,25 @@ curl: (35) error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certifica
|
||||
|
||||
As you can see, TLS-based authentication disallows these request completely.
|
||||
|
||||
## Secure Health and Monitoring
|
||||
|
||||
Often OPA is deployed locally to the host where the client resides (side-car or
|
||||
similar model). In these deployments it is ideal to only expose the API via
|
||||
`localhost` to prevent any remote clients from reaching OPA at all. The downside
|
||||
to this approach is that it blocks remote monitoring systems that require access
|
||||
to `/health` or `/metrics`.
|
||||
|
||||
The solution is to configure OPA with a separate diagnostic listener by
|
||||
providing the `--diagnostic-addr` flag, for example:
|
||||
|
||||
```
|
||||
$ opa run \
|
||||
-s \
|
||||
--addr localhost:8181 \
|
||||
--diagnostic-addr :8282
|
||||
```
|
||||
The configuration above would expose only `/health` and `/metrics` API's on port
|
||||
`8282` while keeping the normal REST API bound to `localhost:8181`
|
||||
|
||||
## Hardened Configuration Example
|
||||
|
||||
|
||||
+34
-5
@@ -64,6 +64,10 @@ type Params struct {
|
||||
// Addrs are the listening addresses that the OPA server will bind to.
|
||||
Addrs *[]string
|
||||
|
||||
// DiagnosticAddrs are the listening addresses that the OPA server will bind to
|
||||
// for read-only diagnostic API's (/health, /metrics, etc)
|
||||
DiagnosticAddrs *[]string
|
||||
|
||||
// InsecureAddr is the listening address that the OPA server will bind to
|
||||
// in addition to Addr if TLS is enabled.
|
||||
InsecureAddr string
|
||||
@@ -238,11 +242,20 @@ func (rt *Runtime) StartServer(ctx context.Context) {
|
||||
// will block until either: an error occurs, the context is canceled, or
|
||||
// a SIGTERM or SIGKILL signal is sent.
|
||||
func (rt *Runtime) Serve(ctx context.Context) error {
|
||||
if rt.Params.Addrs == nil {
|
||||
return fmt.Errorf("at least one address must be configured in runtime parameters")
|
||||
}
|
||||
|
||||
if rt.Params.DiagnosticAddrs == nil {
|
||||
rt.Params.DiagnosticAddrs = &[]string{}
|
||||
}
|
||||
|
||||
setupLogging(rt.Params.Logging)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"addrs": *rt.Params.Addrs,
|
||||
"insecure_addr": rt.Params.InsecureAddr,
|
||||
"addrs": *rt.Params.Addrs,
|
||||
"diagnostic-addrs": *rt.Params.DiagnosticAddrs,
|
||||
"insecure_addr": rt.Params.InsecureAddr,
|
||||
}).Info("Initializing server.")
|
||||
|
||||
if err := rt.Manager.Start(ctx); err != nil {
|
||||
@@ -253,7 +266,7 @@ func (rt *Runtime) Serve(ctx context.Context) error {
|
||||
defer rt.Manager.Stop(ctx)
|
||||
|
||||
var err error
|
||||
rt.server, err = server.New().
|
||||
rt.server = server.New().
|
||||
WithStore(rt.Store).
|
||||
WithManager(rt.Manager).
|
||||
WithCompilerErrorLimit(rt.Params.ErrorLimit).
|
||||
@@ -267,9 +280,13 @@ func (rt *Runtime) Serve(ctx context.Context) error {
|
||||
WithDecisionIDFactory(rt.decisionIDFactory).
|
||||
WithDecisionLoggerWithErr(rt.decisionLogger).
|
||||
WithRuntime(rt.Manager.Info).
|
||||
WithMetrics(rt.metrics).
|
||||
Init(ctx)
|
||||
WithMetrics(rt.metrics)
|
||||
|
||||
if rt.Params.DiagnosticAddrs != nil {
|
||||
rt.server = rt.server.WithDiagnosticAddresses(*rt.Params.DiagnosticAddrs)
|
||||
}
|
||||
|
||||
rt.server, err = rt.server.Init(ctx)
|
||||
if err != nil {
|
||||
logrus.WithField("err", err).Error("Unable to initialize server.")
|
||||
return err
|
||||
@@ -283,6 +300,7 @@ func (rt *Runtime) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
rt.server.Handler = NewLoggingHandler(rt.server.Handler)
|
||||
rt.server.DiagnosticHandler = NewLoggingHandler(rt.server.DiagnosticHandler)
|
||||
|
||||
loops, err := rt.server.Listeners()
|
||||
if err != nil {
|
||||
@@ -322,6 +340,17 @@ func (rt *Runtime) Addrs() []string {
|
||||
return rt.server.Addrs()
|
||||
}
|
||||
|
||||
// DiagnosticAddrs returns a list of diagnostic addresses that the runtime is
|
||||
// listening on (when in server mode). Returns an empty list if it hasn't
|
||||
// started listening.
|
||||
func (rt *Runtime) DiagnosticAddrs() []string {
|
||||
if rt.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rt.server.DiagnosticAddrs()
|
||||
}
|
||||
|
||||
// StartREPL starts the runtime in REPL mode. This function will block the calling goroutine.
|
||||
func (rt *Runtime) StartREPL(ctx context.Context) {
|
||||
|
||||
|
||||
+180
-110
@@ -86,10 +86,12 @@ var unsafeBuiltinsMap = map[string]struct{}{ast.HTTPSend.Name: struct{}{}}
|
||||
|
||||
// Server represents an instance of OPA running in server mode.
|
||||
type Server struct {
|
||||
Handler http.Handler
|
||||
Handler http.Handler
|
||||
DiagnosticHandler http.Handler
|
||||
|
||||
router *mux.Router
|
||||
addrs []string
|
||||
diagAddrs []string
|
||||
insecureAddr string
|
||||
authentication AuthenticationScheme
|
||||
authorization AuthorizationScheme
|
||||
@@ -132,26 +134,9 @@ func New() *Server {
|
||||
|
||||
// Init initializes the server. This function MUST be called before Loop.
|
||||
func (s *Server) Init(ctx context.Context) (*Server, error) {
|
||||
s.initRouter()
|
||||
|
||||
// Add authorization handler. This must come BEFORE authentication handler
|
||||
// so that the latter can run first.
|
||||
switch s.authorization {
|
||||
case AuthorizationBasic:
|
||||
s.Handler = authorizer.NewBasic(
|
||||
s.Handler,
|
||||
s.getCompiler,
|
||||
s.store,
|
||||
authorizer.Runtime(s.runtime),
|
||||
authorizer.Decision(s.manager.Config.DefaultAuthorizationDecisionRef))
|
||||
}
|
||||
|
||||
switch s.authentication {
|
||||
case AuthenticationToken:
|
||||
s.Handler = identifier.NewTokenBased(s.Handler)
|
||||
case AuthenticationTLS:
|
||||
s.Handler = identifier.NewTLSBased(s.Handler)
|
||||
}
|
||||
s.initRouters()
|
||||
s.Handler = s.initHandlerAuth(s.Handler)
|
||||
s.DiagnosticHandler = s.initHandlerAuth(s.DiagnosticHandler)
|
||||
|
||||
txn, err := s.store.NewTransaction(ctx, storage.WriteParams)
|
||||
if err != nil {
|
||||
@@ -224,6 +209,13 @@ func (s *Server) WithAddresses(addrs []string) *Server {
|
||||
return s
|
||||
}
|
||||
|
||||
// WithDiagnosticAddresses sets the listening addresses that the server will
|
||||
// bind to and *only* serve read-only diagnostic API's.
|
||||
func (s *Server) WithDiagnosticAddresses(addrs []string) *Server {
|
||||
s.diagAddrs = addrs
|
||||
return s
|
||||
}
|
||||
|
||||
// WithInsecureAddress sets the listening address that the server will bind to.
|
||||
func (s *Server) WithInsecureAddress(addr string) *Server {
|
||||
s.insecureAddr = addr
|
||||
@@ -323,28 +315,24 @@ func (s *Server) WithRouter(router *mux.Router) *Server {
|
||||
// Listeners returns functions that listen and serve connections.
|
||||
func (s *Server) Listeners() ([]Loop, error) {
|
||||
loops := []Loop{}
|
||||
for _, addr := range s.addrs {
|
||||
parsedURL, err := parseURL(addr, s.cert != nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
handlerBindings := map[httpListenerType]struct {
|
||||
addrs []string
|
||||
handler http.Handler
|
||||
}{
|
||||
defaultListenerType: {s.addrs, s.Handler},
|
||||
diagnosticListenerType: {s.diagAddrs, s.DiagnosticHandler},
|
||||
}
|
||||
|
||||
for t, binding := range handlerBindings {
|
||||
for _, addr := range binding.addrs {
|
||||
loop, listener, err := s.getListener(addr, binding.handler, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.httpListeners = append(s.httpListeners, listener)
|
||||
loops = append(loops, loop)
|
||||
}
|
||||
var loop Loop
|
||||
var listener httpListener
|
||||
switch parsedURL.Scheme {
|
||||
case "unix":
|
||||
loop, listener, err = s.getListenerForUNIXSocket(parsedURL)
|
||||
case "http":
|
||||
loop, listener, err = s.getListenerForHTTPServer(parsedURL)
|
||||
case "https":
|
||||
loop, listener, err = s.getListenerForHTTPSServer(parsedURL)
|
||||
default:
|
||||
err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.httpListeners = append(s.httpListeners, listener)
|
||||
loops = append(loops, loop)
|
||||
}
|
||||
|
||||
if s.insecureAddr != "" {
|
||||
@@ -352,7 +340,7 @@ func (s *Server) Listeners() ([]Loop, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loop, httpListener, err := s.getListenerForHTTPServer(parsedURL)
|
||||
loop, httpListener, err := s.getListenerForHTTPServer(parsedURL, s.Handler, defaultListenerType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -364,12 +352,23 @@ func (s *Server) Listeners() ([]Loop, error) {
|
||||
}
|
||||
|
||||
// Addrs returns a list of addresses that the server is listening on.
|
||||
// if the server hasn't been started it will not return an address.
|
||||
// If the server hasn't been started it will not return an address.
|
||||
func (s *Server) Addrs() []string {
|
||||
return s.addrsForType(defaultListenerType)
|
||||
}
|
||||
|
||||
// DiagnosticAddrs returns a list of addresses that the server is listening on
|
||||
// for the read-only diagnostic API's (eg /health, /metrics, etc)
|
||||
// If the server hasn't been started it will not return an address.
|
||||
func (s *Server) DiagnosticAddrs() []string {
|
||||
return s.addrsForType(diagnosticListenerType)
|
||||
}
|
||||
|
||||
func (s *Server) addrsForType(t httpListenerType) []string {
|
||||
var addrs []string
|
||||
for _, l := range s.httpListeners {
|
||||
a := l.Addr()
|
||||
if a != "" {
|
||||
if a != "" && l.Type() == t {
|
||||
addrs = append(addrs, a)
|
||||
}
|
||||
}
|
||||
@@ -390,27 +389,36 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
type httpListenerType int
|
||||
|
||||
const (
|
||||
defaultListenerType httpListenerType = iota
|
||||
diagnosticListenerType
|
||||
)
|
||||
|
||||
type httpListener interface {
|
||||
Addr() string
|
||||
ListenAndServe() error
|
||||
ListenAndServeTLS(certFile, keyFile string) error
|
||||
Shutdown(ctx context.Context) error
|
||||
Type() httpListenerType
|
||||
}
|
||||
|
||||
// baseHTTPListener is just a wrapper around http.Server
|
||||
type baseHTTPListener struct {
|
||||
s *http.Server
|
||||
l net.Listener
|
||||
t httpListenerType
|
||||
}
|
||||
|
||||
var _ httpListener = (*baseHTTPListener)(nil)
|
||||
|
||||
func newHTTPListener(srvr *http.Server) httpListener {
|
||||
return &baseHTTPListener{srvr, nil}
|
||||
func newHTTPListener(srvr *http.Server, t httpListenerType) httpListener {
|
||||
return &baseHTTPListener{s: srvr, t: t}
|
||||
}
|
||||
|
||||
func newHTTPUnixSocketListener(srvr *http.Server, l net.Listener) httpListener {
|
||||
return &baseHTTPListener{srvr, l}
|
||||
func newHTTPUnixSocketListener(srvr *http.Server, l net.Listener, t httpListenerType) httpListener {
|
||||
return &baseHTTPListener{s: srvr, l: l, t: t}
|
||||
}
|
||||
|
||||
func (b *baseHTTPListener) ListenAndServe() error {
|
||||
@@ -457,18 +465,44 @@ func (b *baseHTTPListener) Shutdown(ctx context.Context) error {
|
||||
return b.s.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) getListenerForHTTPServer(u *url.URL) (Loop, httpListener, error) {
|
||||
httpServer := http.Server{
|
||||
Addr: u.Host,
|
||||
Handler: s.Handler,
|
||||
func (b *baseHTTPListener) Type() httpListenerType {
|
||||
return b.t
|
||||
}
|
||||
|
||||
func (s *Server) getListener(addr string, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
|
||||
parsedURL, err := parseURL(addr, s.cert != nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
l := newHTTPListener(&httpServer)
|
||||
var loop Loop
|
||||
var listener httpListener
|
||||
switch parsedURL.Scheme {
|
||||
case "unix":
|
||||
loop, listener, err = s.getListenerForUNIXSocket(parsedURL, h, t)
|
||||
case "http":
|
||||
loop, listener, err = s.getListenerForHTTPServer(parsedURL, h, t)
|
||||
case "https":
|
||||
loop, listener, err = s.getListenerForHTTPSServer(parsedURL, h, t)
|
||||
default:
|
||||
err = fmt.Errorf("invalid url scheme %q", parsedURL.Scheme)
|
||||
}
|
||||
|
||||
return loop, listener, err
|
||||
}
|
||||
|
||||
func (s *Server) getListenerForHTTPServer(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
|
||||
httpServer := http.Server{
|
||||
Addr: u.Host,
|
||||
Handler: h,
|
||||
}
|
||||
|
||||
l := newHTTPListener(&httpServer, t)
|
||||
|
||||
return l.ListenAndServe, l, nil
|
||||
}
|
||||
|
||||
func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, httpListener, error) {
|
||||
func (s *Server) getListenerForHTTPSServer(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
|
||||
|
||||
if s.cert == nil {
|
||||
return nil, nil, fmt.Errorf("TLS certificate required but not supplied")
|
||||
@@ -476,7 +510,7 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, httpListener, erro
|
||||
|
||||
httpsServer := http.Server{
|
||||
Addr: u.Host,
|
||||
Handler: s.Handler,
|
||||
Handler: h,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{*s.cert},
|
||||
ClientCAs: s.certPool,
|
||||
@@ -486,101 +520,137 @@ func (s *Server) getListenerForHTTPSServer(u *url.URL) (Loop, httpListener, erro
|
||||
httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
l := newHTTPListener(&httpsServer)
|
||||
l := newHTTPListener(&httpsServer, t)
|
||||
|
||||
httpsLoop := func() error { return l.ListenAndServeTLS("", "") }
|
||||
|
||||
return httpsLoop, l, nil
|
||||
}
|
||||
|
||||
func (s *Server) getListenerForUNIXSocket(u *url.URL) (Loop, httpListener, error) {
|
||||
func (s *Server) getListenerForUNIXSocket(u *url.URL, h http.Handler, t httpListenerType) (Loop, httpListener, error) {
|
||||
socketPath := u.Host + u.Path
|
||||
|
||||
// Remove domain socket file in case it already exists.
|
||||
os.Remove(socketPath)
|
||||
|
||||
domainSocketServer := http.Server{Handler: s.Handler}
|
||||
domainSocketServer := http.Server{Handler: h}
|
||||
unixListener, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
l := newHTTPUnixSocketListener(&domainSocketServer, unixListener)
|
||||
l := newHTTPUnixSocketListener(&domainSocketServer, unixListener, t)
|
||||
|
||||
domainSocketLoop := func() error { return domainSocketServer.Serve(unixListener) }
|
||||
return domainSocketLoop, l, nil
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() {
|
||||
router := s.router
|
||||
|
||||
if router == nil {
|
||||
router = mux.NewRouter()
|
||||
func (s *Server) initHandlerAuth(handler http.Handler) http.Handler {
|
||||
// Add authorization handler. This must come BEFORE authentication handler
|
||||
// so that the latter can run first.
|
||||
switch s.authorization {
|
||||
case AuthorizationBasic:
|
||||
handler = authorizer.NewBasic(
|
||||
handler,
|
||||
s.getCompiler,
|
||||
s.store,
|
||||
authorizer.Runtime(s.runtime),
|
||||
authorizer.Decision(s.manager.Config.DefaultAuthorizationDecisionRef))
|
||||
}
|
||||
|
||||
router.UseEncodedPath()
|
||||
router.StrictSlash(true)
|
||||
if s.metrics != nil {
|
||||
s.metrics.RegisterEndpoints(func(path, method string, handler http.Handler) {
|
||||
router.Handle(path, handler).Methods(method)
|
||||
})
|
||||
switch s.authentication {
|
||||
case AuthenticationToken:
|
||||
handler = identifier.NewTokenBased(handler)
|
||||
case AuthenticationTLS:
|
||||
handler = identifier.NewTLSBased(handler)
|
||||
}
|
||||
router.Handle("/health", s.instrumentHandler(http.HandlerFunc(s.unversionedGetHealth), PromHandlerHealth)).Methods(http.MethodGet)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (s *Server) initRouters() {
|
||||
mainRouter := s.router
|
||||
if mainRouter == nil {
|
||||
mainRouter = mux.NewRouter()
|
||||
}
|
||||
|
||||
diagRouter := mux.NewRouter()
|
||||
|
||||
// All routers get the same base configuration *and* diagnostic API's
|
||||
for _, router := range []*mux.Router{mainRouter, diagRouter} {
|
||||
router.StrictSlash(true)
|
||||
router.UseEncodedPath()
|
||||
router.StrictSlash(true)
|
||||
|
||||
if s.metrics != nil {
|
||||
s.metrics.RegisterEndpoints(func(path, method string, handler http.Handler) {
|
||||
router.Handle(path, handler).Methods(method)
|
||||
})
|
||||
}
|
||||
|
||||
router.Handle("/health", s.instrumentHandler(s.unversionedGetHealth, PromHandlerHealth)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
if s.pprofEnabled {
|
||||
router.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
router.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
||||
router.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||
router.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
||||
router.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
||||
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
mainRouter.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mainRouter.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
||||
mainRouter.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||
mainRouter.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
||||
mainRouter.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
||||
mainRouter.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mainRouter.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mainRouter.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mainRouter.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
}
|
||||
s.registerHandler(router, 0, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
|
||||
s.registerHandler(router, 0, "/data", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
|
||||
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1DataDelete, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/data", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
|
||||
s.registerHandler(router, 1, "/policies", http.MethodGet, s.instrumentHandler(s.v1PoliciesList, PromHandlerV1Policies))
|
||||
s.registerHandler(router, 1, "/policies/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1PoliciesDelete, PromHandlerV1Policies))
|
||||
s.registerHandler(router, 1, "/policies/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1PoliciesGet, PromHandlerV1Policies))
|
||||
s.registerHandler(router, 1, "/policies/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1PoliciesPut, PromHandlerV1Policies))
|
||||
s.registerHandler(router, 1, "/query", http.MethodGet, s.instrumentHandler(s.v1QueryGet, PromHandlerV1Query))
|
||||
s.registerHandler(router, 1, "/query", http.MethodPost, s.instrumentHandler(s.v1QueryPost, PromHandlerV1Query))
|
||||
s.registerHandler(router, 1, "/compile", http.MethodPost, s.instrumentHandler(s.v1CompilePost, PromHandlerV1Compile))
|
||||
router.Handle("/", s.instrumentHandler(http.HandlerFunc(s.unversionedPost), PromHandlerIndex)).Methods(http.MethodPost)
|
||||
router.Handle("/", s.instrumentHandler(http.HandlerFunc(s.indexGet), PromHandlerIndex)).Methods(http.MethodGet)
|
||||
|
||||
// Only the main mainRouter gets the OPA API's (data, policies, query, etc)
|
||||
s.registerHandler(mainRouter, 0, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
|
||||
s.registerHandler(mainRouter, 0, "/data", http.MethodPost, s.instrumentHandler(s.v0DataPost, PromHandlerV0Data))
|
||||
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1DataDelete, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data", http.MethodPut, s.instrumentHandler(s.v1DataPut, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data", http.MethodGet, s.instrumentHandler(s.v1DataGet, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data", http.MethodPatch, s.instrumentHandler(s.v1DataPatch, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data/{path:.+}", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/data", http.MethodPost, s.instrumentHandler(s.v1DataPost, PromHandlerV1Data))
|
||||
s.registerHandler(mainRouter, 1, "/policies", http.MethodGet, s.instrumentHandler(s.v1PoliciesList, PromHandlerV1Policies))
|
||||
s.registerHandler(mainRouter, 1, "/policies/{path:.+}", http.MethodDelete, s.instrumentHandler(s.v1PoliciesDelete, PromHandlerV1Policies))
|
||||
s.registerHandler(mainRouter, 1, "/policies/{path:.+}", http.MethodGet, s.instrumentHandler(s.v1PoliciesGet, PromHandlerV1Policies))
|
||||
s.registerHandler(mainRouter, 1, "/policies/{path:.+}", http.MethodPut, s.instrumentHandler(s.v1PoliciesPut, PromHandlerV1Policies))
|
||||
s.registerHandler(mainRouter, 1, "/query", http.MethodGet, s.instrumentHandler(s.v1QueryGet, PromHandlerV1Query))
|
||||
s.registerHandler(mainRouter, 1, "/query", http.MethodPost, s.instrumentHandler(s.v1QueryPost, PromHandlerV1Query))
|
||||
s.registerHandler(mainRouter, 1, "/compile", http.MethodPost, s.instrumentHandler(s.v1CompilePost, PromHandlerV1Compile))
|
||||
mainRouter.Handle("/", s.instrumentHandler(s.unversionedPost, PromHandlerIndex)).Methods(http.MethodPost)
|
||||
mainRouter.Handle("/", s.instrumentHandler(s.indexGet, PromHandlerIndex)).Methods(http.MethodGet)
|
||||
|
||||
// These are catch all handlers that respond 405 for resources that exist but the method is not allowed
|
||||
router.Handle("/v0/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
|
||||
mainRouter.Handle("/v0/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
|
||||
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodPatch, http.MethodPut, http.MethodTrace)
|
||||
router.Handle("/v0/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
|
||||
mainRouter.Handle("/v0/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodGet, http.MethodHead,
|
||||
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodPatch, http.MethodPut,
|
||||
http.MethodTrace)
|
||||
// v1 Data catch all
|
||||
router.Handle("/v1/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
mainRouter.Handle("/v1/data/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
http.MethodConnect, http.MethodOptions, http.MethodTrace)
|
||||
router.Handle("/v1/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
mainRouter.Handle("/v1/data", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace)
|
||||
// Policies catch all
|
||||
router.Handle("/v1/policies", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
mainRouter.Handle("/v1/policies", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut,
|
||||
http.MethodPatch)
|
||||
// Policies (/policies/{path.+} catch all
|
||||
router.Handle("/v1/policies/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
mainRouter.Handle("/v1/policies/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
http.MethodConnect, http.MethodOptions, http.MethodTrace, http.MethodPost)
|
||||
// Query catch all
|
||||
router.Handle("/v1/query/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
mainRouter.Handle("/v1/query/{path:.*}", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPost, http.MethodPut, http.MethodPatch)
|
||||
router.Handle("/v1/query", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
mainRouter.Handle("/v1/query", s.instrumentHandler(writer.HTTPStatus(405), PromHandlerCatch)).Methods(http.MethodHead,
|
||||
http.MethodConnect, http.MethodDelete, http.MethodOptions, http.MethodTrace, http.MethodPut, http.MethodPatch)
|
||||
s.Handler = router
|
||||
|
||||
s.Handler = mainRouter
|
||||
s.DiagnosticHandler = diagRouter
|
||||
}
|
||||
|
||||
func (s *Server) instrumentHandler(handler func(http.ResponseWriter, *http.Request), label string) http.Handler {
|
||||
|
||||
+247
-78
@@ -54,23 +54,15 @@ type trw struct {
|
||||
}
|
||||
|
||||
func TestUnversionedGetHealth(t *testing.T) {
|
||||
|
||||
f := newFixture(t)
|
||||
|
||||
req := newReqUnversioned(http.MethodGet, "/health", "")
|
||||
if err := f.executeRequest(req, 200, `{}`); err != nil {
|
||||
t.Fatalf("Unexpected error while health check: %v", err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 200, `{}`)
|
||||
}
|
||||
|
||||
func TestUnversionedGetHealthBundleNoBundleSet(t *testing.T) {
|
||||
|
||||
f := newFixture(t)
|
||||
|
||||
req := newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
|
||||
if err := f.executeRequest(req, 200, `{}`); err != nil {
|
||||
t.Fatalf("Unexpected error while health check: %v", err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 200, `{}`)
|
||||
}
|
||||
|
||||
func TestUnversionedGetHealthCheckOnlyBundlePlugin(t *testing.T) {
|
||||
@@ -83,18 +75,14 @@ func TestUnversionedGetHealthCheckOnlyBundlePlugin(t *testing.T) {
|
||||
|
||||
// The bundle hasn't been activated yet, expect the health check to fail
|
||||
req := newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
|
||||
if err := f.executeRequest(req, 500, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 500, `{}`)
|
||||
|
||||
// Set the bundle to be activated.
|
||||
f.server.manager.UpdatePluginStatus("bundle", &plugins.Status{State: plugins.StateOK})
|
||||
|
||||
// The heath check should now respond as healthy
|
||||
req = newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
|
||||
if err := f.executeRequest(req, 200, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 200, `{}`)
|
||||
}
|
||||
|
||||
func TestUnversionedGetHealthCheckDiscoveryWithBundle(t *testing.T) {
|
||||
@@ -106,9 +94,7 @@ func TestUnversionedGetHealthCheckDiscoveryWithBundle(t *testing.T) {
|
||||
|
||||
// The discovery bundle hasn't been activated yet, expect the health check to fail
|
||||
req := newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
|
||||
if err := f.executeRequest(req, 500, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 500, `{}`)
|
||||
|
||||
// Set the bundle to be not ready (plugin configured and created, but hasn't activated all bundles yet).
|
||||
f.server.manager.UpdatePluginStatus("discovery", &plugins.Status{State: plugins.StateOK})
|
||||
@@ -116,18 +102,14 @@ func TestUnversionedGetHealthCheckDiscoveryWithBundle(t *testing.T) {
|
||||
|
||||
// The discovery bundle is OK, but the newly configured bundle hasn't been activated yet, expect the health check to fail
|
||||
req = newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
|
||||
if err := f.executeRequest(req, 500, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 500, `{}`)
|
||||
|
||||
// Set the bundle to be activated.
|
||||
f.server.manager.UpdatePluginStatus("bundle", &plugins.Status{State: plugins.StateOK})
|
||||
|
||||
// The heath check should now respond as healthy
|
||||
req = newReqUnversioned(http.MethodGet, "/health?bundles=true", "")
|
||||
if err := f.executeRequest(req, 200, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 200, `{}`)
|
||||
}
|
||||
|
||||
func TestUnversionedGetHealthCheckBundleActivationSingleLegacy(t *testing.T) {
|
||||
@@ -140,9 +122,7 @@ func TestUnversionedGetHealthCheckBundleActivationSingleLegacy(t *testing.T) {
|
||||
|
||||
// The server doesn't know about any bundles, so return a healthy status
|
||||
req := newReqUnversioned(http.MethodGet, "/health?bundle=true", "")
|
||||
if err := f.executeRequest(req, 200, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 200, `{}`)
|
||||
|
||||
err := storage.Txn(ctx, f.server.store, storage.WriteParams, func(txn storage.Transaction) error {
|
||||
return bundle.LegacyWriteManifestToStore(ctx, f.server.store, txn, bundle.Manifest{
|
||||
@@ -156,9 +136,7 @@ func TestUnversionedGetHealthCheckBundleActivationSingleLegacy(t *testing.T) {
|
||||
|
||||
// The heath check still respond as healthy with a legacy bundle found in storage
|
||||
req = newReqUnversioned(http.MethodGet, "/health?bundle=true", "")
|
||||
if err := f.executeRequest(req, 200, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, 200, `{}`)
|
||||
}
|
||||
|
||||
func TestBundlesReady(t *testing.T) {
|
||||
@@ -367,9 +345,7 @@ func TestUnversionedGetHealthCheckDiscoveryWithPlugins(t *testing.T) {
|
||||
}
|
||||
|
||||
req := newReqUnversioned(http.MethodGet, "/health?plugins", "")
|
||||
if err := f.executeRequest(req, tc.exp, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, tc.exp, `{}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -449,9 +425,7 @@ func TestUnversionedGetHealthCheckBundleAndPlugins(t *testing.T) {
|
||||
}
|
||||
|
||||
req := newReqUnversioned(http.MethodGet, "/health?plugins&bundles", "")
|
||||
if err := f.executeRequest(req, tc.exp, `{}`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validateDiagnosticRequest(t, f, req, tc.exp, `{}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3447,35 +3421,25 @@ func TestAuthorization(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Test that bob can do stuff.
|
||||
req1, err := http.NewRequest(http.MethodGet, "http://localhost:8182/v1/data/foo", nil)
|
||||
req1, err := http.NewRequest(http.MethodGet, "http://localhost:8182/health", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req1 = identifier.SetIdentity(req1, "bob")
|
||||
server.Handler.ServeHTTP(recorder, req1)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("Expected success but got: %v", recorder)
|
||||
}
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
validateAuthorizedRequest(t, server, req1, http.StatusOK)
|
||||
|
||||
// Test that alice can't do stuff.
|
||||
req2, err := http.NewRequest(http.MethodGet, "http://localhost:8182/v1/data/foo", nil)
|
||||
req2, err := http.NewRequest(http.MethodGet, "http://localhost:8182/health", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req2 = identifier.SetIdentity(req2, "alice")
|
||||
server.Handler.ServeHTTP(recorder, req2)
|
||||
|
||||
if recorder.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("Expected unauthorized but got: %v", recorder)
|
||||
}
|
||||
validateAuthorizedRequest(t, server, req2, http.StatusUnauthorized)
|
||||
|
||||
// Reverse the policy.
|
||||
update := identifier.SetIdentity(newReqV1(http.MethodPut, "/policies/test", `
|
||||
@@ -3490,24 +3454,38 @@ func TestAuthorization(t *testing.T) {
|
||||
}
|
||||
`), "bob")
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
recorder := httptest.NewRecorder()
|
||||
server.Handler.ServeHTTP(recorder, update)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("Expected policy update to succeed but got: %v", recorder)
|
||||
}
|
||||
|
||||
// Try alice again.
|
||||
recorder = httptest.NewRecorder()
|
||||
server.Handler.ServeHTTP(recorder, req2)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("Expected OK but got: %v", recorder)
|
||||
}
|
||||
validateAuthorizedRequest(t, server, req2, http.StatusOK)
|
||||
|
||||
// Try bob again.
|
||||
recorder = httptest.NewRecorder()
|
||||
server.Handler.ServeHTTP(recorder, req1)
|
||||
if recorder.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("Expected 401 but got: %v", recorder)
|
||||
validateAuthorizedRequest(t, server, req1, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func validateAuthorizedRequest(t *testing.T, s *Server, req *http.Request, exp int) {
|
||||
t.Helper()
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
|
||||
// First check the main router
|
||||
s.Handler.ServeHTTP(r, req)
|
||||
if r.Code != exp {
|
||||
t.Fatalf("(Default Handler) Expected %v but got: %v", exp, r)
|
||||
}
|
||||
|
||||
r = httptest.NewRecorder()
|
||||
|
||||
// Ensure that auth happens for the diagnostic handler as well
|
||||
s.DiagnosticHandler.ServeHTTP(r, req)
|
||||
if r.Code != exp {
|
||||
t.Fatalf("(Diagnostic Handler) Expected %v but got: %v", exp, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3680,7 +3658,7 @@ func newFixture(t *testing.T, opts ...func(*Server)) *fixture {
|
||||
}
|
||||
|
||||
server := New().
|
||||
WithAddresses([]string{":8182"}).
|
||||
WithAddresses([]string{"localhost:8182"}).
|
||||
WithStore(store).
|
||||
WithManager(m)
|
||||
for _, opt := range opts {
|
||||
@@ -3718,18 +3696,26 @@ func (f *fixture) v1TestRequests(trs []tr) error {
|
||||
}
|
||||
|
||||
func (f *fixture) v1(method string, path string, body string, code int, resp string) error {
|
||||
req := newReqV1(method, path, body)
|
||||
return f.executeRequest(req, code, resp)
|
||||
// All v1 API's should 404 for the diagnostic handler
|
||||
if err := f.executeDiagnosticRequest(newReqV1(method, path, body), 404, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.executeRequest(newReqV1(method, path, body), code, resp)
|
||||
}
|
||||
|
||||
func (f *fixture) v0(method string, path string, body string, code int, resp string) error {
|
||||
req := newReqV0(method, path, body)
|
||||
return f.executeRequest(req, code, resp)
|
||||
// All v0 API's should 404 for the diagnostic handler
|
||||
if err := f.executeDiagnosticRequest(newReqV0(method, path, body), 404, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.executeRequest(newReqV0(method, path, body), code, resp)
|
||||
}
|
||||
|
||||
func (f *fixture) executeRequest(req *http.Request, code int, resp string) error {
|
||||
func (f *fixture) executeRequestForHandler(h http.Handler, req *http.Request, code int, resp string) error {
|
||||
f.reset()
|
||||
f.server.Handler.ServeHTTP(f.recorder, req)
|
||||
h.ServeHTTP(f.recorder, req)
|
||||
if f.recorder.Code != code {
|
||||
return fmt.Errorf("Expected code %v from %v %v but got: %+v", code, req.Method, req.URL, f.recorder)
|
||||
}
|
||||
@@ -3757,6 +3743,14 @@ func (f *fixture) executeRequest(req *http.Request, code int, resp string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fixture) executeRequest(req *http.Request, code int, resp string) error {
|
||||
return f.executeRequestForHandler(f.server.Handler, req, code, resp)
|
||||
}
|
||||
|
||||
func (f *fixture) executeDiagnosticRequest(req *http.Request, code int, resp string) error {
|
||||
return f.executeRequestForHandler(f.server.DiagnosticHandler, req, code, resp)
|
||||
}
|
||||
|
||||
func (f *fixture) reset() {
|
||||
f.recorder = httptest.NewRecorder()
|
||||
}
|
||||
@@ -3780,6 +3774,17 @@ func executeRequestsv0(t *testing.T, reqs []tr) {
|
||||
}
|
||||
}
|
||||
|
||||
func validateDiagnosticRequest(t *testing.T, f *fixture, req *http.Request, code int, resp string) {
|
||||
t.Helper()
|
||||
// diagnostic requests need to be available on both the normal handler and diagnostic handler
|
||||
if err := f.executeRequest(req, code, resp); err != nil {
|
||||
t.Errorf("Unexpected error for request %v: %s", req, err)
|
||||
}
|
||||
if err := f.executeDiagnosticRequest(req, code, resp); err != nil {
|
||||
t.Errorf("Unexpected error for request %v: %s", req, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newPolicy(id, s string) types.PolicyV1 {
|
||||
compiler := ast.NewCompiler()
|
||||
parsed := ast.MustParseModule(s)
|
||||
@@ -4041,7 +4046,9 @@ func newClient(t *testing.T, pool *x509.CertPool, clientKeyPair ...string) *http
|
||||
}
|
||||
|
||||
func TestShutdown(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
f := newFixture(t, func(s *Server) {
|
||||
s.WithDiagnosticAddresses([]string{":8443"})
|
||||
})
|
||||
loops, err := f.server.Listeners()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err.Error())
|
||||
@@ -4063,13 +4070,15 @@ func TestShutdown(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShutdownError(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
f := newFixture(t, func(s *Server) {
|
||||
s.WithDiagnosticAddresses([]string{":8443"})
|
||||
})
|
||||
|
||||
errMsg := "failed to shutdown"
|
||||
|
||||
// Add a mock httpListener to the server
|
||||
m := &mockHTTPListener{
|
||||
ShutdownHook: func() error {
|
||||
shutdownHook: func() error {
|
||||
return errors.New(errMsg)
|
||||
},
|
||||
}
|
||||
@@ -4086,7 +4095,9 @@ func TestShutdownError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShutdownMultipleErrors(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
f := newFixture(t, func(s *Server) {
|
||||
s.WithDiagnosticAddresses([]string{":8443"})
|
||||
})
|
||||
|
||||
shutdownErrs := []error{errors.New("err1"), nil, errors.New("err3")}
|
||||
|
||||
@@ -4095,7 +4106,7 @@ func TestShutdownMultipleErrors(t *testing.T) {
|
||||
m := &mockHTTPListener{}
|
||||
if err != nil {
|
||||
retVal := errors.New(err.Error())
|
||||
m.ShutdownHook = func() error {
|
||||
m.shutdownHook = func() error {
|
||||
return retVal
|
||||
}
|
||||
}
|
||||
@@ -4135,7 +4146,7 @@ func TestAddrsWithEmptyListenAddr(t *testing.T) {
|
||||
|
||||
func TestAddrsWithListenAddr(t *testing.T) {
|
||||
s := New()
|
||||
s.httpListeners = []httpListener{&mockHTTPListener{Addrs: ":8181"}}
|
||||
s.httpListeners = []httpListener{&mockHTTPListener{addrs: ":8181"}}
|
||||
a := s.Addrs()
|
||||
if len(a) != 1 || a[0] != ":8181" {
|
||||
t.Errorf("expected only an ':8181' address, got: %+v", a)
|
||||
@@ -4149,7 +4160,7 @@ func TestAddrsWithMixedListenerAddr(t *testing.T) {
|
||||
|
||||
s.httpListeners = []httpListener{}
|
||||
for _, addr := range addrs {
|
||||
s.httpListeners = append(s.httpListeners, &mockHTTPListener{Addrs: addr})
|
||||
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: defaultListenerType})
|
||||
}
|
||||
|
||||
a := s.Addrs()
|
||||
@@ -4171,17 +4182,171 @@ func TestAddrsWithMixedListenerAddr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticAddrsNoListeners(t *testing.T) {
|
||||
s := New()
|
||||
a := s.DiagnosticAddrs()
|
||||
if len(a) != 0 {
|
||||
t.Errorf("expected an empty list of addresses, got: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticAddrsWithEmptyListenAddr(t *testing.T) {
|
||||
s := New()
|
||||
s.httpListeners = []httpListener{&mockHTTPListener{t: diagnosticListenerType}}
|
||||
a := s.DiagnosticAddrs()
|
||||
if len(a) != 0 {
|
||||
t.Errorf("expected an empty list of addresses, got: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticAddrsWithListenAddr(t *testing.T) {
|
||||
s := New()
|
||||
s.httpListeners = []httpListener{&mockHTTPListener{addrs: ":8181", t: diagnosticListenerType}}
|
||||
a := s.DiagnosticAddrs()
|
||||
if len(a) != 1 || a[0] != ":8181" {
|
||||
t.Errorf("expected only an ':8181' address, got: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticAddrsWithMixedListenerAddr(t *testing.T) {
|
||||
s := New()
|
||||
addrs := []string{":8181", "", "unix:///var/tmp/foo.sock"}
|
||||
expected := []string{":8181", "unix:///var/tmp/foo.sock"}
|
||||
|
||||
s.httpListeners = []httpListener{}
|
||||
for _, addr := range addrs {
|
||||
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: diagnosticListenerType})
|
||||
}
|
||||
|
||||
a := s.DiagnosticAddrs()
|
||||
if len(a) != 2 {
|
||||
t.Errorf("expected 2 addresses, got: %+v", a)
|
||||
}
|
||||
|
||||
for _, expectedAddr := range expected {
|
||||
found := false
|
||||
for _, actualAddr := range a {
|
||||
if expectedAddr == actualAddr {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected %q in address list, got: %+v", expectedAddr, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixedAddrTypes(t *testing.T) {
|
||||
s := New()
|
||||
|
||||
s.httpListeners = []httpListener{}
|
||||
|
||||
addrs := map[string]struct{}{"localhost:8181": {}, "localhost:1234": {}, "unix:///var/tmp/foo.sock": {}}
|
||||
for addr := range addrs {
|
||||
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: defaultListenerType})
|
||||
}
|
||||
|
||||
diagAddrs := map[string]struct{}{":8181": {}, "https://127.0.0.1": {}}
|
||||
for addr := range diagAddrs {
|
||||
s.httpListeners = append(s.httpListeners, &mockHTTPListener{addrs: addr, t: diagnosticListenerType})
|
||||
}
|
||||
|
||||
actualAddrs := s.Addrs()
|
||||
if len(actualAddrs) != len(addrs) {
|
||||
t.Errorf("expected %d addresses, got: %+v", len(addrs), actualAddrs)
|
||||
}
|
||||
|
||||
for _, addr := range actualAddrs {
|
||||
if _, ok := addrs[addr]; !ok {
|
||||
t.Errorf("Unexpected address %v", addr)
|
||||
}
|
||||
}
|
||||
|
||||
actualDiagAddrs := s.DiagnosticAddrs()
|
||||
if len(actualDiagAddrs) != len(diagAddrs) {
|
||||
t.Errorf("expected %d addresses, got: %+v", len(diagAddrs), actualDiagAddrs)
|
||||
}
|
||||
|
||||
for _, addr := range actualDiagAddrs {
|
||||
if _, ok := diagAddrs[addr]; !ok {
|
||||
t.Errorf("Unexpected diagnostic address %v", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticRoutes(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
should404 bool
|
||||
}{
|
||||
{"/health", false},
|
||||
{"/metrics", false},
|
||||
{"/debug/pprof/", true},
|
||||
{"/v0/data", true},
|
||||
{"/v0/data/foo", true},
|
||||
{"/v1/data/", true},
|
||||
{"/v1/data/foo", true},
|
||||
{"/v1/policies", true},
|
||||
{"/v1/policies/foo", true},
|
||||
{"/v1/query", true},
|
||||
{"/v1/compile", true},
|
||||
{"/", true},
|
||||
}
|
||||
|
||||
f := newFixture(t, func(s *Server) {
|
||||
s.WithPprofEnabled(true)
|
||||
s.WithMetrics(new(mockMetricsProvider))
|
||||
})
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", tc.path, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
code := http.StatusOK
|
||||
if tc.should404 {
|
||||
code = http.StatusNotFound
|
||||
}
|
||||
f.reset()
|
||||
f.server.DiagnosticHandler.ServeHTTP(f.recorder, req)
|
||||
if f.recorder.Code != code {
|
||||
t.Errorf("Expected code %v from %v %v but got: %+v", code, req.Method, req.URL, f.recorder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type mockHTTPHandler struct{}
|
||||
|
||||
func (m *mockHTTPHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type mockMetricsProvider struct{}
|
||||
|
||||
func (m *mockMetricsProvider) RegisterEndpoints(registrar func(path, method string, handler http.Handler)) {
|
||||
registrar("/metrics", "GET", new(mockHTTPHandler))
|
||||
}
|
||||
|
||||
func (m *mockMetricsProvider) InstrumentHandler(handler http.Handler, label string) http.Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
type listenerHook func() error
|
||||
|
||||
type mockHTTPListener struct {
|
||||
ShutdownHook listenerHook
|
||||
Addrs string
|
||||
shutdownHook listenerHook
|
||||
addrs string
|
||||
t httpListenerType
|
||||
}
|
||||
|
||||
var _ httpListener = (*mockHTTPListener)(nil)
|
||||
|
||||
func (m mockHTTPListener) Addr() string {
|
||||
return m.Addrs
|
||||
return m.addrs
|
||||
}
|
||||
|
||||
func (m mockHTTPListener) ListenAndServe() error {
|
||||
@@ -4194,8 +4359,12 @@ func (m mockHTTPListener) ListenAndServeTLS(certFile, keyFile string) error {
|
||||
|
||||
func (m mockHTTPListener) Shutdown(ctx context.Context) error {
|
||||
var err error
|
||||
if m.ShutdownHook != nil {
|
||||
err = m.ShutdownHook()
|
||||
if m.shutdownHook != nil {
|
||||
err = m.shutdownHook()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m mockHTTPListener) Type() httpListenerType {
|
||||
return m.t
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package diagnostics
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/test/e2e"
|
||||
)
|
||||
|
||||
var testRuntime *e2e.TestRuntime
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
testServerParams := e2e.NewAPIServerTestParams()
|
||||
testServerParams.DiagnosticAddrs = &[]string{":0"}
|
||||
|
||||
var err error
|
||||
testRuntime, err = e2e.NewTestRuntime(testServerParams)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(testRuntime.RunAPIServerTests(m))
|
||||
}
|
||||
|
||||
func TestServerWithDiagnosticAddrHealthCheck(t *testing.T) {
|
||||
if err := testRuntime.HealthCheck(diagURL(t)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure the "main" listener is still OK
|
||||
if err := testRuntime.HealthCheck(testRuntime.URL()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerWithDiagnosticAddrProtectedAPIs(t *testing.T) {
|
||||
cases := []string{
|
||||
"/",
|
||||
"/v0/data",
|
||||
"/v0/data/foo",
|
||||
"/v1/data",
|
||||
"/v1/data/foo",
|
||||
"/v1/policies",
|
||||
"/v1/policies/foo",
|
||||
"/v1/query",
|
||||
"/v1/compile",
|
||||
}
|
||||
|
||||
baseURL := diagURL(t)
|
||||
|
||||
methods := []string{
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodTrace,
|
||||
http.MethodPatch,
|
||||
http.MethodConnect,
|
||||
http.MethodDelete,
|
||||
http.MethodOptions,
|
||||
http.MethodHead,
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
url := baseURL + tc
|
||||
for _, method := range methods {
|
||||
t.Run(fmt.Sprintf("%s %s", method, tc), func(t *testing.T) {
|
||||
assert404(t, method, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func diagURL(t *testing.T) string {
|
||||
t.Helper()
|
||||
addr := testRuntime.Runtime.DiagnosticAddrs()[0]
|
||||
diagURL, err := testRuntime.AddrToURL(addr)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error: ", err)
|
||||
}
|
||||
return diagURL
|
||||
}
|
||||
|
||||
func assert404(t *testing.T, method string, url string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error creating request: %s", err)
|
||||
}
|
||||
resp, err := testRuntime.Client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Unexpected response, expected 404, got: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
}
|
||||
+41
-7
@@ -57,6 +57,7 @@ type TestRuntime struct {
|
||||
Cancel context.CancelFunc
|
||||
Client *http.Client
|
||||
url string
|
||||
diagURL string
|
||||
urlMtx *sync.Mutex
|
||||
}
|
||||
|
||||
@@ -138,6 +139,22 @@ func (t *TestRuntime) URL() string {
|
||||
// will need to determine the URLs themselves.
|
||||
addr := addrs[0]
|
||||
|
||||
parsed, err := t.AddrToURL(addr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
t.url = parsed
|
||||
|
||||
return t.url
|
||||
}
|
||||
|
||||
// AddrToURL generates a full URL from an address, as configured on the runtime.
|
||||
// This can include fully qualified urls, just host/ip, with port, or only port
|
||||
// (eg, "localhost", ":8181", "http://foo", etc). If the runtime is configured
|
||||
// with HTTPS certs it will generate an appropriate URL.
|
||||
func (t *TestRuntime) AddrToURL(addr string) (string, error) {
|
||||
if strings.HasPrefix(addr, ":") {
|
||||
addr = "localhost" + addr
|
||||
}
|
||||
@@ -152,13 +169,10 @@ func (t *TestRuntime) URL() string {
|
||||
|
||||
parsed, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to parse listening address of server: %s", err)
|
||||
os.Exit(1)
|
||||
return "", fmt.Errorf("failed to parse listening address of server: %s", err)
|
||||
}
|
||||
|
||||
t.url = parsed.String()
|
||||
|
||||
return t.url
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
func (t *TestRuntime) runTests(m *testing.M, suppressLogs bool) int {
|
||||
@@ -209,8 +223,8 @@ func (t *TestRuntime) WaitForServer() error {
|
||||
// First make sure it has started listening and we have an address
|
||||
if t.URL() != "" {
|
||||
// Then make sure it has started serving
|
||||
resp, err := http.Get(t.URL() + "/health")
|
||||
if err == nil && resp.StatusCode == http.StatusOK {
|
||||
err := t.HealthCheck(t.URL())
|
||||
if err == nil {
|
||||
logrus.Infof("Test server ready and listening on: %s", t.URL())
|
||||
return nil
|
||||
}
|
||||
@@ -292,3 +306,23 @@ func (t *TestRuntime) GetDataWithInputTyped(path string, input interface{}, resp
|
||||
|
||||
return json.Unmarshal(bs, response)
|
||||
}
|
||||
|
||||
// HealthCheck will query /health and return an error if the server is not healthy
|
||||
func (t *TestRuntime) HealthCheck(url string, params ...string) error {
|
||||
reqURL := url + "/health"
|
||||
if len(params) > 0 {
|
||||
reqURL += "?" + strings.Join(params, "&")
|
||||
}
|
||||
req, err := http.NewRequest("GET", url+"/health", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected error creating request: %s", err)
|
||||
}
|
||||
resp, err := t.Client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unexpected error: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected response: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user