Skip to content

Commit

Permalink
Issue #302: Add read-only mode for UI
Browse files Browse the repository at this point in the history
This patch adds an access mode for the ui endpoint which allows to
disable some or most endpoints with a simple config option.

Fixes #302
  • Loading branch information
magiconair committed May 30, 2017
1 parent 1759b37 commit 9d97241
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 11 deletions.
6 changes: 6 additions & 0 deletions admin/api/manual.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ type manual struct {
}

func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// we need this for testing.
// under normal circumstances this is never nil
if registry.Default == nil {
return
}

switch r.Method {
case "GET":
value, version, err := registry.Default.ReadManual()
Expand Down
38 changes: 28 additions & 10 deletions admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

// Server provides the HTTP server for the admin UI and API.
type Server struct {
Access string
Color string
Title string
Version string
Expand All @@ -22,18 +23,35 @@ type Server struct {

// ListenAndServe starts the admin server.
func (s *Server) ListenAndServe(l config.Listen, tlscfg *tls.Config) error {
http.Handle("/api/config", &api.ConfigHandler{s.Cfg})
http.Handle("/api/manual", &api.ManualHandler{})
http.Handle("/api/routes", &api.RoutesHandler{})
http.Handle("/api/version", &api.VersionHandler{s.Version})
http.Handle("/manual", &ui.ManualHandler{Color: s.Color, Title: s.Title, Version: s.Version, Commands: s.Commands})
http.Handle("/routes", &ui.RoutesHandler{Color: s.Color, Title: s.Title, Version: s.Version})
http.HandleFunc("/logo.svg", ui.HandleLogo)
http.HandleFunc("/health", handleHealth)
http.Handle("/", http.RedirectHandler("/routes", http.StatusSeeOther))
return proxy.ListenAndServeHTTP(l, nil, tlscfg)
return proxy.ListenAndServeHTTP(l, s.handler(), tlscfg)
}

func (s *Server) handler() http.Handler {
mux := http.NewServeMux()

switch s.Access {
case "ro":
mux.HandleFunc("/api/manual", forbidden)
mux.HandleFunc("/manual", forbidden)
case "rw":
mux.Handle("/api/manual", &api.ManualHandler{})
mux.Handle("/manual", &ui.ManualHandler{Color: s.Color, Title: s.Title, Version: s.Version, Commands: s.Commands})
}

mux.Handle("/api/config", &api.ConfigHandler{s.Cfg})
mux.Handle("/api/routes", &api.RoutesHandler{})
mux.Handle("/api/version", &api.VersionHandler{s.Version})
mux.Handle("/routes", &ui.RoutesHandler{Color: s.Color, Title: s.Title, Version: s.Version})
mux.HandleFunc("/logo.svg", ui.HandleLogo)
mux.HandleFunc("/health", handleHealth)
mux.Handle("/", http.RedirectHandler("/routes", http.StatusSeeOther))
return mux
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
}

func forbidden(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Forbidden", http.StatusForbidden)
}
64 changes: 64 additions & 0 deletions admin/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package admin

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestAdminServerAccess(t *testing.T) {
type test struct {
uri string
code int
}

testAccess := func(access string, tests []test) {
srv := &Server{Access: access}
ts := httptest.NewServer(srv.handler())
defer ts.Close()

noRedirectClient := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
for _, tt := range tests {
t.Run(access+tt.uri, func(t *testing.T) {
resp, err := noRedirectClient.Get(ts.URL + tt.uri)
if err != nil {
t.Fatalf("got %v want nil", err)
}
if got, want := resp.StatusCode, tt.code; got != want {
t.Fatalf("got code %d want %d", got, want)
}
})
}
}

roTests := []test{
{"/api/manual", 403},
{"/api/config", 200},
{"/api/routes", 200},
{"/api/version", 200},
{"/manual", 403},
{"/routes", 200},
{"/health", 200},
{"/logo.svg", 200},
{"/", 303},
}

rwTests := []test{
{"/api/manual", 200},
{"/api/config", 200},
{"/api/routes", 200},
{"/api/version", 200},
{"/manual", 200},
{"/routes", 200},
{"/health", 200},
{"/logo.svg", 200},
{"/", 303},
}

testAccess("ro", roTests)
testAccess("rw", rwTests)
}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type UI struct {
Listen Listen
Color string
Title string
Access string
}

type Proxy struct {
Expand Down
3 changes: 2 additions & 1 deletion config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ var defaultConfig = &Config{
Addr: ":9998",
Proto: "http",
},
Color: "light-green",
Color: "light-green",
Access: "rw",
},
}
5 changes: 5 additions & 0 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c
f.BoolVar(&cfg.Registry.Consul.CheckTLSSkipVerify, "registry.consul.register.checkTLSSkipVerify", defaultConfig.Registry.Consul.CheckTLSSkipVerify, "service check TLS verifcation")
f.IntVar(&cfg.Runtime.GOGC, "runtime.gogc", defaultConfig.Runtime.GOGC, "sets runtime.GOGC")
f.IntVar(&cfg.Runtime.GOMAXPROCS, "runtime.gomaxprocs", defaultConfig.Runtime.GOMAXPROCS, "sets runtime.GOMAXPROCS")
f.StringVar(&cfg.UI.Access, "ui.access", defaultConfig.UI.Access, "access mode, one of [ro, rw]")
f.StringVar(&uiListenerValue, "ui.addr", defaultValues.UIListenerValue, "Address the UI/API is listening on")
f.StringVar(&cfg.UI.Color, "ui.color", defaultConfig.UI.Color, "background color of the UI")
f.StringVar(&cfg.UI.Title, "ui.title", defaultConfig.UI.Title, "optional title for the UI")
Expand Down Expand Up @@ -228,6 +229,10 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c
return nil, fmt.Errorf("invalid proxy.matcher: %s", cfg.Proxy.Matcher)
}

if cfg.UI.Access != "ro" && cfg.UI.Access != "rw" {
return nil, fmt.Errorf("invalid ui.access: %s", cfg.UI.Access)
}

// handle deprecations
deprecate := func(name, msg string) {
if f.IsSet(name) {
Expand Down
14 changes: 14 additions & 0 deletions config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,20 @@ func TestLoad(t *testing.T) {
return cfg
},
},
{
args: []string{"-ui.access", "ro"},
cfg: func(cfg *Config) *Config {
cfg.UI.Access = "ro"
return cfg
},
},
{
args: []string{"-ui.access", "rw"},
cfg: func(cfg *Config) *Config {
cfg.UI.Access = "rw"
return cfg
},
},
{
args: []string{"-ui.addr", "1.2.3.4:5555"},
cfg: func(cfg *Config) *Config {
Expand Down
10 changes: 10 additions & 0 deletions fabio.properties
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,16 @@
# runtime.gomaxprocs = -1


# ui.access configures the access mode for the UI.
#
# ro: read-only access
# rw: read-write access
#
# The default is
#
# ui.access = rw


# ui.addr configures the address the UI is listening on.
# The listener uses the same syntax as proxy.addr but
# supports only a single listener. To enable HTTPS
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ func makeTLSConfig(l config.Listen) *tls.Config {
}

func startAdmin(cfg *config.Config) {
log.Printf("[INFO] Admin server access mode %q", cfg.UI.Access)
log.Printf("[INFO] Admin server listening on %q", cfg.UI.Listen.Addr)
go func() {
l := cfg.UI.Listen
tlscfg := makeTLSConfig(l)
srv := &admin.Server{
Access: cfg.UI.Access,
Color: cfg.UI.Color,
Title: cfg.UI.Title,
Version: version,
Expand Down

0 comments on commit 9d97241

Please sign in to comment.