fup: add auth middleware machinery
This allows clients to register an authentication middleware which will be provided values in the context that it can use to make an authn/authz decision.
This commit is contained in:
parent
ccb7d81f1a
commit
5b74ceec4e
2 changed files with 149 additions and 6 deletions
|
@ -59,6 +59,13 @@ type Config struct {
|
||||||
|
|
||||||
// UseDirectDownload decides whether the "pretty" wrapped page or the direct download page is the most appropriate for a given set of parameters.
|
// UseDirectDownload decides whether the "pretty" wrapped page or the direct download page is the most appropriate for a given set of parameters.
|
||||||
UseDirectDownload func(fileExtension string, mimeType string) bool
|
UseDirectDownload func(fileExtension string, mimeType string) bool
|
||||||
|
|
||||||
|
// AuthMiddleware is a Gorilla middleware to provide authentication information.
|
||||||
|
//
|
||||||
|
// It runs on every handler, including public ones, and will be provided with information:
|
||||||
|
// - if the page is an upload page (either the homepage, the paste textbox page, or the upload handler), the IsMutate will return true.
|
||||||
|
// - if the page is an API handler, then IsAPIRequest will return true.
|
||||||
|
AuthMiddleware mux.MiddlewareFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
|
@ -80,6 +87,8 @@ type Application struct {
|
||||||
|
|
||||||
filenameGenerator fngen.FilenameGenerator
|
filenameGenerator fngen.FilenameGenerator
|
||||||
useDirectDownload func(fileExtension string, mimeType string) bool
|
useDirectDownload func(fileExtension string, mimeType string) bool
|
||||||
|
|
||||||
|
authMiddleware mux.MiddlewareFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultUseDirectDownload(fileExtension, mimeType string) bool {
|
func DefaultUseDirectDownload(fileExtension, mimeType string) bool {
|
||||||
|
@ -90,6 +99,39 @@ func DefaultUseDirectDownload(fileExtension, mimeType string) bool {
|
||||||
return !strings.HasPrefix(mimeType, "text/")
|
return !strings.HasPrefix(mimeType, "text/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAPIRequest(r *http.Request) bool {
|
||||||
|
return r.Header.Get("Accept") == "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ctxKeyIsMutate = contextKey("isMutate")
|
||||||
|
ctxKeyIsAPIRequest = contextKey("isAPIRequest")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsAPIRequest determines whether a request was made from an API client rather than from a browser.
|
||||||
|
func IsAPIRequest(ctx context.Context) bool {
|
||||||
|
return ctx.Value(ctxKeyIsAPIRequest).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMutate determines whether a request for the given context is for a "mutate".
|
||||||
|
// This includes things like the HTML for the home page, which in itself is not a mutate but is useless if you're not allowed to auth.
|
||||||
|
func IsMutate(ctx context.Context) bool {
|
||||||
|
return ctx.Value(ctxKeyIsMutate).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contextPopulateMiddleware(isMutate bool) mux.MiddlewareFunc {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyIsMutate, isMutate)
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyIsAPIRequest, isAPIRequest(r))
|
||||||
|
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Application) Handler() http.Handler {
|
func (a *Application) Handler() http.Handler {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
|
@ -102,12 +144,23 @@ func (a *Application) Handler() http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
r.NotFoundHandler = http.HandlerFunc(a.notFound)
|
r.NotFoundHandler = http.HandlerFunc(a.notFound)
|
||||||
r.HandleFunc("/", renderTemplate(a.indexTmpl))
|
|
||||||
r.HandleFunc("/paste", renderTemplate(a.pasteTmpl))
|
authR := r.PathPrefix("/").Subrouter()
|
||||||
r.HandleFunc("/raw/{filename}", a.rawDownload)
|
authR.HandleFunc("/", renderTemplate(a.indexTmpl))
|
||||||
r.HandleFunc("/upload", a.upload).Methods("POST", "PUT")
|
authR.HandleFunc("/paste", renderTemplate(a.pasteTmpl))
|
||||||
r.HandleFunc("/upload/{filename}", a.upload).Methods("PUT")
|
authR.HandleFunc("/upload", a.upload).Methods("POST", "PUT")
|
||||||
r.HandleFunc("/{filename}", a.view)
|
authR.HandleFunc("/upload/{filename}", a.upload).Methods("PUT")
|
||||||
|
|
||||||
|
publicR := r.PathPrefix("/").Subrouter()
|
||||||
|
publicR.HandleFunc("/raw/{filename}", a.rawDownload)
|
||||||
|
publicR.HandleFunc("/{filename}", a.view)
|
||||||
|
|
||||||
|
if a.authMiddleware != nil {
|
||||||
|
authR.Use(contextPopulateMiddleware(true))
|
||||||
|
authR.Use(a.authMiddleware)
|
||||||
|
publicR.Use(contextPopulateMiddleware(false))
|
||||||
|
publicR.Use(a.authMiddleware)
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -168,6 +221,7 @@ func New(ctx context.Context, cfg *Config) (*Application, error) {
|
||||||
useDirectDownload: cfg.UseDirectDownload,
|
useDirectDownload: cfg.UseDirectDownload,
|
||||||
appRoot: cfg.AppRoot,
|
appRoot: cfg.AppRoot,
|
||||||
highlighter: cfg.Highlighter,
|
highlighter: cfg.Highlighter,
|
||||||
|
authMiddleware: cfg.AuthMiddleware,
|
||||||
}
|
}
|
||||||
if a.redirectExpiry == 0 {
|
if a.redirectExpiry == 0 {
|
||||||
a.redirectExpiry = defaultRedirectExpiry
|
a.redirectExpiry = defaultRedirectExpiry
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hg.lukegb.com/lukegb/depot/web/fup/fuphttp"
|
"hg.lukegb.com/lukegb/depot/web/fup/fuphttp"
|
||||||
|
@ -214,3 +215,91 @@ func TestRetrieve(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
var gotReq, lastReqWasMutate, lastReqWasAPI bool
|
||||||
|
|
||||||
|
ccfg := *cfg
|
||||||
|
ccfg.AuthMiddleware = func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
gotReq = true
|
||||||
|
lastReqWasMutate = fuphttp.IsMutate(r.Context())
|
||||||
|
lastReqWasAPI = fuphttp.IsAPIRequest(r.Context())
|
||||||
|
next.ServeHTTP(rw, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := fuphttp.New(ctx, &ccfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fuphttp.New: %v", err)
|
||||||
|
}
|
||||||
|
s := httptest.NewServer(a.Handler())
|
||||||
|
t.Cleanup(s.Close)
|
||||||
|
|
||||||
|
tcs := []struct {
|
||||||
|
name string
|
||||||
|
request func(url string) (*http.Request, error)
|
||||||
|
wantIsMutate bool
|
||||||
|
wantIsAPI bool
|
||||||
|
}{{
|
||||||
|
name: "/ request",
|
||||||
|
request: func(url string) (*http.Request, error) { return http.NewRequest("GET", url, nil) },
|
||||||
|
wantIsMutate: true,
|
||||||
|
wantIsAPI: false,
|
||||||
|
}, {
|
||||||
|
name: "/upload request",
|
||||||
|
request: func(url string) (*http.Request, error) {
|
||||||
|
return http.NewRequest("PUT", url+"/upload/foo.txt", strings.NewReader("slartibartfast\n"))
|
||||||
|
},
|
||||||
|
wantIsMutate: true,
|
||||||
|
wantIsAPI: false,
|
||||||
|
}, {
|
||||||
|
name: "/upload request, application/json",
|
||||||
|
request: func(url string) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest("PUT", url+"/upload/foo.txt", strings.NewReader("slartibartfast\n"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
return req, nil
|
||||||
|
},
|
||||||
|
wantIsMutate: true,
|
||||||
|
wantIsAPI: true,
|
||||||
|
}, {
|
||||||
|
name: "/foo.txt request",
|
||||||
|
request: func(url string) (*http.Request, error) { return http.NewRequest("GET", url+"/foo.txt", nil) },
|
||||||
|
wantIsMutate: false,
|
||||||
|
wantIsAPI: false,
|
||||||
|
}, {
|
||||||
|
name: "/raw/foo.txt request",
|
||||||
|
request: func(url string) (*http.Request, error) { return http.NewRequest("GET", url+"/raw/foo.txt", nil) },
|
||||||
|
wantIsMutate: false,
|
||||||
|
wantIsAPI: false,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
gotReq = false
|
||||||
|
|
||||||
|
req, err := tc.request(s.URL)
|
||||||
|
resp, err := s.Client().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Do: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if !gotReq {
|
||||||
|
t.Fatalf("gotReq = %v; want true", gotReq)
|
||||||
|
}
|
||||||
|
if lastReqWasMutate != tc.wantIsMutate {
|
||||||
|
t.Errorf("lastReqWasMutate = %v; want %v", lastReqWasMutate, tc.wantIsMutate)
|
||||||
|
}
|
||||||
|
if lastReqWasAPI != tc.wantIsAPI {
|
||||||
|
t.Errorf("lastReqWasAPI = %v; want %v", lastReqWasAPI, tc.wantIsAPI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue