Add boolean-return authorization helper for control-flow decisions#5034
Add boolean-return authorization helper for control-flow decisions#5034ZephyrYWZhou wants to merge 1 commit into
Conversation
dimas-b
left a comment
There was a problem hiding this comment.
Thanks for your contribution, @ZephyrYWZhou !
+1 to the intent behind this PR 👍
There are some WIP activities to switch to exception-less authZ calls. Perhaps it might be best to wait for that transition to happen across the codebase.
| authorizer() | ||
| .authorizeOrThrow( | ||
| polarisPrincipal(), | ||
| resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), | ||
| op, | ||
| target, | ||
| null /* secondary */); | ||
| initializeCatalog(); | ||
| return true; |
There was a problem hiding this comment.
This may be a bit premature. Ideally we should switch callers to PolarisAuthorizer.authorize(), but I'm not sure the code is ready for that yet.
@sungwy : WDYT?
There was a problem hiding this comment.
Thanks @dimas-b ! Good to know there's work already happening here.
Would it make sense to merge this as-is for the immediate improvement and then I can follow up to adopt the authorize(state, request) path once that transition is further along? I can hold off until then to avoid churn as well if that is the preferred direction. Either way works for me
There was a problem hiding this comment.
@ZephyrYWZhou : TBH, I do not see much of a change in this PR. The exception is still in the control flow. It just gets caught in a different place 🤔 Did I miss something?
There was a problem hiding this comment.
Yeah you're right @dimas-b, I think the end-of-day right way is to use authorize(state, request).isAllowed() directly so no exception is thrown in the first place. What do you think?
There was a problem hiding this comment.
Yes, I think Polaris code needs to switch to the authorize(state, request) method.
There was a problem hiding this comment.
Revised per your feedback - now uses authorize(state, request).isAllowed() directly, no exceptions in the control flow. Would appreciate another look when you get a chance. Thanks!
flyingImer
left a comment
There was a problem hiding this comment.
Curious why not go through authorize(state, request).isAllowed() instead, PolarisAuthorizer already has that decision-native method and nothing calls it yet. This PR hand-rolls a boolean twin of the throw method instead, and authorizeRegisterTable's flagged as needing the same fix next. Feels like we're growing a second method family per operation instead of just using the one already there. Not blocking, just checking this is the direction you want.
Good question. I looked into it - as far as I can tell, I'm up for being that first caller though if that's the direction folks want. |
Yes, that's the direction the community want to go. We are in the middle of the transition from |
Sounds good thanks @flyrain, let me refactor my PR to reflect upon that direction. Feel free to let me know if there is any related tasks that need to be pick up as well. CC: @sungwy |
a3d8e41 to
c27a018
Compare
Introduce isResolvedBasicTableLikeOperationAuthorized() in CatalogHandler that uses the decision-native authorize(AuthorizationState, AuthorizationRequest) method to check authorization without throwing exceptions. This is the first service-layer caller of PolarisAuthorizer.authorize() for boolean decisions. Refactor authorizeLoadTable in IcebergCatalogHandler to use the new helper, eliminating the ForbiddenException-based control flow entirely. No exception is thrown or caught for the write-delegation probe — the authorization decision is obtained directly from the authorizer. This resolves the TODO at IcebergCatalogHandler: 'Refactor to have a boolean-return version of the helpers so we can fallthrough easily.'
c27a018 to
117cdc8
Compare
|
Revised - the implementation now uses The corresponding test has also been updated to mock the decision-native authorization path. Since this is the first service-layer caller of |
| List.of( | ||
| new SingleTargetAuthorizationIntent( | ||
| op, PolarisSecurableMapper.tableLike(catalogName(), identifier)))); | ||
| return authorizer().authorize(authorizationState, request).isAllowed(); |
There was a problem hiding this comment.
I'm not sure about using authorize() just in one narrow sub-case. This is prone to correctness deviations on different authorizer impl. call paths.
I'd prefer to switch all callers from authorizeOrThrow() to authorize()and remove the old method.
There was a problem hiding this comment.
Make sense to me, it looks like there are 22 authorizeOrThrow call sites across CatalogHandler (9), PolicyCatalogHandler (3), and PolarisAdminService (10). I can take the full migration task there.
There was a problem hiding this comment.
@dimas-b Quick clarification on the scope: when you say "switch all callers from authorizeOrThrow() to authorize() and remove the old method," do you mean removing only the legacy overloads (those taking principal, activatedEntities, op, target, secondary) while keeping authorizeOrThrow(AuthorizationState, AuthorizationRequest)? Or do you mean removing all authorizeOrThrow() variants and having every call site handle AuthorizationDecision directly?
My preference is to keep authorizeOrThrow(state, request) since it's a thin convenience wrapper over authorize().isAllowed() and most call sites simply want to throw on denial. The main benefit, in my view, is removing the legacy overloads that bypass the decision-native path.
CC: @sungwy for opinion as well
There was a problem hiding this comment.
Update on the broader migration - I tried switching all authorizeOrThrow call sites to authorize(state, request) and ran into a fundamental issue.
The legacy path operates on already-resolved entities, while authorize(state, request) re-resolves them via getResolvedSecurable(). Many existing call sites don't populate the resolution manifest in a way that's compatible with this re-resolution, resulting in IllegalStateException: never_registered_top_level_name_and_type_for_resolved_entity.
The loadTable change in this PR works because its manifest is already compatible. Other call sites will require manifest changes in addition to the authorization API switch, which seems to align with the broader migration.
Summary
This PR introduces
isResolvedBasicTableLikeOperationAuthorized()inCatalogHandler—the first service-layer caller ofPolarisAuthorizer.authorize(state, request)for boolean authorization decisions—and refactorsauthorizeLoadTableto use it.Problem
authorizeLoadTablecurrently relies on catchingForbiddenExceptionfor normal control flow. It first attempts write-delegation authorization and, if that fails, falls back to read-delegation authorization. Using exceptions for expected control flow reduces readability and incurs unnecessary overhead from exception creation and stack trace capture. The code already contains a TODO for this improvement:Changes
1.
CatalogHandler.javaAdded
isResolvedBasicTableLikeOperationAuthorized(op, identifier), which uses the decision-nativeauthorize(AuthorizationState, AuthorizationRequest).isAllowed()path instead of relying on exceptions. LikeauthorizeResolvedBasicTableLikeOperationOrThrow(), this helper assumesresolveBasicTableLikeTargetOrThrow()has already been called.2.
IcebergCatalogHandler.javaRefactored
authorizeLoadTableto use the new helper, replacing exception-based control flow with a straightforward conditional.Before
After
3.
IcebergCatalogHandlerTest.javaUpdated
loadCredentialsFallbackResolvesOnceThenAuthorizesReadDelegationto mock the decision-nativeauthorize()path (returningAuthorizationDecision.deny()) instead of configuringauthorizeOrThrow()to throw. Also added a defaultauthorize()mock returningAuthorizationDecision.allow()for the remaining tests.Scope
This PR only refactors the
authorizeLoadTablecall site. A similar exception-based pattern exists inauthorizeRegisterTable, but addressing that will require additional boolean-returning helpers for namespace-level and overwrite authorization. That work will be handled in a follow-up PR.Testing
Updated the existing test in
IcebergCatalogHandlerTestto verify the new decision-native authorization flow. All tests inIcebergCatalogHandlerTestpass.Checklist
IcebergCatalogHandlerauthorize()path