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 |
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.
There was a problem hiding this comment.
I believe the authorize() method is meant to be the main method. So the work to migrate all callers to using it needs to be done at some point.
@ZephyrYWZhou : If you feel like taking that task, your efforts will be greatly appreciated. Yet, it's your choice, of course. It may be possible to make a new smaller preparatory PRs before using authorize() at all call sites.
There was a problem hiding this comment.
Sounds good @dimas-b, I would be willing and beyond thrilled to take that task. I will make smaller preparatory PRs as they will be needed before full migration. Thanks
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. The helper guards against an unresolved target (e.g. a missing table) by throwing notFoundExceptionForTableLikeEntity before delegating to the authorizer, mirroring authorizeResolvedBasicTableLikeOperationOrThrow. Without this guard the real authorizer's getResolvedSecurable() throws IllegalStateException (surfacing as a 500) for missing entities instead of the expected NoSuchTableException (404). Also stub authorize(state, request) in the TestServices mock authorizer to return an "allow" decision, mirroring its no-op authorizeOrThrow behavior, so decision-native callers do not NPE on a null AuthorizationDecision. This resolves the TODO at IcebergCatalogHandler: 'Refactor to have a boolean-return version of the helpers so we can fallthrough easily.'
117cdc8 to
e7fda2d
Compare
|
Re-pushed with the CI fix (null-resolved-path guard + test-fixture stub); checks should be green now. @flyrain would appreciate you retriggering the workflow and your review when you have a chance. |
|
I have created the issue here to track the full migration progress - #5171 within which I put down the planning for the PRs to perform the migration incrementally. BTW here is the first preparatory PR - #5170 - will be appreicated if I can get a reivew on this PR as the first PR of this series @dimas-b. |
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