Core: include missing privilege and target entity in PolarisAuthorizer 403 messages - #4406
Conversation
dimas-b
left a comment
There was a problem hiding this comment.
Thanks for your contribution, @iprithv !
operator has no way to tell which privilege is missing or on which entity [...]
I believe a more robust solution would be to log these details and return a matching unique ID of that message to the client. This way the Polaris Admin can correlated the client-side error to AuthZ grants without exposing any extra (potentially sensitive) information to clients.
That said, please open a corresponding discussion on the dev ML. I think all AuthZ changes should be discussed on dev.
| findMissingPrivileges(polarisPrincipal, activatedEntities, authzOp, targets, secondaries); | ||
| if (!missing.isEmpty()) { | ||
| throw new ForbiddenException( | ||
| "Principal '%s' with activated PrincipalRoles '%s' and activated grants via '%s' is not authorized for op %s; missing %s", |
There was a problem hiding this comment.
I do not think exposing authZ details to the client in case of denied responses is a good idea. This information can potentially be used by malicious clients to further attacks.
There was a problem hiding this comment.
Agreed with @dimas-b. Deny responses should just tell the caller that they're denied, not the shape of the wall they're hitting. Instead, logging the missing privileges should be good.
There was a problem hiding this comment.
ah ok, I get it about not exposing authZ details to clients. updated. thanks!
|
This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days. |
Addresses security review feedback on PR apache#4406: - Detailed missing privilege info (e.g. 'TABLE_CREATE on NAMESPACE ns1') is now logged at INFO level server-side instead of being included in the client-facing ForbiddenException message. - The client-facing 403 response remains generic to avoid leaking authorization metadata to untrusted clients, per SECURITY-THREAT-MODEL.md. - Operators can correlate client errors to server logs via the existing X-Request-ID header already stamped on every request. - findMissingPrivileges() and MissingPrivilege record are retained for server-side logging and debugging. Fixes apache#4406
dimas-b
left a comment
There was a problem hiding this comment.
Functional changes LGTM, but it looks like this PR accumulated some unrelated changes? 🤔
There was a problem hiding this comment.
This file looks odd in current PR 🤔 Would you mind re-applying the functional commits to main from scratch?
There was a problem hiding this comment.
Good catch — those were accidental gradle/server-test-runner/.gradle cache artifacts that should never have been committed (they're gitignored).
Re-applied the functional changes onto current main from scratch in a single clean commit (review changes). The PR now only touches:
PolarisAuthorizerImpl.javaPolarisAuthorizerImplTest.javaCHANGELOG.md(under Unreleased)
There was a problem hiding this comment.
sure, done. thanks!
| LOGGER.info( | ||
| "Authorization denied for principal '{}' on operation '{}': missing {}", | ||
| polarisPrincipal.getName(), | ||
| authzOp, | ||
| missingDetails); | ||
| throw new ForbiddenException( | ||
| "Principal '%s' with activated PrincipalRoles '%s' and activated grants via '%s' is not authorized for op %s", |
There was a problem hiding this comment.
This approach looks reasonable to me. I think we can merge "as is".
However, as I commented earlier, as more admin-friendly approach might be to add a UUID to this message and also log it on line 942. Whoever needs to investigate the authZ denial, can use the UUID to find related log messages easily (hopefully).
I'll leave it up to your @iprithv to choose which way to go for this PR.
There was a problem hiding this comment.
... or use request ID as @sungwy suggested: https://lists.apache.org/thread/0l1x090nmxbbrght3jrgdzqdp24fbbsx
There was a problem hiding this comment.
Thanks — going with the existing request-ID correlation path for this PR (as suggested by @sungwy on the ML).
Default logging already includes %X{requestId} via MDC (LoggingMDCFilter + RequestIdFilter), and the client already receives X-Request-ID. Operators can match a client-side 403 to the server-side INFO log that lists the missing privileges without putting authZ details (or a second synthetic UUID) into the response body.
Leaving the client message generic as-is; happy to follow up later if we want the request ID duplicated into the ForbiddenException text as well.
There was a problem hiding this comment.
Agreed — using the existing request ID / X-Request-ID is enough here. Default log format already stamps requestId from MDC, so no extra UUID in this PR.
There was a problem hiding this comment.
ah okay, went with existing request-ID correlation. default logs include MDC requestId.. client gets X-Request-ID...leaving client message generic...thanks!
a89e0b8 to
801b2af
Compare
Re-apply auth-failure missing-privilege logging onto main from scratch: server-side INFO logs with missing privilege details, generic client 403, and drop accidentally committed gradle/server-test-runner/.gradle cache files.
801b2af to
89b5e39
Compare
PolarisAuthorizerImpl.authorizeOrThrowtoday produces only:the operator has no way to tell which privilege is missing or on which entity, and routinely has to grep the codebase to figure out which grant to add. The explicit TODO at the old short-circuit in
isAuthorized("Collect missing privileges to report all at the end and/or return to code that throws NotAuthorizedException for more useful messages.") calls this out.and this is a diagnostic improvement, not a behavioural change, every operation that was authorized before is still authorized, every one denied is still denied. Only the text of the 403 changes.
secondary failures are labelled
(secondary)so operators can tell which side of aRENAME_TABLE-style op needs grants. Multiple missing privileges are joined with,in one message.new SPI's
AuthorizationDecision.deny(e.getMessage())inPolarisAuthorizerImpl.authorize()inherits the richer text automatically.