From 496077303afa45fcbfa6f639a81424d3dfd24a17 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Wed, 6 May 2026 22:25:18 -0400 Subject: [PATCH 01/12] propose new shapes for AuthorizationRequest --- .../auth/opa/OpaPolarisAuthorizer.java | 19 ++-- .../auth/opa/OpaPolarisAuthorizerTest.java | 55 +++++------ .../auth/ranger/RangerPolarisAuthorizer.java | 8 +- .../core/auth/AuthorizationRequest.java | 91 ++++++++----------- .../core/auth/AuthorizationTargetBinding.java | 64 ------------- .../PairwiseTargetAuthorizationRequest.java | 59 ++++++++++++ .../polaris/core/auth/PolarisAuthorizer.java | 14 ++- .../core/auth/PolarisAuthorizerImpl.java | 12 ++- .../SingleTargetAuthorizationRequest.java | 48 ++++++++++ .../auth/UntargetedAuthorizationRequest.java | 46 ++++++++++ .../core/auth/AuthorizationRequestTest.java | 57 +++--------- .../core/auth/PolarisAuthorizerImplTest.java | 57 +++++------- 12 files changed, 284 insertions(+), 246 deletions(-) delete mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index c1aac773dd8..7ffe427a5ac 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -109,25 +109,32 @@ public OpaPolarisAuthorizer( */ @Override public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { authzState.getResolutionManifest().resolveAll(); } @Override @Nonnull public AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { boolean allowed = queryOpa( buildOpaAuthorizationInput( - request.getPrincipal(), + polarisPrincipal, request.getOperation(), toResourceEntitiesFromSecurables(request.getTargets()), toResourceEntitiesFromSecurables(request.getSecondaries()))); return allowed ? AuthorizationDecision.allow() : AuthorizationDecision.deny( - "OPA denied authorization for " + request.formatForAuthorizationMessage()); + "OPA denied authorization for principal=" + + polarisPrincipal.getName() + + " " + + request.formatForAuthorizationMessage()); } /** @@ -297,8 +304,8 @@ private ImmutableContext buildContext() { private ImmutableResource buildResource( List targets, List secondaries) { // Backward compatibility: keep the existing OPA input shape with separate target and - // secondary lists. Future work can align this with AuthorizationTargetBinding semantics - // using binding tuples like [(target, secondary), ...]. + // secondary lists. Future work can align this with richer request shapes if OPA starts + // consuming pairwise authorization intent directly. return ImmutableResource.builder().targets(targets).secondaries(secondaries).build(); } diff --git a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java index ad9cc1e801d..ce179383a28 100644 --- a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java +++ b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java @@ -51,7 +51,6 @@ import org.apache.polaris.core.auth.AuthorizationDecision; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; -import org.apache.polaris.core.auth.AuthorizationTargetBinding; import org.apache.polaris.core.auth.PathSegment; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; import org.apache.polaris.core.auth.PolarisPrincipal; @@ -579,8 +578,9 @@ void resolveAuthorizationInputsResolvesAll() { PolarisResolutionManifest resolutionManifest = mock(PolarisResolutionManifest.class); AuthorizationState authzState = new AuthorizationState(); authzState.setResolutionManifest(resolutionManifest); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); - authorizer.resolveAuthorizationInputs(authzState, requestWithCatalogTarget()); + authorizer.resolveAuthorizationInputs(authzState, principal, requestWithCatalogTarget()); verify(resolutionManifest).resolveAll(); } @@ -589,6 +589,7 @@ void resolveAuthorizationInputsResolvesAll() { void authorizeUsesIntentInputsAndAllows() throws Exception { final String[] capturedRequestBody = new String[1]; AuthorizationRequest request = requestWithCatalogTarget(); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -613,7 +614,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); ObjectMapper mapper = JsonMapper.builder().build(); JsonNode root = mapper.readTree(capturedRequestBody[0]); @@ -637,6 +638,7 @@ T httpClientExecute( @Test void authorizeDeniesWhenOpaDenies() { AuthorizationRequest request = requestWithCatalogTarget(); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":false}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -659,7 +661,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); assertThat(decision.isAllowed()).isFalse(); assertThat(decision.getMessage()) @@ -678,16 +680,13 @@ void authorizeIncludesStructuredParentsFromSecurable() throws Exception { final String[] capturedRequestBody = new String[1]; AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), PolarisAuthorizableOperation.LOAD_TABLE, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog1"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns1"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns2"), - new PathSegment(PolarisEntityType.TABLE_LIKE, "table1")), - null))); + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns1"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns2"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "table1"))); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -712,7 +711,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); ObjectMapper mapper = JsonMapper.builder().build(); JsonNode root = mapper.readTree(capturedRequestBody[0]); @@ -868,18 +867,16 @@ void authorizeRenameIncludesTargetAndSecondaryPaths() throws Exception { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), PolarisAuthorizableOperation.RENAME_TABLE, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog1"), - new PathSegment(PolarisEntityType.NAMESPACE, "src_ns"), - new PathSegment(PolarisEntityType.TABLE_LIKE, "src_tbl")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog1"), - new PathSegment(PolarisEntityType.NAMESPACE, "dst_ns"), - new PathSegment(PolarisEntityType.TABLE_LIKE, "dst_tbl"))))); + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"), + new PathSegment(PolarisEntityType.NAMESPACE, "src_ns"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "src_tbl")), + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"), + new PathSegment(PolarisEntityType.NAMESPACE, "dst_ns"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "dst_tbl"))); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); OpaPolarisAuthorizer authorizer = new OpaPolarisAuthorizer( @@ -897,7 +894,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); ObjectMapper mapper = JsonMapper.builder().build(); JsonNode root = mapper.readTree(capturedRequestBody[0]); @@ -936,12 +933,8 @@ T httpClientExecute( private AuthorizationRequest requestWithCatalogTarget() { return AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), PolarisAuthorizableOperation.GET_CATALOG, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1")), - null))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))); } private ResolvedPolarisEntity createResolvedEntity(PolarisEntity entity) { diff --git a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java index 271282d0e10..e6f120c0340 100644 --- a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java +++ b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java @@ -82,13 +82,17 @@ public void setRealmContext(RealmContext aRealmContext) { @Override public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { throw new UnsupportedOperationException("resolveAuthorizationInputs is not implemented yet"); } @Override public @Nonnull AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { throw new UnsupportedOperationException("authorize is not implemented yet"); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 576c1046f27..609b70f3762 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -19,71 +19,54 @@ package org.apache.polaris.core.auth; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import org.apache.polaris.core.entity.PolarisEntityType; -import org.apache.polaris.immutables.PolarisImmutable; -import org.immutables.value.Value; /** * Authorization request inputs for pre-authorization and core authorization. * - *

This wrapper keeps authorization inputs together and conveys the intent to be authorized via - * {@link AuthorizationTargetBinding} target bindings. + *

This hierarchy makes the target shape explicit on the request itself while preserving the + * normalized compatibility accessors used by current authorizer implementations. */ -@PolarisImmutable -public interface AuthorizationRequest { +public sealed interface AuthorizationRequest + permits UntargetedAuthorizationRequest, + SingleTargetAuthorizationRequest, + PairwiseTargetAuthorizationRequest { + static AuthorizationRequest of(@Nonnull PolarisAuthorizableOperation operation) { + return new UntargetedAuthorizationRequest(operation); + } + static AuthorizationRequest of( - @Nonnull PolarisPrincipal principal, - @Nonnull PolarisAuthorizableOperation operation, - @Nonnull List targetBindings) { - return ImmutableAuthorizationRequest.builder() - .principal(principal) - .operation(operation) - .targetBindings(targetBindings) - .build(); + @Nonnull PolarisAuthorizableOperation operation, @Nonnull PolarisSecurable target) { + return new SingleTargetAuthorizationRequest(operation, target); } - /** Returns the principal requesting authorization. */ - @Nonnull - PolarisPrincipal getPrincipal(); + static AuthorizationRequest of( + @Nonnull PolarisAuthorizableOperation operation, + @Nullable PolarisSecurable target, + @Nullable PolarisSecurable secondary) { + if (target == null && secondary == null) { + return new UntargetedAuthorizationRequest(operation); + } + if (target != null && secondary == null) { + return new SingleTargetAuthorizationRequest(operation, target); + } + return new PairwiseTargetAuthorizationRequest(operation, target, secondary); + } /** Returns the operation being authorized. */ @Nonnull PolarisAuthorizableOperation getOperation(); - /** Returns the target/secondary target bindings. */ - @Nonnull - List getTargetBindings(); - - /** - * Returns the primary target securables, if any. - * - *

Compatibility accessor derived from {@link #getTargetBindings()}. - */ + /** Returns the primary target securables, if any. */ @Nonnull - @Value.Derived - default List getTargets() { - return getTargetBindings().stream() - .map(AuthorizationTargetBinding::getTarget) - .filter(Objects::nonNull) - .toList(); - } + List getTargets(); - /** - * Returns secondary securables, if any. - * - *

Compatibility accessor derived from {@link #getTargetBindings()}. - */ + /** Returns secondary securables, if any. */ @Nonnull - @Value.Derived - default List getSecondaries() { - return getTargetBindings().stream() - .map(AuthorizationTargetBinding::getSecondary) - .filter(Objects::nonNull) - .toList(); - } + List getSecondaries(); /** * Returns a stable debug string for authorization messages. @@ -93,11 +76,8 @@ default List getSecondaries() { @Nonnull default String formatForAuthorizationMessage() { return String.format( - "operation=%s principal=%s targets=%s secondaries=%s", - getOperation(), - getPrincipal().getName(), - formatSecurables(getTargets()), - formatSecurables(getSecondaries())); + "operation=%s targets=%s secondaries=%s", + getOperation(), formatSecurables(getTargets()), formatSecurables(getSecondaries())); } private static String formatSecurables(List securables) { @@ -107,12 +87,13 @@ private static String formatSecurables(List securables) { } default boolean hasSecurableType(PolarisEntityType... types) { - for (AuthorizationTargetBinding targetBinding : getTargetBindings()) { - if (targetBinding.getTarget() != null && containsType(targetBinding.getTarget(), types)) { + for (PolarisSecurable target : getTargets()) { + if (containsType(target, types)) { return true; } - if (targetBinding.getSecondary() != null - && containsType(targetBinding.getSecondary(), types)) { + } + for (PolarisSecurable secondary : getSecondaries()) { + if (containsType(secondary, types)) { return true; } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java deleted file mode 100644 index 5ac3599f96d..00000000000 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationTargetBinding.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.core.auth; - -import com.google.common.base.Preconditions; -import jakarta.annotation.Nullable; -import org.apache.polaris.immutables.PolarisImmutable; -import org.immutables.value.Value; - -/** A resource binding containing a primary target and optional secondary. */ -@PolarisImmutable -public interface AuthorizationTargetBinding { - static AuthorizationTargetBinding of( - @Nullable PolarisSecurable target, @Nullable PolarisSecurable secondary) { - return ImmutableAuthorizationTargetBinding.builder() - .target(target) - .secondary(secondary) - .build(); - } - - /** Returns the primary target securable for the binding, if any. */ - @Nullable - PolarisSecurable getTarget(); - - /** - * Returns the optional secondary securable associated with the target. - * - *

Secondaries are related resources needed to evaluate the authorization decision but are not - * the direct object of the operation. Examples in current Polaris authorization flows include: - * - *

    - *
  • Table rename: the destination namespace (target is the source table). - *
  • Role grants: the grantee role/principal (target may be the role or the resource being - * granted on). - *
  • Policy attach/detach: the catalog/namespace/table being attached to (target is the - * policy). - *
- */ - @Nullable - PolarisSecurable getSecondary(); - - @Value.Check - default void validate() { - Preconditions.checkState( - getTarget() != null || getSecondary() != null, - "AuthorizationTargetBinding must contain a target or secondary"); - } -} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java new file mode 100644 index 00000000000..fe9e778cbc7 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; + +/** + * Authorization request for operations that may carry both a primary target and a related secondary + * target. + * + *

The primary target may be omitted for legacy root-scoped flows that rely on an implicit root + * primary plus an explicit secondary target. + */ +public record PairwiseTargetAuthorizationRequest( + @Nonnull PolarisAuthorizableOperation operation, + @Nullable PolarisSecurable target, + @Nullable PolarisSecurable secondary) + implements AuthorizationRequest { + public PairwiseTargetAuthorizationRequest { + Preconditions.checkNotNull(operation, "operation must be non-null"); + Preconditions.checkState( + target != null || secondary != null, + "PairwiseTargetAuthorizationRequest must contain a target or secondary"); + } + + @Override + public @Nonnull PolarisAuthorizableOperation getOperation() { + return operation; + } + + @Override + public @Nonnull List getTargets() { + return target == null ? List.of() : List.of(target); + } + + @Override + public @Nonnull List getSecondaries() { + return secondary == null ? List.of() : List.of(secondary); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java index 975eae5f03a..86a3312bb67 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java @@ -38,7 +38,9 @@ public interface PolarisAuthorizer { *

This method should not perform authorization decisions directly. */ void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request); + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request); /** * Core authorization entry point for the new SPI. @@ -48,7 +50,9 @@ void resolveAuthorizationInputs( */ @Nonnull AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request); + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request); /** * Convenience method that throws a {@link ForbiddenException} when authorization is denied. @@ -56,8 +60,10 @@ AuthorizationDecision authorize( *

Implementations should provide allow/deny decisions via {@link #authorize}. */ default void authorizeOrThrow( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { - AuthorizationDecision decision = authorize(authzState, request); + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { + AuthorizationDecision decision = authorize(authzState, polarisPrincipal, request); if (!decision.isAllowed()) { String message = decision.getMessage().orElse("Authorization denied"); throw new ForbiddenException("%s", message); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index bf84a2ad4cc..c81765a2247 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -749,7 +749,9 @@ public PolarisAuthorizerImpl(RealmConfig realmConfig) { @Override public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { PolarisResolutionManifest resolutionManifest = authzState.getResolutionManifest(); resolutionManifest.resolveAll(); } @@ -757,7 +759,9 @@ public void resolveAuthorizationInputs( @Override @Nonnull public AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull AuthorizationRequest request) { PolarisResolutionManifest resolutionManifest = authzState.getResolutionManifest(); RbacOperationSemantics semantics = RbacOperationSemantics.forOperation(request.getOperation()); boolean prependRootContainer = semantics.rooting() == ResolvedPathRooting.ROOT; @@ -778,7 +782,7 @@ public AuthorizationDecision authorize( ? null : getResolvedSecurables(resolutionManifest, secondaries, prependRootContainer); authorizeOrThrow( - request.getPrincipal(), + polarisPrincipal, resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), request.getOperation(), resolvedTargets, @@ -787,7 +791,7 @@ public AuthorizationDecision authorize( } catch (ForbiddenException e) { LOGGER.debug( "Authorization denied for principalName {} operation {} targets {} secondaries {}", - request.getPrincipal().getName(), + polarisPrincipal.getName(), request.getOperation(), request.getTargets(), request.getSecondaries(), diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java new file mode 100644 index 00000000000..16107d67550 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import java.util.List; + +/** Authorization request for operations with one explicit target. */ +public record SingleTargetAuthorizationRequest( + @Nonnull PolarisAuthorizableOperation operation, @Nonnull PolarisSecurable target) + implements AuthorizationRequest { + public SingleTargetAuthorizationRequest { + Preconditions.checkNotNull(operation, "operation must be non-null"); + Preconditions.checkNotNull(target, "target must be non-null"); + } + + @Override + public @Nonnull PolarisAuthorizableOperation getOperation() { + return operation; + } + + @Override + public @Nonnull List getTargets() { + return List.of(target); + } + + @Override + public @Nonnull List getSecondaries() { + return List.of(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java new file mode 100644 index 00000000000..28e46c1ac33 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import jakarta.annotation.Nonnull; +import java.util.List; + +/** Authorization request for operations with no explicit securable target. */ +public record UntargetedAuthorizationRequest(@Nonnull PolarisAuthorizableOperation operation) + implements AuthorizationRequest { + public UntargetedAuthorizationRequest { + Preconditions.checkNotNull(operation, "operation must be non-null"); + } + + @Override + public @Nonnull PolarisAuthorizableOperation getOperation() { + return operation; + } + + @Override + public @Nonnull List getTargets() { + return List.of(); + } + + @Override + public @Nonnull List getSecondaries() { + return List.of(); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index 5bf66e69cf6..744e30814e0 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -21,9 +21,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.List; -import java.util.Map; -import java.util.Set; import org.apache.polaris.core.entity.PolarisEntityType; import org.junit.jupiter.api.Test; @@ -33,12 +30,8 @@ public class AuthorizationRequestTest { void hasSecurableTypeReturnsTrueForPrincipalTarget() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.LOAD_TABLE, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")), - null))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice"))); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL)).isTrue(); } @@ -47,34 +40,23 @@ void hasSecurableTypeReturnsTrueForPrincipalTarget() { void hasSecurableTypeReturnsTrueForPrincipalRoleSecondary() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.ASSIGN_PRINCIPAL_ROLE, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")), + PolarisSecurable.of( + new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isTrue(); } @Test - void hasSecurableTypeReturnsTrueForCatalogRoleAcrossMultipleBindings() { + void hasSecurableTypeReturnsTrueForCatalogRoleSecondary() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns")), - null), - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.CATALOG_ROLE, "catalog-role"))))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog"), + new PathSegment(PolarisEntityType.CATALOG_ROLE, "catalog-role"))); assertThat(request.hasSecurableType(PolarisEntityType.CATALOG_ROLE)).isTrue(); } @@ -83,36 +65,21 @@ void hasSecurableTypeReturnsTrueForCatalogRoleAcrossMultipleBindings() { void hasSecurableTypeReturnsFalseWhenTypeAbsent() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.LOAD_VIEW, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - null))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isFalse(); } @Test - void allowsEmptyTargetBindings() { + void allowsUntargetedRequest() { AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - PolarisAuthorizableOperation.LIST_CATALOGS, - List.of()); + AuthorizationRequest.of(PolarisAuthorizableOperation.LIST_CATALOGS); - assertThat(request.getTargetBindings()).isEmpty(); assertThat(request.getTargets()).isEmpty(); assertThat(request.getSecondaries()).isEmpty(); } - @Test - void throwsWhenTargetBindingHasNoTargetOrSecondary() { - assertThatThrownBy(() -> AuthorizationTargetBinding.of(null, null)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("must contain a target or secondary"); - } - @Test void throwsWhenSecurableDoesNotStartWithTopLevelEntity() { assertThatThrownBy( diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java index 7bee960f72a..43c9f432170 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java @@ -63,18 +63,15 @@ void resolveAuthorizationInputsResolvesAll() { PolarisAuthorizerImpl authorizer = new PolarisAuthorizerImpl(mock(RealmConfig.class)); AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.GET_CATALOG, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - null))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); authzState.setResolutionManifest(manifest); - authorizer.resolveAuthorizationInputs(authzState, request); + authorizer.resolveAuthorizationInputs(authzState, principal, request); verify(manifest).resolveAll(); } @@ -88,6 +85,7 @@ void authorizeUsesRootTargetForRootGrantRequestWithoutPrimaryTarget() { PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); PolarisResolvedPathWrapper rootWrapper = mock(PolarisResolvedPathWrapper.class); PolarisResolvedPathWrapper principalRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); authzState.setResolutionManifest(manifest); when(manifest.getResolvedRootContainerEntityAsPath()).thenReturn(rootWrapper); @@ -105,20 +103,17 @@ void authorizeUsesRootTargetForRootGrantRequestWithoutPrimaryTarget() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE, - List.of( - AuthorizationTargetBinding.of( - null, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))))); + null, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))); - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); assertThat(decision.isAllowed()).isTrue(); verify(authorizer) .authorizeOrThrow( - eq(request.getPrincipal()), + eq(principal), eq(Set.of()), eq(PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE), eq(List.of(rootWrapper)), @@ -133,6 +128,7 @@ void authorizeUsesRootTargetForListCatalogsRequestWithoutPrimaryTarget() { AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); PolarisResolvedPathWrapper rootWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); authzState.setResolutionManifest(manifest); when(manifest.getResolvedRootContainerEntityAsPath()).thenReturn(rootWrapper); @@ -147,17 +143,14 @@ void authorizeUsesRootTargetForListCatalogsRequestWithoutPrimaryTarget() { ArgumentMatchers.>any()); AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - PolarisAuthorizableOperation.LIST_CATALOGS, - List.of()); + AuthorizationRequest.of(PolarisAuthorizableOperation.LIST_CATALOGS); - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); assertThat(decision.isAllowed()).isTrue(); verify(authorizer) .authorizeOrThrow( - eq(request.getPrincipal()), + eq(principal), eq(Set.of()), eq(PolarisAuthorizableOperation.LIST_CATALOGS), eq(List.of(rootWrapper)), @@ -172,6 +165,7 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); PolarisResolvedPathWrapper namespaceWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); authzState.setResolutionManifest(manifest); when(manifest.getResolvedPath( @@ -189,23 +183,19 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.LIST_NAMESPACES, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns")), - null))); + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns"))); - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); assertThat(decision.isAllowed()).isTrue(); verify(manifest) .getResolvedPath(ResolvedPathKey.of(List.of("ns"), PolarisEntityType.NAMESPACE), true); verify(authorizer) .authorizeOrThrow( - eq(request.getPrincipal()), + eq(principal), eq(Set.of()), eq(PolarisAuthorizableOperation.LIST_NAMESPACES), eq(List.of(namespaceWrapper)), @@ -218,6 +208,7 @@ void authorizeReturnsDenyDecision() { AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); authzState.setResolutionManifest(manifest); when(manifest.getResolvedTopLevelEntity("catalog", PolarisEntityType.CATALOG)) @@ -234,14 +225,10 @@ void authorizeReturnsDenyDecision() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), PolarisAuthorizableOperation.GET_CATALOG, - List.of( - AuthorizationTargetBinding.of( - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - null))); + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); - AuthorizationDecision decision = authorizer.authorize(authzState, request); + AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); assertThat(decision.isAllowed()).isFalse(); assertThat(decision.getMessage()).hasValue("missing privilege"); From 51dc64921424a63f3c6c6392c0016ebb192e46df Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Mon, 11 May 2026 22:02:27 -0400 Subject: [PATCH 02/12] additional tests --- .../auth/opa/OpaPolarisAuthorizer.java | 24 ++++ .../auth/opa/OpaPolarisAuthorizerTest.java | 131 +++++++++++++++++- .../auth/ranger/RangerPolarisAuthorizer.java | 9 ++ .../core/auth/AuthorizationRequest.java | 5 +- .../polaris/core/auth/PolarisAuthorizer.java | 56 +++++++- .../core/auth/PolarisAuthorizerImpl.java | 23 +++ .../core/auth/AuthorizationRequestTest.java | 8 ++ .../core/auth/PolarisAuthorizerImplTest.java | 123 ++++++++++++++++ 8 files changed, 373 insertions(+), 6 deletions(-) diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index 7ffe427a5ac..6b6513b64d6 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.io.IOException; @@ -115,6 +116,16 @@ public void resolveAuthorizationInputs( authzState.getResolutionManifest().resolveAll(); } + @Override + public void resolveAuthorizationInputs( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + Preconditions.checkArgument( + !requests.isEmpty(), "Authorization request batch must contain at least one request"); + authzState.getResolutionManifest().resolveAll(); + } + @Override @Nonnull public AuthorizationDecision authorize( @@ -137,6 +148,19 @@ public AuthorizationDecision authorize( + request.formatForAuthorizationMessage()); } + @Override + @Nonnull + public AuthorizationDecision authorize( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + Preconditions.checkArgument( + !requests.isEmpty(), "Authorization request batch must contain at least one request"); + // Batch OPA evaluation remains sequential for backward compatibility until OPA adopts a + // batch-native payload schema for both homogeneous and heterogeneous request sets. + return PolarisAuthorizer.super.authorize(authzState, polarisPrincipal, requests); + } + /** * Authorizes a single target and secondary entity for the given principal and operation. * diff --git a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java index ce179383a28..5e84010eaec 100644 --- a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java +++ b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java @@ -34,6 +34,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -670,9 +671,7 @@ T httpClientExecute( assertThat(message) .contains("OPA denied authorization") .contains("operation=GET_CATALOG") - .contains("principal=alice") - .contains("targets=[CATALOG:catalog-1]") - .contains("secondaries=[]")); + .contains("principal=alice")); } @Test @@ -931,6 +930,132 @@ T httpClientExecute( .isEqualTo(expectedDestination); } + @Test + void authorizeBatchEvaluatesHomogeneousRequestsSequentially() throws Exception { + final List capturedRequestBodies = new ArrayList<>(); + HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); + @SuppressWarnings("resource") + ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); + mockResponse.setEntity(mockEntity); + + PolarisResolutionManifest resolutionManifest = mock(PolarisResolutionManifest.class); + AuthorizationState authzState = new AuthorizationState(); + authzState.setResolutionManifest(resolutionManifest); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); + + OpaPolarisAuthorizer authorizer = + new OpaPolarisAuthorizer( + URI.create("http://opa.example.com:8181/v1/data/polaris/allow"), + mock(CloseableHttpClient.class), + JsonMapper.builder().build(), + null) { + @Override + T httpClientExecute( + ClassicHttpRequest request, HttpClientResponseHandler responseHandler) + throws HttpException, IOException { + capturedRequestBodies.add( + new String( + request.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8)); + return responseHandler.handleResponse(mockResponse); + } + }; + + AuthorizationDecision decision = + authorizer.authorize( + authzState, + principal, + List.of( + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))), + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-2"))))); + + ObjectMapper mapper = JsonMapper.builder().build(); + JsonNode firstRoot = mapper.readTree(capturedRequestBodies.get(0)); + JsonNode secondRoot = mapper.readTree(capturedRequestBodies.get(1)); + + assertThat(decision.isAllowed()).isTrue(); + assertThat(capturedRequestBodies).hasSize(2); + assertThat(firstRoot.path("input").path("action").asText()).isEqualTo("GET_CATALOG"); + assertThat(secondRoot.path("input").path("action").asText()).isEqualTo("GET_CATALOG"); + JsonNode expectedFirstTargets = + mapper.readTree( + """ + [ + { + "type": "CATALOG", + "name": "catalog-1", + "parents": [] + } + ] + """); + JsonNode expectedSecondTargets = + mapper.readTree( + """ + [ + { + "type": "CATALOG", + "name": "catalog-2", + "parents": [] + } + ] + """); + assertThat(firstRoot.path("input").path("resource").path("targets")) + .isEqualTo(expectedFirstTargets); + assertThat(secondRoot.path("input").path("resource").path("targets")) + .isEqualTo(expectedSecondTargets); + assertThat(firstRoot.path("input").path("resource").path("secondaries")).isEmpty(); + assertThat(secondRoot.path("input").path("resource").path("secondaries")).isEmpty(); + } + + @Test + void authorizeBatchFallsBackToSequentialEvaluationForMixedOperations() throws Exception { + final int[] requestCount = new int[1]; + HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); + @SuppressWarnings("resource") + ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); + mockResponse.setEntity(mockEntity); + + PolarisResolutionManifest resolutionManifest = mock(PolarisResolutionManifest.class); + AuthorizationState authzState = new AuthorizationState(); + authzState.setResolutionManifest(resolutionManifest); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); + + OpaPolarisAuthorizer authorizer = + new OpaPolarisAuthorizer( + URI.create("http://opa.example.com:8181/v1/data/polaris/allow"), + mock(CloseableHttpClient.class), + JsonMapper.builder().build(), + null) { + @Override + T httpClientExecute( + ClassicHttpRequest request, HttpClientResponseHandler responseHandler) + throws HttpException, IOException { + requestCount[0]++; + return responseHandler.handleResponse(mockResponse); + } + }; + + AuthorizationDecision decision = + authorizer.authorize( + authzState, + principal, + List.of( + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))), + AuthorizationRequest.of( + PolarisAuthorizableOperation.LIST_NAMESPACES, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog-1"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns"))))); + + assertThat(decision.isAllowed()).isTrue(); + assertThat(requestCount[0]).isEqualTo(2); + } + private AuthorizationRequest requestWithCatalogTarget() { return AuthorizationRequest.of( PolarisAuthorizableOperation.GET_CATALOG, diff --git a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java index e6f120c0340..b37d6a59b2f 100644 --- a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java +++ b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java @@ -88,6 +88,15 @@ public void resolveAuthorizationInputs( throw new UnsupportedOperationException("resolveAuthorizationInputs is not implemented yet"); } + @Override + public void resolveAuthorizationInputs( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + throw new UnsupportedOperationException( + "Batch resolveAuthorizationInputs is not implemented yet"); + } + @Override public @Nonnull AuthorizationDecision authorize( @Nonnull AuthorizationState authzState, diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 609b70f3762..1a410a76f2a 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -48,7 +48,8 @@ static AuthorizationRequest of( @Nullable PolarisSecurable target, @Nullable PolarisSecurable secondary) { if (target == null && secondary == null) { - return new UntargetedAuthorizationRequest(operation); + throw new IllegalStateException( + "Targeted AuthorizationRequest must contain a target or secondary"); } if (target != null && secondary == null) { return new SingleTargetAuthorizationRequest(operation, target); @@ -71,7 +72,7 @@ static AuthorizationRequest of( /** * Returns a stable debug string for authorization messages. * - *

