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:
Luke Granger-Brown 2021-03-23 00:45:28 +00:00
parent ccb7d81f1a
commit 5b74ceec4e
2 changed files with 149 additions and 6 deletions

View file

@ -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

View file

@ -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)
}
})
}
}