- bouncer.Dispatch, WithDispatch, Dispatched: context-only pre-resolved
principal marker honoured by Registry.Middleware for listed guard names
- cabana.ResolveBackend honours a backend-kind marker; additive
ResolveBackendToken, ScopeLimitFor and BackendPrincipal resolvers
- surf.Router.Handler and compileHandlers: one compiled chain per route,
shared by BuildRouter validation and compile
- smoke tests TestDispatchSmoke, TestDispatchedSmoke, TestDispatchMarkerSmoke
- READMEs and docs/services/{authentication,routing}.md updated
143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
package surf
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
// Smoke test for the in-process dispatch tracer of phase 19.7 (D-17): a
|
|
// request carrying the bouncer dispatch marker runs a guarded route through
|
|
// surf's compiled chain as the pre-resolved principal, while nothing an HTTP
|
|
// request can carry is treated as one.
|
|
|
|
// refusingGuard never authenticates a request and writes its own 401.
|
|
type refusingGuard struct{}
|
|
|
|
func (refusingGuard) AuthenticateCredential(*http.Request) (*bouncer.Principal, any, error) {
|
|
return nil, nil, errors.New("acme: no token")
|
|
}
|
|
|
|
func (refusingGuard) WriteUnauthorized(w http.ResponseWriter, err error) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"error":true,"message":"Unauthenticated"}`))
|
|
}
|
|
|
|
// dispatchPlugin registers bouncer guard "acme" as middleware "acme.auth"
|
|
// and one guarded route.
|
|
type dispatchPlugin struct{}
|
|
|
|
func (dispatchPlugin) ID() string { return "acme.items" }
|
|
func (dispatchPlugin) Requires() []string { return nil }
|
|
func (dispatchPlugin) Register(*backpack.App) error { return nil }
|
|
func (dispatchPlugin) Boot(*backpack.App) error { return nil }
|
|
|
|
func (p dispatchPlugin) Middlewares() map[string]pact.Middleware {
|
|
guards := bouncer.NewRegistry()
|
|
if err := guards.Register(p.ID(), "acme", refusingGuard{}); err != nil {
|
|
panic(err)
|
|
}
|
|
auth, err := guards.Middleware("acme")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return map[string]pact.Middleware{"acme.auth": auth}
|
|
}
|
|
|
|
func (dispatchPlugin) Routes(r pact.Router) error {
|
|
r.Group("/acme", Use("acme.auth"), func(g pact.Router) {
|
|
g.Get("/items/{id}", func(w http.ResponseWriter, req *http.Request) {
|
|
user, _ := bouncer.User(req.Context())
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"user":` + itoa(user.ID) + `,"item":"` + req.PathValue("id") + `"}`))
|
|
})
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func itoa(n uint) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var b []byte
|
|
for n > 0 {
|
|
b = append([]byte{byte('0' + n%10)}, b...)
|
|
n /= 10
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func TestDispatchSmoke(t *testing.T) {
|
|
app := transportApp(t, "development", map[string]string{"http.yaml": transportBodyLimits})
|
|
router, err := BuildRouter(app, []party.Plugin{dispatchPlugin{}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := router.Handler("GET", "/acme/items/{id}/missing"); ok {
|
|
t.Fatal("Handler found an unregistered route")
|
|
}
|
|
h, ok := router.Handler("GET", "/acme/items/{id}")
|
|
if !ok || h == nil {
|
|
t.Fatal("Handler did not find GET /acme/items/{id} after BuildRouter")
|
|
}
|
|
var nilRouter *Router
|
|
if _, ok := nilRouter.Handler("GET", "/acme/items/{id}"); ok {
|
|
t.Fatal("nil router found a handler")
|
|
}
|
|
|
|
serve := func(set func(*http.Request) *http.Request) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodGet, "/acme/items/7", nil)
|
|
req.SetPathValue("id", "7")
|
|
req.Pattern = "GET /acme/items/{id}"
|
|
req = set(req)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
marker := func(guards ...string) func(*http.Request) *http.Request {
|
|
return func(req *http.Request) *http.Request {
|
|
d := &bouncer.Dispatch{Guards: guards, Principal: &bouncer.Principal{ID: 42}}
|
|
return req.WithContext(bouncer.WithDispatch(req.Context(), d))
|
|
}
|
|
}
|
|
|
|
rec := serve(marker("acme"))
|
|
if rec.Code != http.StatusOK || strings.TrimSpace(rec.Body.String()) != `{"user":42,"item":"7"}` {
|
|
t.Fatalf("dispatch with the marker = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if rec := serve(func(req *http.Request) *http.Request { return req }); rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("request without the marker = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
forged := serve(func(req *http.Request) *http.Request {
|
|
req.Header.Set("X-Summer-Dispatch", "42")
|
|
req.Header.Set("Authorization", "Dispatch 42")
|
|
req.AddCookie(&http.Cookie{Name: "summer_dispatch", Value: "42"})
|
|
return req
|
|
})
|
|
if forged.Code != http.StatusUnauthorized {
|
|
t.Fatalf("forged headers = %d %s", forged.Code, forged.Body.String())
|
|
}
|
|
if rec := serve(marker("other")); rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("marker for another guard = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// The assembled handler serves the same route over HTTP unchanged.
|
|
full, err := router.compile()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec = httptest.NewRecorder()
|
|
full.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/acme/items/7", nil))
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("HTTP request without a token = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|