fix(09): WR-10 keep cabana's own backend guard when it is activated twice on one application

This commit is contained in:
Jakub Zych
2026-10-01 21:31:46 +02:00
parent 299d220b51
commit 2d96adf775
5 changed files with 40 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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