Repository navigation
fix(auth): return an error when RBAC, basic auth or OAuth cannot be enabled - #4387
Conversation
…nnot be enabled EnableRBAC, EnableBasicAuth and EnableOAuth log and return on unusable input (missing or invalid RBAC config, no/odd credentials, bad JWKS URL), so the app serves every route without auth. Add EnableRBACWithError, EnableBasicAuthWithError and EnableOAuthWithError, which return the error and install nothing, and deprecate the old methods. They keep their behaviour but now log that auth is DISABLED and name the replacement. Fixes gofr-dev#3763
aryanmehrotra
left a comment
There was a problem hiding this comment.
Thanks for digging into this, and for the production write-up in #3763. The fail-open is real, and the gap is on our side: nothing in the docs shows how to avoid it.
The capability already exists in the public API, though. rbac.LoadPermissions, rbac.Middleware and app.UseMiddleware have been exported since the RBAC middleware landed, so an app can already fail closed without any new methods on App:
cfg, err := rbac.LoadPermissions("configs/rbac.json", app.Logger(), app.Metrics(), otel.Tracer("gofr-rbac"))
if err != nil {
app.Logger().Fatalf("rbac: %v", err)
}
app.UseMiddleware(rbac.Middleware(cfg))I ran this against development (a091e65c9):
| Config | Existing API above | EnableRBAC(path) |
|---|---|---|
| missing file | exits 1, FATAL rbac: failed to read RBAC config file … |
starts; /admin returns 200 with no role, salesman and admin |
| malformed JSON | exits 1, failed to parse JSON config file … |
— |
| fails validation | exits 1, invalid RBAC config: endpoint[0]: … |
— |
| valid | 401 no role / 403 salesman / 200 admin |
401 / 403 / 200 |
For basic auth and OAuth, the inputs that currently disable auth are arguments the caller writes, so they can be checked before the call.
So rather than adding …WithError twins and deprecating three methods, could we keep this PR to:
- Docs: a "Failing closed on a bad config" section in the RBAC guide with the snippet above, and a pointer to it from the troubleshooting entry.
- Example: a fail-closed RBAC example under
examples/. - Optional: keep the louder
Authorization is DISABLED …log line inEnableRBAC, pointing at the new docs section. It's a small change and it makes the failure hard to miss.
Two reasons for not adding the twins:
- Setup methods on
Applog and continue. 3 of the 62 exported methods return an error. - Where gofr does add configurability, it uses functional options (
EnableMCP,AddLLM,AddReadinessCheck). AWithErrortwin for each method would be a second pattern that we'd have to maintain.
With the scope reduced to docs and an example, this is a docs: change rather than fix(auth).
|
Thanks for running it against The default is the bug. Your table's The workaround isn't the same as On "setup methods log and continue": I went through the 61 exported
So log-and-continue is safe for most setup methods because the failure shows up later. For these three it doesn't, and that's the case the error return is for. On functional options: I'd prefer them too, but none of the three signatures can take one without breaking callers. The twin-plus-deprecation isn't a new pattern either; gofr already uses it. The I'll add the docs section and the example you asked for to this PR as well. |
|
Thanks, you're right on the points I checked against I ran the branch against
The twins work, but existing callers stay open until they find the new method, and Could we return the error from the existing methods instead? func (a *App) EnableRBAC(configPath ...string) error
func (a *App) EnableBasicAuth(credentials ...string) error
func (a *App) EnableOAuth(jwksEndpoint string, refreshInterval int, options ...jwt.ParserOption) error
Could you also update the docs and the example to the Out of scope here, to track separately: routes missing from the RBAC config pass through (documented behaviour), and RBAC doesn't cover gRPC. |
…nabled EnableRBAC, EnableBasicAuth and EnableOAuth now return an error instead of the separate ...WithError methods, which are removed along with the deprecations. On bad input each still installs no middleware and logs that the mechanism is DISABLED, so callers that ignore the error still see it. Existing call statements compile unchanged; golangci-lint's errcheck flags the unchecked result. Docs and the auth example use the if err != nil form.
|
Thanks, done in edcf26a. I checked the errcheck point: a bare One thing worth a line in the release notes: adding a return value keeps plain calls compiling, but it does break code that declares these methods in its own interface (for example, a mock of Agreed on the out-of-scope items. #4390 (for #3935) is where unlisted routes come up, and I'll rework it on top of this once it merges. |
EnableRBACWithError is gone (gofr-dev#4387 now makes EnableRBAC return an error), so strictness can no longer be tied to a new, unreleased method: a check that stops startup would stop existing apps that call EnableRBAC. A dead rule and an uncovered route are both logged at error level, and the app keeps starting.
aryanmehrotra
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. I ran edcf26ad9 locally against development (da4653594):
| Bad input | development |
this PR, error checked | this PR, error ignored |
|---|---|---|---|
RBAC: no default file / missing / bad JSON / .txt / fails validation |
starts, /admin → 200 for everyone |
exits 1 with the reason | starts, 200, Authorization is DISABLED logged |
| Basic auth: zero or odd args | starts, 200 | exits 1 | starts, 200, DISABLED logged |
OAuth: ftp:// or path-only URL |
starts, 200 | exits 1 | starts, 200, DISABLED logged |
- Valid RBAC: 401 no role / 403
salesman/ 200admin, JSON and YAML, default path too. - OAuth against a live JWKS with real RS256 tokens: valid → 200; wrong key, expired, missing
exp, garbage → 401. - errcheck flags all three bare calls, and the interface /
func(...string)breakage behaves as the PR description says. -raceclean; changingEnableBasicAuthto swallow its error failsTestEnableBasicAuth_Errors, so the new tests catch it; 0 new lint issues.
One docs nit inline, non-blocking. Please also mention the interface / func(...string) breakage in the release notes as you suggested.
Umang01-hash
left a comment
There was a problem hiding this comment.
Verified independently at d6e6c23, including a live server E2E: configured basic auth gives 401/401/200 (no/wrong/correct creds), all four misconfig cases return the expected non-nil errors, and an ignored error serves 200 unprotected + logs the DISABLED warning — fail-closed when handled, detectable when not. Coverage 95.9→96.0, golangci-lint --new-from-rev clean, tests are load-bearing (revert-to-red confirmed). Scoping is right — only the three fail-open methods return an error; EnableAPIKeyAuth and the Func/Validator variants already install middleware (fail-closed).
Adding an error return is source-compatible for the usual call-as-statement usage (confirmed it still compiles), but it's an apidiff-incompatible signature change (a method-value assignment breaks), so worth a release-note line calling it an API change.
One coordination flag, not a code issue: #4168 (slim builds) also rewrites auth.go — it splits the gRPC interceptors out so auth.go no longer imports grpc, whereas this PR keeps them inline. Whichever merges second needs a hand-merge; taking this auth.go wholesale after #4168 would reintroduce the grpc import and silently break the gofr_nogrpc build (only the slim-tag CI job catches it).
LGTM.
aryanmehrotra
left a comment
There was a problem hiding this comment.
Re-verified at d6e6c23: the k8s-guide nit is fixed, and the merged-in change touches no PR file. Targeted tests pass with -race, 0 new lint issues. The live matrix is unchanged from edcf26ad9: every bad input exits 1 when the error is checked, and valid configs give 401/403/200. LGTM.
EnableRBACWithError is gone (gofr-dev#4387 now makes EnableRBAC return an error), so strictness can no longer be tied to a new, unreleased method: a check that stops startup would stop existing apps that call EnableRBAC. A dead rule and an uncovered route are both logged at error level, and the app keeps starting.
…nabled (gofr-dev#4387) * fix(auth): let callers stop startup when RBAC, basic auth or OAuth cannot be enabled * docs(auth): check the Enable* error in the Kubernetes guide and EnableRBAC godoc --------- Co-authored-by: Aryan Mehrotra <aryanmehrotra2000@gmail.com> Co-authored-by: Umang Mundhra <mundhraumang.02@gmail.com>
Description:
Fixes #3763.
EnableRBAClogs and returns when its config cannot be loaded, so the app starts and serves every route with no role checks. A config path that resolves differently inside a container is enough to trigger it, and the only signal is one error line at startup.EnableBasicAuth(no credentials, or an odd number of arguments) andEnableOAuth(JWKS URL that does not parse, has no scheme or host, or is not http/https) have the same shape, so this PR covers all three.The three methods now return an error:
EnableRBAC(configPath ...string) errorEnableBasicAuth(credentials ...string) errorEnableOAuth(jwksEndpoint string, refreshInterval int, options ...jwt.ParserOption) errorOn bad input a method installs no middleware, logs
<reason>. <Authorization | Basic authentication | OAuth authentication> is DISABLED: all routes are served without it. Check the error returned by <method> to stop startup instead, and returns a wrapped error (path or reason included). The caller decides what to do, typicallyapp.Logger().Fatalf("%v", err). The framework does not exit on its own, so apps that genuinely want to start without auth still can.One gap closed along the way:
EnableRBAC()with no argument and none of the default files present resolved to an empty path and failed on reading"". It now returns a clear "no RBAC config file found at configs/rbac.json, configs/rbac.yaml or configs/rbac.yml".Breaking Changes (if applicable):
Existing call statements such as
app.EnableRBAC("configs/rbac.json")compile unchanged. golangci-lint'serrcheckflags the unchecked result, and apps that ignore it behave as before but with the louder DISABLED log. Code that declares these methods in its own interface (for example a mock ofApp) or stores one as afunc(...string)value must add theerrorresult. There are no such uses inside gofr.Additional Information:
Test_EnableBasicAuthasserted 200 for odd arguments and the RBAC failure cases only checkedrouter != nil, which is how the fail-open went unnoticed.if err := …; err != nilform: RBAC guide (plus a "Config Load Failures" section and troubleshooting entry), authentication guide, auth-in-Kubernetes guide, and theusing-http-auth-middlewareexample.Logger().Fatalf("%v", err)rather thanLogger().Fatal(err): the latter logs"message":{}for anerrorvalue, dropping the reason.EnableAPIKeyAuth()with zero keys already fails closed; nil validators in the…WithValidatorvariants; routes missing from the RBAC config pass through (documented behaviour); RBAC does not cover gRPC.Checklist:
goimportandgolangci-lint.