From 22c58d37254743b8415cda1f06f49b65a1b33f10 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Thu, 5 Feb 2026 21:22:00 -0500 Subject: [PATCH 1/3] auth refactor --- .../auth/opa/OpaPolarisAuthorizer.java | 142 +++++++++ .../auth/opa/test/OpaAdminServiceIT.java | 14 + .../core/auth/AuthorizationCallContext.java | 46 +++ .../core/auth/AuthorizationRequest.java | 62 ++++ .../auth/PolarisAuthorizableOperation.java | 137 ++++---- .../polaris/core/auth/PolarisAuthorizer.java | 92 ++++++ .../core/auth/PolarisAuthorizerImpl.java | 22 ++ .../polaris/core/auth/PolarisSecurable.java | 74 +++++ .../resolver/PolarisResolutionManifest.java | 66 +++- .../core/persistence/resolver/Resolvable.java | 29 ++ .../service/admin/PolarisAdminService.java | 294 ++++++++++-------- .../catalog/common/CatalogHandler.java | 243 +++++++++------ .../service/catalog/common/CatalogUtils.java | 12 +- .../iceberg/IcebergCatalogHandler.java | 24 +- .../service/catalog/policy/PolicyCatalog.java | 43 ++- .../catalog/policy/PolicyCatalogHandler.java | 95 ++++-- .../catalog/policy/PolicyCatalogUtils.java | 18 +- .../PolarisPassthroughResolutionView.java | 18 ++ 18 files changed, 1089 insertions(+), 342 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.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 6c216121ef..cf9045b838 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 @@ -41,10 +41,14 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.polaris.core.auth.AuthorizationCallContext; +import org.apache.polaris.core.auth.AuthorizationRequest; 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.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.extension.auth.opa.model.ImmutableActor; import org.apache.polaris.extension.auth.opa.model.ImmutableContext; @@ -107,6 +111,37 @@ public OpaPolarisAuthorizer( * @param secondary the secondary entity (if any) * @throws ForbiddenException if authorization is denied by OPA */ + @Override + public void preAuthorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + // No-op for OPA; external PDP does not require RBAC entity resolution. + } + + @Override + public void authorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + if (request.getOperation().isRbacAdminOperation()) { + throw new ForbiddenException("OPA denied admin operation"); + } + boolean allowed = + queryOpaIntent( + request.getPrincipal(), + request.getOperation(), + request.getTargets(), + request.getSecondaries()); + if (!allowed) { + throw new ForbiddenException( + "Principal '%s' is not authorized for op %s", + request.getPrincipal().getName(), request.getOperation()); + } + } + + @Override + public void authorizeOrThrow( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + authorize(ctx, request); + } + @Override public void authorizeOrThrow( @Nonnull PolarisPrincipal polarisPrincipal, @@ -192,6 +227,31 @@ private boolean queryOpa( } } + private boolean queryOpaIntent( + PolarisPrincipal principal, + PolarisAuthorizableOperation op, + List targets, + List secondaries) { + try { + String inputJson = buildOpaInputJson(principal, op, targets, secondaries); + + HttpPost httpPost = new HttpPost(policyUri); + httpPost.setHeader("Content-Type", "application/json"); + + if (tokenProvider != null) { + String token = tokenProvider.getToken(); + if (token != null && !token.isEmpty()) { + httpPost.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); + } + } + + httpPost.setEntity(new StringEntity(inputJson, ContentType.APPLICATION_JSON)); + return httpClientExecute(httpPost, this::queryOpaCheckResponse); + } catch (HttpException | IOException e) { + throw new RuntimeException("OPA query failed", e); + } + } + @VisibleForTesting T httpClientExecute( ClassicHttpRequest request, HttpClientResponseHandler responseHandler) @@ -295,6 +355,54 @@ private String buildOpaInputJson( return objectMapper.writeValueAsString(request); } + private String buildOpaInputJson( + PolarisPrincipal principal, + PolarisAuthorizableOperation op, + List targets, + List secondaries) + throws IOException { + + var actor = + ImmutableActor.builder() + .principal(principal.getName()) + .addAllRoles(principal.getRoles()) + .build(); + + List targetEntities = new ArrayList<>(); + if (targets != null) { + for (PolarisSecurable target : targets) { + ResourceEntity entity = buildResourceEntity(target); + if (entity != null) { + targetEntities.add(entity); + } + } + } + + List secondaryEntities = new ArrayList<>(); + if (secondaries != null) { + for (PolarisSecurable secondary : secondaries) { + ResourceEntity entity = buildResourceEntity(secondary); + if (entity != null) { + secondaryEntities.add(entity); + } + } + } + + var resource = + ImmutableResource.builder().targets(targetEntities).secondaries(secondaryEntities).build(); + var context = ImmutableContext.builder().requestId(UUID.randomUUID().toString()).build(); + var input = + ImmutableOpaAuthorizationInput.builder() + .actor(actor) + .action(op.name()) + .resource(resource) + .context(context) + .build(); + var request = ImmutableOpaRequest.builder().input(input).build(); + + return objectMapper.writeValueAsString(request); + } + /** * Builds a resource entity from a resolved path wrapper. * @@ -332,4 +440,38 @@ private ResourceEntity buildResourceEntity(@Nullable PolarisResolvedPathWrapper return builder.build(); } + + @Nullable + private ResourceEntity buildResourceEntity(@Nullable PolarisSecurable securable) { + if (securable == null) { + return null; + } + + var builder = + ImmutableResourceEntity.builder() + .type(securable.getEntityType().name()) + .name( + securable.getNameParts().isEmpty() + ? "" + : securable.getNameParts().get(securable.getNameParts().size() - 1)); + + List parts = securable.getNameParts(); + if (parts.size() > 1) { + List parents = new ArrayList<>(); + PolarisEntityType parentType = + switch (securable.getEntityType()) { + case TABLE_LIKE, POLICY, NAMESPACE -> PolarisEntityType.NAMESPACE; + default -> null; + }; + if (parentType != null) { + for (int i = 0; i < parts.size() - 1; i++) { + parents.add( + ImmutableResourceEntity.builder().type(parentType.name()).name(parts.get(i)).build()); + } + builder.parents(parents); + } + } + + return builder.build(); + } } diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java index 9d84030a89..b64af6e229 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java @@ -139,6 +139,20 @@ void listCatalogsAuthorization() { .statusCode(200); } + @Test + void rbacAdminOperationsAreDeniedUnderOpa() { + String rootToken = baseRootToken; + String principalRole = "opa-pr-role-deny-" + UUID.randomUUID().toString().replace("-", ""); + + given() + .contentType(ContentType.JSON) + .header("Authorization", "Bearer " + rootToken) + .body(toJson(Map.of("name", principalRole, "properties", Map.of()))) + .post("/api/management/v1/principal-roles") + .then() + .statusCode(403); + } + @Test void createCatalogAuthorization() throws Exception { String rootToken = getRootToken(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java new file mode 100644 index 0000000000..637673c6a0 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.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 jakarta.annotation.Nullable; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; + +/** + * Request-scoped authorization context carrying state shared across authorization phases. + * + *

Distinct from {@link org.apache.polaris.core.context.CallContext}. This context is intended to + * carry authorization-specific state, such as a {@link PolarisResolutionManifest}. + */ +public class AuthorizationCallContext { + private PolarisResolutionManifest resolutionManifest; + + public AuthorizationCallContext() {} + + public AuthorizationCallContext(@Nullable PolarisResolutionManifest resolutionManifest) { + this.resolutionManifest = resolutionManifest; + } + + public @Nullable PolarisResolutionManifest getResolutionManifest() { + return resolutionManifest; + } + + public void setResolutionManifest(@Nullable PolarisResolutionManifest resolutionManifest) { + this.resolutionManifest = resolutionManifest; + } +} 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 new file mode 100644 index 0000000000..ad1c39a755 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationRequest.java @@ -0,0 +1,62 @@ +/* + * 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 java.util.List; + +/** + * Authorization request inputs for pre-authorization and core authorization. + * + *

This wrapper keeps authorization inputs together while preserving legacy semantics. + */ +public final class AuthorizationRequest { + private final PolarisPrincipal principal; + private final PolarisAuthorizableOperation operation; + private final List targets; + private final List secondaries; + + public AuthorizationRequest( + @Nonnull PolarisPrincipal principal, + @Nonnull PolarisAuthorizableOperation operation, + @Nullable List targets, + @Nullable List secondaries) { + this.principal = principal; + this.operation = operation; + this.targets = targets; + this.secondaries = secondaries; + } + + public @Nonnull PolarisPrincipal getPrincipal() { + return principal; + } + + public @Nonnull PolarisAuthorizableOperation getOperation() { + return operation; + } + + public @Nullable List getTargets() { + return targets; + } + + public @Nullable List getSecondaries() { + return secondaries; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java index 9d12cc148a..4154072cc1 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java @@ -153,66 +153,66 @@ public enum PolarisAuthorizableOperation { GET_CATALOG(CATALOG_READ_PROPERTIES), UPDATE_CATALOG(CATALOG_WRITE_PROPERTIES), DELETE_CATALOG(CATALOG_DROP), - LIST_PRINCIPALS(PRINCIPAL_LIST), - CREATE_PRINCIPAL(PRINCIPAL_CREATE), - GET_PRINCIPAL(PRINCIPAL_READ_PROPERTIES), - UPDATE_PRINCIPAL(PRINCIPAL_WRITE_PROPERTIES), - DELETE_PRINCIPAL(PRINCIPAL_DROP), - ROTATE_CREDENTIALS(PRINCIPAL_ROTATE_CREDENTIALS), - RESET_CREDENTIALS(PRINCIPAL_RESET_CREDENTIALS), - LIST_PRINCIPAL_ROLES_ASSIGNED(PRINCIPAL_LIST_GRANTS), - ASSIGN_PRINCIPAL_ROLE(PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE), + LIST_PRINCIPALS(true, PRINCIPAL_LIST), + CREATE_PRINCIPAL(true, PRINCIPAL_CREATE), + GET_PRINCIPAL(true, PRINCIPAL_READ_PROPERTIES), + UPDATE_PRINCIPAL(true, PRINCIPAL_WRITE_PROPERTIES), + DELETE_PRINCIPAL(true, PRINCIPAL_DROP), + ROTATE_CREDENTIALS(true, PRINCIPAL_ROTATE_CREDENTIALS), + RESET_CREDENTIALS(true, PRINCIPAL_RESET_CREDENTIALS), + LIST_PRINCIPAL_ROLES_ASSIGNED(true, PRINCIPAL_LIST_GRANTS), + ASSIGN_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE), REVOKE_PRINCIPAL_ROLE( - PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE), - LIST_PRINCIPAL_ROLES(PRINCIPAL_ROLE_LIST), - CREATE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_CREATE), - GET_PRINCIPAL_ROLE(PRINCIPAL_ROLE_READ_PROPERTIES), - UPDATE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_WRITE_PROPERTIES), - DELETE_PRINCIPAL_ROLE(PRINCIPAL_ROLE_DROP), - LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS), - LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS), - ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE), + true, PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE), + LIST_PRINCIPAL_ROLES(true, PRINCIPAL_ROLE_LIST), + CREATE_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_CREATE), + GET_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_READ_PROPERTIES), + UPDATE_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_WRITE_PROPERTIES), + DELETE_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_DROP), + LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_LIST_GRANTS), + LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_LIST_GRANTS), + ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE(true, CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE), REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE( - CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_CATALOG_ROLES(CATALOG_ROLE_LIST), - CREATE_CATALOG_ROLE(CATALOG_ROLE_CREATE), - GET_CATALOG_ROLE(CATALOG_ROLE_READ_PROPERTIES), - UPDATE_CATALOG_ROLE(CATALOG_ROLE_WRITE_PROPERTIES), - DELETE_CATALOG_ROLE(CATALOG_ROLE_DROP), - LIST_ASSIGNEE_PRINCIPAL_ROLES_FOR_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS), - LIST_GRANTS_FOR_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS), - ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE(SERVICE_MANAGE_ACCESS), + true, CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_CATALOG_ROLES(true, CATALOG_ROLE_LIST), + CREATE_CATALOG_ROLE(true, CATALOG_ROLE_CREATE), + GET_CATALOG_ROLE(true, CATALOG_ROLE_READ_PROPERTIES), + UPDATE_CATALOG_ROLE(true, CATALOG_ROLE_WRITE_PROPERTIES), + DELETE_CATALOG_ROLE(true, CATALOG_ROLE_DROP), + LIST_ASSIGNEE_PRINCIPAL_ROLES_FOR_CATALOG_ROLE(true, CATALOG_ROLE_LIST_GRANTS), + LIST_GRANTS_FOR_CATALOG_ROLE(true, CATALOG_ROLE_LIST_GRANTS), + ADD_ROOT_GRANT_TO_PRINCIPAL_ROLE(true, SERVICE_MANAGE_ACCESS), REVOKE_ROOT_GRANT_FROM_PRINCIPAL_ROLE( - SERVICE_MANAGE_ACCESS, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_ROOT(SERVICE_MANAGE_ACCESS), - ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE(PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE), + true, SERVICE_MANAGE_ACCESS, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_ROOT(true, SERVICE_MANAGE_ACCESS), + ADD_PRINCIPAL_GRANT_TO_PRINCIPAL_ROLE(true, PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE), REVOKE_PRINCIPAL_GRANT_FROM_PRINCIPAL_ROLE( - PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_PRINCIPAL(PRINCIPAL_LIST_GRANTS), - ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE(PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE), + true, PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_PRINCIPAL(true, PRINCIPAL_LIST_GRANTS), + ADD_PRINCIPAL_ROLE_GRANT_TO_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE), REVOKE_PRINCIPAL_ROLE_GRANT_FROM_PRINCIPAL_ROLE( - PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS), - ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE), + true, PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE, PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_PRINCIPAL_ROLE(true, PRINCIPAL_ROLE_LIST_GRANTS), + ADD_CATALOG_ROLE_GRANT_TO_CATALOG_ROLE(true, CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE), REVOKE_CATALOG_ROLE_GRANT_FROM_CATALOG_ROLE( - CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_CATALOG_ROLE(CATALOG_ROLE_LIST_GRANTS), - ADD_CATALOG_GRANT_TO_CATALOG_ROLE(CATALOG_MANAGE_GRANTS_ON_SECURABLE), + true, CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_CATALOG_ROLE(true, CATALOG_ROLE_LIST_GRANTS), + ADD_CATALOG_GRANT_TO_CATALOG_ROLE(true, CATALOG_MANAGE_GRANTS_ON_SECURABLE), REVOKE_CATALOG_GRANT_FROM_CATALOG_ROLE( - CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_CATALOG(CATALOG_LIST_GRANTS), - ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE(NAMESPACE_MANAGE_GRANTS_ON_SECURABLE), + true, CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_CATALOG(true, CATALOG_LIST_GRANTS), + ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE(true, NAMESPACE_MANAGE_GRANTS_ON_SECURABLE), REVOKE_NAMESPACE_GRANT_FROM_CATALOG_ROLE( - NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_NAMESPACE(NAMESPACE_LIST_GRANTS), - ADD_TABLE_GRANT_TO_CATALOG_ROLE(TABLE_MANAGE_GRANTS_ON_SECURABLE), + true, NAMESPACE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_NAMESPACE(true, NAMESPACE_LIST_GRANTS), + ADD_TABLE_GRANT_TO_CATALOG_ROLE(true, TABLE_MANAGE_GRANTS_ON_SECURABLE), REVOKE_TABLE_GRANT_FROM_CATALOG_ROLE( - TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_TABLE(TABLE_LIST_GRANTS), - ADD_VIEW_GRANT_TO_CATALOG_ROLE(VIEW_MANAGE_GRANTS_ON_SECURABLE), + true, TABLE_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_TABLE(true, TABLE_LIST_GRANTS), + ADD_VIEW_GRANT_TO_CATALOG_ROLE(true, VIEW_MANAGE_GRANTS_ON_SECURABLE), REVOKE_VIEW_GRANT_FROM_CATALOG_ROLE( - VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), - LIST_GRANTS_ON_VIEW(VIEW_LIST_GRANTS), + true, VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + LIST_GRANTS_ON_VIEW(true, VIEW_LIST_GRANTS), CREATE_POLICY(POLICY_CREATE), LOAD_POLICY(POLICY_READ), DROP_POLICY(POLICY_DROP), @@ -227,9 +227,9 @@ public enum PolarisAuthorizableOperation { GET_APPLICABLE_POLICIES_ON_CATALOG(CATALOG_READ_PROPERTIES), GET_APPLICABLE_POLICIES_ON_NAMESPACE(NAMESPACE_READ_PROPERTIES), GET_APPLICABLE_POLICIES_ON_TABLE(TABLE_READ_PROPERTIES), - ADD_POLICY_GRANT_TO_CATALOG_ROLE(POLICY_MANAGE_GRANTS_ON_SECURABLE), + ADD_POLICY_GRANT_TO_CATALOG_ROLE(true, POLICY_MANAGE_GRANTS_ON_SECURABLE), REVOKE_POLICY_GRANT_FROM_CATALOG_ROLE( - POLICY_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), + true, POLICY_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), ASSIGN_TABLE_UUID(TABLE_ASSIGN_UUID), UPGRADE_TABLE_FORMAT_VERSION(TABLE_UPGRADE_FORMAT_VERSION), ADD_TABLE_SCHEMA(TABLE_ADD_SCHEMA), @@ -248,37 +248,58 @@ public enum PolarisAuthorizableOperation { REMOVE_TABLE_STATISTICS(TABLE_REMOVE_STATISTICS), REMOVE_TABLE_PARTITION_SPECS(TABLE_REMOVE_PARTITION_SPECS); + private final boolean rbacAdminOperation; private final EnumSet privilegesOnTarget; private final EnumSet privilegesOnSecondary; /** Most common case -- single privilege on target entities. */ PolarisAuthorizableOperation(PolarisPrivilege targetPrivilege) { - this(targetPrivilege == null ? null : EnumSet.of(targetPrivilege), null); + this(false, targetPrivilege == null ? null : EnumSet.of(targetPrivilege), null); } /** Require multiple simultaneous privileges on target entities. */ PolarisAuthorizableOperation(EnumSet privilegesOnTarget) { - this(privilegesOnTarget, null); + this(false, privilegesOnTarget, null); } /** Single privilege on target entities, multiple privileges on secondary. */ PolarisAuthorizableOperation( PolarisPrivilege targetPrivilege, EnumSet privilegesOnSecondary) { - this(targetPrivilege == null ? null : EnumSet.of(targetPrivilege), privilegesOnSecondary); + this( + false, targetPrivilege == null ? null : EnumSet.of(targetPrivilege), privilegesOnSecondary); } /** Single privilege on target, single privilege on targetParent. */ PolarisAuthorizableOperation( PolarisPrivilege targetPrivilege, PolarisPrivilege secondaryPrivilege) { this( + false, targetPrivilege == null ? null : EnumSet.of(targetPrivilege), secondaryPrivilege == null ? null : EnumSet.of(secondaryPrivilege)); } - /** EnumSets on target, targetParent */ + /** RBAC admin operation with single privilege on target entities. */ + PolarisAuthorizableOperation(boolean rbacAdminOperation, PolarisPrivilege targetPrivilege) { + this(rbacAdminOperation, targetPrivilege == null ? null : EnumSet.of(targetPrivilege), null); + } + + /** RBAC admin operation with single privilege on target and secondary. */ PolarisAuthorizableOperation( + boolean rbacAdminOperation, + PolarisPrivilege targetPrivilege, + PolarisPrivilege secondaryPrivilege) { + this( + rbacAdminOperation, + targetPrivilege == null ? null : EnumSet.of(targetPrivilege), + secondaryPrivilege == null ? null : EnumSet.of(secondaryPrivilege)); + } + + /** Base constructor. */ + PolarisAuthorizableOperation( + boolean rbacAdminOperation, EnumSet privilegesOnTarget, EnumSet privilegesOnSecondary) { + this.rbacAdminOperation = rbacAdminOperation; this.privilegesOnTarget = privilegesOnTarget == null ? EnumSet.noneOf(PolarisPrivilege.class) : privilegesOnTarget; this.privilegesOnSecondary = @@ -287,6 +308,10 @@ public enum PolarisAuthorizableOperation { : privilegesOnSecondary; } + public boolean isRbacAdminOperation() { + return rbacAdminOperation; + } + public EnumSet getPrivilegesOnTarget() { return privilegesOnTarget; } 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 55c3792067..1da0890d55 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 @@ -23,11 +23,102 @@ import java.util.List; import java.util.Set; import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; /** Interface for invoking authorization checks. */ public interface PolarisAuthorizer { + /** + * Pre-authorization hook for resolving authorizer-specific inputs. + * + *

Default implementation is a no-op to preserve legacy behavior. + */ + default void preAuthorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) {} + /** + * Core authorization entry point for the new SPI. + * + *

Default implementation delegates to legacy {@code authorizeOrThrow(...)} to preserve + * behavior for existing authorizers. + */ + default void authorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + authorizeInternal(ctx, request, true /* throwOnDeny */); + } + + /** + * Backwards-compatible external API that throws on deny for legacy call sites. + * + *

Default implementation delegates to shared authorization logic. + */ + default void authorizeOrThrow( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + authorizeInternal(ctx, request, true /* throwOnDeny */); + } + + /** + * Shared authorization logic used by both new and legacy entry points. + * + *

Default implementation adapts intent inputs to the legacy RBAC SPI to preserve behavior. + */ + private void authorizeInternal( + @Nonnull AuthorizationCallContext ctx, + @Nonnull AuthorizationRequest request, + boolean throwOnDeny) { + PolarisResolutionManifest manifest = ctx.getResolutionManifest(); + Set activatedEntities = + manifest == null ? Set.of() : manifest.getAllActivatedCatalogRoleAndPrincipalRoles(); + List resolvedTargets = + resolveSecurables(manifest, request.getTargets()); + List resolvedSecondaries = + resolveSecurables(manifest, request.getSecondaries()); + if (throwOnDeny) { + authorizeOrThrow( + request.getPrincipal(), + activatedEntities, + request.getOperation(), + resolvedTargets, + resolvedSecondaries); + } + } + + private static List resolveSecurables( + PolarisResolutionManifest manifest, List securables) { + if (securables == null) { + return null; + } + if (manifest == null) { + return null; + } + List resolved = new java.util.ArrayList<>(securables.size()); + for (PolarisSecurable securable : securables) { + PolarisEntityType type = securable.getEntityType(); + switch (type) { + case ROOT: + resolved.add(manifest.getResolvedRootContainerEntityAsPath()); + break; + case CATALOG: + if (manifest.hasTopLevelName(securable.getName(), type)) { + resolved.add(manifest.getResolvedTopLevelEntity(securable.getName(), type)); + } else { + resolved.add(manifest.getResolvedReferenceCatalogEntity()); + } + break; + case PRINCIPAL: + case PRINCIPAL_ROLE: + resolved.add(manifest.getResolvedTopLevelEntity(securable.getName(), type)); + break; + default: + resolved.add(manifest.getResolvedPath(securable, true)); + break; + } + } + return resolved; + } + + @Deprecated void authorizeOrThrow( @Nonnull PolarisPrincipal polarisPrincipal, @Nonnull Set activatedEntities, @@ -35,6 +126,7 @@ void authorizeOrThrow( @Nullable PolarisResolvedPathWrapper target, @Nullable PolarisResolvedPathWrapper secondary); + @Deprecated 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 489bc4fdae..09ce1a5d1f 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 @@ -127,6 +127,7 @@ import com.google.common.collect.SetMultimap; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -140,6 +141,8 @@ import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.ResolvedPolarisEntity; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolver.Resolvable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -908,4 +911,23 @@ public boolean hasTransitivePrivilege( resolvedPath); return false; } + + @Override + public void preAuthorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + PolarisResolutionManifest manifest = ctx.getResolutionManifest(); + if (manifest == null || manifest.hasResolution()) { + return; + } + + // Phase 2: preserve existing RBAC ordering by resolving the manifest within preAuthorize. + manifest.resolveSelections( + EnumSet.of( + Resolvable.CALLER_PRINCIPAL, + Resolvable.CALLER_PRINCIPAL_ROLES, + Resolvable.CATALOG_ROLES, + Resolvable.REFERENCE_CATALOG, + Resolvable.REQUESTED_PATHS, + Resolvable.TOP_LEVEL_ENTITIES)); + } } 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 new file mode 100644 index 0000000000..4179d8683f --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisSecurable.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 java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.apache.polaris.core.entity.PolarisEntityType; + +/** + * Intent-only target reference for authorization decisions. + * + *

Represents the target name and entity type without any resolved RBAC state. + */ +public final class PolarisSecurable { + private final PolarisEntityType entityType; + private final List nameParts; + + public PolarisSecurable(@Nonnull PolarisEntityType entityType, @Nonnull List nameParts) { + this.entityType = Objects.requireNonNull(entityType, "entityType"); + this.nameParts = List.copyOf(Objects.requireNonNull(nameParts, "nameParts")); + } + + public @Nonnull PolarisEntityType getEntityType() { + return entityType; + } + + public @Nonnull List getNameParts() { + return Collections.unmodifiableList(nameParts); + } + + public @Nonnull String getName() { + return String.join(".", nameParts); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PolarisSecurable that = (PolarisSecurable) o; + return entityType == that.entityType && nameParts.equals(that.nameParts); + } + + @Override + public int hashCode() { + return Objects.hash(entityType, nameParts); + } + + @Override + public String toString() { + return "PolarisSecurable{" + "entityType=" + entityType + ", nameParts=" + nameParts + '}'; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java index 4f38758dbe..f40fe31282 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java @@ -68,6 +68,18 @@ public class PolarisResolutionManifest implements PolarisResolutionManifestCatal // Set when resolveAll is called private ResolverStatus primaryResolverStatus = null; + public boolean hasResolution() { + return primaryResolverStatus != null; + } + + public ResolverStatus getResolverStatus() { + diagnostics.checkNotNull( + primaryResolverStatus, + "resolver_not_run_before_access", + "resolveAll() must be called before reading resolution results"); + return primaryResolverStatus; + } + private boolean isResolveAllSucceeded() { diagnostics.checkNotNull( primaryResolverStatus, @@ -106,6 +118,10 @@ public void addTopLevelName(String entityName, PolarisEntityType entityType, boo } } + public boolean hasTopLevelName(String entityName, PolarisEntityType entityType) { + return addedTopLevelNames.containsEntry(entityName, entityType); + } + /** * Adds a path that will be statically resolved with the primary Resolver when resolveAll() is * called, and which contributes to the resolution status of whether all paths have successfully @@ -131,6 +147,29 @@ public void addPassthroughPath(ResolverPath path, Object key) { passthroughPaths.put(key, path); } + /** Adds an alias key for a previously added path key. */ + public void addPathAlias(Object existingKey, Object aliasKey) { + diagnostics.check( + pathLookup.containsKey(existingKey), + "invalid_key_for_path_alias", + "existingKey={} pathLookup={}", + existingKey, + pathLookup); + pathLookup.put(aliasKey, pathLookup.get(existingKey)); + } + + /** Adds an alias key for a previously added passthrough path key. */ + public void addPassthroughAlias(Object existingKey, Object aliasKey) { + diagnostics.check( + passthroughPaths.containsKey(existingKey), + "invalid_key_for_passthrough_alias", + "existingKey={} passthroughPaths={}", + existingKey, + passthroughPaths); + passthroughPaths.put(aliasKey, passthroughPaths.get(existingKey)); + addPathAlias(existingKey, aliasKey); + } + public ResolverStatus resolveAll() { primaryResolverStatus = primaryResolver.resolveAll(); // TODO: This could be a race condition where a Principal is dropped after initial authn @@ -143,6 +182,16 @@ public ResolverStatus resolveAll() { return primaryResolverStatus; } + /** + * Resolves explicitly requested components. + * + *

Phase 1 behavior delegates to {@link #resolveAll()} to preserve existing semantics until + * resolver selection is fully implemented. + */ + public ResolverStatus resolveSelections(Set selections) { + return resolveAll(); + } + public boolean getIsPassthroughFacade() { return primaryResolver.getIsPassthroughFacade(); } @@ -179,12 +228,17 @@ public PolarisResolvedPathWrapper getResolvedPath( */ @Override public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { - diagnostics.check( - passthroughPaths.containsKey(key), - "invalid_key_for_passthrough_resolved_path", - "key={} passthroughPaths={}", - key, - passthroughPaths); + if (!passthroughPaths.containsKey(key)) { + if (pathLookup.containsKey(key)) { + return getResolvedPath(key); + } + diagnostics.check( + false, + "invalid_key_for_passthrough_resolved_path", + "key={} passthroughPaths={}", + key, + passthroughPaths); + } ResolverPath requestedPath = passthroughPaths.get(key); // Run a single-use Resolver for this path. diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.java new file mode 100644 index 0000000000..984d594758 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolvable.java @@ -0,0 +1,29 @@ +/* + * 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.persistence.resolver; + +/** Explicit resolution components for {@link PolarisResolutionManifest#resolveSelections}. */ +public enum Resolvable { + CALLER_PRINCIPAL, + CALLER_PRINCIPAL_ROLES, + CATALOG_ROLES, + REFERENCE_CATALOG, + REQUESTED_PATHS, + TOP_LEVEL_ENTITIES +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index a80d289651..21741f3e0f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -74,9 +74,12 @@ import org.apache.polaris.core.admin.model.UpdatePrincipalRoleRequest; import org.apache.polaris.core.admin.model.ViewGrant; import org.apache.polaris.core.admin.model.ViewPrivilege; +import org.apache.polaris.core.auth.AuthorizationCallContext; +import org.apache.polaris.core.auth.AuthorizationRequest; 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.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; @@ -87,6 +90,7 @@ import org.apache.polaris.core.entity.NamespaceEntity; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.core.entity.PolarisEntityCore; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; @@ -188,6 +192,40 @@ private PolarisResolutionManifest newResolutionManifest(@Nullable String catalog return resolutionManifestFactory.createResolutionManifest(polarisPrincipal, catalogName); } + private AuthorizationRequest newAuthorizationRequest(PolarisAuthorizableOperation op) { + return new AuthorizationRequest(polarisPrincipal, op, null, null); + } + + private AuthorizationRequest newAuthorizationRequest( + PolarisAuthorizableOperation op, + List targets, + List secondaries) { + return new AuthorizationRequest(polarisPrincipal, op, targets, secondaries); + } + + private PolarisSecurable newSecurable(PolarisEntityType type, List nameParts) { + return new PolarisSecurable(type, nameParts); + } + + private PolarisSecurable newTopLevelSecurable(PolarisEntityType type, String name) { + return newSecurable(type, List.of(name)); + } + + private PolarisSecurable newNamespaceSecurable(Namespace namespace) { + return newSecurable(PolarisEntityType.NAMESPACE, Arrays.asList(namespace.levels())); + } + + private PolarisSecurable newTableLikeSecurable(TableIdentifier identifier) { + return newSecurable( + PolarisEntityType.TABLE_LIKE, PolarisCatalogHelpers.tableIdentifierToList(identifier)); + } + + private PolarisSecurable newPolicySecurable(PolicyIdentifier identifier) { + return newSecurable( + PolarisEntityType.POLICY, + PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName())); + } + private static PrincipalEntity getPrincipalByName( PolarisResolutionManifest resolutionManifest, String principalName) { return Optional.ofNullable( @@ -217,7 +255,9 @@ private static CatalogEntity getCatalogByName( private static CatalogRoleEntity getCatalogRoleByName( PolarisResolutionManifest resolutionManifest, String catalogRoleName) { - return Optional.ofNullable(resolutionManifest.getResolvedPath(catalogRoleName)) + PolarisSecurable catalogRoleSecurable = + new PolarisSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); + return Optional.ofNullable(resolutionManifest.getResolvedPath(catalogRoleSecurable)) .map(PolarisResolvedPathWrapper::getRawLeafEntity) .map(CatalogRoleEntity::of) .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); @@ -225,15 +265,12 @@ private static CatalogRoleEntity getCatalogRoleByName( private void authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation op) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(null); - resolutionManifest.resolveAll(); - PolarisResolvedPathWrapper rootContainerWrapper = - resolutionManifest.getResolvedRootContainerEntityAsPath(); - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedPrincipalRoleEntities(), - op, - rootContainerWrapper, - null /* secondary */); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + PolarisSecurable rootSecurable = + newSecurable( + PolarisEntityType.ROOT, List.of(PolarisEntityConstants.getRootContainerName())); + authorizer.authorize(authzContext, newAuthorizationRequest(op, List.of(rootSecurable), null)); } private PolarisResolutionManifest authorizeBasicTopLevelEntityOperationOrThrow( @@ -251,13 +288,16 @@ private PolarisResolutionManifest authorizeBasicTopLevelEntityOperationOrThrow( @Nullable String referenceCatalogName) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(referenceCatalogName); resolutionManifest.addTopLevelName(topLevelEntityName, entityType, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException( "TopLevelEntity of type %s does not exist: %s", entityType, topLevelEntityName); } PolarisResolvedPathWrapper topLevelEntityWrapper = resolutionManifest.getResolvedTopLevelEntity(topLevelEntityName, entityType); + PolarisSecurable topLevelSecurable = newTopLevelSecurable(entityType, topLevelEntityName); PolarisEntity entity = topLevelEntityWrapper.getResolvedLeafEntity().getEntity(); if (isSelfEntity(entity) && isSelfOperation(op)) { @@ -266,12 +306,8 @@ private PolarisResolutionManifest authorizeBasicTopLevelEntityOperationOrThrow( .addKeyValue("principalName", topLevelEntityName) .log("Allowing rotate own credentials"); } else { - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - topLevelEntityWrapper, - null /* secondary */); + authorizer.authorize( + authzContext, newAuthorizationRequest(op, List.of(topLevelSecurable), null)); } return resolutionManifest; } @@ -302,20 +338,21 @@ private static boolean isSelfOperation(PolarisAuthorizableOperation op) { private PolarisResolutionManifest authorizeBasicCatalogRoleOperationOrThrow( PolarisAuthorizableOperation op, String catalogName, String catalogRoleName) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(catalogName); + PolarisSecurable catalogRoleSecurable = + newSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); resolutionManifest.addPath( new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - resolutionManifest.resolveAll(); - PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(catalogRoleName, true); + catalogRoleSecurable); + resolutionManifest.addPathAlias(catalogRoleSecurable, catalogRoleName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + PolarisResolvedPathWrapper target = + resolutionManifest.getResolvedPath(catalogRoleSecurable, true); if (target == null) { throw new NotFoundException("CatalogRole does not exist: %s", catalogRoleName); } - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - null /* secondary */); + authorizer.authorize( + authzContext, newAuthorizationRequest(op, List.of(catalogRoleSecurable), null)); return resolutionManifest; } @@ -324,7 +361,14 @@ private PolarisResolutionManifest authorizeGrantOnRootContainerToPrincipalRoleOp PolarisResolutionManifest resolutionManifest = newResolutionManifest(null); resolutionManifest.addTopLevelName( principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); + PolarisSecurable rootSecurable = + newSecurable( + PolarisEntityType.ROOT, List.of(PolarisEntityConstants.getRootContainerName())); + PolarisSecurable principalRoleSecurable = + newTopLevelSecurable(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException( @@ -332,18 +376,9 @@ private PolarisResolutionManifest authorizeGrantOnRootContainerToPrincipalRoleOp status.getFailedToResolvedEntityName(), principalRoleName); } - PolarisResolvedPathWrapper rootContainerWrapper = - resolutionManifest.getResolvedRootContainerEntityAsPath(); - PolarisResolvedPathWrapper principalRoleWrapper = - resolutionManifest.getResolvedTopLevelEntity( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - rootContainerWrapper, - principalRoleWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(rootSecurable), List.of(principalRoleSecurable))); return resolutionManifest; } @@ -354,7 +389,13 @@ private PolarisResolutionManifest authorizeGrantOnPrincipalRoleToPrincipalOperat principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); resolutionManifest.addTopLevelName( principalName, PolarisEntityType.PRINCIPAL, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); + PolarisSecurable principalRoleSecurable = + newTopLevelSecurable(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + PolarisSecurable principalSecurable = + newTopLevelSecurable(PolarisEntityType.PRINCIPAL, principalName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException( @@ -362,18 +403,9 @@ private PolarisResolutionManifest authorizeGrantOnPrincipalRoleToPrincipalOperat status.getFailedToResolvedEntityName(), principalRoleName, principalName); } - PolarisResolvedPathWrapper principalRoleWrapper = - resolutionManifest.getResolvedTopLevelEntity( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); - PolarisResolvedPathWrapper principalWrapper = - resolutionManifest.getResolvedTopLevelEntity(principalName, PolarisEntityType.PRINCIPAL); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - principalRoleWrapper, - principalWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(principalRoleSecurable), List.of(principalSecurable))); return resolutionManifest; } @@ -383,12 +415,18 @@ private PolarisResolutionManifest authorizeGrantOnCatalogRoleToPrincipalRoleOper String catalogRoleName, String principalRoleName) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(catalogName); + PolarisSecurable catalogRoleSecurable = + newSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); resolutionManifest.addPath( new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); + catalogRoleSecurable); resolutionManifest.addTopLevelName( principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); + PolarisSecurable principalRoleSecurable = + newTopLevelSecurable(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException( @@ -400,18 +438,10 @@ private PolarisResolutionManifest authorizeGrantOnCatalogRoleToPrincipalRoleOper status.getFailedToResolvePath(), catalogName, catalogRoleName, principalRoleName); } - PolarisResolvedPathWrapper principalRoleWrapper = - resolutionManifest.getResolvedTopLevelEntity( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE); - PolarisResolvedPathWrapper catalogRoleWrapper = - resolutionManifest.getResolvedPath(catalogRoleName, true); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - catalogRoleWrapper, - principalRoleWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest( + op, List.of(catalogRoleSecurable), List.of(principalRoleSecurable))); return resolutionManifest; } @@ -420,10 +450,16 @@ private PolarisResolutionManifest authorizeGrantOnCatalogOperationOrThrow( PolarisResolutionManifest resolutionManifest = newResolutionManifest(catalogName); resolutionManifest.addTopLevelName( catalogName, PolarisEntityType.CATALOG, false /* isOptional */); + PolarisSecurable catalogSecurable = + newTopLevelSecurable(PolarisEntityType.CATALOG, catalogName); + PolarisSecurable catalogRoleSecurable = + newSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); resolutionManifest.addPath( new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); + catalogRoleSecurable); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException("Catalog not found: %s", catalogName); @@ -431,16 +467,9 @@ private PolarisResolutionManifest authorizeGrantOnCatalogOperationOrThrow( throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); } - PolarisResolvedPathWrapper catalogWrapper = - resolutionManifest.getResolvedTopLevelEntity(catalogName, PolarisEntityType.CATALOG); - PolarisResolvedPathWrapper catalogRoleWrapper = - resolutionManifest.getResolvedPath(catalogRoleName, true); - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - catalogWrapper, - catalogRoleWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(catalogSecurable), List.of(catalogRoleSecurable))); return resolutionManifest; } @@ -450,13 +479,20 @@ private PolarisResolutionManifest authorizeGrantOnNamespaceOperationOrThrow( Namespace namespace, String catalogRoleName) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(catalogName); + PolarisSecurable namespaceSecurable = newNamespaceSecurable(namespace); resolutionManifest.addPassthroughPath( new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); + namespaceSecurable); + resolutionManifest.addPassthroughAlias(namespaceSecurable, namespace); + PolarisSecurable catalogRoleSecurable = + newSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); resolutionManifest.addPath( new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); + catalogRoleSecurable); + resolutionManifest.addPathAlias(catalogRoleSecurable, catalogRoleName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException("Catalog not found: %s", catalogName); @@ -469,17 +505,9 @@ private PolarisResolutionManifest authorizeGrantOnNamespaceOperationOrThrow( } } - PolarisResolvedPathWrapper namespaceWrapper = - resolutionManifest.getResolvedPath(namespace, true); - PolarisResolvedPathWrapper catalogRoleWrapper = - resolutionManifest.getResolvedPath(catalogRoleName, true); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - namespaceWrapper, - catalogRoleWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(namespaceSecurable), List.of(catalogRoleSecurable))); return resolutionManifest; } @@ -490,18 +518,27 @@ private PolarisResolutionManifest authorizeGrantOnTableLikeOperationOrThrow( TableIdentifier identifier, String catalogRoleName) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(catalogName); + PolarisSecurable namespaceSecurable = newNamespaceSecurable(identifier.namespace()); + PolarisSecurable tableLikeSecurable = newTableLikeSecurable(identifier); resolutionManifest.addPassthroughPath( new ResolverPath( Arrays.asList(identifier.namespace().levels()), PolarisEntityType.NAMESPACE), - identifier.namespace()); + namespaceSecurable); + resolutionManifest.addPassthroughAlias(namespaceSecurable, identifier.namespace()); resolutionManifest.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE), - identifier); + tableLikeSecurable); + resolutionManifest.addPassthroughAlias(tableLikeSecurable, identifier); + PolarisSecurable catalogRoleSecurable = + newSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); resolutionManifest.addPath( new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); + catalogRoleSecurable); + resolutionManifest.addPathAlias(catalogRoleSecurable, catalogRoleName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException("Catalog not found: %s", catalogName); @@ -516,7 +553,10 @@ private PolarisResolutionManifest authorizeGrantOnTableLikeOperationOrThrow( CatalogEntity catalogEntity = getCatalogByName(resolutionManifest, catalogName); PolarisResolvedPathWrapper tableLikeWrapper = resolutionManifest.getResolvedPath( - identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE, true); + tableLikeSecurable, + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + true); boolean rbacForFederatedCatalogsEnabled = realmConfig.getConfig( FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS, catalogEntity); @@ -525,15 +565,9 @@ private PolarisResolutionManifest authorizeGrantOnTableLikeOperationOrThrow( CatalogHandler.throwNotFoundExceptionForTableLikeEntity(identifier, subTypes); } - PolarisResolvedPathWrapper catalogRoleWrapper = - resolutionManifest.getResolvedPath(catalogRoleName, true); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - tableLikeWrapper, - catalogRoleWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(tableLikeSecurable), List.of(catalogRoleSecurable))); return resolutionManifest; } @@ -543,15 +577,22 @@ private PolarisResolutionManifest authorizeGrantOnPolicyOperationOrThrow( PolicyIdentifier identifier, String catalogRoleName) { PolarisResolutionManifest resolutionManifest = newResolutionManifest(catalogName); + PolarisSecurable policySecurable = newPolicySecurable(identifier); resolutionManifest.addPath( new ResolverPath( PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName()), PolarisEntityType.POLICY), - identifier); + policySecurable); + resolutionManifest.addPathAlias(policySecurable, identifier); + PolarisSecurable catalogRoleSecurable = + newSecurable(PolarisEntityType.CATALOG_ROLE, List.of(catalogRoleName)); resolutionManifest.addPath( new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); + catalogRoleSecurable); + resolutionManifest.addPathAlias(catalogRoleSecurable, catalogRoleName); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { throw new NotFoundException("Catalog not found: %s", catalogName); } else if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { @@ -562,16 +603,9 @@ private PolarisResolutionManifest authorizeGrantOnPolicyOperationOrThrow( } } - PolarisResolvedPathWrapper policyWrapper = resolutionManifest.getResolvedPath(identifier, true); - PolarisResolvedPathWrapper catalogRoleWrapper = - resolutionManifest.getResolvedPath(catalogRoleName, true); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - policyWrapper, - catalogRoleWrapper); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(policySecurable), List.of(catalogRoleSecurable))); return resolutionManifest; } @@ -1692,7 +1726,8 @@ public PrivilegeResult grantPrivilegeOnNamespaceToRole( CatalogEntity catalogEntity = getCatalogByName(resolutionManifest, catalogName); CatalogRoleEntity catalogRoleEntity = getCatalogRoleByName(resolutionManifest, catalogRoleName); - PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getResolvedPath(newNamespaceSecurable(namespace)); if (resolvedPathWrapper == null || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { boolean rbacForFederatedCatalogsEnabled = @@ -1735,7 +1770,8 @@ public PrivilegeResult revokePrivilegeOnNamespaceFromRole( CatalogRoleEntity catalogRoleEntity = getCatalogRoleByName(resolutionManifest, catalogRoleName); - PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getResolvedPath(newNamespaceSecurable(namespace)); if (resolvedPathWrapper == null || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { throw new NotFoundException("Namespace %s not found", namespace); @@ -1808,7 +1844,7 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( syntheticNamespace = PolarisEntity.of(result.getEntity()); } else if (result.getReturnStatus() == BaseResult.ReturnStatus.ENTITY_ALREADY_EXISTS) { PolarisResolvedPathWrapper partialPath = - resolutionManifest.getPassthroughResolvedPath(namespace); + resolutionManifest.getPassthroughResolvedPath(newNamespaceSecurable(namespace)); PolarisEntity partialLeafEntity = partialPath != null ? partialPath.getRawLeafEntity() : null; if (partialLeafEntity == null @@ -1830,7 +1866,7 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( currentParent = syntheticNamespace; } PolarisResolvedPathWrapper resolvedPathWrapper = - resolutionManifest.getPassthroughResolvedPath(namespace); + resolutionManifest.getPassthroughResolvedPath(newNamespaceSecurable(namespace)); return resolvedPathWrapper; } @@ -2131,7 +2167,9 @@ private PrivilegeResult grantPrivilegeOnTableLikeToRole( PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath( - identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE); + newTableLikeSecurable(identifier), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE); if (resolvedPathWrapper == null || !subTypes.contains(resolvedPathWrapper.getRawLeafEntity().getSubType())) { boolean rbacForFederatedCatalogsEnabled = @@ -2221,7 +2259,7 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( syntheticTableEntity); PolarisResolvedPathWrapper completePathWrapper = - resolutionManifest.getPassthroughResolvedPath(identifier); + resolutionManifest.getPassthroughResolvedPath(newTableLikeSecurable(identifier)); PolarisEntity leafEntity = completePathWrapper != null ? completePathWrapper.getRawLeafEntity() : null; if (completePathWrapper == null @@ -2276,7 +2314,8 @@ private PrivilegeResult grantPrivilegeOnPolicyEntityToRole( CatalogEntity catalogEntity = getCatalogByName(resolutionManifest, catalogName); CatalogRoleEntity catalogRoleEntity = getCatalogRoleByName(resolutionManifest, catalogRoleName); - PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(identifier); + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getResolvedPath(newPolicySecurable(identifier)); if (resolvedPathWrapper == null) { throw new NoSuchPolicyException(String.format("Policy not exists: %s", identifier)); } @@ -2300,7 +2339,8 @@ private PrivilegeResult revokePrivilegeOnPolicyEntityFromRole( CatalogEntity catalogEntity = getCatalogByName(resolutionManifest, catalogName); CatalogRoleEntity catalogRoleEntity = getCatalogRoleByName(resolutionManifest, catalogRoleName); - PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(identifier); + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getResolvedPath(newPolicySecurable(identifier)); if (resolvedPathWrapper == null) { throw new NoSuchPolicyException(String.format("Policy not exists: %s", identifier)); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java index 9d5778ac29..dd20e4a97f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java @@ -24,7 +24,6 @@ import java.util.Arrays; import java.util.EnumSet; import java.util.List; -import java.util.Optional; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.AlreadyExistsException; @@ -32,9 +31,12 @@ import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.NoSuchViewException; import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthorizationCallContext; +import org.apache.polaris.core.auth.AuthorizationRequest; 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.catalog.ExternalCatalogFactory; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.config.RealmConfig; @@ -98,6 +100,36 @@ protected PolarisResolutionManifest newResolutionManifest() { return resolutionManifestFactory.createResolutionManifest(polarisPrincipal, catalogName); } + private AuthorizationRequest newAuthorizationRequest(PolarisAuthorizableOperation op) { + return new AuthorizationRequest(polarisPrincipal, op, null, null); + } + + private AuthorizationRequest newAuthorizationRequest( + PolarisAuthorizableOperation op, + List targets, + List secondaries) { + return new AuthorizationRequest(polarisPrincipal, op, targets, secondaries); + } + + protected PolarisSecurable newSecurable(PolarisEntityType type, List nameParts) { + return new PolarisSecurable(type, nameParts); + } + + protected PolarisSecurable newNamespaceSecurable(Namespace namespace) { + return newSecurable(PolarisEntityType.NAMESPACE, Arrays.asList(namespace.levels())); + } + + protected PolarisSecurable newTableLikeSecurable(TableIdentifier identifier) { + return newSecurable( + PolarisEntityType.TABLE_LIKE, PolarisCatalogHelpers.tableIdentifierToList(identifier)); + } + + protected PolarisSecurable newPolicySecurable(PolicyIdentifier identifier) { + return newSecurable( + PolarisEntityType.POLICY, + PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName())); + } + /** Initialize the catalog once authorized. Called after all `authorize...` methods. */ protected abstract void initializeCatalog(); @@ -113,51 +145,53 @@ protected void authorizeBasicNamespaceOperationOrThrow( List extraPassthroughTableLikes, List extraPassThroughPolicies) { resolutionManifest = newResolutionManifest(); + PolarisSecurable namespaceSecurable = newNamespaceSecurable(namespace); resolutionManifest.addPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); + new ResolverPath(namespaceSecurable.getNameParts(), PolarisEntityType.NAMESPACE), + namespaceSecurable); + resolutionManifest.addPathAlias(namespaceSecurable, namespace); if (extraPassthroughNamespaces != null) { for (Namespace ns : extraPassthroughNamespaces) { + PolarisSecurable nsSecurable = newNamespaceSecurable(ns); resolutionManifest.addPassthroughPath( new ResolverPath( Arrays.asList(ns.levels()), PolarisEntityType.NAMESPACE, true /* optional */), - ns); + nsSecurable); + resolutionManifest.addPassthroughAlias(nsSecurable, ns); } } if (extraPassthroughTableLikes != null) { for (TableIdentifier id : extraPassthroughTableLikes) { + PolarisSecurable tableSecurable = newTableLikeSecurable(id); resolutionManifest.addPassthroughPath( new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(id), - PolarisEntityType.TABLE_LIKE, - true /* optional */), - id); + tableSecurable.getNameParts(), PolarisEntityType.TABLE_LIKE, true /* optional */), + tableSecurable); + resolutionManifest.addPassthroughAlias(tableSecurable, id); } } if (extraPassThroughPolicies != null) { for (PolicyIdentifier id : extraPassThroughPolicies) { + PolarisSecurable policySecurable = newPolicySecurable(id); resolutionManifest.addPassthroughPath( new ResolverPath( - PolarisCatalogHelpers.identifierToList(id.getNamespace(), id.getName()), - PolarisEntityType.POLICY, - true /* optional */), - id); + policySecurable.getNameParts(), PolarisEntityType.POLICY, true /* optional */), + policySecurable); + resolutionManifest.addPassthroughAlias(policySecurable, id); } } - resolutionManifest.resolveAll(); - PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + PolarisResolvedPathWrapper target = + resolutionManifest.getResolvedPath(namespaceSecurable, true); if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - null /* secondary */); + authorizer.authorize( + authzContext, newAuthorizationRequest(op, List.of(namespaceSecurable), null)); initializeCatalog(); } @@ -167,9 +201,11 @@ protected void authorizeCreateNamespaceUnderNamespaceOperationOrThrow( resolutionManifest = newResolutionManifest(); Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(namespace); + PolarisSecurable parentNamespaceSecurable = newNamespaceSecurable(parentNamespace); resolutionManifest.addPath( - new ResolverPath(Arrays.asList(parentNamespace.levels()), PolarisEntityType.NAMESPACE), - parentNamespace); + new ResolverPath(parentNamespaceSecurable.getNameParts(), PolarisEntityType.NAMESPACE), + parentNamespaceSecurable); + resolutionManifest.addPathAlias(parentNamespaceSecurable, parentNamespace); // When creating an entity under a namespace, the authz target is the parentNamespace, but we // must also add the actual path that will be created as an "optional" passthrough resolution @@ -178,18 +214,17 @@ protected void authorizeCreateNamespaceUnderNamespaceOperationOrThrow( resolutionManifest.addPassthroughPath( new ResolverPath( Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE, true /* optional */), - namespace); - resolutionManifest.resolveAll(); - PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(parentNamespace, true); + newNamespaceSecurable(namespace)); + resolutionManifest.addPassthroughAlias(newNamespaceSecurable(namespace), namespace); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + PolarisResolvedPathWrapper target = + resolutionManifest.getResolvedPath(parentNamespaceSecurable, true); if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", parentNamespace); } - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - null /* secondary */); + authorizer.authorize( + authzContext, newAuthorizationRequest(op, List.of(parentNamespaceSecurable), null)); initializeCatalog(); } @@ -199,9 +234,11 @@ protected void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( Namespace namespace = identifier.namespace(); resolutionManifest = newResolutionManifest(); + PolarisSecurable namespaceSecurable = newNamespaceSecurable(namespace); resolutionManifest.addPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); + new ResolverPath(namespaceSecurable.getNameParts(), PolarisEntityType.NAMESPACE), + namespaceSecurable); + resolutionManifest.addPathAlias(namespaceSecurable, namespace); // When creating an entity under a namespace, the authz target is the namespace, but we must // also @@ -214,18 +251,17 @@ protected void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE, true /* optional */), - identifier); - resolutionManifest.resolveAll(); - PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); + newTableLikeSecurable(identifier)); + resolutionManifest.addPassthroughAlias(newTableLikeSecurable(identifier), identifier); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + PolarisResolvedPathWrapper target = + resolutionManifest.getResolvedPath(namespaceSecurable, true); if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - null /* secondary */); + authorizer.authorize( + authzContext, newAuthorizationRequest(op, List.of(namespaceSecurable), null)); initializeCatalog(); } @@ -239,14 +275,24 @@ protected void ensureResolutionManifestForTable(TableIdentifier identifier) { if (resolutionManifest == null) { resolutionManifest = newResolutionManifest(); + PolarisSecurable namespaceSecurable = newNamespaceSecurable(identifier.namespace()); + resolutionManifest.addPassthroughPath( + new ResolverPath(namespaceSecurable.getNameParts(), PolarisEntityType.NAMESPACE), + namespaceSecurable); + resolutionManifest.addPassthroughAlias(namespaceSecurable, identifier.namespace()); + // The underlying Catalog is also allowed to fetch "fresh" versions of the target entity. + PolarisSecurable tableSecurable = newTableLikeSecurable(identifier); resolutionManifest.addPassthroughPath( new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), - PolarisEntityType.TABLE_LIKE, - true /* optional */), - identifier); - resolutionManifest.resolveAll(); + tableSecurable.getNameParts(), PolarisEntityType.TABLE_LIKE, true /* optional */), + tableSecurable); + resolutionManifest.addPassthroughAlias(tableSecurable, identifier); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize( + authzContext, + new AuthorizationRequest( + polarisPrincipal, PolarisAuthorizableOperation.LOAD_TABLE, null, null)); } } @@ -260,19 +306,20 @@ protected void authorizeBasicTableLikeOperationsOrThrow( PolarisEntitySubType subType, TableIdentifier identifier) { ensureResolutionManifestForTable(identifier); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + PolarisAuthorizableOperation primaryOp = ops.iterator().next(); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(primaryOp)); + PolarisSecurable targetSecurable = newTableLikeSecurable(identifier); PolarisResolvedPathWrapper target = - resolutionManifest.getResolvedPath(identifier, PolarisEntityType.TABLE_LIKE, subType, true); + resolutionManifest.getResolvedPath( + targetSecurable, PolarisEntityType.TABLE_LIKE, subType, true); if (target == null) { throwNotFoundExceptionForTableLikeEntity(identifier, List.of(subType)); } for (PolarisAuthorizableOperation op : ops) { - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - null /* secondary */); + authorizer.authorize( + authzContext, newAuthorizationRequest(op, List.of(targetSecurable), null)); } initializeCatalog(); @@ -284,14 +331,19 @@ protected void authorizeCollectionOfTableLikeOperationOrThrow( List ids) { resolutionManifest = newResolutionManifest(); ids.forEach( - identifier -> - resolutionManifest.addPassthroughPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), - PolarisEntityType.TABLE_LIKE), - identifier)); - - ResolverStatus status = resolutionManifest.resolveAll(); + identifier -> { + PolarisSecurable tableSecurable = newTableLikeSecurable(identifier); + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE), + tableSecurable); + resolutionManifest.addPassthroughAlias(tableSecurable, identifier); + }); + + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); // If one of the paths failed to resolve, throw exception based on the one that // we first failed to resolve. @@ -302,27 +354,22 @@ protected void authorizeCollectionOfTableLikeOperationOrThrow( throwNotFoundExceptionForTableLikeEntity(identifier, List.of(subType)); } - List targets = + List targets = ids.stream() .map( - identifier -> - Optional.ofNullable( - resolutionManifest.getResolvedPath( - identifier, PolarisEntityType.TABLE_LIKE, subType, true)) - .orElseThrow( - () -> - subType == ICEBERG_TABLE - ? new NoSuchTableException( - "Table does not exist: %s", identifier) - : new NoSuchViewException( - "View does not exist: %s", identifier))) + identifier -> { + PolarisSecurable securable = newTableLikeSecurable(identifier); + if (resolutionManifest.getResolvedPath( + securable, PolarisEntityType.TABLE_LIKE, subType, true) + == null) { + throw subType == ICEBERG_TABLE + ? new NoSuchTableException("Table does not exist: %s", identifier) + : new NoSuchViewException("View does not exist: %s", identifier); + } + return securable; + }) .toList(); - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - targets, - null /* secondaries */); + authorizer.authorize(authzContext, newAuthorizationRequest(op, targets, null)); initializeCatalog(); } @@ -334,24 +381,33 @@ protected void authorizeRenameTableLikeOperationOrThrow( TableIdentifier dst) { resolutionManifest = newResolutionManifest(); // Add src, dstParent, and dst(optional) + PolarisSecurable srcSecurable = newTableLikeSecurable(src); resolutionManifest.addPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(src), PolarisEntityType.TABLE_LIKE), - src); + srcSecurable); + resolutionManifest.addPathAlias(srcSecurable, src); + PolarisSecurable dstNamespaceSecurable = newNamespaceSecurable(dst.namespace()); resolutionManifest.addPath( - new ResolverPath(Arrays.asList(dst.namespace().levels()), PolarisEntityType.NAMESPACE), - dst.namespace()); + new ResolverPath(dstNamespaceSecurable.getNameParts(), PolarisEntityType.NAMESPACE), + dstNamespaceSecurable); + resolutionManifest.addPathAlias(dstNamespaceSecurable, dst.namespace()); + PolarisSecurable dstSecurable = newTableLikeSecurable(dst); resolutionManifest.addPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(dst), PolarisEntityType.TABLE_LIKE, true /* optional */), - dst); - ResolverStatus status = resolutionManifest.resolveAll(); + dstSecurable); + resolutionManifest.addPathAlias(dstSecurable, dst); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize(authzContext, newAuthorizationRequest(op)); + ResolverStatus status = resolutionManifest.getResolverStatus(); if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED && status.getFailedToResolvePath().getLastEntityType() == PolarisEntityType.NAMESPACE) { throw new NoSuchNamespaceException("Namespace does not exist: %s", dst.namespace()); - } else if (resolutionManifest.getResolvedPath(src, PolarisEntityType.TABLE_LIKE, subType) + } else if (resolutionManifest.getResolvedPath( + srcSecurable, PolarisEntityType.TABLE_LIKE, subType) == null) { throwNotFoundExceptionForTableLikeEntity(dst, List.of(subType)); } @@ -363,7 +419,7 @@ protected void authorizeRenameTableLikeOperationOrThrow( // type. // TODO: Possibly modify the exception thrown depending on whether the caller has privileges // on the parent namespace. - PolarisEntitySubType dstLeafSubType = resolutionManifest.getLeafSubType(dst); + PolarisEntitySubType dstLeafSubType = resolutionManifest.getLeafSubType(dstSecurable); switch (dstLeafSubType) { case ICEBERG_TABLE: @@ -380,16 +436,9 @@ protected void authorizeRenameTableLikeOperationOrThrow( break; } - PolarisResolvedPathWrapper target = - resolutionManifest.getResolvedPath(src, PolarisEntityType.TABLE_LIKE, subType, true); - PolarisResolvedPathWrapper secondary = - resolutionManifest.getResolvedPath(dst.namespace(), true); - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - secondary); + authorizer.authorize( + authzContext, + newAuthorizationRequest(op, List.of(srcSecurable), List.of(dstNamespaceSecurable))); initializeCatalog(); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java index 999b363fbc..fe4c8ff761 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java @@ -25,6 +25,8 @@ import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.auth.PolarisSecurable; +import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.entity.PolarisEntitySubType; @@ -45,13 +47,19 @@ public class CatalogUtils { */ public static PolarisResolvedPathWrapper findResolvedStorageEntity( PolarisResolutionManifestCatalogView resolvedEntityView, TableIdentifier tableIdentifier) { + PolarisSecurable tableSecurable = + new PolarisSecurable( + PolarisEntityType.TABLE_LIKE, + PolarisCatalogHelpers.tableIdentifierToList(tableIdentifier)); PolarisResolvedPathWrapper resolvedTableEntities = resolvedEntityView.getResolvedPath( - tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); + tableSecurable, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntities != null) { return resolvedTableEntities; } - return resolvedEntityView.getResolvedPath(tableIdentifier.namespace()); + return resolvedEntityView.getResolvedPath( + new PolarisSecurable( + PolarisEntityType.NAMESPACE, List.of(tableIdentifier.namespace().levels()))); } /** diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java index c5276ef6b5..3d050406ce 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java @@ -83,6 +83,8 @@ import org.apache.iceberg.rest.responses.LoadViewResponse; import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse; import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthorizationCallContext; +import org.apache.polaris.core.auth.AuthorizationRequest; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.auth.PolarisPrincipal; @@ -105,8 +107,8 @@ import org.apache.polaris.core.persistence.dao.entity.EntityWithPath; import org.apache.polaris.core.persistence.pagination.Page; import org.apache.polaris.core.persistence.pagination.PageToken; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; -import org.apache.polaris.core.persistence.resolver.Resolver; import org.apache.polaris.core.persistence.resolver.ResolverFactory; import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.rest.PolarisEndpoints; @@ -351,7 +353,7 @@ public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) { Map filteredProperties = reservedProperties.removeReservedProperties( resolutionManifest - .getPassthroughResolvedPath(namespace) + .getPassthroughResolvedPath(newNamespaceSecurable(namespace)) .getRawLeafEntity() .getPropertiesAsMap()); return CreateNamespaceResponse.builder() @@ -672,7 +674,8 @@ public boolean sendNotification(TableIdentifier identifier, NotificationRequest * @return the Polaris table entity for the table or null for external catalogs */ private @Nullable IcebergTableLikeEntity getTableEntity(TableIdentifier tableIdentifier) { - PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(tableIdentifier); + PolarisResolvedPathWrapper target = + resolutionManifest.getResolvedPath(newTableLikeSecurable(tableIdentifier)); PolarisEntity rawLeafEntity = target.getRawLeafEntity(); if (rawLeafEntity.getType() == PolarisEntityType.TABLE_LIKE) { return IcebergTableLikeEntity.of(rawLeafEntity); @@ -1325,12 +1328,19 @@ public ConfigResponse getConfig() { if (catalogName == null) { throw new BadRequestException("Please specify a warehouse"); } - Resolver resolver = resolverFactory.createResolver(polarisPrincipal, catalogName); - ResolverStatus resolverStatus = resolver.resolveAll(); - if (!resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { + PolarisResolutionManifest manifest = newResolutionManifest(); + AuthorizationCallContext authzContext = new AuthorizationCallContext(manifest); + authorizer.preAuthorize( + authzContext, + new AuthorizationRequest( + polarisPrincipal, PolarisAuthorizableOperation.GET_CATALOG, null, null)); + ResolverStatus resolverStatus = manifest.getResolverStatus(); + if (resolverStatus == null + || !resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { throw new NotFoundException("Unable to find warehouse %s", catalogName); } - ResolvedPolarisEntity resolvedReferenceCatalog = resolver.getResolvedReferenceCatalog(); + ResolvedPolarisEntity resolvedReferenceCatalog = + manifest.getResolvedReferenceCatalogEntity().getResolvedLeafEntity(); Map properties = PolarisEntity.of(resolvedReferenceCatalog.getEntity()).getPropertiesAsMap(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java index 9b2f7c8a62..61c552ac0a 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java @@ -41,6 +41,8 @@ import org.apache.iceberg.exceptions.BadRequestException; import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.polaris.core.auth.PolarisSecurable; +import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntity; @@ -89,10 +91,20 @@ public PolicyCatalog( this.metaStoreManager = metaStoreManager; } + private PolarisSecurable newNamespaceSecurable(Namespace namespace) { + return new PolarisSecurable(PolarisEntityType.NAMESPACE, Arrays.asList(namespace.levels())); + } + + private PolarisSecurable newPolicySecurable(PolicyIdentifier identifier) { + return new PolarisSecurable( + PolarisEntityType.POLICY, + PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName())); + } + public Policy createPolicy( PolicyIdentifier policyIdentifier, String type, String description, String content) { PolarisResolvedPathWrapper resolvedParent = - resolvedEntityView.getResolvedPath(policyIdentifier.getNamespace()); + resolvedEntityView.getResolvedPath(newNamespaceSecurable(policyIdentifier.getNamespace())); if (resolvedParent == null) { // Illegal state because the namespace should've already been in the static resolution set. throw new IllegalStateException( @@ -103,7 +115,9 @@ public Policy createPolicy( PolarisResolvedPathWrapper resolvedPolicyEntities = resolvedEntityView.getPassthroughResolvedPath( - policyIdentifier, PolarisEntityType.POLICY, PolarisEntitySubType.NULL_SUBTYPE); + newPolicySecurable(policyIdentifier), + PolarisEntityType.POLICY, + PolarisEntitySubType.NULL_SUBTYPE); PolicyEntity entity = PolicyEntity.of( @@ -157,7 +171,8 @@ public Policy createPolicy( } public List listPolicies(Namespace namespace, @Nullable PolicyType policyType) { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = + resolvedEntityView.getResolvedPath(newNamespaceSecurable(namespace)); if (resolvedEntities == null) { throw new IllegalStateException( String.format("Failed to fetch resolved namespace '%s'", namespace)); @@ -454,7 +469,8 @@ private List getFullPath(Namespace namespace, String targetName) return List.of(catalogEntity); } else if (Strings.isNullOrEmpty(targetName)) { // namespace - var resolvedTargetEntity = resolvedEntityView.getResolvedPath(namespace); + var resolvedTargetEntity = + resolvedEntityView.getResolvedPath(newNamespaceSecurable(namespace)); if (resolvedTargetEntity == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } @@ -465,7 +481,11 @@ private List getFullPath(Namespace namespace, String targetName) // only Iceberg tables are supported var resolvedTableEntity = resolvedEntityView.getResolvedPath( - tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); + new PolarisSecurable( + PolarisEntityType.TABLE_LIKE, + PolarisCatalogHelpers.tableIdentifierToList(tableIdentifier)), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntity == null) { throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); } @@ -485,7 +505,9 @@ private String getIdentifier(PolicyAttachmentTarget target) { private PolarisResolvedPathWrapper getResolvedPathWrapper(PolicyIdentifier policyIdentifier) { var resolvedEntities = resolvedEntityView.getPassthroughResolvedPath( - policyIdentifier, PolarisEntityType.POLICY, PolarisEntitySubType.NULL_SUBTYPE); + newPolicySecurable(policyIdentifier), + PolarisEntityType.POLICY, + PolarisEntitySubType.NULL_SUBTYPE); if (resolvedEntities == null || resolvedEntities.getResolvedLeafEntity() == null) { throw new NoSuchPolicyException(String.format("Policy does not exist: %s", policyIdentifier)); } @@ -499,7 +521,8 @@ private PolarisResolvedPathWrapper getResolvedPathWrapper( case CATALOG -> resolvedEntityView.getResolvedReferenceCatalogEntity(); case NAMESPACE -> { var namespace = Namespace.of(target.getPath().toArray(new String[0])); - var resolvedTargetEntity = resolvedEntityView.getResolvedPath(namespace); + var resolvedTargetEntity = + resolvedEntityView.getResolvedPath(newNamespaceSecurable(namespace)); if (resolvedTargetEntity == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } @@ -510,7 +533,11 @@ private PolarisResolvedPathWrapper getResolvedPathWrapper( // only Iceberg tables are supported var resolvedTableEntity = resolvedEntityView.getResolvedPath( - tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); + new PolarisSecurable( + PolarisEntityType.TABLE_LIKE, + PolarisCatalogHelpers.tableIdentifierToList(tableIdentifier)), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntity == null) { throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java index 712193e400..1ccaf3990f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java @@ -30,9 +30,12 @@ import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.NotFoundException; import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthorizationCallContext; +import org.apache.polaris.core.auth.AuthorizationRequest; 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.catalog.ExternalCatalogFactory; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.context.CallContext; @@ -90,6 +93,10 @@ protected void initializeCatalog() { this.policyCatalog = new PolicyCatalog(metaStoreManager, callContext, this.resolutionManifest); } + private PolarisSecurable newCatalogSecurable() { + return new PolarisSecurable(PolarisEntityType.CATALOG, List.of(catalogName)); + } + public ListPoliciesResponse listPolicies(Namespace parent, @Nullable PolicyType policyType) { PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_POLICY; authorizeBasicNamespaceOperationOrThrow(op, parent); @@ -166,25 +173,26 @@ public GetApplicablePoliciesResponse getApplicablePolicies( private void authorizeBasicPolicyOperationOrThrow( PolarisAuthorizableOperation op, PolicyIdentifier identifier) { resolutionManifest = newResolutionManifest(); + PolarisSecurable policySecurable = newPolicySecurable(identifier); resolutionManifest.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName()), PolarisEntityType.POLICY, true /* optional */), - identifier); - resolutionManifest.resolveAll(); + policySecurable); + resolutionManifest.addPassthroughAlias(policySecurable, identifier); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize( + authzContext, new AuthorizationRequest(polarisPrincipal, op, null, null)); - PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(identifier, true); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(policySecurable, true); if (target == null) { throw new NoSuchPolicyException(String.format("Policy does not exist: %s", identifier)); } - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - target, - null /* secondary */); + authorizer.authorize( + authzContext, + new AuthorizationRequest(polarisPrincipal, op, List.of(policySecurable), null)); initializeCatalog(); } @@ -214,19 +222,18 @@ private void authorizeGetApplicablePoliciesOperationOrThrow( private void authorizeBasicCatalogOperationOrThrow(PolarisAuthorizableOperation op) { resolutionManifest = newResolutionManifest(); - resolutionManifest.resolveAll(); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize( + authzContext, new AuthorizationRequest(polarisPrincipal, op, null, null)); PolarisResolvedPathWrapper targetCatalog = resolutionManifest.getResolvedReferenceCatalogEntity(); if (targetCatalog == null) { throw new NotFoundException("Catalog not found"); } - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - targetCatalog, - null); + authorizer.authorize( + authzContext, + new AuthorizationRequest(polarisPrincipal, op, List.of(newCatalogSecurable()), null)); initializeCatalog(); } @@ -234,56 +241,65 @@ private void authorizeBasicCatalogOperationOrThrow(PolarisAuthorizableOperation private void authorizePolicyMappingOperationOrThrow( PolicyIdentifier identifier, PolicyAttachmentTarget target, boolean isAttach) { resolutionManifest = newResolutionManifest(); + PolarisSecurable policySecurable = newPolicySecurable(identifier); resolutionManifest.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName()), PolarisEntityType.POLICY, true /* optional */), - identifier); + policySecurable); + resolutionManifest.addPassthroughAlias(policySecurable, identifier); + PolarisSecurable targetSecurable = null; switch (target.getType()) { - case CATALOG -> {} + case CATALOG -> targetSecurable = newCatalogSecurable(); case NAMESPACE -> { Namespace targetNamespace = Namespace.of(target.getPath().toArray(new String[0])); + targetSecurable = newNamespaceSecurable(targetNamespace); resolutionManifest.addPath( new ResolverPath(Arrays.asList(targetNamespace.levels()), PolarisEntityType.NAMESPACE), - targetNamespace); + targetSecurable); + resolutionManifest.addPathAlias(targetSecurable, targetNamespace); } case TABLE_LIKE -> { TableIdentifier targetIdentifier = TableIdentifier.of(target.getPath().toArray(new String[0])); + targetSecurable = newTableLikeSecurable(targetIdentifier); resolutionManifest.addPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(targetIdentifier), PolarisEntityType.TABLE_LIKE), - targetIdentifier); + targetSecurable); + resolutionManifest.addPathAlias(targetSecurable, targetIdentifier); } default -> throw new IllegalArgumentException("Unsupported target type: " + target.getType()); } - ResolverStatus status = resolutionManifest.resolveAll(); + PolarisAuthorizableOperation preAuthOp = + determinePolicyMappingOperation(target.getType(), isAttach); + AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); + authorizer.preAuthorize( + authzContext, new AuthorizationRequest(polarisPrincipal, preAuthOp, null, null)); + ResolverStatus status = resolutionManifest.getResolverStatus(); throwNotFoundExceptionIfFailToResolve(status, identifier); PolarisResolvedPathWrapper policyWrapper = resolutionManifest.getPassthroughResolvedPath( - identifier, PolarisEntityType.POLICY, PolarisEntitySubType.NULL_SUBTYPE); + policySecurable, PolarisEntityType.POLICY, PolarisEntitySubType.NULL_SUBTYPE); if (policyWrapper == null) { throw new NoSuchPolicyException(String.format("Policy does not exist: %s", identifier)); } PolarisResolvedPathWrapper targetWrapper = - PolicyCatalogUtils.getResolvedPathWrapper(resolutionManifest, target); + PolicyCatalogUtils.getResolvedPathWrapper(resolutionManifest, target, targetSecurable); PolarisAuthorizableOperation op = determinePolicyMappingOperation(target, targetWrapper, isAttach); - - authorizer.authorizeOrThrow( - polarisPrincipal, - resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), - op, - policyWrapper, - targetWrapper); + authorizer.authorize( + authzContext, + new AuthorizationRequest( + polarisPrincipal, op, List.of(policySecurable), List.of(targetSecurable))); initializeCatalog(); } @@ -312,6 +328,25 @@ private PolarisAuthorizableOperation determinePolicyMappingOperation( }; } + private PolarisAuthorizableOperation determinePolicyMappingOperation( + PolicyAttachmentTarget.TypeEnum targetType, boolean isAttach) { + return switch (targetType) { + case CATALOG -> + isAttach + ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_CATALOG + : PolarisAuthorizableOperation.DETACH_POLICY_FROM_CATALOG; + case NAMESPACE -> + isAttach + ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_NAMESPACE + : PolarisAuthorizableOperation.DETACH_POLICY_FROM_NAMESPACE; + case TABLE_LIKE -> + isAttach + ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_TABLE + : PolarisAuthorizableOperation.DETACH_POLICY_FROM_TABLE; + default -> throw new IllegalArgumentException("Unsupported target type: " + targetType); + }; + } + private void throwNotFoundExceptionIfFailToResolve( ResolverStatus status, PolicyIdentifier identifier) { if ((status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED)) { diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java index 42364a3e45..426f318142 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java @@ -19,10 +19,9 @@ package org.apache.polaris.service.catalog.policy; import jakarta.annotation.Nonnull; -import org.apache.iceberg.catalog.Namespace; -import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.polaris.core.auth.PolarisSecurable; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -33,26 +32,27 @@ public class PolicyCatalogUtils { public static PolarisResolvedPathWrapper getResolvedPathWrapper( @Nonnull PolarisResolutionManifest resolutionManifest, - @Nonnull PolicyAttachmentTarget target) { + @Nonnull PolicyAttachmentTarget target, + @Nonnull PolarisSecurable targetSecurable) { return switch (target.getType()) { // get the current catalog entity, since policy cannot apply across catalog at this moment case CATALOG -> resolutionManifest.getResolvedReferenceCatalogEntity(); case NAMESPACE -> { - var namespace = Namespace.of(target.getPath().toArray(new String[0])); - var resolvedTargetEntity = resolutionManifest.getResolvedPath(namespace); + var resolvedTargetEntity = resolutionManifest.getResolvedPath(targetSecurable); if (resolvedTargetEntity == null) { - throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + throw new NoSuchNamespaceException( + "Namespace does not exist: %s", targetSecurable.getNameParts()); } yield resolvedTargetEntity; } case TABLE_LIKE -> { - var tableIdentifier = TableIdentifier.of(target.getPath().toArray(new String[0])); // only Iceberg tables are supported var resolvedTableEntity = resolutionManifest.getResolvedPath( - tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); + targetSecurable, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntity == null) { - throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); + throw new NoSuchTableException( + "Iceberg Table does not exist: %s", targetSecurable.getNameParts()); } yield resolvedTableEntity; } diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java index c647c17015..178b37ca98 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java @@ -22,6 +22,7 @@ import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.auth.PolarisSecurable; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; @@ -74,6 +75,11 @@ public PolarisResolvedPathWrapper getResolvedPath(Object key) { namespace); manifest.resolveAll(); return manifest.getResolvedPath(namespace); + } else if (key instanceof PolarisSecurable securable) { + manifest.addPath( + new ResolverPath(securable.getNameParts(), securable.getEntityType()), securable); + manifest.resolveAll(); + return manifest.getResolvedPath(securable); } else { throw new IllegalStateException( String.format( @@ -92,6 +98,10 @@ public PolarisResolvedPathWrapper getResolvedPath( identifier); manifest.resolveAll(); return manifest.getResolvedPath(identifier, entityType, subType); + } else if (key instanceof PolarisSecurable securable) { + manifest.addPath(new ResolverPath(securable.getNameParts(), entityType), securable); + manifest.resolveAll(); + return manifest.getResolvedPath(securable, entityType, subType); } else if (key instanceof PolicyIdentifier policyIdentifier) { manifest.addPath( new ResolverPath( @@ -118,6 +128,10 @@ public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), namespace); return manifest.getPassthroughResolvedPath(namespace); + } else if (key instanceof PolarisSecurable securable) { + manifest.addPassthroughPath( + new ResolverPath(securable.getNameParts(), securable.getEntityType()), securable); + return manifest.getPassthroughResolvedPath(securable); } else { throw new IllegalStateException( String.format( @@ -135,6 +149,10 @@ public PolarisResolvedPathWrapper getPassthroughResolvedPath( new ResolverPath(PolarisCatalogHelpers.tableIdentifierToList(identifier), entityType), identifier); return manifest.getPassthroughResolvedPath(identifier, entityType, subType); + } else if (key instanceof PolarisSecurable securable) { + manifest.addPassthroughPath( + new ResolverPath(securable.getNameParts(), entityType), securable); + return manifest.getPassthroughResolvedPath(securable, entityType, subType); } else if (key instanceof PolicyIdentifier policyIdentifier) { manifest.addPassthroughPath( new ResolverPath( From c2554b69d38247dcad1dc961a0222b46006ddf57 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Fri, 6 Feb 2026 09:02:09 -0500 Subject: [PATCH 2/3] fix tests --- .../auth/opa/OpaPolarisAuthorizer.java | 14 +- .../auth/opa/test/OpaAdminServiceIT.java | 267 +----------------- .../opa/test/OpaFileTokenIntegrationTest.java | 40 ++- .../opa/test/OpaGenericTableHandlerIT.java | 18 -- .../opa/test/OpaIcebergCatalogHandlerIT.java | 17 -- .../auth/opa/test/OpaIntegrationTest.java | 40 ++- .../opa/test/OpaPolicyCatalogHandlerIT.java | 21 -- .../core/auth/AuthorizationCallContext.java | 15 +- .../polaris/core/auth/PolarisAuthorizer.java | 84 +----- .../core/auth/PolarisAuthorizerImpl.java | 56 +++- .../apache/polaris/service/TestServices.java | 42 ++- 11 files changed, 164 insertions(+), 450 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 cf9045b838..bc670ba7d1 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 @@ -26,6 +26,7 @@ import java.io.IOException; import java.net.URI; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.UUID; @@ -50,6 +51,8 @@ import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest.Resolvable; import org.apache.polaris.extension.auth.opa.model.ImmutableActor; import org.apache.polaris.extension.auth.opa.model.ImmutableContext; import org.apache.polaris.extension.auth.opa.model.ImmutableOpaAuthorizationInput; @@ -114,7 +117,16 @@ public OpaPolarisAuthorizer( @Override public void preAuthorize( @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { - // No-op for OPA; external PDP does not require RBAC entity resolution. + PolarisResolutionManifest manifest = ctx.getResolutionManifest(); + if (manifest.hasResolution()) { + return; + } + // Resolve requested entities without RBAC role resolution. + manifest.resolveSelections( + EnumSet.of( + Resolvable.REFERENCE_CATALOG, + Resolvable.REQUESTED_PATHS, + Resolvable.TOP_LEVEL_ENTITIES)); } @Override diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java index b64af6e229..c2a8e14614 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaAdminServiceIT.java @@ -23,17 +23,10 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import io.restassured.http.ContentType; -import java.net.URI; import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.UUID; -import org.apache.iceberg.PartitionSpec; -import org.apache.iceberg.Schema; -import org.apache.iceberg.TableMetadata; -import org.apache.iceberg.TableMetadataParser; -import org.apache.iceberg.types.Types; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -68,68 +61,21 @@ void setupBaseCatalog() throws Exception { @Test void assignCatalogRoleToPrincipalRole() { String rootToken = baseRootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); - String catalogRole = "opa-cat-role-" + UUID.randomUUID().toString().replace("-", ""); - String principalRole = "opa-pr-role-" + UUID.randomUUID().toString().replace("-", ""); - // create catalog role + // RBAC catalog role management is denied under OPA given() .contentType(ContentType.JSON) .header("Authorization", "Bearer " + rootToken) .body(toJson(Map.of("name", catalogRole, "properties", Map.of()))) .post("/api/management/v1/catalogs/{cat}/catalog-roles", baseCatalogName) .then() - .statusCode(201); - - // create principal role - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(Map.of("name", principalRole, "properties", Map.of()))) - .post("/api/management/v1/principal-roles") - .then() - .statusCode(201); - - Map grantRequest = - Map.of("catalogRole", Map.of("name", catalogRole, "properties", Map.of())); - - // stranger cannot bind - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + strangerToken) - .body(toJson(grantRequest)) - .put( - "/api/management/v1/principal-roles/{pr}/catalog-roles/{cat}", - principalRole, - baseCatalogName) - .then() .statusCode(403); - - // root binds successfully - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(grantRequest)) - .put( - "/api/management/v1/principal-roles/{pr}/catalog-roles/{cat}", - principalRole, - baseCatalogName) - .then() - .statusCode(201); } @Test void listCatalogsAuthorization() { String rootToken = baseRootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); - - // stranger cannot list catalogs - given() - .header("Authorization", "Bearer " + strangerToken) - .get("/api/management/v1/catalogs") - .then() - .statusCode(403); // root lists catalogs successfully given() @@ -156,7 +102,6 @@ void rbacAdminOperationsAreDeniedUnderOpa() { @Test void createCatalogAuthorization() throws Exception { String rootToken = getRootToken(); - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); String catalogName = "opa-cat-create-" + UUID.randomUUID().toString().replace("-", ""); String baseLocation = @@ -172,15 +117,6 @@ void createCatalogAuthorization() throws Exception { "storageConfigInfo", Map.of("storageType", "FILE", "allowedLocations", List.of(baseLocation))); - // Stranger cannot create catalog - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + strangerToken) - .body(toJson(createCatalogRequest)) - .post("/api/management/v1/catalogs") - .then() - .statusCode(403); - // Root creates catalog given() .contentType(ContentType.JSON) @@ -200,233 +136,44 @@ void createCatalogAuthorization() throws Exception { @Test void grantTablePrivilegesAuthorization() throws Exception { String rootToken = baseRootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); - String catalogName = "opa-grant-cat-" + UUID.randomUUID().toString().replace("-", ""); - String namespace = "ns_" + UUID.randomUUID().toString().replace("-", ""); - String tableName = "tbl_" + UUID.randomUUID().toString().replace("-", ""); String catalogRole = "role_" + UUID.randomUUID().toString().replace("-", ""); - Path tempDir = Files.createTempDirectory("opa-grant"); - String baseLocation = tempDir.toUri().toString(); - String allowedPrefix = baseLocation + (baseLocation.endsWith("/") ? "" : "/") + namespace; - createFileCatalog( - rootToken, catalogName, baseLocation, List.of(allowedPrefix, allowedPrefix + "/")); - - createNamespace(rootToken, catalogName, namespace); - - Map registerPayload = - buildRegisterTableRequest(tableName, baseLocation, namespace); - Map grantRequest = - Map.of( - "grant", - Map.of( - "type", - "table", - "namespace", - List.of(namespace), - "tableName", - tableName, - "privilege", - "TABLE_READ_DATA")); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(registerPayload)) - .post("/api/catalog/v1/{cat}/namespaces/{ns}/register", catalogName, namespace) - .then() - .statusCode(200); - + // RBAC grant management is denied under OPA given() .contentType(ContentType.JSON) .header("Authorization", "Bearer " + rootToken) .body(toJson(Map.of("name", catalogRole, "properties", Map.of()))) - .post("/api/management/v1/catalogs/{cat}/catalog-roles", catalogName) - .then() - .statusCode(201); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + strangerToken) - .body(toJson(grantRequest)) - .put( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/grants", - catalogName, - catalogRole) + .post("/api/management/v1/catalogs/{cat}/catalog-roles", baseCatalogName) .then() .statusCode(403); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(grantRequest)) - .put( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/grants", - catalogName, - catalogRole) - .then() - .statusCode(201); } @Test void listAssigneePrincipalRolesForCatalogRole() { String rootToken = baseRootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); - - String catalogRole = "opa-cat-role-" + UUID.randomUUID().toString().replace("-", ""); - String principalRole = "opa-pr-role-" + UUID.randomUUID().toString().replace("-", ""); + // RBAC principal role management is denied under OPA given() .contentType(ContentType.JSON) .header("Authorization", "Bearer " + rootToken) - .body(toJson(Map.of("name", catalogRole, "properties", Map.of()))) - .post("/api/management/v1/catalogs/{cat}/catalog-roles", baseCatalogName) - .then() - .statusCode(201); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(Map.of("name", principalRole, "properties", Map.of()))) + .body(toJson(Map.of("name", "opa-pr-role-deny", "properties", Map.of()))) .post("/api/management/v1/principal-roles") .then() - .statusCode(201); - - Map grantRequest = - Map.of("catalogRole", Map.of("name", catalogRole, "properties", Map.of())); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(grantRequest)) - .put( - "/api/management/v1/principal-roles/{pr}/catalog-roles/{cat}", - principalRole, - baseCatalogName) - .then() - .statusCode(201); - - given() - .header("Authorization", "Bearer " + strangerToken) - .get( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/principal-roles", - baseCatalogName, - catalogRole) - .then() .statusCode(403); - - given() - .header("Authorization", "Bearer " + rootToken) - .get( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/principal-roles", - baseCatalogName, - catalogRole) - .then() - .statusCode(200); } @Test void listGrantsForCatalogRole() throws Exception { String rootToken = baseRootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); - String catalogName = "opa-grant-list-cat-" + UUID.randomUUID().toString().replace("-", ""); - String namespace = "ns_" + UUID.randomUUID().toString().replace("-", ""); - String tableName = "tbl_" + UUID.randomUUID().toString().replace("-", ""); String catalogRole = "role_" + UUID.randomUUID().toString().replace("-", ""); - Path tempDir = Files.createTempDirectory("opa-grant-list"); - String baseLocation = tempDir.toUri().toString(); - String allowedPrefix = baseLocation + (baseLocation.endsWith("/") ? "" : "/") + namespace; - createFileCatalog( - rootToken, catalogName, baseLocation, List.of(allowedPrefix, allowedPrefix + "/")); - createNamespace(rootToken, catalogName, namespace); - - Map registerPayload = - buildRegisterTableRequest(tableName, baseLocation, namespace); - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(registerPayload)) - .post("/api/catalog/v1/{cat}/namespaces/{ns}/register", catalogName, namespace) - .then() - .statusCode(200); - + // RBAC catalog role management is denied under OPA given() .contentType(ContentType.JSON) .header("Authorization", "Bearer " + rootToken) .body(toJson(Map.of("name", catalogRole, "properties", Map.of()))) - .post("/api/management/v1/catalogs/{cat}/catalog-roles", catalogName) - .then() - .statusCode(201); - - Map grantRequest = - Map.of( - "grant", - Map.of( - "type", - "table", - "namespace", - List.of(namespace), - "tableName", - tableName, - "privilege", - "TABLE_READ_DATA")); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + rootToken) - .body(toJson(grantRequest)) - .put( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/grants", - catalogName, - catalogRole) - .then() - .statusCode(201); - - given() - .header("Authorization", "Bearer " + strangerToken) - .get( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/grants", - catalogName, - catalogRole) + .post("/api/management/v1/catalogs/{cat}/catalog-roles", baseCatalogName) .then() .statusCode(403); - - given() - .header("Authorization", "Bearer " + rootToken) - .get( - "/api/management/v1/catalogs/{cat}/catalog-roles/{role}/grants", - catalogName, - catalogRole) - .then() - .statusCode(200); - } - - private Map buildRegisterTableRequest( - String tableName, String baseLocation, String namespace) throws Exception { - String tableLocation = - baseLocation + (baseLocation.endsWith("/") ? "" : "/") + namespace + "/" + tableName; - Schema schema = - new Schema( - Types.NestedField.required(1, "id", Types.LongType.get()), - Types.NestedField.required(2, "data", Types.StringType.get())); - PartitionSpec spec = PartitionSpec.unpartitioned(); - TableMetadata metadata = TableMetadata.newTableMetadata(schema, spec, tableLocation, Map.of()); - Path metadataPath = - Path.of( - URI.create( - tableLocation - + (tableLocation.endsWith("/") ? "" : "/") - + "metadata/v1.metadata.json")); - Files.createDirectories(metadataPath.getParent()); - Files.writeString(metadataPath, TableMetadataParser.toJson(metadata)); - - return Map.of( - "name", - tableName, - "metadata-location", - metadataPath.toUri().toString(), - "stage-create", - false); } } diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaFileTokenIntegrationTest.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaFileTokenIntegrationTest.java index 81c2a04364..101a84769a 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaFileTokenIntegrationTest.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaFileTokenIntegrationTest.java @@ -19,10 +19,10 @@ package org.apache.polaris.extension.auth.opa.test; import static io.restassured.RestAssured.given; -import static org.assertj.core.api.Assertions.assertThatNoException; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; +import java.util.Map; import org.junit.jupiter.api.Test; /** @@ -50,37 +50,31 @@ void testOpaAllowsRootUser() { } @Test - void testCreatePrincipalAndGetToken() { - // Test the helper method createPrincipalAndGetToken - // useful for debugging and ensuring that the helper method works correctly - assertThatNoException().isThrownBy(() -> createPrincipalAndGetToken("test-user")); - } - - @Test - void testOpaPolicyDeniesStrangerUser() { - // Create a "stranger" principal and get its access token - String strangerToken = createPrincipalAndGetToken("stranger"); + void testOpaDeniesRbacPrincipalCreation() { + String rootToken = getRootToken(); - // Use the stranger token to test OPA authorization - should be denied + Map createPrincipalBody = + Map.of("principal", Map.of("name", "opa-test-user", "properties", Map.of())); given() - .header("Authorization", "Bearer " + strangerToken) + .contentType("application/json") + .header("Authorization", "Bearer " + rootToken) + .body(toJson(createPrincipalBody)) .when() - .get("/api/management/v1/catalogs") + .post("/api/management/v1/principals") .then() - .statusCode(403); // Should be forbidden by OPA policy - stranger is denied + .statusCode(403); } @Test - void testOpaAllowsAdminUser() { - // Create an "admin" principal and get its access token - String adminToken = createPrincipalAndGetToken("admin"); + void testOpaDeniesRbacPrincipalRoleCreation() { + String rootToken = getRootToken(); - // Use the admin token to test OPA authorization - should be allowed given() - .header("Authorization", "Bearer " + adminToken) - .when() - .get("/api/management/v1/catalogs") + .contentType("application/json") + .header("Authorization", "Bearer " + rootToken) + .body(toJson(Map.of("name", "opa-test-role", "properties", Map.of()))) + .post("/api/management/v1/principal-roles") .then() - .statusCode(200); // Should succeed - admin user is allowed by policy + .statusCode(403); } } diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaGenericTableHandlerIT.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaGenericTableHandlerIT.java index 54a8d41a45..7772164171 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaGenericTableHandlerIT.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaGenericTableHandlerIT.java @@ -65,19 +65,11 @@ void setupBaseCatalog(@TempDir Path tempDir) throws Exception { @Test void genericTableCreateAndDropAuthorization() throws Exception { String rootToken = this.rootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); String tableName = "gt_" + UUID.randomUUID().toString().replace("-", ""); Map tablePayload = Map.of("name", tableName, "format", "ICEBERG", "doc", "doc", "properties", Map.of()); - // Stranger cannot list generic tables - given() - .header("Authorization", "Bearer " + strangerToken) - .get("/api/catalog/polaris/v1/{cat}/namespaces/{ns}/generic-tables", catalogName, namespace) - .then() - .statusCode(403); - // Root lists generic tables (initially empty) given() .header("Authorization", "Bearer " + rootToken) @@ -85,16 +77,6 @@ void genericTableCreateAndDropAuthorization() throws Exception { .then() .statusCode(200); - // Stranger cannot create generic table - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + strangerToken) - .body(toJson(tablePayload)) - .post( - "/api/catalog/polaris/v1/{cat}/namespaces/{ns}/generic-tables", catalogName, namespace) - .then() - .statusCode(403); - // Root creates generic table given() .contentType(ContentType.JSON) diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIcebergCatalogHandlerIT.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIcebergCatalogHandlerIT.java index 882de5adfc..0ea47a05f0 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIcebergCatalogHandlerIT.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIcebergCatalogHandlerIT.java @@ -78,16 +78,8 @@ void setupBaseCatalog(@TempDir Path tempDir) throws Exception { @Test void tableCreateAndDropAuthorization() throws Exception { String rootToken = this.rootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); String tableName = "tbl_" + UUID.randomUUID().toString().replace("-", ""); - // Stranger cannot list namespaces for the catalog - given() - .header("Authorization", "Bearer " + strangerToken) - .get("/api/catalog/v1/{cat}/namespaces", catalogName) - .then() - .statusCode(403); - // Root can list namespaces given() .header("Authorization", "Bearer " + rootToken) @@ -98,15 +90,6 @@ void tableCreateAndDropAuthorization() throws Exception { Map createTableRequest = buildCreateTableRequest(tableName, baseLocation, namespace); - // Stranger cannot register table - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + strangerToken) - .body(toJson(createTableRequest)) - .post("/api/catalog/v1/{cat}/namespaces/{ns}/register", catalogName, namespace) - .then() - .statusCode(403); - // Root registers table given() .contentType(ContentType.JSON) diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIntegrationTest.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIntegrationTest.java index 1cf4cb6deb..6fd7cc8c93 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIntegrationTest.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaIntegrationTest.java @@ -19,10 +19,10 @@ package org.apache.polaris.extension.auth.opa.test; import static io.restassured.RestAssured.given; -import static org.assertj.core.api.Assertions.assertThatNoException; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; +import java.util.Map; import org.junit.jupiter.api.Test; @QuarkusTest @@ -44,37 +44,31 @@ void testOpaAllowsRootUser() { } @Test - void testCreatePrincipalAndGetToken() { - // Test the helper method createPrincipalAndGetToken - // useful for debugging and ensuring that the helper method works correctly - assertThatNoException().isThrownBy(() -> createPrincipalAndGetToken("test-user")); - } - - @Test - void testOpaPolicyDeniesStrangerUser() { - // Create a "stranger" principal and get its access token - String strangerToken = createPrincipalAndGetToken("stranger"); + void testOpaDeniesRbacPrincipalCreation() { + String rootToken = getRootToken(); - // Use the stranger token to test OPA authorization - should be denied + Map createPrincipalBody = + Map.of("principal", Map.of("name", "opa-test-user", "properties", Map.of())); given() - .header("Authorization", "Bearer " + strangerToken) + .contentType("application/json") + .header("Authorization", "Bearer " + rootToken) + .body(toJson(createPrincipalBody)) .when() - .get("/api/management/v1/catalogs") + .post("/api/management/v1/principals") .then() - .statusCode(403); // Should be forbidden by OPA policy - stranger is denied + .statusCode(403); } @Test - void testOpaAllowsAdminUser() { - // Create an "admin" principal and get its access token - String adminToken = createPrincipalAndGetToken("admin"); + void testOpaDeniesRbacPrincipalRoleCreation() { + String rootToken = getRootToken(); - // Use the admin token to test OPA authorization - should be allowed given() - .header("Authorization", "Bearer " + adminToken) - .when() - .get("/api/management/v1/catalogs") + .contentType("application/json") + .header("Authorization", "Bearer " + rootToken) + .body(toJson(Map.of("name", "opa-test-role", "properties", Map.of()))) + .post("/api/management/v1/principal-roles") .then() - .statusCode(200); // Should succeed - admin user is allowed by policy + .statusCode(403); } } diff --git a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaPolicyCatalogHandlerIT.java b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaPolicyCatalogHandlerIT.java index 6400a47c04..efc4555814 100644 --- a/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaPolicyCatalogHandlerIT.java +++ b/extensions/auth/opa/tests/src/intTest/java/org/apache/polaris/extension/auth/opa/test/OpaPolicyCatalogHandlerIT.java @@ -65,7 +65,6 @@ void setupBaseCatalog(@TempDir Path tempDir) throws Exception { @Test void policyListAndAttachAuthorization() throws Exception { String rootToken = this.rootToken; - String strangerToken = createPrincipalAndGetToken("stranger-" + UUID.randomUUID()); String policyName = "pol_" + UUID.randomUUID().toString().replace("-", ""); Map createPolicyRequest = @@ -91,13 +90,6 @@ void policyListAndAttachAuthorization() throws Exception { .then() .statusCode(200); - // Stranger cannot list policies - given() - .header("Authorization", "Bearer " + strangerToken) - .get("/api/catalog/polaris/v1/{cat}/namespaces/{ns}/policies", catalogName, namespace) - .then() - .statusCode(403); - // Root lists policies given() .header("Authorization", "Bearer " + rootToken) @@ -108,19 +100,6 @@ void policyListAndAttachAuthorization() throws Exception { Map attachRequest = Map.of("target", Map.of("type", "catalog", "path", List.of()), "parameters", Map.of()); - // Stranger cannot attach policy - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + strangerToken) - .body(toJson(attachRequest)) - .put( - "/api/catalog/polaris/v1/{cat}/namespaces/{ns}/policies/{policy}/mappings", - catalogName, - namespace, - policyName) - .then() - .statusCode(403); - // Root attaches policy to catalog given() .contentType(ContentType.JSON) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java index 637673c6a0..cb46ed75e9 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/AuthorizationCallContext.java @@ -18,7 +18,8 @@ */ package org.apache.polaris.core.auth; -import jakarta.annotation.Nullable; +import jakarta.annotation.Nonnull; +import java.util.Objects; import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; /** @@ -30,17 +31,15 @@ public class AuthorizationCallContext { private PolarisResolutionManifest resolutionManifest; - public AuthorizationCallContext() {} - - public AuthorizationCallContext(@Nullable PolarisResolutionManifest resolutionManifest) { - this.resolutionManifest = resolutionManifest; + public AuthorizationCallContext(@Nonnull PolarisResolutionManifest resolutionManifest) { + this.resolutionManifest = Objects.requireNonNull(resolutionManifest, "resolutionManifest"); } - public @Nullable PolarisResolutionManifest getResolutionManifest() { + public @Nonnull PolarisResolutionManifest getResolutionManifest() { return resolutionManifest; } - public void setResolutionManifest(@Nullable PolarisResolutionManifest resolutionManifest) { - this.resolutionManifest = resolutionManifest; + public void setResolutionManifest(@Nonnull PolarisResolutionManifest resolutionManifest) { + this.resolutionManifest = Objects.requireNonNull(resolutionManifest, "resolutionManifest"); } } 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 1da0890d55..94fab8efb3 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 @@ -23,99 +23,29 @@ import java.util.List; import java.util.Set; import org.apache.polaris.core.entity.PolarisBaseEntity; -import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; /** Interface for invoking authorization checks. */ public interface PolarisAuthorizer { /** * Pre-authorization hook for resolving authorizer-specific inputs. * - *

Default implementation is a no-op to preserve legacy behavior. + *

Implementations may resolve or validate any inputs needed to make an authorization decision. */ - default void preAuthorize( - @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) {} + void preAuthorize(@Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request); - /** - * Core authorization entry point for the new SPI. - * - *

Default implementation delegates to legacy {@code authorizeOrThrow(...)} to preserve - * behavior for existing authorizers. - */ - default void authorize( - @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { - authorizeInternal(ctx, request, true /* throwOnDeny */); - } + /** Core authorization entry point for the new SPI. */ + void authorize(@Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request); /** * Backwards-compatible external API that throws on deny for legacy call sites. * - *

Default implementation delegates to shared authorization logic. + *

Default implementation delegates to {@link #authorize(AuthorizationCallContext, + * AuthorizationRequest)}. */ default void authorizeOrThrow( @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { - authorizeInternal(ctx, request, true /* throwOnDeny */); - } - - /** - * Shared authorization logic used by both new and legacy entry points. - * - *

Default implementation adapts intent inputs to the legacy RBAC SPI to preserve behavior. - */ - private void authorizeInternal( - @Nonnull AuthorizationCallContext ctx, - @Nonnull AuthorizationRequest request, - boolean throwOnDeny) { - PolarisResolutionManifest manifest = ctx.getResolutionManifest(); - Set activatedEntities = - manifest == null ? Set.of() : manifest.getAllActivatedCatalogRoleAndPrincipalRoles(); - List resolvedTargets = - resolveSecurables(manifest, request.getTargets()); - List resolvedSecondaries = - resolveSecurables(manifest, request.getSecondaries()); - if (throwOnDeny) { - authorizeOrThrow( - request.getPrincipal(), - activatedEntities, - request.getOperation(), - resolvedTargets, - resolvedSecondaries); - } - } - - private static List resolveSecurables( - PolarisResolutionManifest manifest, List securables) { - if (securables == null) { - return null; - } - if (manifest == null) { - return null; - } - List resolved = new java.util.ArrayList<>(securables.size()); - for (PolarisSecurable securable : securables) { - PolarisEntityType type = securable.getEntityType(); - switch (type) { - case ROOT: - resolved.add(manifest.getResolvedRootContainerEntityAsPath()); - break; - case CATALOG: - if (manifest.hasTopLevelName(securable.getName(), type)) { - resolved.add(manifest.getResolvedTopLevelEntity(securable.getName(), type)); - } else { - resolved.add(manifest.getResolvedReferenceCatalogEntity()); - } - break; - case PRINCIPAL: - case PRINCIPAL_ROLE: - resolved.add(manifest.getResolvedTopLevelEntity(securable.getName(), type)); - break; - default: - resolved.add(manifest.getResolvedPath(securable, true)); - break; - } - } - return resolved; + authorize(ctx, request); } @Deprecated 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 09ce1a5d1f..2828f5147f 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 @@ -127,6 +127,7 @@ import com.google.common.collect.SetMultimap; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Set; @@ -137,6 +138,7 @@ import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.core.entity.PolarisEntityCore; +import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -916,7 +918,7 @@ public boolean hasTransitivePrivilege( public void preAuthorize( @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { PolarisResolutionManifest manifest = ctx.getResolutionManifest(); - if (manifest == null || manifest.hasResolution()) { + if (manifest.hasResolution()) { return; } @@ -930,4 +932,56 @@ public void preAuthorize( Resolvable.REQUESTED_PATHS, Resolvable.TOP_LEVEL_ENTITIES)); } + + @Override + public void authorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + PolarisResolutionManifest manifest = ctx.getResolutionManifest(); + Set activatedEntities = + manifest == null ? Set.of() : manifest.getAllActivatedCatalogRoleAndPrincipalRoles(); + List resolvedTargets = + resolveSecurables(manifest, request.getTargets()); + List resolvedSecondaries = + resolveSecurables(manifest, request.getSecondaries()); + authorizeOrThrow( + request.getPrincipal(), + activatedEntities, + request.getOperation(), + resolvedTargets, + resolvedSecondaries); + } + + private static List resolveSecurables( + PolarisResolutionManifest manifest, List securables) { + if (securables == null) { + return null; + } + if (manifest == null) { + return null; + } + List resolved = new ArrayList<>(securables.size()); + for (PolarisSecurable securable : securables) { + PolarisEntityType type = securable.getEntityType(); + switch (type) { + case ROOT: + resolved.add(manifest.getResolvedRootContainerEntityAsPath()); + break; + case CATALOG: + if (manifest.hasTopLevelName(securable.getName(), type)) { + resolved.add(manifest.getResolvedTopLevelEntity(securable.getName(), type)); + } else { + resolved.add(manifest.getResolvedReferenceCatalogEntity()); + } + break; + case PRINCIPAL: + case PRINCIPAL_ROLE: + resolved.add(manifest.getResolvedTopLevelEntity(securable.getName(), type)); + break; + default: + resolved.add(manifest.getResolvedPath(securable, true)); + break; + } + } + return resolved; + } } diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index 30303121e1..0392d7720e 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -31,6 +31,7 @@ import java.time.Clock; import java.time.Instant; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -38,6 +39,9 @@ import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthorizationCallContext; +import org.apache.polaris.core.auth.AuthorizationRequest; +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.catalog.ExternalCatalogFactory; @@ -47,11 +51,13 @@ import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.credentials.connection.ConnectionCredentialVendor; +import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.cache.EntityCache; import org.apache.polaris.core.persistence.dao.entity.CreatePrincipalResult; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -208,7 +214,41 @@ public Builder withEventDelegator(boolean useEventDelegator) { public TestServices build() { PolarisConfigurationStore configurationStore = new MockedConfigurationStore(config); - PolarisAuthorizer authorizer = Mockito.mock(PolarisAuthorizer.class); + PolarisAuthorizer authorizer = + new PolarisAuthorizer() { + @Override + public void preAuthorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) { + if (ctx.getResolutionManifest() != null + && !ctx.getResolutionManifest().hasResolution()) { + ctx.getResolutionManifest().resolveAll(); + } + } + + @Override + public void authorize( + @Nonnull AuthorizationCallContext ctx, @Nonnull AuthorizationRequest request) {} + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable PolarisResolvedPathWrapper target, + @Nullable PolarisResolvedPathWrapper secondary) { + // No-op for tests. + } + + @Override + public void authorizeOrThrow( + @Nonnull PolarisPrincipal polarisPrincipal, + @Nonnull Set activatedEntities, + @Nonnull PolarisAuthorizableOperation authzOp, + @Nullable List targets, + @Nullable List secondaries) { + // No-op for tests. + } + }; // Application level PolarisStorageIntegrationProviderImpl storageIntegrationProvider = From 565fc65c1f14aa811d3e44a4c92b599a78bb42a3 Mon Sep 17 00:00:00 2001 From: "Sung Yun (CODE SIGNING KEY)" Date: Fri, 6 Feb 2026 21:54:37 -0500 Subject: [PATCH 3/3] merge fixes --- .../auth/opa/OpaPolarisAuthorizer.java | 2 +- .../catalog/common/CatalogHandler.java | 29 ++++++++------- .../iceberg/IcebergCatalogHandler.java | 10 +++--- .../catalog/policy/PolicyCatalogHandler.java | 36 ++++++++++--------- 4 files changed, 42 insertions(+), 35 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 bc670ba7d1..33bd9b7809 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 @@ -52,7 +52,7 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest.Resolvable; +import org.apache.polaris.core.persistence.resolver.Resolvable; import org.apache.polaris.extension.auth.opa.model.ImmutableActor; import org.apache.polaris.extension.auth.opa.model.ImmutableContext; import org.apache.polaris.extension.auth.opa.model.ImmutableOpaAuthorizationInput; diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java index f0c73c2220..610992c58d 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java @@ -177,8 +177,8 @@ protected void authorizeBasicNamespaceOperationOrThrow( if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } - authorizer().authorize( - authzContext, newAuthorizationRequest(op, List.of(namespaceSecurable), null)); + authorizer() + .authorize(authzContext, newAuthorizationRequest(op, List.of(namespaceSecurable), null)); initializeCatalog(); } @@ -210,8 +210,9 @@ protected void authorizeCreateNamespaceUnderNamespaceOperationOrThrow( if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", parentNamespace); } - authorizer().authorize( - authzContext, newAuthorizationRequest(op, List.of(parentNamespaceSecurable), null)); + authorizer() + .authorize( + authzContext, newAuthorizationRequest(op, List.of(parentNamespaceSecurable), null)); initializeCatalog(); } @@ -247,8 +248,8 @@ protected void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } - authorizer().authorize( - authzContext, newAuthorizationRequest(op, List.of(namespaceSecurable), null)); + authorizer() + .authorize(authzContext, newAuthorizationRequest(op, List.of(namespaceSecurable), null)); initializeCatalog(); } @@ -276,8 +277,9 @@ protected void ensureResolutionManifestForTable(TableIdentifier identifier) { tableSecurable); resolutionManifest.addPassthroughAlias(tableSecurable, identifier); AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); - authorizer().preAuthorize( - authzContext, newAuthorizationRequest(PolarisAuthorizableOperation.LOAD_TABLE)); + authorizer() + .preAuthorize( + authzContext, newAuthorizationRequest(PolarisAuthorizableOperation.LOAD_TABLE)); } } @@ -303,8 +305,8 @@ protected void authorizeBasicTableLikeOperationsOrThrow( } for (PolarisAuthorizableOperation op : ops) { - authorizer().authorize( - authzContext, newAuthorizationRequest(op, List.of(targetSecurable), null)); + authorizer() + .authorize(authzContext, newAuthorizationRequest(op, List.of(targetSecurable), null)); } initializeCatalog(); @@ -421,9 +423,10 @@ protected void authorizeRenameTableLikeOperationOrThrow( break; } - authorizer().authorize( - authzContext, - newAuthorizationRequest(op, List.of(srcSecurable), List.of(dstNamespaceSecurable))); + authorizer() + .authorize( + authzContext, + newAuthorizationRequest(op, List.of(srcSecurable), List.of(dstNamespaceSecurable))); initializeCatalog(); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java index 0d73ceb89e..4c62abc7e8 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java @@ -104,7 +104,6 @@ import org.apache.polaris.core.persistence.pagination.Page; import org.apache.polaris.core.persistence.pagination.PageToken; import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; -import org.apache.polaris.core.persistence.resolver.Resolver; import org.apache.polaris.core.persistence.resolver.ResolverFactory; import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.rest.PolarisEndpoints; @@ -1330,10 +1329,11 @@ public ConfigResponse getConfig() { } PolarisResolutionManifest manifest = newResolutionManifest(); AuthorizationCallContext authzContext = new AuthorizationCallContext(manifest); - authorizer().preAuthorize( - authzContext, - new AuthorizationRequest( - polarisPrincipal(), PolarisAuthorizableOperation.GET_CATALOG, null, null)); + authorizer() + .preAuthorize( + authzContext, + new AuthorizationRequest( + polarisPrincipal(), PolarisAuthorizableOperation.GET_CATALOG, null, null)); ResolverStatus resolverStatus = manifest.getResolverStatus(); if (resolverStatus == null || !resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java index e26ccd3831..d55d7aefdd 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java @@ -153,17 +153,18 @@ private void authorizeBasicPolicyOperationOrThrow( policySecurable); resolutionManifest.addPassthroughAlias(policySecurable, identifier); AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); - authorizer().preAuthorize( - authzContext, new AuthorizationRequest(polarisPrincipal(), op, null, null)); + authorizer() + .preAuthorize(authzContext, new AuthorizationRequest(polarisPrincipal(), op, null, null)); PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(policySecurable, true); if (target == null) { throw new NoSuchPolicyException(String.format("Policy does not exist: %s", identifier)); } - authorizer().authorize( - authzContext, - new AuthorizationRequest(polarisPrincipal(), op, List.of(policySecurable), null)); + authorizer() + .authorize( + authzContext, + new AuthorizationRequest(polarisPrincipal(), op, List.of(policySecurable), null)); initializeCatalog(); } @@ -194,17 +195,18 @@ private void authorizeGetApplicablePoliciesOperationOrThrow( private void authorizeBasicCatalogOperationOrThrow(PolarisAuthorizableOperation op) { resolutionManifest = newResolutionManifest(); AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); - authorizer().preAuthorize( - authzContext, new AuthorizationRequest(polarisPrincipal(), op, null, null)); + authorizer() + .preAuthorize(authzContext, new AuthorizationRequest(polarisPrincipal(), op, null, null)); PolarisResolvedPathWrapper targetCatalog = resolutionManifest.getResolvedReferenceCatalogEntity(); if (targetCatalog == null) { throw new NotFoundException("Catalog not found"); } - authorizer().authorize( - authzContext, - new AuthorizationRequest(polarisPrincipal(), op, List.of(newCatalogSecurable()), null)); + authorizer() + .authorize( + authzContext, + new AuthorizationRequest(polarisPrincipal(), op, List.of(newCatalogSecurable()), null)); initializeCatalog(); } @@ -249,8 +251,9 @@ private void authorizePolicyMappingOperationOrThrow( PolarisAuthorizableOperation preAuthOp = determinePolicyMappingOperation(target.getType(), isAttach); AuthorizationCallContext authzContext = new AuthorizationCallContext(resolutionManifest); - authorizer().preAuthorize( - authzContext, new AuthorizationRequest(polarisPrincipal(), preAuthOp, null, null)); + authorizer() + .preAuthorize( + authzContext, new AuthorizationRequest(polarisPrincipal(), preAuthOp, null, null)); ResolverStatus status = resolutionManifest.getResolverStatus(); throwNotFoundExceptionIfFailToResolve(status, identifier); @@ -267,10 +270,11 @@ private void authorizePolicyMappingOperationOrThrow( PolarisAuthorizableOperation op = determinePolicyMappingOperation(target, targetWrapper, isAttach); - authorizer().authorize( - authzContext, - new AuthorizationRequest( - polarisPrincipal(), op, List.of(policySecurable), List.of(targetSecurable))); + authorizer() + .authorize( + authzContext, + new AuthorizationRequest( + polarisPrincipal(), op, List.of(policySecurable), List.of(targetSecurable))); initializeCatalog(); }