diff --git a/modules/bouncer/README.md b/modules/bouncer/README.md index 01ebe36..0973255 100644 --- a/modules/bouncer/README.md +++ b/modules/bouncer/README.md @@ -78,6 +78,7 @@ func Login(secret string) (string, error) { | `bouncer.Registry` | Named guard registry; `bouncer.NewRegistry` creates one. | | `bouncer.Registry.Register` | Registers a guard under a name on behalf of a plugin; duplicate names fail. | | `bouncer.Registry.Middleware` | Returns HTTP middleware for a registered guard; unknown names fail. | +| `bouncer.Registry.Owner` | The plugin ID that registered a named guard. | | `bouncer.NewJWTGuard` | Frontend JWT guard reading the bearer header and optional cookies. | | `bouncer.NewBackendJWTGuard` | Admin JWT guard that requires the backend audience. | | `bouncer.Middleware` | Standalone middleware that validates a bearer token and loads the user. | diff --git a/modules/bouncer/registry.go b/modules/bouncer/registry.go index 116b5a7..1304b7e 100644 --- a/modules/bouncer/registry.go +++ b/modules/bouncer/registry.go @@ -54,6 +54,16 @@ func (reg *Registry) Register(pluginID, name string, g any) error { return nil } +// Owner returns the plugin ID that registered the guard name. The second +// result is false when no guard has that name. +func (reg *Registry) Owner(name string) (string, bool) { + if reg == nil { + return "", false + } + ng, ok := reg.guards[name] + return ng.pluginID, ok +} + // Middleware derives an http middleware from a registered guard. Unknown // names fail (fail boot, mirrors surf.RegisterMiddleware's contract). // On Authenticate/AuthenticateCredential success: WithUser (+WithCredential diff --git a/modules/bouncer/registry_test.go b/modules/bouncer/registry_test.go index 4ad8c1d..ce4851c 100644 --- a/modules/bouncer/registry_test.go +++ b/modules/bouncer/registry_test.go @@ -289,3 +289,20 @@ func TestRegisterAcceptsValidGuards(t *testing.T) { t.Fatal(err) } } + +func TestRegistryOwner(t *testing.T) { + reg := NewRegistry() + if _, ok := reg.Owner("backend"); ok { + t.Fatal("an unregistered guard reported an owner") + } + if err := reg.Register("acme.owner", "backend", NewBackendJWTGuard("test-secret-for-registry-owner", nil, nil, nil)); err != nil { + t.Fatal(err) + } + if owner, ok := reg.Owner("backend"); !ok || owner != "acme.owner" { + t.Fatalf("Owner = %q, %v; want acme.owner", owner, ok) + } + var nilReg *Registry + if _, ok := nilReg.Owner("backend"); ok { + t.Fatal("a nil registry reported an owner") + } +} diff --git a/modules/cabana/backend_guard_collision_test.go b/modules/cabana/backend_guard_collision_test.go index bf15cbe..de2a7b6 100644 --- a/modules/cabana/backend_guard_collision_test.go +++ b/modules/cabana/backend_guard_collision_test.go @@ -39,4 +39,8 @@ func TestActivateRefusesAForeignBackendGuard(t *testing.T) { if _, err := cabana.Activate(clean, []party.Plugin{demoPlugin{fsys: demoFS()}}); err != nil { t.Fatalf("Activate without a conflicting guard: %v", err) } + // A second assembly on the same application finds cabana's own guard. + if _, err := cabana.Activate(clean, []party.Plugin{demoPlugin{fsys: demoFS()}}); err != nil { + t.Fatalf("second Activate on one application: %v", err) + } } diff --git a/modules/cabana/http.go b/modules/cabana/http.go index 255c7c2..8ae2629 100644 --- a/modules/cabana/http.go +++ b/modules/cabana/http.go @@ -117,8 +117,14 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) { // provider), so cabana always registers its own and never mounts the API // behind a guard another plugin already put under the name "backend": a // taken name fails boot instead of silently replacing admin authentication. - if err := guards.Register("summercms.cabana", "backend", guard); err != nil { - return nil, fmt.Errorf("cabana: backend guard: %w", err) + // Activating twice on one application (a test assembling two handlers) + // finds cabana's own guard and keeps it. + if owner, taken := guards.Owner("backend"); !taken { + if err := guards.Register("summercms.cabana", "backend", guard); err != nil { + return nil, fmt.Errorf("cabana: backend guard: %w", err) + } + } else if owner != "summercms.cabana" { + return nil, fmt.Errorf("cabana: backend guard: guard %q is already registered by %s; the admin API needs its own audience-checking guard under that name", "backend", owner) } mw, err := guards.Middleware("backend") if err != nil {