Includes the operation, principal name, formatted targets, and formatted secondaries. + *

Includes the operation, formatted targets, and formatted secondaries. */ @Nonnull default String formatForAuthorizationMessage() { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java index 86a3312bb67..56b5f4364cb 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.core.auth; +import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.List; @@ -42,11 +43,24 @@ void resolveAuthorizationInputs( @Nonnull PolarisPrincipal polarisPrincipal, @Nonnull AuthorizationRequest request); + /** + * Resolve authorizer-specific inputs for a batch of authorization requests that share one + * principal. + * + *

Implementations must define their own batch pre-resolution behavior explicitly because + * merged manifest registration is authorizer-specific. + */ + void resolveAuthorizationInputs( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests); + /** * Core authorization entry point for the new SPI. * *

Implementations should rely on any required state in {@link AuthorizationState} and the - * intent captured by {@link AuthorizationRequest} (principal, operation, and target securables). + * intent captured by {@link AuthorizationRequest} (operation and target securables), together + * with the explicit {@link PolarisPrincipal} argument. */ @Nonnull AuthorizationDecision authorize( @@ -54,6 +68,29 @@ AuthorizationDecision authorize( @Nonnull PolarisPrincipal polarisPrincipal, @Nonnull AuthorizationRequest request); + /** + * Core authorization entry point for a batch of requests that share one principal. + * + *

The default behavior preserves semantics by evaluating requests independently in order and + * returning the first denial. Implementations may override this to batch homogeneous requests + * into a single downstream authorization call. + */ + @Nonnull + default AuthorizationDecision authorize( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + Preconditions.checkArgument( + !requests.isEmpty(), "Authorization request batch must contain at least one request"); + for (AuthorizationRequest request : requests) { + AuthorizationDecision decision = authorize(authzState, polarisPrincipal, request); + if (!decision.isAllowed()) { + return decision; + } + } + return AuthorizationDecision.allow(); + } + /** * Convenience method that throws a {@link ForbiddenException} when authorization is denied. * @@ -70,6 +107,23 @@ default void authorizeOrThrow( } } + /** + * Convenience method that throws when any request in the batch is denied. + * + *

The default behavior delegates to {@link #authorize(AuthorizationState, PolarisPrincipal, + * List)}. + */ + default void authorizeOrThrow( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + AuthorizationDecision decision = authorize(authzState, polarisPrincipal, requests); + if (!decision.isAllowed()) { + String message = decision.getMessage().orElse("Authorization denied"); + throw new ForbiddenException("%s", message); + } + } + void authorizeOrThrow( @Nonnull PolarisPrincipal polarisPrincipal, @Nonnull Set activatedEntities, diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index c81765a2247..cc3d9ffb35a 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -756,6 +756,16 @@ public void resolveAuthorizationInputs( resolutionManifest.resolveAll(); } + @Override + public void resolveAuthorizationInputs( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + Preconditions.checkArgument( + !requests.isEmpty(), "Authorization request batch must contain at least one request"); + authzState.getResolutionManifest().resolveAll(); + } + @Override @Nonnull public AuthorizationDecision authorize( @@ -800,6 +810,19 @@ public AuthorizationDecision authorize( } } + @Override + @Nonnull + public AuthorizationDecision authorize( + @Nonnull AuthorizationState authzState, + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull List requests) { + Preconditions.checkArgument( + !requests.isEmpty(), "Authorization request batch must contain at least one request"); + // RBAC has no external batch payload contract to preserve, so batch authorization remains a + // sequential evaluation of single-request checks. + return PolarisAuthorizer.super.authorize(authzState, polarisPrincipal, requests); + } + private List getResolvedSecurables( PolarisResolutionManifest resolutionManifest, List securables, diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index 744e30814e0..4f7e2b16875 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -80,6 +80,14 @@ void allowsUntargetedRequest() { assertThat(request.getSecondaries()).isEmpty(); } + @Test + void throwsWhenTargetedFactoryHasNoTargetOrSecondary() { + assertThatThrownBy( + () -> AuthorizationRequest.of(PolarisAuthorizableOperation.GET_CATALOG, null, null)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("must contain a target or secondary"); + } + @Test void throwsWhenSecurableDoesNotStartWithTopLevelEntity() { assertThatThrownBy( diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java index 43c9f432170..313fe19b338 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -202,6 +203,128 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { eq(null)); } + @Test + void authorizeBatchEvaluatesHomogeneousRequestsSequentially() { + PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); + AuthorizationState authzState = new AuthorizationState(); + PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); + PolarisResolvedPathWrapper firstCatalogWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisResolvedPathWrapper secondCatalogWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); + + authzState.setResolutionManifest(manifest); + when(manifest.getResolvedTopLevelEntity("catalog1", PolarisEntityType.CATALOG)) + .thenReturn(firstCatalogWrapper); + when(manifest.getResolvedTopLevelEntity("catalog2", PolarisEntityType.CATALOG)) + .thenReturn(secondCatalogWrapper); + when(manifest.getAllActivatedCatalogRoleAndPrincipalRoles()).thenReturn(Set.of()); + doNothing() + .when(authorizer) + .authorizeOrThrow( + any(PolarisPrincipal.class), + ArgumentMatchers.any(), + eq(PolarisAuthorizableOperation.GET_CATALOG), + ArgumentMatchers.any(), + ArgumentMatchers.>any()); + + AuthorizationDecision decision = + authorizer.authorize( + authzState, + principal, + List.of( + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog1"))), + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog2"))))); + + assertThat(decision.isAllowed()).isTrue(); + verify(authorizer, times(1)) + .authorizeOrThrow( + eq(principal), + eq(Set.of()), + eq(PolarisAuthorizableOperation.GET_CATALOG), + eq(List.of(firstCatalogWrapper)), + eq(null)); + verify(authorizer, times(1)) + .authorizeOrThrow( + eq(principal), + eq(Set.of()), + eq(PolarisAuthorizableOperation.GET_CATALOG), + eq(List.of(secondCatalogWrapper)), + eq(null)); + } + + @Test + void authorizeBatchFallsBackToSequentialEvaluationForMixedOperations() { + PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); + AuthorizationState authzState = new AuthorizationState(); + PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisResolvedPathWrapper namespaceWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); + + authzState.setResolutionManifest(manifest); + when(manifest.getResolvedTopLevelEntity("catalog", PolarisEntityType.CATALOG)) + .thenReturn(catalogWrapper); + when(manifest.getResolvedPath( + ResolvedPathKey.of(List.of("ns"), PolarisEntityType.NAMESPACE), true)) + .thenReturn(namespaceWrapper); + when(manifest.getAllActivatedCatalogRoleAndPrincipalRoles()).thenReturn(Set.of()); + doNothing() + .when(authorizer) + .authorizeOrThrow( + any(PolarisPrincipal.class), + ArgumentMatchers.any(), + any(PolarisAuthorizableOperation.class), + ArgumentMatchers.any(), + ArgumentMatchers.>any()); + + AuthorizationDecision decision = + authorizer.authorize( + authzState, + principal, + List.of( + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))), + AuthorizationRequest.of( + PolarisAuthorizableOperation.LIST_NAMESPACES, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns"))))); + + assertThat(decision.isAllowed()).isTrue(); + verify(authorizer, times(1)) + .authorizeOrThrow( + eq(principal), + eq(Set.of()), + eq(PolarisAuthorizableOperation.GET_CATALOG), + eq(List.of(catalogWrapper)), + eq(null)); + verify(authorizer, times(1)) + .authorizeOrThrow( + eq(principal), + eq(Set.of()), + eq(PolarisAuthorizableOperation.LIST_NAMESPACES), + eq(List.of(namespaceWrapper)), + eq(null)); + } + + @Test + void authorizeBatchThrowsWhenEmpty() { + PolarisAuthorizerImpl authorizer = new PolarisAuthorizerImpl(mock(RealmConfig.class)); + AuthorizationState authzState = new AuthorizationState(); + authzState.setResolutionManifest(mock(PolarisResolutionManifest.class)); + PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); + + org.assertj.core.api.Assertions.assertThatThrownBy( + () -> authorizer.authorize(authzState, principal, List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must contain at least one request"); + } + @Test void authorizeReturnsDenyDecision() { PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); From ff449527bf6ce133731e421f002827ca4403992e Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Mon, 11 May 2026 22:19:57 -0400 Subject: [PATCH 03/12] minor changes --- .../auth/opa/OpaPolarisAuthorizer.java | 6 ++--- .../auth/opa/OpaPolarisAuthorizerTest.java | 4 +-- .../core/auth/AuthorizationRequest.java | 26 ------------------- .../polaris/core/auth/PolarisAuthorizer.java | 6 ++--- .../core/auth/AuthorizationRequestTest.java | 21 ++++++++++++++- .../core/auth/PolarisAuthorizerImplTest.java | 4 +-- 6 files changed, 30 insertions(+), 37 deletions(-) diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index 6b6513b64d6..e3744c7b617 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -144,8 +144,8 @@ public AuthorizationDecision authorize( : AuthorizationDecision.deny( "OPA denied authorization for principal=" + polarisPrincipal.getName() - + " " - + request.formatForAuthorizationMessage()); + + " operation=" + + request.getOperation()); } @Override @@ -157,7 +157,7 @@ public AuthorizationDecision authorize( Preconditions.checkArgument( !requests.isEmpty(), "Authorization request batch must contain at least one request"); // Batch OPA evaluation remains sequential for backward compatibility until OPA adopts a - // batch-native payload schema for both homogeneous and heterogeneous request sets. + // batch-native payload schema. return PolarisAuthorizer.super.authorize(authzState, polarisPrincipal, requests); } diff --git a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java index 5e84010eaec..e2d75c4f5f2 100644 --- a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java +++ b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java @@ -931,7 +931,7 @@ T httpClientExecute( } @Test - void authorizeBatchEvaluatesHomogeneousRequestsSequentially() throws Exception { + void authorizeSingleOperationBatchEvaluatesSequentially() throws Exception { final List capturedRequestBodies = new ArrayList<>(); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") @@ -1011,7 +1011,7 @@ T httpClientExecute( } @Test - void authorizeBatchFallsBackToSequentialEvaluationForMixedOperations() throws Exception { + void authorizeMultiOperationBatchEvaluatesSequentially() throws Exception { final int[] requestCount = new int[1]; HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 1a410a76f2a..3778f3e296f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -21,7 +21,6 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.List; -import java.util.stream.Collectors; import org.apache.polaris.core.entity.PolarisEntityType; /** @@ -47,13 +46,6 @@ static AuthorizationRequest of( @Nonnull PolarisAuthorizableOperation operation, @Nullable PolarisSecurable target, @Nullable PolarisSecurable secondary) { - if (target == null && secondary == null) { - throw new IllegalStateException( - "Targeted AuthorizationRequest must contain a target or secondary"); - } - if (target != null && secondary == null) { - return new SingleTargetAuthorizationRequest(operation, target); - } return new PairwiseTargetAuthorizationRequest(operation, target, secondary); } @@ -69,24 +61,6 @@ static AuthorizationRequest of( @Nonnull List getSecondaries(); - /** - * Returns a stable debug string for authorization messages. - * - *

Includes the operation, formatted targets, and formatted secondaries. - */ - @Nonnull - default String formatForAuthorizationMessage() { - return String.format( - "operation=%s targets=%s secondaries=%s", - getOperation(), formatSecurables(getTargets()), formatSecurables(getSecondaries())); - } - - private static String formatSecurables(List securables) { - return securables.stream() - .map(PolarisSecurable::formatForAuthorizationMessage) - .collect(Collectors.joining(", ", "[", "]")); - } - default boolean hasSecurableType(PolarisEntityType... types) { for (PolarisSecurable target : getTargets()) { if (containsType(target, types)) { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java index 56b5f4364cb..aad4cbaa5d0 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java @@ -48,7 +48,7 @@ void resolveAuthorizationInputs( * principal. * *

Implementations must define their own batch pre-resolution behavior explicitly because - * merged manifest registration is authorizer-specific. + * manifest registration is authorizer-specific. */ void resolveAuthorizationInputs( @Nonnull AuthorizationState authzState, @@ -72,8 +72,8 @@ AuthorizationDecision authorize( * Core authorization entry point for a batch of requests that share one principal. * *

The default behavior preserves semantics by evaluating requests independently in order and - * returning the first denial. Implementations may override this to batch homogeneous requests - * into a single downstream authorization call. + * returning the first denial. Implementations may override this to use a single batched + * downstream authorization call. */ @Nonnull default AuthorizationDecision authorize( diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index 4f7e2b16875..6bcae4fe2ea 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -33,6 +33,7 @@ void hasSecurableTypeReturnsTrueForPrincipalTarget() { PolarisAuthorizableOperation.LOAD_TABLE, PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice"))); + assertThat(request).isInstanceOf(SingleTargetAuthorizationRequest.class); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL)).isTrue(); } @@ -45,6 +46,7 @@ void hasSecurableTypeReturnsTrueForPrincipalRoleSecondary() { PolarisSecurable.of( new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))); + assertThat(request).isInstanceOf(PairwiseTargetAuthorizationRequest.class); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isTrue(); } @@ -76,6 +78,7 @@ void allowsUntargetedRequest() { AuthorizationRequest request = AuthorizationRequest.of(PolarisAuthorizableOperation.LIST_CATALOGS); + assertThat(request).isInstanceOf(UntargetedAuthorizationRequest.class); assertThat(request.getTargets()).isEmpty(); assertThat(request.getSecondaries()).isEmpty(); } @@ -85,7 +88,23 @@ void throwsWhenTargetedFactoryHasNoTargetOrSecondary() { assertThatThrownBy( () -> AuthorizationRequest.of(PolarisAuthorizableOperation.GET_CATALOG, null, null)) .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("must contain a target or secondary"); + .hasMessageContaining( + "PairwiseTargetAuthorizationRequest must contain a target or secondary"); + } + + @Test + void threeArgFactoryAlwaysCreatesPairwiseRequest() { + AuthorizationRequest request = + AuthorizationRequest.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), + null); + + assertThat(request).isInstanceOf(PairwiseTargetAuthorizationRequest.class); + assertThat(request.getTargets()) + .containsExactly( + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); + assertThat(request.getSecondaries()).isEmpty(); } @Test diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java index 313fe19b338..0cb752b4c4c 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java @@ -204,7 +204,7 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { } @Test - void authorizeBatchEvaluatesHomogeneousRequestsSequentially() { + void authorizeSingleOperationBatchEvaluatesSequentially() { PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); @@ -257,7 +257,7 @@ void authorizeBatchEvaluatesHomogeneousRequestsSequentially() { } @Test - void authorizeBatchFallsBackToSequentialEvaluationForMixedOperations() { + void authorizeMultiOperationBatchEvaluatesSequentially() { PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); From 633cb88161f6c9c6bcda392b1a534dd310c67c54 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Wed, 13 May 2026 08:47:36 -0400 Subject: [PATCH 04/12] test nit --- .../polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java index e2d75c4f5f2..f64bfebdef6 100644 --- a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java +++ b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java @@ -973,11 +973,10 @@ T httpClientExecute( PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-2"))))); ObjectMapper mapper = JsonMapper.builder().build(); - JsonNode firstRoot = mapper.readTree(capturedRequestBodies.get(0)); - JsonNode secondRoot = mapper.readTree(capturedRequestBodies.get(1)); - assertThat(decision.isAllowed()).isTrue(); assertThat(capturedRequestBodies).hasSize(2); + JsonNode firstRoot = mapper.readTree(capturedRequestBodies.get(0)); + JsonNode secondRoot = mapper.readTree(capturedRequestBodies.get(1)); assertThat(firstRoot.path("input").path("action").asText()).isEqualTo("GET_CATALOG"); assertThat(secondRoot.path("input").path("action").asText()).isEqualTo("GET_CATALOG"); JsonNode expectedFirstTargets = From 96623d5ad6edef30d24ef0dfb3ee22abeb1552e1 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Wed, 13 May 2026 18:32:23 -0400 Subject: [PATCH 05/12] adopt feedback - thanks dmitri --- .../auth/opa/OpaPolarisAuthorizer.java | 18 ++---- .../core/auth/AuthorizationRequest.java | 28 ++++----- .../PairwiseTargetAuthorizationRequest.java | 9 ++- .../core/auth/PolarisAuthorizerImpl.java | 60 ++++++++----------- .../SingleTargetAuthorizationRequest.java | 10 ++-- .../auth/UntargetedAuthorizationRequest.java | 10 ++-- .../core/auth/AuthorizationRequestTest.java | 17 +++--- 7 files changed, 61 insertions(+), 91 deletions(-) diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index e3744c7b617..69bdb984b64 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -137,8 +137,8 @@ public AuthorizationDecision authorize( buildOpaAuthorizationInput( polarisPrincipal, request.getOperation(), - toResourceEntitiesFromSecurables(request.getTargets()), - toResourceEntitiesFromSecurables(request.getSecondaries()))); + toResourceEntitiesFromSecurable(request.getTarget()), + toResourceEntitiesFromSecurable(request.getSecondary()))); return allowed ? AuthorizationDecision.allow() : AuthorizationDecision.deny( @@ -393,16 +393,8 @@ private List toResourceEntitiesFromResolvedPaths( } @Nonnull - private List toResourceEntitiesFromSecurables( - @Nullable List securables) { - if (securables == null || securables.isEmpty()) { - return List.of(); - } - - List entities = new ArrayList<>(); - for (PolarisSecurable securable : securables) { - entities.add(buildResourceEntity(securable)); - } - return entities; + private List toResourceEntitiesFromSecurable( + @Nullable PolarisSecurable securable) { + return securable == null ? List.of() : List.of(buildResourceEntity(securable)); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 3778f3e296f..db90085c1db 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -20,14 +20,12 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import java.util.List; import org.apache.polaris.core.entity.PolarisEntityType; /** * Authorization request inputs for pre-authorization and core authorization. * - *

This hierarchy makes the target shape explicit on the request itself while preserving the - * normalized compatibility accessors used by current authorizer implementations. + *

This hierarchy makes the target shape explicit on the request itself. */ public sealed interface AuthorizationRequest permits UntargetedAuthorizationRequest, @@ -53,24 +51,20 @@ static AuthorizationRequest of( @Nonnull PolarisAuthorizableOperation getOperation(); - /** Returns the primary target securables, if any. */ - @Nonnull - List getTargets(); + /** Returns the primary target securable, if any. */ + @Nullable + PolarisSecurable getTarget(); - /** Returns secondary securables, if any. */ - @Nonnull - List getSecondaries(); + /** Returns the secondary securable, if any. */ + @Nullable + PolarisSecurable getSecondary(); default boolean hasSecurableType(PolarisEntityType... types) { - for (PolarisSecurable target : getTargets()) { - if (containsType(target, types)) { - return true; - } + if (getTarget() != null && containsType(getTarget(), types)) { + return true; } - for (PolarisSecurable secondary : getSecondaries()) { - if (containsType(secondary, types)) { - return true; - } + if (getSecondary() != null && containsType(getSecondary(), types)) { + return true; } return false; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java index fe9e778cbc7..b9011422097 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java @@ -21,7 +21,6 @@ import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import java.util.List; /** * Authorization request for operations that may carry both a primary target and a related secondary @@ -48,12 +47,12 @@ public record PairwiseTargetAuthorizationRequest( } @Override - public @Nonnull List getTargets() { - return target == null ? List.of() : List.of(target); + public @Nullable PolarisSecurable getTarget() { + return target; } @Override - public @Nonnull List getSecondaries() { - return secondary == null ? List.of() : List.of(secondary); + public @Nullable PolarisSecurable getSecondary() { + return secondary; } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index cc3d9ffb35a..69dd3438a06 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -776,21 +776,22 @@ public AuthorizationDecision authorize( RbacOperationSemantics semantics = RbacOperationSemantics.forOperation(request.getOperation()); boolean prependRootContainer = semantics.rooting() == ResolvedPathRooting.ROOT; try { - List targets = request.getTargets(); List resolvedTargets; - if (targets.isEmpty()) { + PolarisSecurable target = request.getTarget(); + if (target == null) { resolvedTargets = prependRootContainer ? List.of(resolutionManifest.getResolvedRootContainerEntityAsPath()) : null; } else { - resolvedTargets = getResolvedSecurables(resolutionManifest, targets, prependRootContainer); + resolvedTargets = + List.of(getResolvedSecurable(resolutionManifest, target, prependRootContainer)); } - List secondaries = request.getSecondaries(); + PolarisSecurable secondary = request.getSecondary(); List resolvedSecondaries = - semantics.secondaryPrivileges().isEmpty() || secondaries.isEmpty() + semantics.secondaryPrivileges().isEmpty() || secondary == null ? null - : getResolvedSecurables(resolutionManifest, secondaries, prependRootContainer); + : List.of(getResolvedSecurable(resolutionManifest, secondary, prependRootContainer)); authorizeOrThrow( polarisPrincipal, resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), @@ -803,8 +804,8 @@ public AuthorizationDecision authorize( "Authorization denied for principalName {} operation {} targets {} secondaries {}", polarisPrincipal.getName(), request.getOperation(), - request.getTargets(), - request.getSecondaries(), + request.getTarget(), + request.getSecondary(), e); return AuthorizationDecision.deny(e.getMessage()); } @@ -823,38 +824,25 @@ public AuthorizationDecision authorize( return PolarisAuthorizer.super.authorize(authzState, polarisPrincipal, requests); } - private List getResolvedSecurables( - PolarisResolutionManifest resolutionManifest, - List securables, - boolean prependRootContainer) { - return securables.stream() - .map( - securable -> { - PolarisResolvedPathWrapper resolvedSecurable = - getResolvedSecurable(resolutionManifest, securable, prependRootContainer); - Preconditions.checkState( - resolvedSecurable != null, - "Resolved path for securable is null for entityType=%s leaf=%s parents=%s", - securable.getLeaf().entityType(), - securable.getLeaf(), - securable.getParents()); - return resolvedSecurable; - }) - .toList(); - } - private PolarisResolvedPathWrapper getResolvedSecurable( PolarisResolutionManifest resolutionManifest, PolarisSecurable securable, boolean prependRootContainer) { - if (securable.getLeaf().entityType().isTopLevel()) { - // Ignore prependRootContainer for top-level entities. - return resolutionManifest.getResolvedTopLevelEntity( - securable.getLeaf().name(), securable.getLeaf().entityType()); - } - return resolutionManifest.getResolvedPath( - ResolvedPathKey.of(getPathNamesWithinCatalog(securable), securable.getLeaf().entityType()), - prependRootContainer); + PolarisResolvedPathWrapper resolvedSecurable = + securable.getLeaf().entityType().isTopLevel() + ? resolutionManifest.getResolvedTopLevelEntity( + securable.getLeaf().name(), securable.getLeaf().entityType()) + : resolutionManifest.getResolvedPath( + ResolvedPathKey.of( + getPathNamesWithinCatalog(securable), securable.getLeaf().entityType()), + prependRootContainer); + Preconditions.checkState( + resolvedSecurable != null, + "Resolved path for securable is null for entityType=%s leaf=%s parents=%s", + securable.getLeaf().entityType(), + securable.getLeaf(), + securable.getParents()); + return resolvedSecurable; } private List getPathNamesWithinCatalog(PolarisSecurable securable) { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java index 16107d67550..60ee18ee884 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java @@ -20,7 +20,7 @@ import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; -import java.util.List; +import jakarta.annotation.Nullable; /** Authorization request for operations with one explicit target. */ public record SingleTargetAuthorizationRequest( @@ -37,12 +37,12 @@ public record SingleTargetAuthorizationRequest( } @Override - public @Nonnull List getTargets() { - return List.of(target); + public @Nonnull PolarisSecurable getTarget() { + return target; } @Override - public @Nonnull List getSecondaries() { - return List.of(); + public @Nullable PolarisSecurable getSecondary() { + return null; } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java index 28e46c1ac33..1747186dc92 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java @@ -20,7 +20,7 @@ import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; -import java.util.List; +import jakarta.annotation.Nullable; /** Authorization request for operations with no explicit securable target. */ public record UntargetedAuthorizationRequest(@Nonnull PolarisAuthorizableOperation operation) @@ -35,12 +35,12 @@ public record UntargetedAuthorizationRequest(@Nonnull PolarisAuthorizableOperati } @Override - public @Nonnull List getTargets() { - return List.of(); + public @Nullable PolarisSecurable getTarget() { + return null; } @Override - public @Nonnull List getSecondaries() { - return List.of(); + public @Nullable PolarisSecurable getSecondary() { + return null; } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index 6bcae4fe2ea..43e3bd6e696 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -79,8 +79,8 @@ void allowsUntargetedRequest() { AuthorizationRequest.of(PolarisAuthorizableOperation.LIST_CATALOGS); assertThat(request).isInstanceOf(UntargetedAuthorizationRequest.class); - assertThat(request.getTargets()).isEmpty(); - assertThat(request.getSecondaries()).isEmpty(); + assertThat(request.getTarget()).isNull(); + assertThat(request.getSecondary()).isNull(); } @Test @@ -94,17 +94,14 @@ void throwsWhenTargetedFactoryHasNoTargetOrSecondary() { @Test void threeArgFactoryAlwaysCreatesPairwiseRequest() { + PolarisSecurable target = + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")); AuthorizationRequest request = - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - null); + AuthorizationRequest.of(PolarisAuthorizableOperation.GET_CATALOG, target, null); assertThat(request).isInstanceOf(PairwiseTargetAuthorizationRequest.class); - assertThat(request.getTargets()) - .containsExactly( - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); - assertThat(request.getSecondaries()).isEmpty(); + assertThat(request.getTarget()).isEqualTo(target); + assertThat(request.getSecondary()).isNull(); } @Test From 9354d3c821015fd0675663e775dcf11b62c45da8 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Tue, 19 May 2026 16:31:20 -0400 Subject: [PATCH 06/12] use authorizationIntent --- .../auth/opa/OpaPolarisAuthorizer.java | 60 ++++--------- .../auth/opa/OpaPolarisAuthorizerTest.java | 67 +++++++------- .../auth/ranger/RangerPolarisAuthorizer.java | 17 +--- .../core/auth/AuthorizationIntent.java | 74 ++++++++++++++++ .../core/auth/AuthorizationRequest.java | 81 ++++++++--------- ...=> PairwiseTargetAuthorizationIntent.java} | 10 +-- .../polaris/core/auth/PolarisAuthorizer.java | 70 ++------------- .../core/auth/PolarisAuthorizerImpl.java | 60 +++++-------- ...a => SingleTargetAuthorizationIntent.java} | 8 +- ...ava => TargetlessAuthorizationIntent.java} | 8 +- .../core/auth/AuthorizationRequestTest.java | 83 ++++++++++------- .../core/auth/PolarisAuthorizerImplTest.java | 88 ++++++++++--------- 12 files changed, 307 insertions(+), 319 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java rename polaris-core/src/main/java/org/apache/polaris/core/auth/{PairwiseTargetAuthorizationRequest.java => PairwiseTargetAuthorizationIntent.java} (84%) rename polaris-core/src/main/java/org/apache/polaris/core/auth/{SingleTargetAuthorizationRequest.java => SingleTargetAuthorizationIntent.java} (88%) rename polaris-core/src/main/java/org/apache/polaris/core/auth/{UntargetedAuthorizationRequest.java => TargetlessAuthorizationIntent.java} (83%) diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index 69bdb984b64..0d03ce8068d 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.io.IOException; @@ -43,6 +42,7 @@ import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationIntent; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; import org.apache.polaris.core.auth.PathSegment; @@ -110,55 +110,31 @@ public OpaPolarisAuthorizer( */ @Override public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { - authzState.getResolutionManifest().resolveAll(); - } - - @Override - public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - Preconditions.checkArgument( - !requests.isEmpty(), "Authorization request batch must contain at least one request"); + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { authzState.getResolutionManifest().resolveAll(); } @Override @Nonnull public AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { - boolean allowed = - queryOpa( - buildOpaAuthorizationInput( - polarisPrincipal, - request.getOperation(), - toResourceEntitiesFromSecurable(request.getTarget()), - toResourceEntitiesFromSecurable(request.getSecondary()))); - return allowed - ? AuthorizationDecision.allow() - : AuthorizationDecision.deny( + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + for (AuthorizationIntent intent : request.intents()) { + boolean allowed = + queryOpa( + buildOpaAuthorizationInput( + request.principal(), + intent.getOperation(), + toResourceEntitiesFromSecurable(intent.getTarget()), + toResourceEntitiesFromSecurable(intent.getSecondary()))); + if (!allowed) { + return AuthorizationDecision.deny( "OPA denied authorization for principal=" - + polarisPrincipal.getName() + + request.principal().getName() + " operation=" - + request.getOperation()); - } - - @Override - @Nonnull - public AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - Preconditions.checkArgument( - !requests.isEmpty(), "Authorization request batch must contain at least one request"); - // Batch OPA evaluation remains sequential for backward compatibility until OPA adopts a - // batch-native payload schema. - return PolarisAuthorizer.super.authorize(authzState, polarisPrincipal, requests); + + intent.getOperation()); + } + } + return AuthorizationDecision.allow(); } /** diff --git a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java index f64bfebdef6..b6acc412957 100644 --- a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java +++ b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java @@ -50,6 +50,7 @@ import org.apache.hc.core5.http.io.entity.HttpEntities; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.polaris.core.auth.AuthorizationDecision; +import org.apache.polaris.core.auth.AuthorizationIntent; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; import org.apache.polaris.core.auth.PathSegment; @@ -581,7 +582,7 @@ void resolveAuthorizationInputsResolvesAll() { authzState.setResolutionManifest(resolutionManifest); PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); - authorizer.resolveAuthorizationInputs(authzState, principal, requestWithCatalogTarget()); + authorizer.resolveAuthorizationInputs(authzState, requestWithCatalogTarget(principal)); verify(resolutionManifest).resolveAll(); } @@ -589,8 +590,8 @@ void resolveAuthorizationInputsResolvesAll() { @Test void authorizeUsesIntentInputsAndAllows() throws Exception { final String[] capturedRequestBody = new String[1]; - AuthorizationRequest request = requestWithCatalogTarget(); PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); + AuthorizationRequest request = requestWithCatalogTarget(principal); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -615,7 +616,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); ObjectMapper mapper = JsonMapper.builder().build(); JsonNode root = mapper.readTree(capturedRequestBody[0]); @@ -638,8 +639,8 @@ T httpClientExecute( @Test void authorizeDeniesWhenOpaDenies() { - AuthorizationRequest request = requestWithCatalogTarget(); PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); + AuthorizationRequest request = requestWithCatalogTarget(principal); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":false}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -662,7 +663,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); assertThat(decision.isAllowed()).isFalse(); assertThat(decision.getMessage()) @@ -679,13 +680,13 @@ void authorizeIncludesStructuredParentsFromSecurable() throws Exception { final String[] capturedRequestBody = new String[1]; AuthorizationRequest request = AuthorizationRequest.of( + PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), PolarisAuthorizableOperation.LOAD_TABLE, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog1"), new PathSegment(PolarisEntityType.NAMESPACE, "ns1"), new PathSegment(PolarisEntityType.NAMESPACE, "ns2"), new PathSegment(PolarisEntityType.TABLE_LIKE, "table1"))); - PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -710,7 +711,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); ObjectMapper mapper = JsonMapper.builder().build(); JsonNode root = mapper.readTree(capturedRequestBody[0]); @@ -866,6 +867,7 @@ void authorizeRenameIncludesTargetAndSecondaryPaths() throws Exception { AuthorizationRequest request = AuthorizationRequest.of( + PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), PolarisAuthorizableOperation.RENAME_TABLE, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog1"), @@ -875,7 +877,6 @@ void authorizeRenameIncludesTargetAndSecondaryPaths() throws Exception { new PathSegment(PolarisEntityType.CATALOG, "catalog1"), new PathSegment(PolarisEntityType.NAMESPACE, "dst_ns"), new PathSegment(PolarisEntityType.TABLE_LIKE, "dst_tbl"))); - PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); OpaPolarisAuthorizer authorizer = new OpaPolarisAuthorizer( @@ -893,7 +894,7 @@ T httpClientExecute( } }; - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); ObjectMapper mapper = JsonMapper.builder().build(); JsonNode root = mapper.readTree(capturedRequestBody[0]); @@ -931,7 +932,7 @@ T httpClientExecute( } @Test - void authorizeSingleOperationBatchEvaluatesSequentially() throws Exception { + void authorizeSingleOperationMultiIntentRequestEvaluatesSequentially() throws Exception { final List capturedRequestBodies = new ArrayList<>(); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") @@ -963,14 +964,17 @@ T httpClientExecute( AuthorizationDecision decision = authorizer.authorize( authzState, - principal, - List.of( - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))), - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-2"))))); + AuthorizationRequest.of( + principal, + List.of( + AuthorizationIntent.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))), + AuthorizationIntent.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog-2")))))); ObjectMapper mapper = JsonMapper.builder().build(); assertThat(decision.isAllowed()).isTrue(); @@ -1010,7 +1014,7 @@ T httpClientExecute( } @Test - void authorizeMultiOperationBatchEvaluatesSequentially() throws Exception { + void authorizeUpdateTableMultiIntentRequestEvaluatesSequentially() throws Exception { final int[] requestCount = new int[1]; HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") @@ -1021,6 +1025,11 @@ void authorizeMultiOperationBatchEvaluatesSequentially() throws Exception { AuthorizationState authzState = new AuthorizationState(); authzState.setResolutionManifest(resolutionManifest); PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")); + PolarisSecurable tableTarget = + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog-1"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "table-1")); OpaPolarisAuthorizer authorizer = new OpaPolarisAuthorizer( @@ -1040,23 +1049,21 @@ T httpClientExecute( AuthorizationDecision decision = authorizer.authorize( authzState, - principal, - List.of( - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))), - AuthorizationRequest.of( - PolarisAuthorizableOperation.LIST_NAMESPACES, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog-1"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns"))))); + AuthorizationRequest.of( + principal, + List.of( + AuthorizationIntent.of( + PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES, tableTarget), + AuthorizationIntent.of( + PolarisAuthorizableOperation.SET_TABLE_SNAPSHOT_REF, tableTarget)))); assertThat(decision.isAllowed()).isTrue(); assertThat(requestCount[0]).isEqualTo(2); } - private AuthorizationRequest requestWithCatalogTarget() { + private AuthorizationRequest requestWithCatalogTarget(PolarisPrincipal principal) { return AuthorizationRequest.of( + principal, PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))); } diff --git a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java index b37d6a59b2f..271282d0e10 100644 --- a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java +++ b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisAuthorizer.java @@ -82,26 +82,13 @@ public void setRealmContext(RealmContext aRealmContext) { @Override public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { throw new UnsupportedOperationException("resolveAuthorizationInputs is not implemented yet"); } - @Override - public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - throw new UnsupportedOperationException( - "Batch resolveAuthorizationInputs is not implemented yet"); - } - @Override public @Nonnull AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { throw new UnsupportedOperationException("authorize is not implemented yet"); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java new file mode 100644 index 00000000000..0691077261b --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.apache.polaris.core.entity.PolarisEntityType; + +/** Authorization intent describing an operation and its target resource shape. */ +public sealed interface AuthorizationIntent + permits TargetlessAuthorizationIntent, + SingleTargetAuthorizationIntent, + PairwiseTargetAuthorizationIntent { + static AuthorizationIntent of(@Nonnull PolarisAuthorizableOperation operation) { + return new TargetlessAuthorizationIntent(operation); + } + + static AuthorizationIntent of( + @Nonnull PolarisAuthorizableOperation operation, @Nonnull PolarisSecurable target) { + return new SingleTargetAuthorizationIntent(operation, target); + } + + static AuthorizationIntent of( + @Nonnull PolarisAuthorizableOperation operation, + @Nullable PolarisSecurable target, + @Nullable PolarisSecurable secondary) { + return new PairwiseTargetAuthorizationIntent(operation, target, secondary); + } + + @Nonnull + PolarisAuthorizableOperation getOperation(); + + @Nullable + PolarisSecurable getTarget(); + + @Nullable + PolarisSecurable getSecondary(); + + default boolean hasSecurableType(PolarisEntityType... types) { + if (getTarget() != null && containsType(getTarget(), types)) { + return true; + } + if (getSecondary() != null && containsType(getSecondary(), types)) { + return true; + } + return false; + } + + static boolean containsType(PolarisSecurable securable, PolarisEntityType... types) { + PolarisEntityType entityType = securable.getLeaf().entityType(); + for (PolarisEntityType type : types) { + if (entityType == type) { + return true; + } + } + return false; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index db90085c1db..79a0d284abf 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -18,64 +18,53 @@ */ package org.apache.polaris.core.auth; +import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; +import java.util.List; import org.apache.polaris.core.entity.PolarisEntityType; -/** - * Authorization request inputs for pre-authorization and core authorization. - * - *

This hierarchy makes the target shape explicit on the request itself. - */ -public sealed interface AuthorizationRequest - permits UntargetedAuthorizationRequest, - SingleTargetAuthorizationRequest, - PairwiseTargetAuthorizationRequest { - static AuthorizationRequest of(@Nonnull PolarisAuthorizableOperation operation) { - return new UntargetedAuthorizationRequest(operation); +/** Full authorization request containing the subject and one or more authorization intents. */ +public record AuthorizationRequest( + @Nonnull PolarisPrincipal principal, @Nonnull List intents) { + public AuthorizationRequest { + Preconditions.checkNotNull(principal, "principal must be non-null"); + Preconditions.checkNotNull(intents, "intents must be non-null"); + intents = List.copyOf(intents); + Preconditions.checkArgument( + !intents.isEmpty(), "Authorization request must contain at least one intent"); } - static AuthorizationRequest of( - @Nonnull PolarisAuthorizableOperation operation, @Nonnull PolarisSecurable target) { - return new SingleTargetAuthorizationRequest(operation, target); + public static AuthorizationRequest of( + @Nonnull PolarisPrincipal principal, @Nonnull AuthorizationIntent intent) { + return new AuthorizationRequest(principal, List.of(intent)); } - static AuthorizationRequest of( - @Nonnull PolarisAuthorizableOperation operation, - @Nullable PolarisSecurable target, - @Nullable PolarisSecurable secondary) { - return new PairwiseTargetAuthorizationRequest(operation, target, secondary); + public static AuthorizationRequest of( + @Nonnull PolarisPrincipal principal, @Nonnull List intents) { + return new AuthorizationRequest(principal, intents); } - /** Returns the operation being authorized. */ - @Nonnull - PolarisAuthorizableOperation getOperation(); - - /** Returns the primary target securable, if any. */ - @Nullable - PolarisSecurable getTarget(); + public static AuthorizationRequest of( + @Nonnull PolarisPrincipal principal, @Nonnull PolarisAuthorizableOperation operation) { + return of(principal, AuthorizationIntent.of(operation)); + } - /** Returns the secondary securable, if any. */ - @Nullable - PolarisSecurable getSecondary(); + public static AuthorizationRequest of( + @Nonnull PolarisPrincipal principal, + @Nonnull PolarisAuthorizableOperation operation, + @Nonnull PolarisSecurable target) { + return of(principal, AuthorizationIntent.of(operation, target)); + } - default boolean hasSecurableType(PolarisEntityType... types) { - if (getTarget() != null && containsType(getTarget(), types)) { - return true; - } - if (getSecondary() != null && containsType(getSecondary(), types)) { - return true; - } - return false; + public static AuthorizationRequest of( + @Nonnull PolarisPrincipal principal, + @Nonnull PolarisAuthorizableOperation operation, + PolarisSecurable target, + PolarisSecurable secondary) { + return of(principal, AuthorizationIntent.of(operation, target, secondary)); } - static boolean containsType(PolarisSecurable securable, PolarisEntityType... types) { - PolarisEntityType entityType = securable.getLeaf().entityType(); - for (PolarisEntityType type : types) { - if (entityType == type) { - return true; - } - } - return false; + public boolean hasSecurableType(PolarisEntityType... types) { + return intents.stream().anyMatch(intent -> intent.hasSecurableType(types)); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java similarity index 84% rename from polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java rename to polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java index b9011422097..3d0cb90f3f9 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java @@ -23,22 +23,22 @@ import jakarta.annotation.Nullable; /** - * Authorization request for operations that may carry both a primary target and a related secondary + * Authorization intent for operations that may carry both a primary target and a related secondary * target. * *

The primary target may be omitted for legacy root-scoped flows that rely on an implicit root * primary plus an explicit secondary target. */ -public record PairwiseTargetAuthorizationRequest( +public record PairwiseTargetAuthorizationIntent( @Nonnull PolarisAuthorizableOperation operation, @Nullable PolarisSecurable target, @Nullable PolarisSecurable secondary) - implements AuthorizationRequest { - public PairwiseTargetAuthorizationRequest { + implements AuthorizationIntent { + public PairwiseTargetAuthorizationIntent { Preconditions.checkNotNull(operation, "operation must be non-null"); Preconditions.checkState( target != null || secondary != null, - "PairwiseTargetAuthorizationRequest must contain a target or secondary"); + "PairwiseTargetAuthorizationIntent must contain a target or secondary"); } @Override diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java index aad4cbaa5d0..9f3bbcc3df5 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java @@ -18,7 +18,6 @@ */ package org.apache.polaris.core.auth; -import com.google.common.base.Preconditions; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.List; @@ -39,57 +38,17 @@ public interface PolarisAuthorizer { *

This method should not perform authorization decisions directly. */ void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request); - - /** - * Resolve authorizer-specific inputs for a batch of authorization requests that share one - * principal. - * - *

Implementations must define their own batch pre-resolution behavior explicitly because - * manifest registration is authorizer-specific. - */ - void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests); + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request); /** * Core authorization entry point for the new SPI. * *

Implementations should rely on any required state in {@link AuthorizationState} and the - * intent captured by {@link AuthorizationRequest} (operation and target securables), together - * with the explicit {@link PolarisPrincipal} argument. + * request captured by {@link AuthorizationRequest}. */ @Nonnull AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request); - - /** - * Core authorization entry point for a batch of requests that share one principal. - * - *

The default behavior preserves semantics by evaluating requests independently in order and - * returning the first denial. Implementations may override this to use a single batched - * downstream authorization call. - */ - @Nonnull - default AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - Preconditions.checkArgument( - !requests.isEmpty(), "Authorization request batch must contain at least one request"); - for (AuthorizationRequest request : requests) { - AuthorizationDecision decision = authorize(authzState, polarisPrincipal, request); - if (!decision.isAllowed()) { - return decision; - } - } - return AuthorizationDecision.allow(); - } + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request); /** * Convenience method that throws a {@link ForbiddenException} when authorization is denied. @@ -97,27 +56,8 @@ default AuthorizationDecision authorize( *

Implementations should provide allow/deny decisions via {@link #authorize}. */ default void authorizeOrThrow( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { - AuthorizationDecision decision = authorize(authzState, polarisPrincipal, request); - if (!decision.isAllowed()) { - String message = decision.getMessage().orElse("Authorization denied"); - throw new ForbiddenException("%s", message); - } - } - - /** - * Convenience method that throws when any request in the batch is denied. - * - *

The default behavior delegates to {@link #authorize(AuthorizationState, PolarisPrincipal, - * List)}. - */ - default void authorizeOrThrow( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - AuthorizationDecision decision = authorize(authzState, polarisPrincipal, requests); + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + AuthorizationDecision decision = authorize(authzState, request); if (!decision.isAllowed()) { String message = decision.getMessage().orElse("Authorization denied"); throw new ForbiddenException("%s", message); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index 69dd3438a06..6f995345da5 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -749,35 +749,36 @@ public PolarisAuthorizerImpl(RealmConfig realmConfig) { @Override public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { PolarisResolutionManifest resolutionManifest = authzState.getResolutionManifest(); resolutionManifest.resolveAll(); } - @Override - public void resolveAuthorizationInputs( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - Preconditions.checkArgument( - !requests.isEmpty(), "Authorization request batch must contain at least one request"); - authzState.getResolutionManifest().resolveAll(); - } - @Override @Nonnull public AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull AuthorizationRequest request) { + @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { PolarisResolutionManifest resolutionManifest = authzState.getResolutionManifest(); - RbacOperationSemantics semantics = RbacOperationSemantics.forOperation(request.getOperation()); + for (AuthorizationIntent intent : request.intents()) { + AuthorizationDecision decision = + authorizeIntent(authzState, request.principal(), resolutionManifest, intent); + if (!decision.isAllowed()) { + return decision; + } + } + return AuthorizationDecision.allow(); + } + + private AuthorizationDecision authorizeIntent( + AuthorizationState authzState, + PolarisPrincipal polarisPrincipal, + PolarisResolutionManifest resolutionManifest, + AuthorizationIntent intent) { + RbacOperationSemantics semantics = RbacOperationSemantics.forOperation(intent.getOperation()); boolean prependRootContainer = semantics.rooting() == ResolvedPathRooting.ROOT; try { List resolvedTargets; - PolarisSecurable target = request.getTarget(); + PolarisSecurable target = intent.getTarget(); if (target == null) { resolvedTargets = prependRootContainer @@ -787,7 +788,7 @@ public AuthorizationDecision authorize( resolvedTargets = List.of(getResolvedSecurable(resolutionManifest, target, prependRootContainer)); } - PolarisSecurable secondary = request.getSecondary(); + PolarisSecurable secondary = intent.getSecondary(); List resolvedSecondaries = semantics.secondaryPrivileges().isEmpty() || secondary == null ? null @@ -795,7 +796,7 @@ public AuthorizationDecision authorize( authorizeOrThrow( polarisPrincipal, resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - request.getOperation(), + intent.getOperation(), resolvedTargets, resolvedSecondaries); return AuthorizationDecision.allow(); @@ -803,27 +804,14 @@ public AuthorizationDecision authorize( LOGGER.debug( "Authorization denied for principalName {} operation {} targets {} secondaries {}", polarisPrincipal.getName(), - request.getOperation(), - request.getTarget(), - request.getSecondary(), + intent.getOperation(), + intent.getTarget(), + intent.getSecondary(), e); return AuthorizationDecision.deny(e.getMessage()); } } - @Override - @Nonnull - public AuthorizationDecision authorize( - @Nonnull AuthorizationState authzState, - @Nonnull PolarisPrincipal polarisPrincipal, - @Nonnull List requests) { - Preconditions.checkArgument( - !requests.isEmpty(), "Authorization request batch must contain at least one request"); - // RBAC has no external batch payload contract to preserve, so batch authorization remains a - // sequential evaluation of single-request checks. - return PolarisAuthorizer.super.authorize(authzState, polarisPrincipal, requests); - } - private PolarisResolvedPathWrapper getResolvedSecurable( PolarisResolutionManifest resolutionManifest, PolarisSecurable securable, diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java similarity index 88% rename from polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java rename to polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java index 60ee18ee884..5ef2ea516b2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java @@ -22,11 +22,11 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -/** Authorization request for operations with one explicit target. */ -public record SingleTargetAuthorizationRequest( +/** Authorization intent for operations with one explicit target. */ +public record SingleTargetAuthorizationIntent( @Nonnull PolarisAuthorizableOperation operation, @Nonnull PolarisSecurable target) - implements AuthorizationRequest { - public SingleTargetAuthorizationRequest { + implements AuthorizationIntent { + public SingleTargetAuthorizationIntent { Preconditions.checkNotNull(operation, "operation must be non-null"); Preconditions.checkNotNull(target, "target must be non-null"); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java similarity index 83% rename from polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java rename to polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java index 1747186dc92..9b9baf47781 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/UntargetedAuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java @@ -22,10 +22,10 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -/** Authorization request for operations with no explicit securable target. */ -public record UntargetedAuthorizationRequest(@Nonnull PolarisAuthorizableOperation operation) - implements AuthorizationRequest { - public UntargetedAuthorizationRequest { +/** Authorization intent for operations with no explicit securable target. */ +public record TargetlessAuthorizationIntent(@Nonnull PolarisAuthorizableOperation operation) + implements AuthorizationIntent { + public TargetlessAuthorizationIntent { Preconditions.checkNotNull(operation, "operation must be non-null"); } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index 43e3bd6e696..e4e47d164b2 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -21,6 +21,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.apache.polaris.core.entity.PolarisEntityType; import org.junit.jupiter.api.Test; @@ -30,10 +33,12 @@ public class AuthorizationRequestTest { void hasSecurableTypeReturnsTrueForPrincipalTarget() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisAuthorizableOperation.LOAD_TABLE, - PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice"))); + PolarisPrincipal.of("alice", Map.of(), Set.of("role")), + AuthorizationIntent.of( + PolarisAuthorizableOperation.LOAD_TABLE, + PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")))); - assertThat(request).isInstanceOf(SingleTargetAuthorizationRequest.class); + assertThat(request.intents().get(0)).isInstanceOf(SingleTargetAuthorizationIntent.class); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL)).isTrue(); } @@ -41,12 +46,14 @@ void hasSecurableTypeReturnsTrueForPrincipalTarget() { void hasSecurableTypeReturnsTrueForPrincipalRoleSecondary() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisAuthorizableOperation.ASSIGN_PRINCIPAL_ROLE, - PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))); - - assertThat(request).isInstanceOf(PairwiseTargetAuthorizationRequest.class); + PolarisPrincipal.of("alice", Map.of(), Set.of("role")), + AuthorizationIntent.of( + PolarisAuthorizableOperation.ASSIGN_PRINCIPAL_ROLE, + PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")), + PolarisSecurable.of( + new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin")))); + + assertThat(request.intents().get(0)).isInstanceOf(PairwiseTargetAuthorizationIntent.class); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isTrue(); } @@ -54,11 +61,13 @@ void hasSecurableTypeReturnsTrueForPrincipalRoleSecondary() { void hasSecurableTypeReturnsTrueForCatalogRoleSecondary() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisAuthorizableOperation.ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.CATALOG_ROLE, "catalog-role"))); + PolarisPrincipal.of("alice", Map.of(), Set.of("role")), + AuthorizationIntent.of( + PolarisAuthorizableOperation.ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog"), + new PathSegment(PolarisEntityType.CATALOG_ROLE, "catalog-role")))); assertThat(request.hasSecurableType(PolarisEntityType.CATALOG_ROLE)).isTrue(); } @@ -67,41 +76,55 @@ void hasSecurableTypeReturnsTrueForCatalogRoleSecondary() { void hasSecurableTypeReturnsFalseWhenTypeAbsent() { AuthorizationRequest request = AuthorizationRequest.of( - PolarisAuthorizableOperation.LOAD_VIEW, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); + PolarisPrincipal.of("alice", Map.of(), Set.of("role")), + AuthorizationIntent.of( + PolarisAuthorizableOperation.LOAD_VIEW, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")))); assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isFalse(); } @Test - void allowsUntargetedRequest() { + void allowsTargetlessIntent() { AuthorizationRequest request = - AuthorizationRequest.of(PolarisAuthorizableOperation.LIST_CATALOGS); + AuthorizationRequest.of( + PolarisPrincipal.of("alice", Map.of(), Set.of("role")), + AuthorizationIntent.of(PolarisAuthorizableOperation.LIST_CATALOGS)); - assertThat(request).isInstanceOf(UntargetedAuthorizationRequest.class); - assertThat(request.getTarget()).isNull(); - assertThat(request.getSecondary()).isNull(); + assertThat(request.intents().get(0)).isInstanceOf(TargetlessAuthorizationIntent.class); + assertThat(request.intents().get(0).getTarget()).isNull(); + assertThat(request.intents().get(0).getSecondary()).isNull(); } @Test - void throwsWhenTargetedFactoryHasNoTargetOrSecondary() { + void throwsWhenIntentFactoryHasNoTargetOrSecondary() { assertThatThrownBy( - () -> AuthorizationRequest.of(PolarisAuthorizableOperation.GET_CATALOG, null, null)) + () -> AuthorizationIntent.of(PolarisAuthorizableOperation.GET_CATALOG, null, null)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining( - "PairwiseTargetAuthorizationRequest must contain a target or secondary"); + "PairwiseTargetAuthorizationIntent must contain a target or secondary"); } @Test - void threeArgFactoryAlwaysCreatesPairwiseRequest() { + void threeArgIntentFactoryAlwaysCreatesPairwiseIntent() { PolarisSecurable target = PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")); - AuthorizationRequest request = - AuthorizationRequest.of(PolarisAuthorizableOperation.GET_CATALOG, target, null); + AuthorizationIntent intent = + AuthorizationIntent.of(PolarisAuthorizableOperation.GET_CATALOG, target, null); - assertThat(request).isInstanceOf(PairwiseTargetAuthorizationRequest.class); - assertThat(request.getTarget()).isEqualTo(target); - assertThat(request.getSecondary()).isNull(); + assertThat(intent).isInstanceOf(PairwiseTargetAuthorizationIntent.class); + assertThat(intent.getTarget()).isEqualTo(target); + assertThat(intent.getSecondary()).isNull(); + } + + @Test + void requestRequiresAtLeastOneIntent() { + assertThatThrownBy( + () -> + AuthorizationRequest.of( + PolarisPrincipal.of("alice", Map.of(), Set.of("role")), List.of())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must contain at least one intent"); } @Test diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java index 0cb752b4c4c..401bab0775b 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java @@ -67,12 +67,13 @@ void resolveAuthorizationInputsResolvesAll() { PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); AuthorizationRequest request = AuthorizationRequest.of( + principal, PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); authzState.setResolutionManifest(manifest); - authorizer.resolveAuthorizationInputs(authzState, principal, request); + authorizer.resolveAuthorizationInputs(authzState, request); verify(manifest).resolveAll(); } @@ -104,12 +105,13 @@ void authorizeUsesRootTargetForRootGrantRequestWithoutPrimaryTarget() { AuthorizationRequest request = AuthorizationRequest.of( + principal, PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE, null, PolarisSecurable.of( new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))); - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); assertThat(decision.isAllowed()).isTrue(); verify(authorizer) @@ -144,9 +146,9 @@ void authorizeUsesRootTargetForListCatalogsRequestWithoutPrimaryTarget() { ArgumentMatchers.>any()); AuthorizationRequest request = - AuthorizationRequest.of(PolarisAuthorizableOperation.LIST_CATALOGS); + AuthorizationRequest.of(principal, PolarisAuthorizableOperation.LIST_CATALOGS); - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); assertThat(decision.isAllowed()).isTrue(); verify(authorizer) @@ -184,12 +186,13 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { AuthorizationRequest request = AuthorizationRequest.of( + principal, PolarisAuthorizableOperation.LIST_NAMESPACES, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog"), new PathSegment(PolarisEntityType.NAMESPACE, "ns"))); - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); assertThat(decision.isAllowed()).isTrue(); verify(manifest) @@ -204,7 +207,7 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { } @Test - void authorizeSingleOperationBatchEvaluatesSequentially() { + void authorizeSingleOperationMultiIntentRequestEvaluatesSequentially() { PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); @@ -230,14 +233,17 @@ void authorizeSingleOperationBatchEvaluatesSequentially() { AuthorizationDecision decision = authorizer.authorize( authzState, - principal, - List.of( - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog1"))), - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog2"))))); + AuthorizationRequest.of( + principal, + List.of( + AuthorizationIntent.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"))), + AuthorizationIntent.of( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog2")))))); assertThat(decision.isAllowed()).isTrue(); verify(authorizer, times(1)) @@ -257,20 +263,17 @@ void authorizeSingleOperationBatchEvaluatesSequentially() { } @Test - void authorizeMultiOperationBatchEvaluatesSequentially() { + void authorizeUpdateTableMultiIntentRequestEvaluatesSequentially() { PolarisAuthorizerImpl authorizer = spy(new PolarisAuthorizerImpl(mock(RealmConfig.class))); AuthorizationState authzState = new AuthorizationState(); PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); - PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); - PolarisResolvedPathWrapper namespaceWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisResolvedPathWrapper tableWrapper = mock(PolarisResolvedPathWrapper.class); PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); authzState.setResolutionManifest(manifest); - when(manifest.getResolvedTopLevelEntity("catalog", PolarisEntityType.CATALOG)) - .thenReturn(catalogWrapper); when(manifest.getResolvedPath( - ResolvedPathKey.of(List.of("ns"), PolarisEntityType.NAMESPACE), true)) - .thenReturn(namespaceWrapper); + ResolvedPathKey.of(List.of("ns", "table"), PolarisEntityType.TABLE_LIKE), true)) + .thenReturn(tableWrapper); when(manifest.getAllActivatedCatalogRoleAndPrincipalRoles()).thenReturn(Set.of()); doNothing() .when(authorizer) @@ -281,48 +284,48 @@ void authorizeMultiOperationBatchEvaluatesSequentially() { ArgumentMatchers.any(), ArgumentMatchers.>any()); + PolarisSecurable tableTarget = + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "table")); + AuthorizationDecision decision = authorizer.authorize( authzState, - principal, - List.of( - AuthorizationRequest.of( - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))), - AuthorizationRequest.of( - PolarisAuthorizableOperation.LIST_NAMESPACES, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns"))))); + AuthorizationRequest.of( + principal, + List.of( + AuthorizationIntent.of( + PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES, tableTarget), + AuthorizationIntent.of( + PolarisAuthorizableOperation.SET_TABLE_SNAPSHOT_REF, tableTarget)))); assertThat(decision.isAllowed()).isTrue(); verify(authorizer, times(1)) .authorizeOrThrow( eq(principal), eq(Set.of()), - eq(PolarisAuthorizableOperation.GET_CATALOG), - eq(List.of(catalogWrapper)), + eq(PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES), + eq(List.of(tableWrapper)), eq(null)); verify(authorizer, times(1)) .authorizeOrThrow( eq(principal), eq(Set.of()), - eq(PolarisAuthorizableOperation.LIST_NAMESPACES), - eq(List.of(namespaceWrapper)), + eq(PolarisAuthorizableOperation.SET_TABLE_SNAPSHOT_REF), + eq(List.of(tableWrapper)), eq(null)); } @Test - void authorizeBatchThrowsWhenEmpty() { - PolarisAuthorizerImpl authorizer = new PolarisAuthorizerImpl(mock(RealmConfig.class)); - AuthorizationState authzState = new AuthorizationState(); - authzState.setResolutionManifest(mock(PolarisResolutionManifest.class)); + void authorizationRequestThrowsWhenIntentsAreEmpty() { PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); org.assertj.core.api.Assertions.assertThatThrownBy( - () -> authorizer.authorize(authzState, principal, List.of())) + () -> AuthorizationRequest.of(principal, List.of())) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("must contain at least one request"); + .hasMessageContaining("must contain at least one intent"); } @Test @@ -348,10 +351,11 @@ void authorizeReturnsDenyDecision() { AuthorizationRequest request = AuthorizationRequest.of( + principal, PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); - AuthorizationDecision decision = authorizer.authorize(authzState, principal, request); + AuthorizationDecision decision = authorizer.authorize(authzState, request); assertThat(decision.isAllowed()).isFalse(); assertThat(decision.getMessage()).hasValue("missing privilege"); From bb91c0527548ca14c62d48e8b5e9779490c060ad Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Tue, 19 May 2026 22:00:08 -0400 Subject: [PATCH 07/12] update doc - thanks yufei --- .../java/org/apache/polaris/core/auth/PolarisAuthorizer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java index 9f3bbcc3df5..07de59a8803 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizer.java @@ -45,6 +45,10 @@ void resolveAuthorizationInputs( * *

Implementations should rely on any required state in {@link AuthorizationState} and the * request captured by {@link AuthorizationRequest}. + * + *

When a request contains multiple intents, they form a single batch contract: implementations + * must AND-combine the intents in that request and may short-circuit evaluation on the first + * deny. */ @Nonnull AuthorizationDecision authorize( From 65b582c4d985f2a5edb28b9202bab8e303637ace Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Thu, 21 May 2026 10:04:12 -0400 Subject: [PATCH 08/12] fix ci --- .../core/auth/AuthorizationIntent.java | 9 +++------ .../core/auth/AuthorizationRequest.java | 20 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java index 0ec74d527d9..5ddf4fd43c5 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java @@ -43,14 +43,11 @@ static AuthorizationIntent of( return new PairwiseTargetAuthorizationIntent(operation, target, secondary); } - @NonNull - PolarisAuthorizableOperation getOperation(); + @NonNull PolarisAuthorizableOperation getOperation(); - @Nullable - PolarisSecurable getTarget(); + @Nullable PolarisSecurable getTarget(); - @Nullable - PolarisSecurable getSecondary(); + @Nullable PolarisSecurable getSecondary(); default boolean hasSecurableType(PolarisEntityType... types) { if (getTarget() != null && containsType(getTarget(), types)) { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 79a0d284abf..42e7ba57580 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -19,13 +19,13 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; -import jakarta.annotation.Nonnull; import java.util.List; import org.apache.polaris.core.entity.PolarisEntityType; +import org.jspecify.annotations.NonNull; /** Full authorization request containing the subject and one or more authorization intents. */ public record AuthorizationRequest( - @Nonnull PolarisPrincipal principal, @Nonnull List intents) { + @NonNull PolarisPrincipal principal, @NonNull List intents) { public AuthorizationRequest { Preconditions.checkNotNull(principal, "principal must be non-null"); Preconditions.checkNotNull(intents, "intents must be non-null"); @@ -35,30 +35,30 @@ public record AuthorizationRequest( } public static AuthorizationRequest of( - @Nonnull PolarisPrincipal principal, @Nonnull AuthorizationIntent intent) { + @NonNull PolarisPrincipal principal, @NonNull AuthorizationIntent intent) { return new AuthorizationRequest(principal, List.of(intent)); } public static AuthorizationRequest of( - @Nonnull PolarisPrincipal principal, @Nonnull List intents) { + @NonNull PolarisPrincipal principal, @NonNull List intents) { return new AuthorizationRequest(principal, intents); } public static AuthorizationRequest of( - @Nonnull PolarisPrincipal principal, @Nonnull PolarisAuthorizableOperation operation) { + @NonNull PolarisPrincipal principal, @NonNull PolarisAuthorizableOperation operation) { return of(principal, AuthorizationIntent.of(operation)); } public static AuthorizationRequest of( - @Nonnull PolarisPrincipal principal, - @Nonnull PolarisAuthorizableOperation operation, - @Nonnull PolarisSecurable target) { + @NonNull PolarisPrincipal principal, + @NonNull PolarisAuthorizableOperation operation, + @NonNull PolarisSecurable target) { return of(principal, AuthorizationIntent.of(operation, target)); } public static AuthorizationRequest of( - @Nonnull PolarisPrincipal principal, - @Nonnull PolarisAuthorizableOperation operation, + @NonNull PolarisPrincipal principal, + @NonNull PolarisAuthorizableOperation operation, PolarisSecurable target, PolarisSecurable secondary) { return of(principal, AuthorizationIntent.of(operation, target, secondary)); From 657c4d37f98f2887f10997e4b18a5b59de09a5d1 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Sun, 24 May 2026 17:54:27 -0400 Subject: [PATCH 09/12] adopt feedback on AuthorizationRequest shape --- .../auth/opa/OpaPolarisAuthorizer.java | 34 +++++++++++----- .../core/auth/AuthorizationIntent.java | 13 +++--- .../PairwiseTargetAuthorizationIntent.java | 10 ----- .../core/auth/PolarisAuthorizerImpl.java | 40 +++++++++++++------ .../auth/SingleTargetAuthorizationIntent.java | 11 ----- .../auth/TargetlessAuthorizationIntent.java | 11 ----- .../core/auth/AuthorizationRequestTest.java | 7 ++-- 7 files changed, 60 insertions(+), 66 deletions(-) diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index 0d03ce8068d..3a3e5336fe9 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -45,11 +45,14 @@ import org.apache.polaris.core.auth.AuthorizationIntent; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; +import org.apache.polaris.core.auth.PairwiseTargetAuthorizationIntent; import org.apache.polaris.core.auth.PathSegment; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.auth.PolarisSecurable; +import org.apache.polaris.core.auth.SingleTargetAuthorizationIntent; +import org.apache.polaris.core.auth.TargetlessAuthorizationIntent; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.ResolvedPolarisEntity; @@ -119,19 +122,32 @@ public void resolveAuthorizationInputs( public AuthorizationDecision authorize( @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { for (AuthorizationIntent intent : request.intents()) { + PolarisAuthorizableOperation operation = intent.getOperation(); + List targets; + List secondaries; + switch (intent) { + case TargetlessAuthorizationIntent ignored -> { + targets = List.of(); + secondaries = List.of(); + } + case SingleTargetAuthorizationIntent singleTargetIntent -> { + targets = toResourceEntitiesFromSecurable(singleTargetIntent.target()); + secondaries = List.of(); + } + case PairwiseTargetAuthorizationIntent pairwiseTargetIntent -> { + targets = toResourceEntitiesFromSecurable(pairwiseTargetIntent.target()); + secondaries = toResourceEntitiesFromSecurable(pairwiseTargetIntent.secondary()); + } + } boolean allowed = queryOpa( - buildOpaAuthorizationInput( - request.principal(), - intent.getOperation(), - toResourceEntitiesFromSecurable(intent.getTarget()), - toResourceEntitiesFromSecurable(intent.getSecondary()))); + buildOpaAuthorizationInput(request.principal(), operation, targets, secondaries)); if (!allowed) { return AuthorizationDecision.deny( "OPA denied authorization for principal=" + request.principal().getName() + " operation=" - + intent.getOperation()); + + operation); } } return AuthorizationDecision.allow(); @@ -303,9 +319,9 @@ private ImmutableContext buildContext() { private ImmutableResource buildResource( List targets, List secondaries) { - // Backward compatibility: keep the existing OPA input shape with separate target and - // secondary lists. Future work can align this with richer request shapes if OPA starts - // consuming pairwise authorization intent directly. + // Keep the existing OPA input shape by always emitting target and secondary lists, using + // empty lists when an intent does not carry that slot. Future work can revisit the payload + // shape if OPA starts consuming intent subtype distinctions directly. return ImmutableResource.builder().targets(targets).secondaries(secondaries).build(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java index 5ddf4fd43c5..dd0f7230f6c 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java @@ -45,16 +45,13 @@ static AuthorizationIntent of( @NonNull PolarisAuthorizableOperation getOperation(); - @Nullable PolarisSecurable getTarget(); - - @Nullable PolarisSecurable getSecondary(); - default boolean hasSecurableType(PolarisEntityType... types) { - if (getTarget() != null && containsType(getTarget(), types)) { - return true; + if (this instanceof SingleTargetAuthorizationIntent intent) { + return containsType(intent.target(), types); } - if (getSecondary() != null && containsType(getSecondary(), types)) { - return true; + if (this instanceof PairwiseTargetAuthorizationIntent intent) { + return (intent.target() != null && containsType(intent.target(), types)) + || (intent.secondary() != null && containsType(intent.secondary(), types)); } return false; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java index bc15a7925ec..95d576ce468 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java @@ -45,14 +45,4 @@ public record PairwiseTargetAuthorizationIntent( public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } - - @Override - public @Nullable PolarisSecurable getTarget() { - return target; - } - - @Override - public @Nullable PolarisSecurable getSecondary() { - return secondary; - } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index daccbd3a02b..cc691626f9d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -778,21 +778,37 @@ private AuthorizationDecision authorizeIntent( boolean prependRootContainer = semantics.rooting() == ResolvedPathRooting.ROOT; try { List resolvedTargets; - PolarisSecurable target = intent.getTarget(); - if (target == null) { + List resolvedSecondaries; + if (intent instanceof TargetlessAuthorizationIntent) { resolvedTargets = prependRootContainer ? List.of(resolutionManifest.getResolvedRootContainerEntityAsPath()) : null; - } else { + resolvedSecondaries = null; + } else if (intent instanceof SingleTargetAuthorizationIntent singleTargetIntent) { + resolvedTargets = + List.of( + getResolvedSecurable( + resolutionManifest, singleTargetIntent.target(), prependRootContainer)); + resolvedSecondaries = null; + } else if (intent instanceof PairwiseTargetAuthorizationIntent pairwiseIntent) { resolvedTargets = - List.of(getResolvedSecurable(resolutionManifest, target, prependRootContainer)); + pairwiseIntent.target() == null + ? (prependRootContainer + ? List.of(resolutionManifest.getResolvedRootContainerEntityAsPath()) + : null) + : List.of( + getResolvedSecurable( + resolutionManifest, pairwiseIntent.target(), prependRootContainer)); + resolvedSecondaries = + semantics.secondaryPrivileges().isEmpty() || pairwiseIntent.secondary() == null + ? null + : List.of( + getResolvedSecurable( + resolutionManifest, pairwiseIntent.secondary(), prependRootContainer)); + } else { + throw new IllegalStateException("Unsupported authorization intent: " + intent.getClass()); } - PolarisSecurable secondary = intent.getSecondary(); - List resolvedSecondaries = - semantics.secondaryPrivileges().isEmpty() || secondary == null - ? null - : List.of(getResolvedSecurable(resolutionManifest, secondary, prependRootContainer)); authorizeOrThrow( polarisPrincipal, resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), @@ -802,11 +818,9 @@ private AuthorizationDecision authorizeIntent( return AuthorizationDecision.allow(); } catch (ForbiddenException e) { LOGGER.debug( - "Authorization denied for principalName {} operation {} targets {} secondaries {}", + "Authorization denied for principalName {} intent {}", polarisPrincipal.getName(), - intent.getOperation(), - intent.getTarget(), - intent.getSecondary(), + intent, e); return AuthorizationDecision.deny(e.getMessage()); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java index dfa103674c5..9f7394ac375 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java @@ -20,7 +20,6 @@ import com.google.common.base.Preconditions; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; /** Authorization intent for operations with one explicit target. */ public record SingleTargetAuthorizationIntent( @@ -35,14 +34,4 @@ public record SingleTargetAuthorizationIntent( public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } - - @Override - public @NonNull PolarisSecurable getTarget() { - return target; - } - - @Override - public @Nullable PolarisSecurable getSecondary() { - return null; - } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java index 2e86c7b919c..3a59d46401c 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java @@ -20,7 +20,6 @@ import com.google.common.base.Preconditions; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; /** Authorization intent for operations with no explicit securable target. */ public record TargetlessAuthorizationIntent(@NonNull PolarisAuthorizableOperation operation) @@ -33,14 +32,4 @@ public record TargetlessAuthorizationIntent(@NonNull PolarisAuthorizableOperatio public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } - - @Override - public @Nullable PolarisSecurable getTarget() { - return null; - } - - @Override - public @Nullable PolarisSecurable getSecondary() { - return null; - } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index e4e47d164b2..dad16d7c23d 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -92,8 +92,6 @@ void allowsTargetlessIntent() { AuthorizationIntent.of(PolarisAuthorizableOperation.LIST_CATALOGS)); assertThat(request.intents().get(0)).isInstanceOf(TargetlessAuthorizationIntent.class); - assertThat(request.intents().get(0).getTarget()).isNull(); - assertThat(request.intents().get(0).getSecondary()).isNull(); } @Test @@ -113,8 +111,9 @@ void threeArgIntentFactoryAlwaysCreatesPairwiseIntent() { AuthorizationIntent.of(PolarisAuthorizableOperation.GET_CATALOG, target, null); assertThat(intent).isInstanceOf(PairwiseTargetAuthorizationIntent.class); - assertThat(intent.getTarget()).isEqualTo(target); - assertThat(intent.getSecondary()).isNull(); + PairwiseTargetAuthorizationIntent pairwiseIntent = (PairwiseTargetAuthorizationIntent) intent; + assertThat(pairwiseIntent.target()).isEqualTo(target); + assertThat(pairwiseIntent.secondary()).isNull(); } @Test From cbc5e0683530d3db1db1b26e055a5d654d18cd0e Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Sun, 24 May 2026 17:54:35 -0400 Subject: [PATCH 10/12] adopt feedback on AuthorizationRequest shape --- .../apache/polaris/core/auth/AuthorizationIntent.java | 11 +---------- .../core/auth/PairwiseTargetAuthorizationIntent.java | 7 +++++++ .../core/auth/SingleTargetAuthorizationIntent.java | 6 ++++++ .../core/auth/TargetlessAuthorizationIntent.java | 6 ++++++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java index dd0f7230f6c..84d3caf8183 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java @@ -45,16 +45,7 @@ static AuthorizationIntent of( @NonNull PolarisAuthorizableOperation getOperation(); - default boolean hasSecurableType(PolarisEntityType... types) { - if (this instanceof SingleTargetAuthorizationIntent intent) { - return containsType(intent.target(), types); - } - if (this instanceof PairwiseTargetAuthorizationIntent intent) { - return (intent.target() != null && containsType(intent.target(), types)) - || (intent.secondary() != null && containsType(intent.secondary(), types)); - } - return false; - } + boolean hasSecurableType(PolarisEntityType... types); static boolean containsType(PolarisSecurable securable, PolarisEntityType... types) { PolarisEntityType entityType = securable.getLeaf().entityType(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java index 95d576ce468..1c1c3ed926c 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; +import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -45,4 +46,10 @@ public record PairwiseTargetAuthorizationIntent( public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } + + @Override + public boolean hasSecurableType(PolarisEntityType... types) { + return (target() != null && AuthorizationIntent.containsType(target(), types)) + || (secondary() != null && AuthorizationIntent.containsType(secondary(), types)); + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java index 9f7394ac375..5921c1cb3e0 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; +import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; /** Authorization intent for operations with one explicit target. */ @@ -34,4 +35,9 @@ public record SingleTargetAuthorizationIntent( public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } + + @Override + public boolean hasSecurableType(PolarisEntityType... types) { + return AuthorizationIntent.containsType(target(), types); + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java index 3a59d46401c..c189a8c5fdf 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; +import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; /** Authorization intent for operations with no explicit securable target. */ @@ -32,4 +33,9 @@ public record TargetlessAuthorizationIntent(@NonNull PolarisAuthorizableOperatio public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } + + @Override + public boolean hasSecurableType(PolarisEntityType... types) { + return false; + } } From 0f91ce6646b19f96fa63dfb88b0a9f91db711fb1 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Wed, 27 May 2026 11:53:29 -0400 Subject: [PATCH 11/12] adopt nits --- .../auth/opa/OpaPolarisAuthorizer.java | 33 +------------------ .../core/auth/AuthorizationIntent.java | 12 +------ .../core/auth/AuthorizationRequest.java | 4 +-- .../PairwiseTargetAuthorizationIntent.java | 6 ++-- .../polaris/core/auth/PolarisSecurable.java | 11 +++++++ .../auth/SingleTargetAuthorizationIntent.java | 4 +-- .../auth/TargetlessAuthorizationIntent.java | 2 +- 7 files changed, 21 insertions(+), 51 deletions(-) diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index cc0380d66c0..6060e3f4dee 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -120,8 +120,7 @@ public void resolveAuthorizationInputs( @Override @NonNull public AuthorizationDecision authorize( -<<<<<<< auth-op-model-spike - @Nonnull AuthorizationState authzState, @Nonnull AuthorizationRequest request) { + @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { for (AuthorizationIntent intent : request.intents()) { PolarisAuthorizableOperation operation = intent.getOperation(); List targets; @@ -152,20 +151,6 @@ public AuthorizationDecision authorize( } } return AuthorizationDecision.allow(); -======= - @NonNull AuthorizationState authzState, @NonNull AuthorizationRequest request) { - boolean allowed = - queryOpa( - buildOpaAuthorizationInput( - request.getPrincipal(), - request.getOperation(), - toResourceEntitiesFromSecurables(request.getTargets()), - toResourceEntitiesFromSecurables(request.getSecondaries()))); - return allowed - ? AuthorizationDecision.allow() - : AuthorizationDecision.deny( - "OPA denied authorization for " + request.formatForAuthorizationMessage()); ->>>>>>> main } /** @@ -399,24 +384,8 @@ private List toResourceEntitiesFromResolvedPaths( return entities; } -<<<<<<< auth-op-model-spike - @Nonnull private List toResourceEntitiesFromSecurable( @Nullable PolarisSecurable securable) { return securable == null ? List.of() : List.of(buildResourceEntity(securable)); -======= - @NonNull - private List toResourceEntitiesFromSecurables( - @Nullable List securables) { - if (securables == null || securables.isEmpty()) { - return List.of(); - } - - List entities = new ArrayList<>(); - for (PolarisSecurable securable : securables) { - entities.add(buildResourceEntity(securable)); - } - return entities; ->>>>>>> main } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java index 84d3caf8183..9d0822a3ff7 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java @@ -45,15 +45,5 @@ static AuthorizationIntent of( @NonNull PolarisAuthorizableOperation getOperation(); - boolean hasSecurableType(PolarisEntityType... types); - - static boolean containsType(PolarisSecurable securable, PolarisEntityType... types) { - PolarisEntityType entityType = securable.getLeaf().entityType(); - for (PolarisEntityType type : types) { - if (entityType == type) { - return true; - } - } - return false; - } + boolean hasSecurableType(PolarisEntityType type); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 42e7ba57580..7ba14fd1afc 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -64,7 +64,7 @@ public static AuthorizationRequest of( return of(principal, AuthorizationIntent.of(operation, target, secondary)); } - public boolean hasSecurableType(PolarisEntityType... types) { - return intents.stream().anyMatch(intent -> intent.hasSecurableType(types)); + public boolean hasSecurableType(PolarisEntityType type) { + return intents.stream().anyMatch(intent -> intent.hasSecurableType(type)); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java index 1c1c3ed926c..6bc396961ae 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java @@ -48,8 +48,8 @@ public record PairwiseTargetAuthorizationIntent( } @Override - public boolean hasSecurableType(PolarisEntityType... types) { - return (target() != null && AuthorizationIntent.containsType(target(), types)) - || (secondary() != null && AuthorizationIntent.containsType(secondary(), types)); + public boolean hasSecurableType(PolarisEntityType type) { + return (target() != null && target().leafHasType(type)) + || (secondary() != null && secondary().leafHasType(type)); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java index 9d2429aab7d..dbea066a610 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java @@ -83,6 +83,17 @@ default String formatForAuthorizationMessage() { .collect(Collectors.joining(".")); } + /** Returns whether the leaf segment has any of the provided types. */ + default boolean leafHasType(PolarisEntityType... types) { + PolarisEntityType entityType = getLeaf().entityType(); + for (PolarisEntityType type : types) { + if (entityType == type) { + return true; + } + } + return false; + } + @Value.Check default void validate() { Preconditions.checkState( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java index 5921c1cb3e0..3e211102585 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java @@ -37,7 +37,7 @@ public record SingleTargetAuthorizationIntent( } @Override - public boolean hasSecurableType(PolarisEntityType... types) { - return AuthorizationIntent.containsType(target(), types); + public boolean hasSecurableType(PolarisEntityType type) { + return target().leafHasType(type); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java index c189a8c5fdf..59b43232ac6 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java @@ -35,7 +35,7 @@ public record TargetlessAuthorizationIntent(@NonNull PolarisAuthorizableOperatio } @Override - public boolean hasSecurableType(PolarisEntityType... types) { + public boolean hasSecurableType(PolarisEntityType type) { return false; } } From 901edfac6f3052a4513d1246854c1fa6f7a35f34 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Mon, 1 Jun 2026 12:22:04 -0400 Subject: [PATCH 12/12] define more explicit shapes for AuthorizationIntent --- .../auth/opa/OpaPolarisAuthorizer.java | 28 +++++- .../auth/opa/OpaPolarisAuthorizerTest.java | 79 ++++++++-------- .../core/auth/AuthorizationIntent.java | 25 +---- .../core/auth/AuthorizationRequest.java | 35 ------- .../core/auth/PolarisAuthorizerImpl.java | 55 ++++++++--- .../polaris/core/auth/PolarisSecurable.java | 25 ----- ... PolicyAttachmentAuthorizationIntent.java} | 29 ++---- .../PrivilegeGrantAuthorizationIntent.java | 40 ++++++++ .../core/auth/RenameAuthorizationIntent.java | 40 ++++++++ .../RoleAssignmentAuthorizationIntent.java | 40 ++++++++ ...RootPrivilegeGrantAuthorizationIntent.java | 37 ++++++++ .../auth/SingleTargetAuthorizationIntent.java | 6 -- .../auth/TargetlessAuthorizationIntent.java | 6 -- .../core/auth/AuthorizationRequestTest.java | 92 ++----------------- .../core/auth/PolarisAuthorizerImplTest.java | 59 +++++++----- 15 files changed, 315 insertions(+), 281 deletions(-) rename polaris-core/src/main/java/org/apache/polaris/core/auth/{PairwiseTargetAuthorizationIntent.java => PolicyAttachmentAuthorizationIntent.java} (57%) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/PrivilegeGrantAuthorizationIntent.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/RenameAuthorizationIntent.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/RoleAssignmentAuthorizationIntent.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/RootPrivilegeGrantAuthorizationIntent.java diff --git a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java index 6060e3f4dee..7799bb22a55 100644 --- a/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java +++ b/extensions/auth/opa/impl/src/main/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizer.java @@ -43,12 +43,16 @@ import org.apache.polaris.core.auth.AuthorizationIntent; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; -import org.apache.polaris.core.auth.PairwiseTargetAuthorizationIntent; import org.apache.polaris.core.auth.PathSegment; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.auth.PolarisSecurable; +import org.apache.polaris.core.auth.PolicyAttachmentAuthorizationIntent; +import org.apache.polaris.core.auth.PrivilegeGrantAuthorizationIntent; +import org.apache.polaris.core.auth.RenameAuthorizationIntent; +import org.apache.polaris.core.auth.RoleAssignmentAuthorizationIntent; +import org.apache.polaris.core.auth.RootPrivilegeGrantAuthorizationIntent; import org.apache.polaris.core.auth.SingleTargetAuthorizationIntent; import org.apache.polaris.core.auth.TargetlessAuthorizationIntent; import org.apache.polaris.core.entity.PolarisBaseEntity; @@ -134,9 +138,25 @@ public AuthorizationDecision authorize( targets = toResourceEntitiesFromSecurable(singleTargetIntent.target()); secondaries = List.of(); } - case PairwiseTargetAuthorizationIntent pairwiseTargetIntent -> { - targets = toResourceEntitiesFromSecurable(pairwiseTargetIntent.target()); - secondaries = toResourceEntitiesFromSecurable(pairwiseTargetIntent.secondary()); + case RenameAuthorizationIntent renameIntent -> { + targets = toResourceEntitiesFromSecurable(renameIntent.from()); + secondaries = toResourceEntitiesFromSecurable(renameIntent.to()); + } + case PolicyAttachmentAuthorizationIntent policyAttachmentIntent -> { + targets = toResourceEntitiesFromSecurable(policyAttachmentIntent.policy()); + secondaries = toResourceEntitiesFromSecurable(policyAttachmentIntent.attachedTo()); + } + case RoleAssignmentAuthorizationIntent roleAssignmentIntent -> { + targets = toResourceEntitiesFromSecurable(roleAssignmentIntent.role()); + secondaries = toResourceEntitiesFromSecurable(roleAssignmentIntent.assignee()); + } + case PrivilegeGrantAuthorizationIntent privilegeGrantIntent -> { + targets = toResourceEntitiesFromSecurable(privilegeGrantIntent.grantTarget()); + secondaries = toResourceEntitiesFromSecurable(privilegeGrantIntent.grantee()); + } + case RootPrivilegeGrantAuthorizationIntent rootPrivilegeGrantIntent -> { + targets = List.of(); + secondaries = toResourceEntitiesFromSecurable(rootPrivilegeGrantIntent.grantee()); } } boolean allowed = diff --git a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java index b6acc412957..02009d8d519 100644 --- a/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java +++ b/extensions/auth/opa/impl/src/test/java/org/apache/polaris/extension/auth/opa/OpaPolarisAuthorizerTest.java @@ -50,13 +50,14 @@ import org.apache.hc.core5.http.io.entity.HttpEntities; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.polaris.core.auth.AuthorizationDecision; -import org.apache.polaris.core.auth.AuthorizationIntent; import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.AuthorizationState; import org.apache.polaris.core.auth.PathSegment; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.auth.PolarisSecurable; +import org.apache.polaris.core.auth.RenameAuthorizationIntent; +import org.apache.polaris.core.auth.SingleTargetAuthorizationIntent; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntityConstants; @@ -507,15 +508,13 @@ T httpClientExecute( PolarisPrincipal.of("test-user", Map.of(), Collections.emptySet()); PolarisAuthorizableOperation mockOperation = PolarisAuthorizableOperation.LOAD_TABLE; + PolarisResolvedPathWrapper target = null; + PolarisResolvedPathWrapper secondary = null; assertThatNoException() .isThrownBy( () -> { authorizer.authorizeOrThrow( - mockPrincipal, - Collections.emptySet(), - mockOperation, - (PolarisResolvedPathWrapper) null, - (PolarisResolvedPathWrapper) null); + mockPrincipal, Collections.emptySet(), mockOperation, target, secondary); }); } @@ -553,17 +552,15 @@ T httpClientExecute( PolarisPrincipal.of("test-user", Map.of(), Collections.emptySet()); PolarisAuthorizableOperation mockOperation = PolarisAuthorizableOperation.LOAD_TABLE; + PolarisResolvedPathWrapper target = null; + PolarisResolvedPathWrapper secondary = null; // Execute authorization (should not throw since we mocked allow=true) assertThatNoException() .isThrownBy( () -> { authorizer.authorizeOrThrow( - mockPrincipal, - Collections.emptySet(), - mockOperation, - (PolarisResolvedPathWrapper) null, - (PolarisResolvedPathWrapper) null); + mockPrincipal, Collections.emptySet(), mockOperation, target, secondary); }); } @@ -679,14 +676,16 @@ T httpClientExecute( void authorizeIncludesStructuredParentsFromSecurable() throws Exception { final String[] capturedRequestBody = new String[1]; AuthorizationRequest request = - AuthorizationRequest.of( + new AuthorizationRequest( PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), - PolarisAuthorizableOperation.LOAD_TABLE, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog1"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns1"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns2"), - new PathSegment(PolarisEntityType.TABLE_LIKE, "table1"))); + List.of( + new SingleTargetAuthorizationIntent( + PolarisAuthorizableOperation.LOAD_TABLE, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns1"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns2"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "table1"))))); HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") ClassicHttpResponse mockResponse = new BasicClassicHttpResponse(200); @@ -866,17 +865,19 @@ void authorizeRenameIncludesTargetAndSecondaryPaths() throws Exception { authzState.setResolutionManifest(resolutionManifest); AuthorizationRequest request = - AuthorizationRequest.of( + new AuthorizationRequest( PolarisPrincipal.of("alice", Map.of(), Set.of("role-1")), - PolarisAuthorizableOperation.RENAME_TABLE, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog1"), - new PathSegment(PolarisEntityType.NAMESPACE, "src_ns"), - new PathSegment(PolarisEntityType.TABLE_LIKE, "src_tbl")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog1"), - new PathSegment(PolarisEntityType.NAMESPACE, "dst_ns"), - new PathSegment(PolarisEntityType.TABLE_LIKE, "dst_tbl"))); + List.of( + new RenameAuthorizationIntent( + PolarisAuthorizableOperation.RENAME_TABLE, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"), + new PathSegment(PolarisEntityType.NAMESPACE, "src_ns"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "src_tbl")), + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog1"), + new PathSegment(PolarisEntityType.NAMESPACE, "dst_ns"), + new PathSegment(PolarisEntityType.TABLE_LIKE, "dst_tbl"))))); OpaPolarisAuthorizer authorizer = new OpaPolarisAuthorizer( @@ -964,14 +965,14 @@ T httpClientExecute( AuthorizationDecision decision = authorizer.authorize( authzState, - AuthorizationRequest.of( + new AuthorizationRequest( principal, List.of( - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))), - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog-2")))))); @@ -1014,7 +1015,7 @@ T httpClientExecute( } @Test - void authorizeUpdateTableMultiIntentRequestEvaluatesSequentially() throws Exception { + void authorizeUpdateTableMultiIntentRequestEvaluatesSequentially() { final int[] requestCount = new int[1]; HttpEntity mockEntity = HttpEntities.create("{\"result\":{\"allow\":true}}"); @SuppressWarnings("resource") @@ -1049,12 +1050,12 @@ T httpClientExecute( AuthorizationDecision decision = authorizer.authorize( authzState, - AuthorizationRequest.of( + new AuthorizationRequest( principal, List.of( - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES, tableTarget), - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.SET_TABLE_SNAPSHOT_REF, tableTarget)))); assertThat(decision.isAllowed()).isTrue(); @@ -1062,10 +1063,12 @@ T httpClientExecute( } private AuthorizationRequest requestWithCatalogTarget(PolarisPrincipal principal) { - return AuthorizationRequest.of( + return new AuthorizationRequest( principal, - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))); + List.of( + new SingleTargetAuthorizationIntent( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog-1"))))); } private ResolvedPolarisEntity createResolvedEntity(PolarisEntity entity) { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java index 9d0822a3ff7..1acf0c4fd9a 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationIntent.java @@ -18,32 +18,17 @@ */ package org.apache.polaris.core.auth; -import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; /** Authorization intent describing an operation and its target resource shape. */ public sealed interface AuthorizationIntent permits TargetlessAuthorizationIntent, SingleTargetAuthorizationIntent, - PairwiseTargetAuthorizationIntent { - static AuthorizationIntent of(@NonNull PolarisAuthorizableOperation operation) { - return new TargetlessAuthorizationIntent(operation); - } - - static AuthorizationIntent of( - @NonNull PolarisAuthorizableOperation operation, @NonNull PolarisSecurable target) { - return new SingleTargetAuthorizationIntent(operation, target); - } - - static AuthorizationIntent of( - @NonNull PolarisAuthorizableOperation operation, - @Nullable PolarisSecurable target, - @Nullable PolarisSecurable secondary) { - return new PairwiseTargetAuthorizationIntent(operation, target, secondary); - } + RenameAuthorizationIntent, + PolicyAttachmentAuthorizationIntent, + RoleAssignmentAuthorizationIntent, + PrivilegeGrantAuthorizationIntent, + RootPrivilegeGrantAuthorizationIntent { @NonNull PolarisAuthorizableOperation getOperation(); - - boolean hasSecurableType(PolarisEntityType type); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java index 7ba14fd1afc..30fa943d46e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -20,7 +20,6 @@ import com.google.common.base.Preconditions; import java.util.List; -import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; /** Full authorization request containing the subject and one or more authorization intents. */ @@ -33,38 +32,4 @@ public record AuthorizationRequest( Preconditions.checkArgument( !intents.isEmpty(), "Authorization request must contain at least one intent"); } - - public static AuthorizationRequest of( - @NonNull PolarisPrincipal principal, @NonNull AuthorizationIntent intent) { - return new AuthorizationRequest(principal, List.of(intent)); - } - - public static AuthorizationRequest of( - @NonNull PolarisPrincipal principal, @NonNull List intents) { - return new AuthorizationRequest(principal, intents); - } - - public static AuthorizationRequest of( - @NonNull PolarisPrincipal principal, @NonNull PolarisAuthorizableOperation operation) { - return of(principal, AuthorizationIntent.of(operation)); - } - - public static AuthorizationRequest of( - @NonNull PolarisPrincipal principal, - @NonNull PolarisAuthorizableOperation operation, - @NonNull PolarisSecurable target) { - return of(principal, AuthorizationIntent.of(operation, target)); - } - - public static AuthorizationRequest of( - @NonNull PolarisPrincipal principal, - @NonNull PolarisAuthorizableOperation operation, - PolarisSecurable target, - PolarisSecurable secondary) { - return of(principal, AuthorizationIntent.of(operation, target, secondary)); - } - - public boolean hasSecurableType(PolarisEntityType type) { - return intents.stream().anyMatch(intent -> intent.hasSecurableType(type)); - } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index cc691626f9d..8378c984e5b 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -761,7 +761,7 @@ public AuthorizationDecision authorize( PolarisResolutionManifest resolutionManifest = authzState.getResolutionManifest(); for (AuthorizationIntent intent : request.intents()) { AuthorizationDecision decision = - authorizeIntent(authzState, request.principal(), resolutionManifest, intent); + authorizeIntent(request.principal(), resolutionManifest, intent); if (!decision.isAllowed()) { return decision; } @@ -770,7 +770,6 @@ public AuthorizationDecision authorize( } private AuthorizationDecision authorizeIntent( - AuthorizationState authzState, PolarisPrincipal polarisPrincipal, PolarisResolutionManifest resolutionManifest, AuthorizationIntent intent) { @@ -791,21 +790,47 @@ private AuthorizationDecision authorizeIntent( getResolvedSecurable( resolutionManifest, singleTargetIntent.target(), prependRootContainer)); resolvedSecondaries = null; - } else if (intent instanceof PairwiseTargetAuthorizationIntent pairwiseIntent) { + } else if (intent instanceof RenameAuthorizationIntent renameIntent) { resolvedTargets = - pairwiseIntent.target() == null - ? (prependRootContainer - ? List.of(resolutionManifest.getResolvedRootContainerEntityAsPath()) - : null) - : List.of( - getResolvedSecurable( - resolutionManifest, pairwiseIntent.target(), prependRootContainer)); + List.of( + getResolvedSecurable( + resolutionManifest, renameIntent.from(), prependRootContainer)); + resolvedSecondaries = + List.of( + getResolvedSecurable(resolutionManifest, renameIntent.to(), prependRootContainer)); + } else if (intent instanceof PolicyAttachmentAuthorizationIntent policyAttachmentIntent) { + resolvedTargets = + List.of( + getResolvedSecurable( + resolutionManifest, policyAttachmentIntent.policy(), prependRootContainer)); + resolvedSecondaries = + List.of( + getResolvedSecurable( + resolutionManifest, policyAttachmentIntent.attachedTo(), prependRootContainer)); + } else if (intent instanceof RoleAssignmentAuthorizationIntent roleAssignmentIntent) { + resolvedTargets = + List.of( + getResolvedSecurable( + resolutionManifest, roleAssignmentIntent.role(), prependRootContainer)); + resolvedSecondaries = + List.of( + getResolvedSecurable( + resolutionManifest, roleAssignmentIntent.assignee(), prependRootContainer)); + } else if (intent instanceof PrivilegeGrantAuthorizationIntent privilegeGrantIntent) { + resolvedTargets = + List.of( + getResolvedSecurable( + resolutionManifest, privilegeGrantIntent.grantTarget(), prependRootContainer)); + resolvedSecondaries = + List.of( + getResolvedSecurable( + resolutionManifest, privilegeGrantIntent.grantee(), prependRootContainer)); + } else if (intent instanceof RootPrivilegeGrantAuthorizationIntent rootPrivilegeGrantIntent) { + resolvedTargets = List.of(resolutionManifest.getResolvedRootContainerEntityAsPath()); resolvedSecondaries = - semantics.secondaryPrivileges().isEmpty() || pairwiseIntent.secondary() == null - ? null - : List.of( - getResolvedSecurable( - resolutionManifest, pairwiseIntent.secondary(), prependRootContainer)); + List.of( + getResolvedSecurable( + resolutionManifest, rootPrivilegeGrantIntent.grantee(), prependRootContainer)); } else { throw new IllegalStateException("Unsupported authorization intent: " + intent.getClass()); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java index dbea066a610..30034fe19ef 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java @@ -20,7 +20,6 @@ import com.google.common.base.Preconditions; import java.util.List; -import java.util.stream.Collectors; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.immutables.PolarisImmutable; import org.immutables.value.Value; @@ -70,30 +69,6 @@ default List getParents() { return pathSegments.subList(0, pathSegments.size() - 1); } - /** - * Returns a stable debug string for authorization messages. - * - *

For example, a table securable may render as {@code - * CATALOG:catalog1.NAMESPACE:ns1.TABLE_LIKE:table1}. - */ - @NonNull - default String formatForAuthorizationMessage() { - return getPathSegments().stream() - .map(segment -> segment.entityType() + ":" + segment.name()) - .collect(Collectors.joining(".")); - } - - /** Returns whether the leaf segment has any of the provided types. */ - default boolean leafHasType(PolarisEntityType... types) { - PolarisEntityType entityType = getLeaf().entityType(); - for (PolarisEntityType type : types) { - if (entityType == type) { - return true; - } - } - return false; - } - @Value.Check default void validate() { Preconditions.checkState( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolicyAttachmentAuthorizationIntent.java similarity index 57% rename from polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java rename to polaris-core/src/main/java/org/apache/polaris/core/auth/PolicyAttachmentAuthorizationIntent.java index 6bc396961ae..b76e21d10f3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PairwiseTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolicyAttachmentAuthorizationIntent.java @@ -19,37 +19,22 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; -import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; -import org.jspecify.annotations.Nullable; -/** - * Authorization intent for operations that may carry both a primary target and a related secondary - * target. - * - *

The primary target may be omitted for legacy root-scoped flows that rely on an implicit root - * primary plus an explicit secondary target. - */ -public record PairwiseTargetAuthorizationIntent( +/** Authorization intent for attaching or detaching a policy to another securable. */ +public record PolicyAttachmentAuthorizationIntent( @NonNull PolarisAuthorizableOperation operation, - @Nullable PolarisSecurable target, - @Nullable PolarisSecurable secondary) + @NonNull PolarisSecurable policy, + @NonNull PolarisSecurable attachedTo) implements AuthorizationIntent { - public PairwiseTargetAuthorizationIntent { + public PolicyAttachmentAuthorizationIntent { Preconditions.checkNotNull(operation, "operation must be non-null"); - Preconditions.checkState( - target != null || secondary != null, - "PairwiseTargetAuthorizationIntent must contain a target or secondary"); + Preconditions.checkNotNull(policy, "policy must be non-null"); + Preconditions.checkNotNull(attachedTo, "attachedTo must be non-null"); } @Override public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } - - @Override - public boolean hasSecurableType(PolarisEntityType type) { - return (target() != null && target().leafHasType(type)) - || (secondary() != null && secondary().leafHasType(type)); - } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PrivilegeGrantAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PrivilegeGrantAuthorizationIntent.java new file mode 100644 index 00000000000..697c3a35baf --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PrivilegeGrantAuthorizationIntent.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import org.jspecify.annotations.NonNull; + +/** Authorization intent for granting or revoking privileges on a securable for a grantee. */ +public record PrivilegeGrantAuthorizationIntent( + @NonNull PolarisAuthorizableOperation operation, + @NonNull PolarisSecurable grantTarget, + @NonNull PolarisSecurable grantee) + implements AuthorizationIntent { + public PrivilegeGrantAuthorizationIntent { + Preconditions.checkNotNull(operation, "operation must be non-null"); + Preconditions.checkNotNull(grantTarget, "grantTarget must be non-null"); + Preconditions.checkNotNull(grantee, "grantee must be non-null"); + } + + @Override + public @NonNull PolarisAuthorizableOperation getOperation() { + return operation; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/RenameAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/RenameAuthorizationIntent.java new file mode 100644 index 00000000000..b03ed3a9855 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/RenameAuthorizationIntent.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import org.jspecify.annotations.NonNull; + +/** Authorization intent for rename operations over a source and destination securable. */ +public record RenameAuthorizationIntent( + @NonNull PolarisAuthorizableOperation operation, + @NonNull PolarisSecurable from, + @NonNull PolarisSecurable to) + implements AuthorizationIntent { + public RenameAuthorizationIntent { + Preconditions.checkNotNull(operation, "operation must be non-null"); + Preconditions.checkNotNull(from, "from must be non-null"); + Preconditions.checkNotNull(to, "to must be non-null"); + } + + @Override + public @NonNull PolarisAuthorizableOperation getOperation() { + return operation; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/RoleAssignmentAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/RoleAssignmentAuthorizationIntent.java new file mode 100644 index 00000000000..346a378876c --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/RoleAssignmentAuthorizationIntent.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import org.jspecify.annotations.NonNull; + +/** Authorization intent for assigning or revoking a role for an assignee. */ +public record RoleAssignmentAuthorizationIntent( + @NonNull PolarisAuthorizableOperation operation, + @NonNull PolarisSecurable role, + @NonNull PolarisSecurable assignee) + implements AuthorizationIntent { + public RoleAssignmentAuthorizationIntent { + Preconditions.checkNotNull(operation, "operation must be non-null"); + Preconditions.checkNotNull(role, "role must be non-null"); + Preconditions.checkNotNull(assignee, "assignee must be non-null"); + } + + @Override + public @NonNull PolarisAuthorizableOperation getOperation() { + return operation; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/RootPrivilegeGrantAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/RootPrivilegeGrantAuthorizationIntent.java new file mode 100644 index 00000000000..388a47635b8 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/RootPrivilegeGrantAuthorizationIntent.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.auth; + +import com.google.common.base.Preconditions; +import org.jspecify.annotations.NonNull; + +/** Authorization intent for granting or revoking root privileges for a grantee. */ +public record RootPrivilegeGrantAuthorizationIntent( + @NonNull PolarisAuthorizableOperation operation, @NonNull PolarisSecurable grantee) + implements AuthorizationIntent { + public RootPrivilegeGrantAuthorizationIntent { + Preconditions.checkNotNull(operation, "operation must be non-null"); + Preconditions.checkNotNull(grantee, "grantee must be non-null"); + } + + @Override + public @NonNull PolarisAuthorizableOperation getOperation() { + return operation; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java index 3e211102585..9f7394ac375 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/SingleTargetAuthorizationIntent.java @@ -19,7 +19,6 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; -import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; /** Authorization intent for operations with one explicit target. */ @@ -35,9 +34,4 @@ public record SingleTargetAuthorizationIntent( public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } - - @Override - public boolean hasSecurableType(PolarisEntityType type) { - return target().leafHasType(type); - } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java index 59b43232ac6..3a59d46401c 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/TargetlessAuthorizationIntent.java @@ -19,7 +19,6 @@ package org.apache.polaris.core.auth; import com.google.common.base.Preconditions; -import org.apache.polaris.core.entity.PolarisEntityType; import org.jspecify.annotations.NonNull; /** Authorization intent for operations with no explicit securable target. */ @@ -33,9 +32,4 @@ public record TargetlessAuthorizationIntent(@NonNull PolarisAuthorizableOperatio public @NonNull PolarisAuthorizableOperation getOperation() { return operation; } - - @Override - public boolean hasSecurableType(PolarisEntityType type) { - return false; - } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java index dad16d7c23d..60c6bf9e42e 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/AuthorizationRequestTest.java @@ -18,7 +18,6 @@ */ package org.apache.polaris.core.auth; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; @@ -30,97 +29,20 @@ public class AuthorizationRequestTest { @Test - void hasSecurableTypeReturnsTrueForPrincipalTarget() { - AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - AuthorizationIntent.of( - PolarisAuthorizableOperation.LOAD_TABLE, - PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")))); - - assertThat(request.intents().get(0)).isInstanceOf(SingleTargetAuthorizationIntent.class); - assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL)).isTrue(); - } - - @Test - void hasSecurableTypeReturnsTrueForPrincipalRoleSecondary() { - AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - AuthorizationIntent.of( - PolarisAuthorizableOperation.ASSIGN_PRINCIPAL_ROLE, - PolarisSecurable.of(new PathSegment(PolarisEntityType.PRINCIPAL, "alice")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin")))); - - assertThat(request.intents().get(0)).isInstanceOf(PairwiseTargetAuthorizationIntent.class); - assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isTrue(); - } - - @Test - void hasSecurableTypeReturnsTrueForCatalogRoleSecondary() { - AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - AuthorizationIntent.of( - PolarisAuthorizableOperation.ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")), - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.CATALOG_ROLE, "catalog-role")))); - - assertThat(request.hasSecurableType(PolarisEntityType.CATALOG_ROLE)).isTrue(); - } - - @Test - void hasSecurableTypeReturnsFalseWhenTypeAbsent() { - AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - AuthorizationIntent.of( - PolarisAuthorizableOperation.LOAD_VIEW, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")))); - - assertThat(request.hasSecurableType(PolarisEntityType.PRINCIPAL_ROLE)).isFalse(); - } - - @Test - void allowsTargetlessIntent() { - AuthorizationRequest request = - AuthorizationRequest.of( - PolarisPrincipal.of("alice", Map.of(), Set.of("role")), - AuthorizationIntent.of(PolarisAuthorizableOperation.LIST_CATALOGS)); - - assertThat(request.intents().get(0)).isInstanceOf(TargetlessAuthorizationIntent.class); - } - - @Test - void throwsWhenIntentFactoryHasNoTargetOrSecondary() { + void rootPrivilegeGrantIntentRejectsNullGrantee() { assertThatThrownBy( - () -> AuthorizationIntent.of(PolarisAuthorizableOperation.GET_CATALOG, null, null)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining( - "PairwiseTargetAuthorizationIntent must contain a target or secondary"); - } - - @Test - void threeArgIntentFactoryAlwaysCreatesPairwiseIntent() { - PolarisSecurable target = - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog")); - AuthorizationIntent intent = - AuthorizationIntent.of(PolarisAuthorizableOperation.GET_CATALOG, target, null); - - assertThat(intent).isInstanceOf(PairwiseTargetAuthorizationIntent.class); - PairwiseTargetAuthorizationIntent pairwiseIntent = (PairwiseTargetAuthorizationIntent) intent; - assertThat(pairwiseIntent.target()).isEqualTo(target); - assertThat(pairwiseIntent.secondary()).isNull(); + () -> + new RootPrivilegeGrantAuthorizationIntent( + PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("grantee must be non-null"); } @Test void requestRequiresAtLeastOneIntent() { assertThatThrownBy( () -> - AuthorizationRequest.of( + new AuthorizationRequest( PolarisPrincipal.of("alice", Map.of(), Set.of("role")), List.of())) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("must contain at least one intent"); diff --git a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java index 401bab0775b..6c815995e26 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/auth/PolarisAuthorizerImplTest.java @@ -66,10 +66,12 @@ void resolveAuthorizationInputsResolvesAll() { PolarisResolutionManifest manifest = mock(PolarisResolutionManifest.class); PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); AuthorizationRequest request = - AuthorizationRequest.of( + new AuthorizationRequest( principal, - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); + List.of( + new SingleTargetAuthorizationIntent( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))))); authzState.setResolutionManifest(manifest); @@ -104,12 +106,13 @@ void authorizeUsesRootTargetForRootGrantRequestWithoutPrimaryTarget() { ArgumentMatchers.>any()); AuthorizationRequest request = - AuthorizationRequest.of( + new AuthorizationRequest( principal, - PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE, - null, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))); + List.of( + new RootPrivilegeGrantAuthorizationIntent( + PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.PRINCIPAL_ROLE, "analytics-admin"))))); AuthorizationDecision decision = authorizer.authorize(authzState, request); @@ -120,7 +123,7 @@ void authorizeUsesRootTargetForRootGrantRequestWithoutPrimaryTarget() { eq(Set.of()), eq(PolarisAuthorizableOperation.ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE), eq(List.of(rootWrapper)), - eq(null)); + eq(List.of(principalRoleWrapper))); } @Test @@ -146,7 +149,9 @@ void authorizeUsesRootTargetForListCatalogsRequestWithoutPrimaryTarget() { ArgumentMatchers.>any()); AuthorizationRequest request = - AuthorizationRequest.of(principal, PolarisAuthorizableOperation.LIST_CATALOGS); + new AuthorizationRequest( + principal, + List.of(new TargetlessAuthorizationIntent(PolarisAuthorizableOperation.LIST_CATALOGS))); AuthorizationDecision decision = authorizer.authorize(authzState, request); @@ -185,12 +190,14 @@ void authorizeResolvesNamespaceTargetUsingCatalog() { ArgumentMatchers.>any()); AuthorizationRequest request = - AuthorizationRequest.of( + new AuthorizationRequest( principal, - PolarisAuthorizableOperation.LIST_NAMESPACES, - PolarisSecurable.of( - new PathSegment(PolarisEntityType.CATALOG, "catalog"), - new PathSegment(PolarisEntityType.NAMESPACE, "ns"))); + List.of( + new SingleTargetAuthorizationIntent( + PolarisAuthorizableOperation.LIST_NAMESPACES, + PolarisSecurable.of( + new PathSegment(PolarisEntityType.CATALOG, "catalog"), + new PathSegment(PolarisEntityType.NAMESPACE, "ns"))))); AuthorizationDecision decision = authorizer.authorize(authzState, request); @@ -233,14 +240,14 @@ void authorizeSingleOperationMultiIntentRequestEvaluatesSequentially() { AuthorizationDecision decision = authorizer.authorize( authzState, - AuthorizationRequest.of( + new AuthorizationRequest( principal, List.of( - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog1"))), - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.GET_CATALOG, PolarisSecurable.of( new PathSegment(PolarisEntityType.CATALOG, "catalog2")))))); @@ -293,12 +300,12 @@ void authorizeUpdateTableMultiIntentRequestEvaluatesSequentially() { AuthorizationDecision decision = authorizer.authorize( authzState, - AuthorizationRequest.of( + new AuthorizationRequest( principal, List.of( - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES, tableTarget), - AuthorizationIntent.of( + new SingleTargetAuthorizationIntent( PolarisAuthorizableOperation.SET_TABLE_SNAPSHOT_REF, tableTarget)))); assertThat(decision.isAllowed()).isTrue(); @@ -323,7 +330,7 @@ void authorizationRequestThrowsWhenIntentsAreEmpty() { PolarisPrincipal principal = PolarisPrincipal.of("alice", Map.of(), Set.of("role")); org.assertj.core.api.Assertions.assertThatThrownBy( - () -> AuthorizationRequest.of(principal, List.of())) + () -> new AuthorizationRequest(principal, List.of())) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("must contain at least one intent"); } @@ -350,10 +357,12 @@ void authorizeReturnsDenyDecision() { ArgumentMatchers.>any()); AuthorizationRequest request = - AuthorizationRequest.of( + new AuthorizationRequest( principal, - PolarisAuthorizableOperation.GET_CATALOG, - PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))); + List.of( + new SingleTargetAuthorizationIntent( + PolarisAuthorizableOperation.GET_CATALOG, + PolarisSecurable.of(new PathSegment(PolarisEntityType.CATALOG, "catalog"))))); AuthorizationDecision decision = authorizer.authorize(authzState, request);