Three defects in the RBAC middleware, all present on development and in released versions. None
were introduced by #3934 — they surfaced while reviewing it, and are filed together because the
first two share a root cause and a single fix.
1. Per-request pattern matching races, and grows the router without bound
pkg/gofr/rbac/endpoint_matcher.go:91 builds a route per rule, per request, on the config's shared
router:
route := router.NewRoute().Path(pattern)
gorilla/mux NewRoute appends to the router it is called on (mux@v1.8.1/mux.go:279-284):
func (r *Router) NewRoute() *Route {
route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes}
r.routes = append(r.routes, route)
return route
}
config.muxRouter is created once at load and shared across every request, so that append is both
unsynchronised and permanent.
Unbounded growth. Walking the router after driving requests through rbac.Middleware:
0 routes at start -> 114 routes after 100 requests
It never shrinks. A long-lived pod accumulates routes for the whole life of the process.
Data race. 50 goroutines x 20 requests through the middleware, under -race:
WARNING: DATA RACE
mux.(*Router).NewRoute() mux.go:282
rbac.matchMuxPattern() endpoint_matcher.go:91
rbac.matchesKey() config.go:484
rbac.findEndpointByPattern() config.go:439
rbac.getEndpointForRequest() endpoint_matcher.go:210
The existing suites do not catch this because they drive requests sequentially; it only appears
under concurrency, which is the only way a server runs it.
Reproduction: any RBAC config containing a mux pattern ({id}, {path:.*}), with concurrent
requests to a path that reaches the pattern scan.
2. Authorizing one request costs hundreds to thousands of allocations
Resolving a single pattern path on development, -benchmem:
| rules in config |
ns/op |
allocs/op |
| 6 |
18,165 |
533 |
| 21 |
58,471 |
1,648 |
| 51 |
127,909 |
3,734 |
This is the cost of recompiling mux patterns per rule per request. RBAC middleware runs on every
request, so this is on the hot path of every route in an application that enables it.
Both of the above are fixed by the same change
#3935 proposes reading mux.CurrentRoute(r).GetPathTemplate() — the router has already resolved the
request by the time middleware runs, so its answer is free and authoritative. That removes
matchMuxPattern entirely, and with it the race, the growth and most of the allocations. This issue
is concrete evidence for prioritising that one.
There is also a smaller, independent allocation in the ordered resolver introduced by #3934, worth
folding in whenever this is touched:
func (r *endpointRule) matchesMethod(methodUpper string) bool {
return matchesHTTPMethod(methodUpper, []string{r.method}) // slice per rule, per request
}
Comparing r.method directly, or storing the one-element slice on the rule at build time, removes
it.
3. Multi-role JWTs silently authorize nothing
extractRoleFromJWT (pkg/gofr/rbac/middleware.go) falls back to fmt.Sprintf("%v", role) when the
claim is not a string:
claim "admin" -> role="admin" matches
claim ["admin","viewer"] -> role="[admin viewer]" matches no configured role
claim ["admin"] -> role="[admin]" matches no configured role
An array is the default shape emitted by Keycloak, Auth0 and Entra ID, so an ordinary setup gets
403 on every request, with no error logged and nothing in the config to explain why.
It fails closed, so this is not an authorization hole — but it makes JWT-based RBAC appear broken to
anyone whose identity provider emits the common shape.
Decision needed: should a role array mean "holds all of these" (union of the permissions of each
named role), or should it be rejected explicitly at load with a clear error? Either is better than
formatting it into a string that can never match.
Suggested sequencing
1 and 2 land together via #3935. 3 is independent and needs a semantics decision first.
Three defects in the RBAC middleware, all present on
developmentand in released versions. Nonewere introduced by #3934 — they surfaced while reviewing it, and are filed together because the
first two share a root cause and a single fix.
1. Per-request pattern matching races, and grows the router without bound
pkg/gofr/rbac/endpoint_matcher.go:91builds a route per rule, per request, on the config's sharedrouter:
gorilla/mux
NewRouteappends to the router it is called on (mux@v1.8.1/mux.go:279-284):config.muxRouteris created once at load and shared across every request, so that append is bothunsynchronised and permanent.
Unbounded growth. Walking the router after driving requests through
rbac.Middleware:It never shrinks. A long-lived pod accumulates routes for the whole life of the process.
Data race. 50 goroutines x 20 requests through the middleware, under
-race:The existing suites do not catch this because they drive requests sequentially; it only appears
under concurrency, which is the only way a server runs it.
Reproduction: any RBAC config containing a mux pattern (
{id},{path:.*}), with concurrentrequests to a path that reaches the pattern scan.
2. Authorizing one request costs hundreds to thousands of allocations
Resolving a single pattern path on
development,-benchmem:This is the cost of recompiling mux patterns per rule per request. RBAC middleware runs on every
request, so this is on the hot path of every route in an application that enables it.
Both of the above are fixed by the same change
#3935 proposes reading
mux.CurrentRoute(r).GetPathTemplate()— the router has already resolved therequest by the time middleware runs, so its answer is free and authoritative. That removes
matchMuxPatternentirely, and with it the race, the growth and most of the allocations. This issueis concrete evidence for prioritising that one.
There is also a smaller, independent allocation in the ordered resolver introduced by #3934, worth
folding in whenever this is touched:
Comparing
r.methoddirectly, or storing the one-element slice on the rule at build time, removesit.
3. Multi-role JWTs silently authorize nothing
extractRoleFromJWT(pkg/gofr/rbac/middleware.go) falls back tofmt.Sprintf("%v", role)when theclaim is not a string:
An array is the default shape emitted by Keycloak, Auth0 and Entra ID, so an ordinary setup gets
403on every request, with no error logged and nothing in the config to explain why.It fails closed, so this is not an authorization hole — but it makes JWT-based RBAC appear broken to
anyone whose identity provider emits the common shape.
Decision needed: should a role array mean "holds all of these" (union of the permissions of each
named role), or should it be rejected explicitly at load with a clear error? Either is better than
formatting it into a string that can never match.
Suggested sequencing
1 and 2 land together via #3935. 3 is independent and needs a semantics decision first.