From 10d4a70c29462093825cd79bf46117bdeba27b9b Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Mon, 9 Dec 2024 17:05:24 +0100 Subject: [PATCH] Hide entity resolution from service implementations Currently all service implementations, both the Polaris admin service and the (Iceberg REST) catalog service need knowledge about the implementation details about _how exactly_ entities and their instances are managed. This is rather a persistence implementation concern, not a service implementation concern. Services should tell, if necessary, tell a properly abstracted builder which entities it will need. This change abstracts the entity resolution via interfaces and concrete implementation classes, eliminating the hard dependency of services to concrete persistence specific implementations. --- ...pseLinkPolarisMetaStoreManagerFactory.java | 2 +- ...olarisEclipseLinkMetaStoreSessionImpl.java | 2 +- ...olarisEclipseLinkMetaStoreManagerTest.java | 2 +- .../polaris/core/auth/PolarisAuthorizer.java | 2 +- .../core/auth/PolarisAuthorizerImpl.java | 4 +- .../core/entity/PolarisEntitySubType.java | 17 +- .../core/entity/PolarisEntityType.java | 35 +- .../polaris/core/entity/TaskEntity.java | 2 +- .../persistence/EntityNotFoundException.java | 77 ++ .../persistence/PolarisEntityManager.java | 25 +- .../persistence/PolarisMetaStoreManager.java | 12 + .../{ => impl}/PolarisEntityResolver.java | 3 +- .../PolarisMetaStoreManagerImpl.java | 26 +- .../{ => impl}/PolarisObjectMapperUtil.java | 10 +- .../PolarisResolutionManifest.java | 287 ++---- .../PolarisResolutionManifestBuilder.java | 191 ++++ .../TransactionWorkspaceMetaStoreManager.java | 18 +- .../LocalPolarisMetaStoreManagerFactory.java | 7 +- .../PolarisTreeMapMetaStoreSessionImpl.java | 5 +- .../inmem}/PolarisTreeMapStore.java | 2 +- .../PolarisResolvedPathWrapper.java | 2 +- .../resolution/ResolutionManifest.java | 82 ++ .../resolution/ResolutionManifestBuilder.java | 94 ++ .../ResolvedPolarisEntity.java | 2 +- .../PolarisResolutionManifestCatalogView.java | 38 - .../core/persistence/resolver/Resolver.java | 928 +----------------- .../persistence/resolver/ResolverBuilder.java | 92 ++ .../resolver/ResolverBuilderImpl.java | 797 +++++++++++++++ .../resolver/ResolverException.java | 71 ++ .../persistence/resolver/ResolverImpl.java | 118 +++ .../persistence/resolver/ResolverStatus.java | 106 -- .../core/persistence/EntityCacheTest.java | 3 + .../core/persistence/ResolverTest.java | 334 +++---- .../PolarisObjectMapperUtilTest.java | 2 +- .../PolarisTreeMapMetaStoreManagerTest.java | 5 +- .../cache/StorageCredentialCacheTest.java | 8 +- .../BasePolarisMetaStoreManagerTest.java | 1 + .../PolarisTestMetaStoreManager.java | 1 + .../service/admin/PolarisAdminService.java | 211 ++-- .../service/catalog/BasePolarisCatalog.java | 140 ++- .../catalog/IcebergCatalogAdapter.java | 14 +- .../catalog/PolarisCatalogHandlerWrapper.java | 200 ++-- .../context/CallContextCatalogFactory.java | 4 +- .../PolarisCallContextCatalogFactory.java | 8 +- ...nMemoryPolarisMetaStoreManagerFactory.java | 6 +- .../service/admin/PolarisAuthzTestBase.java | 4 +- ...PolarisCatalogHandlerWrapperAuthzTest.java | 4 +- .../PolarisPassthroughResolutionView.java | 175 ++-- 48 files changed, 2303 insertions(+), 1876 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/EntityNotFoundException.java rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => impl}/PolarisEntityResolver.java (99%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => impl}/PolarisMetaStoreManagerImpl.java (98%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => impl}/PolarisObjectMapperUtil.java (98%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{resolver => impl}/PolarisResolutionManifest.java (53%) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisResolutionManifestBuilder.java rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => impl}/TransactionWorkspaceMetaStoreManager.java (94%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => local}/LocalPolarisMetaStoreManagerFactory.java (96%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => local/inmem}/PolarisTreeMapMetaStoreSessionImpl.java (98%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => local/inmem}/PolarisTreeMapStore.java (99%) rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => resolution}/PolarisResolvedPathWrapper.java (97%) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifest.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifestBuilder.java rename polaris-core/src/main/java/org/apache/polaris/core/persistence/{ => resolution}/ResolvedPolarisEntity.java (98%) delete mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilder.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilderImpl.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverException.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverImpl.java delete mode 100644 polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverStatus.java rename polaris-core/src/test/java/org/apache/polaris/core/persistence/{ => impl}/PolarisObjectMapperUtilTest.java (98%) rename polaris-core/src/test/java/org/apache/polaris/core/persistence/{ => local/inmem}/PolarisTreeMapMetaStoreManagerTest.java (86%) diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java index 21e466b78..aa6f0959a 100644 --- a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/EclipseLinkPolarisMetaStoreManagerFactory.java @@ -24,9 +24,9 @@ import jakarta.inject.Inject; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.context.RealmContext; -import org.apache.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisMetaStoreSession; +import org.apache.polaris.core.persistence.local.LocalPolarisMetaStoreManagerFactory; import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider; /** diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java index 710695214..733badcd6 100644 --- a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java @@ -64,10 +64,10 @@ import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; import org.apache.polaris.core.exceptions.AlreadyExistsException; -import org.apache.polaris.core.persistence.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.persistence.PolarisMetaStoreSession; import org.apache.polaris.core.persistence.PrincipalSecretsGenerator; import org.apache.polaris.core.persistence.RetryOnConcurrencyException; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; import org.apache.polaris.core.storage.PolarisStorageIntegration; import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider; diff --git a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java index f0a307e61..78ae016f8 100644 --- a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java +++ b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java @@ -33,8 +33,8 @@ import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; import org.apache.polaris.core.persistence.BasePolarisMetaStoreManagerTest; -import org.apache.polaris.core.persistence.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.persistence.PolarisTestMetaStoreManager; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; import org.apache.polaris.jpa.models.ModelPrincipalSecrets; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; 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 31e69b083..937895295 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,7 +23,7 @@ import java.util.List; import java.util.Set; import org.apache.polaris.core.entity.PolarisBaseEntity; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.PolarisResolvedPathWrapper; /** Interface for invoking authorization checks. */ public interface PolarisAuthorizer { 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 96f449acb..72dff571d 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 @@ -106,8 +106,8 @@ import org.apache.polaris.core.entity.PolarisEntityCore; import org.apache.polaris.core.entity.PolarisGrantRecord; 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.resolution.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.ResolvedPolarisEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntitySubType.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntitySubType.java index c206d0789..483724cba 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntitySubType.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntitySubType.java @@ -27,11 +27,11 @@ public enum PolarisEntitySubType { // ANY_SUBTYPE is not stored but is used to indicate that any subtype entities should be // returned, for example when doing a list operation or checking if a table like object of // name X exists - ANY_SUBTYPE(-1, null), + ANY_SUBTYPE(-1, null, "Table or view"), // the NULL value is used when an entity has no subtype, i.e. NOT_APPLICABLE really - NULL_SUBTYPE(0, null), - TABLE(2, PolarisEntityType.TABLE_LIKE), - VIEW(3, PolarisEntityType.TABLE_LIKE); + NULL_SUBTYPE(0, null, "(null)"), + TABLE(2, PolarisEntityType.TABLE_LIKE, "Table"), + VIEW(3, PolarisEntityType.TABLE_LIKE, "View"); // to efficiently map the code of a subtype to its corresponding subtype enum, use a reverse // array which is initialized below @@ -63,10 +63,13 @@ public enum PolarisEntitySubType { // parent type for this entity private final PolarisEntityType parentType; - PolarisEntitySubType(int code, PolarisEntityType parentType) { + private final String readableName; + + PolarisEntitySubType(int code, PolarisEntityType parentType, String readableName) { // remember the id of this entity this.code = code; this.parentType = parentType; + this.readableName = readableName; } /** @@ -111,4 +114,8 @@ public PolarisEntityType getParentType() { return null; } + + public String readableName() { + return readableName; + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java index af50eed6f..dc6898e7b 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java @@ -24,17 +24,17 @@ /** Types of entities with their id */ public enum PolarisEntityType { - NULL_TYPE(0, null, false, false), - ROOT(1, null, false, false), - PRINCIPAL(2, ROOT, true, false), - PRINCIPAL_ROLE(3, ROOT, true, false), - CATALOG(4, ROOT, false, false), - CATALOG_ROLE(5, CATALOG, true, false), - NAMESPACE(6, CATALOG, false, true), + NULL_TYPE(0, null, false, false, "(null)"), + ROOT(1, null, false, false, "Root"), + PRINCIPAL(2, ROOT, true, false, "Principal"), + PRINCIPAL_ROLE(3, ROOT, true, false, "Principal role"), + CATALOG(4, ROOT, false, false, "Catalog"), + CATALOG_ROLE(5, CATALOG, true, false, "Catalog role"), + NAMESPACE(6, CATALOG, false, true, "Namespace"), // generic table is either a view or a real table - TABLE_LIKE(7, NAMESPACE, false, false), - TASK(8, ROOT, false, false), - FILE(9, TABLE_LIKE, false, false); + TABLE_LIKE(7, NAMESPACE, false, false, "Table/view"), + TASK(8, ROOT, false, false, "Task"), + FILE(9, TABLE_LIKE, false, false, "File"); // to efficiently map a code to its corresponding entity type, use a reverse array which // is initialized below @@ -70,13 +70,20 @@ public enum PolarisEntityType { // parent entity type, null for an ACCOUNT private final PolarisEntityType parentType; - - PolarisEntityType(int id, PolarisEntityType parentType, boolean isGrantee, boolean sefRef) { + private final String readableName; + + PolarisEntityType( + int id, + PolarisEntityType parentType, + boolean isGrantee, + boolean sefRef, + String readableName) { // remember the id of this entity this.code = id; this.isGrantee = isGrantee; this.parentType = parentType; this.parentSelfReference = sefRef; + this.readableName = readableName; } /** @@ -132,4 +139,8 @@ public boolean isTopLevel() { public PolarisEntityType getParentType() { return this.parentType; } + + public String readableName() { + return readableName; + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/TaskEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/TaskEntity.java index 5a0add8d4..4f0c03b68 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/TaskEntity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/TaskEntity.java @@ -20,7 +20,7 @@ import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.context.CallContext; -import org.apache.polaris.core.persistence.PolarisObjectMapperUtil; +import org.apache.polaris.core.persistence.impl.PolarisObjectMapperUtil; /** * Represents an asynchronous task entity in the persistence layer. A task executor is responsible diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/EntityNotFoundException.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/EntityNotFoundException.java new file mode 100644 index 000000000..2a570416b --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/EntityNotFoundException.java @@ -0,0 +1,77 @@ +/* + * 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; + +import java.util.Optional; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.NoSuchViewException; +import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; + +public class EntityNotFoundException extends RuntimeException { + private final PolarisEntityType entityType; + private final Optional subType; + private final String name; + + public EntityNotFoundException( + PolarisEntityType entityType, Optional subType, String name) { + super( + subType.map(PolarisEntitySubType::readableName).orElseGet(entityType::readableName) + + " " + + name); + this.entityType = entityType; + this.subType = subType; + this.name = name; + } + + public EntityNotFoundException(PolarisEntityType entityType, String name) { + this(entityType, Optional.empty(), name); + } + + public RuntimeException asGenericIcebergNotFoundException() { + throw new NotFoundException( + "%s does not exist: %s", + subType.map(PolarisEntitySubType::readableName).orElseGet(entityType::readableName), name); + } + + public RuntimeException asSpecializedIcebergNotFoundException() { + switch (entityType) { + case NAMESPACE: + return new NoSuchNamespaceException("Namespace does not exist: %s", name); + case TABLE_LIKE: + return subType + .map( + sub -> { + switch (sub) { + case TABLE: + return new NoSuchTableException("Table does not exist: %s", name); + case VIEW: + return new NoSuchViewException("View does not exist: %s", name); + default: + return asGenericIcebergNotFoundException(); + } + }) + .orElseGet(this::asGenericIcebergNotFoundException); + default: + return asGenericIcebergNotFoundException(); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityManager.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityManager.java index 3d0d2457a..e82f77b21 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityManager.java @@ -30,8 +30,10 @@ import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.persistence.cache.EntityCache; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; -import org.apache.polaris.core.persistence.resolver.Resolver; +import org.apache.polaris.core.persistence.resolution.ResolutionManifestBuilder; +import org.apache.polaris.core.persistence.resolution.ResolvedPolarisEntity; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverBuilderImpl; import org.apache.polaris.core.storage.cache.StorageCredentialCache; /** @@ -62,11 +64,11 @@ public PolarisEntityManager( this.credentialCache = credentialCache; } - public Resolver prepareResolver( + public ResolverBuilder prepareResolver( @Nonnull CallContext callContext, @Nonnull AuthenticatedPolarisPrincipal authenticatedPrincipal, @Nullable String referenceCatalogName) { - return new Resolver( + return new ResolverBuilderImpl( callContext.getPolarisCallContext(), metaStoreManager, authenticatedPrincipal.getPrincipalEntity().getId(), @@ -78,16 +80,17 @@ public Resolver prepareResolver( referenceCatalogName); } - public PolarisResolutionManifest prepareResolutionManifest( + public ResolutionManifestBuilder prepareResolutionManifest( @Nonnull CallContext callContext, @Nonnull AuthenticatedPolarisPrincipal authenticatedPrincipal, @Nullable String referenceCatalogName) { - PolarisResolutionManifest manifest = - new PolarisResolutionManifest( - callContext, this, authenticatedPrincipal, referenceCatalogName); - manifest.setSimulatedResolvedRootContainerEntity( - getSimulatedResolvedRootContainerEntity(callContext)); - return manifest; + return this.metaStoreManager + .newResolutionManifestBuilder( + callContext, + authenticatedPrincipal, + () -> prepareResolver(callContext, authenticatedPrincipal, referenceCatalogName), + referenceCatalogName) + .withRootContainerEntity(getSimulatedResolvedRootContainerEntity(callContext)); } /** diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java index 69652cb87..42a924812 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManager.java @@ -25,9 +25,12 @@ import jakarta.annotation.Nullable; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; import org.apache.polaris.core.auth.PolarisGrantManager; import org.apache.polaris.core.auth.PolarisSecretsManager; +import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntityActiveRecord; @@ -36,6 +39,8 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; import org.apache.polaris.core.persistence.cache.PolarisRemoteCache; +import org.apache.polaris.core.persistence.resolution.ResolutionManifestBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; import org.apache.polaris.core.storage.PolarisCredentialVendor; /** @@ -72,6 +77,13 @@ public interface PolarisMetaStoreManager @Nonnull BaseResult purge(@Nonnull PolarisCallContext callCtx); + @Nonnull + ResolutionManifestBuilder newResolutionManifestBuilder( + @Nonnull CallContext callContext, + @Nonnull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @Nonnull Supplier resolverSupplier, + @Nullable String referenceCatalogName); + /** the return for an entity lookup call */ class EntityResult extends BaseResult { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityResolver.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisEntityResolver.java similarity index 99% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityResolver.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisEntityResolver.java index 3d795d735..4df532ebf 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisEntityResolver.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisEntityResolver.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.impl; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -32,6 +32,7 @@ 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.persistence.PolarisMetaStoreSession; /** * Utility class used by the meta store manager to ensure that all entities which had been resolved diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManagerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisMetaStoreManagerImpl.java similarity index 98% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManagerImpl.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisMetaStoreManagerImpl.java index b90c7e51c..ec7acd8d9 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisMetaStoreManagerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisMetaStoreManagerImpl.java @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.impl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.ArrayList; @@ -34,8 +33,11 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.AsyncTaskType; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisChangeTrackingVersions; @@ -51,6 +53,11 @@ import org.apache.polaris.core.entity.PolarisPrincipalSecrets; import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.entity.PolarisTaskConstants; +import org.apache.polaris.core.persistence.BaseResult; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.PolarisMetaStoreSession; +import org.apache.polaris.core.persistence.resolution.ResolutionManifestBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; import org.apache.polaris.core.storage.PolarisCredentialProperty; import org.apache.polaris.core.storage.PolarisStorageActions; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -62,7 +69,6 @@ * Default implementation of the Polaris Meta Store Manager. Uses the underlying meta store to store * and retrieve all Polaris metadata */ -@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") public class PolarisMetaStoreManagerImpl implements PolarisMetaStoreManager { private static final Logger LOGGER = LoggerFactory.getLogger(PolarisMetaStoreManagerImpl.class); @@ -1006,6 +1012,20 @@ public Map deserializeProperties(PolarisCallContext callCtx, Str : new PrincipalSecretsResult(secrets); } + @Override + @Nonnull + public ResolutionManifestBuilder newResolutionManifestBuilder( + @Nonnull CallContext callContext, + @Nonnull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @Nonnull Supplier resolverSupplier, + @Nullable String referenceCatalogName) { + return new PolarisResolutionManifestBuilder( + authenticatedPrincipal, + referenceCatalogName, + resolverSupplier, + callContext.getPolarisCallContext().getDiagServices()); + } + /** See {@link #} */ private @Nullable PolarisPrincipalSecrets rotatePrincipalSecrets( @Nonnull PolarisCallContext callCtx, diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtil.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisObjectMapperUtil.java similarity index 98% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtil.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisObjectMapperUtil.java index a91d2bbab..8ab660564 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtil.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisObjectMapperUtil.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.impl; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; @@ -37,7 +37,7 @@ import org.slf4j.LoggerFactory; /** A mapper to serialize/deserialize polaris objects. */ -public class PolarisObjectMapperUtil { +public final class PolarisObjectMapperUtil { private static final Logger LOGGER = LoggerFactory.getLogger(PolarisObjectMapperUtil.class); /** mapper, allows to serialize/deserialize properties to/from JSON */ @@ -50,6 +50,8 @@ private static ObjectMapper configureMapper() { return mapper; } + private PolarisObjectMapperUtil() {} + /** * Given the internal property as a map of key/value pairs, serialize it to a String * @@ -187,8 +189,4 @@ public int getAttemptCount() { return null; } } - - long now() { - return 0; - } } 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/impl/PolarisResolutionManifest.java similarity index 53% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifest.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisResolutionManifest.java index 629e282e1..c07688633 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/impl/PolarisResolutionManifest.java @@ -16,29 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence.resolver; +package org.apache.polaris.core.persistence.impl; -import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; import org.apache.polaris.core.PolarisDiagnostics; -import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; -import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntityConstants; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; -import org.apache.polaris.core.entity.PrincipalRoleEntity; -import org.apache.polaris.core.persistence.PolarisEntityManager; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; -import org.apache.polaris.core.persistence.ResolvedPolarisEntity; import org.apache.polaris.core.persistence.cache.EntityCacheEntry; +import org.apache.polaris.core.persistence.resolution.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; +import org.apache.polaris.core.persistence.resolution.ResolvedPolarisEntity; +import org.apache.polaris.core.persistence.resolver.Resolver; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverException; +import org.apache.polaris.core.persistence.resolver.ResolverPath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,111 +52,59 @@ *

Implemented as a wrapper around a Resolver with helper methods and book-keeping to better * function as a lookup manifest for downstream callers. */ -public class PolarisResolutionManifest implements PolarisResolutionManifestCatalogView { +final class PolarisResolutionManifest implements ResolutionManifest { private static final Logger LOGGER = LoggerFactory.getLogger(PolarisResolutionManifest.class); - private final PolarisEntityManager entityManager; - private final CallContext callContext; - private final AuthenticatedPolarisPrincipal authenticatedPrincipal; + private final Supplier resolverSupplier; private final String catalogName; private final Resolver primaryResolver; private final PolarisDiagnostics diagnostics; - private final Map pathLookup = new HashMap<>(); - private final List addedPaths = new ArrayList<>(); - private final Multimap addedTopLevelNames = HashMultimap.create(); - - private final Map passthroughPaths = new HashMap<>(); + private final Map pathLookup; + private final List addedPaths; + private final Multimap addedTopLevelNames; + private final Map passthroughPaths; // For applicable operations, this represents the topmost root entity which services as an // authorization parent for all other entities that reside at the root level, such as // Catalog, Principal, and PrincipalRole. // This simulated entity will be used if the actual resolver fails to resolve the rootContainer // on the backend due to compatibility mismatches. - private ResolvedPolarisEntity simulatedResolvedRootContainerEntity = null; - - private int currentPathIndex = 0; - - // Set when resolveAll is called - private ResolverStatus primaryResolverStatus = null; - - public PolarisResolutionManifest( - CallContext callContext, - PolarisEntityManager entityManager, - AuthenticatedPolarisPrincipal authenticatedPrincipal, - String catalogName) { - this.entityManager = entityManager; - this.callContext = callContext; - this.authenticatedPrincipal = authenticatedPrincipal; + private final ResolvedPolarisEntity rootContainerEntity; + + PolarisResolutionManifest( + Supplier resolverSupplier, + String catalogName, + Resolver primaryResolver, + PolarisDiagnostics diagnostics, + Map pathLookup, + List addedPaths, + Multimap addedTopLevelNames, + Map passthroughPaths, + ResolvedPolarisEntity rootContainerEntity) { + this.resolverSupplier = resolverSupplier; this.catalogName = catalogName; - this.primaryResolver = - entityManager.prepareResolver(callContext, authenticatedPrincipal, catalogName); - this.diagnostics = callContext.getPolarisCallContext().getDiagServices(); - - // TODO: Make the rootContainer lookup no longer optional in the persistence store. - // For now, we'll try to resolve the rootContainer as "optional", and only if we fail to find - // it, we'll use the "simulated" rootContainer entity. - addTopLevelName(PolarisEntityConstants.getRootContainerName(), PolarisEntityType.ROOT, true); - } - - /** Adds a name of a top-level entity (Catalog, Principal, PrincipalRole) to be resolved. */ - public void addTopLevelName(String entityName, PolarisEntityType entityType, boolean isOptional) { - addedTopLevelNames.put(entityName, entityType); - if (isOptional) { - primaryResolver.addOptionalEntityByName(entityType, entityName); - } else { - primaryResolver.addEntityByName(entityType, entityName); - } - } - - /** - * 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 - * resolved. - * - * @param key the friendly lookup key for retrieving resolvedPaths after resolveAll(); typically - * might be a Namespace or TableIdentifier object. - */ - public void addPath(ResolverPath path, Object key) { - primaryResolver.addPath(path); - pathLookup.put(key, currentPathIndex); - addedPaths.add(path); - ++currentPathIndex; - } - - /** - * Adds a path that is allowed to be dynamically resolved with a new Resolver when - * getPassthroughResolvedPath is called. These paths are also included in the primary static - * resolution set resolved during resolveAll(). - */ - public void addPassthroughPath(ResolverPath path, Object key) { - addPath(path, key); - passthroughPaths.put(key, path); - } - - public ResolverStatus resolveAll() { - primaryResolverStatus = primaryResolver.resolveAll(); - // TODO: This could be a race condition where a Principal is dropped after initial authn - // but before the resolution attempt; consider whether 403 forbidden is more appropriate. - diagnostics.check( - primaryResolverStatus.getStatus() - != ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST, - "caller_principal_does_not_exist_at_resolution_time"); - - // activated principal roles are known, add them to the call context - if (primaryResolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { - List activatedPrincipalRoles = - primaryResolver.getResolvedCallerPrincipalRoles().stream() - .map(ce -> PrincipalRoleEntity.of(ce.getEntity())) - .collect(Collectors.toList()); - this.authenticatedPrincipal.setActivatedPrincipalRoles(activatedPrincipalRoles); - } - return primaryResolverStatus; + this.primaryResolver = primaryResolver; + this.diagnostics = diagnostics; + this.pathLookup = pathLookup; + this.addedPaths = addedPaths; + this.addedTopLevelNames = addedTopLevelNames; + this.passthroughPaths = passthroughPaths; + this.rootContainerEntity = rootContainerEntity; } @Override public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity() { - return getResolvedReferenceCatalogEntity(false); + // This is a server error instead of being able to legitimately return null, since this means + // a callsite failed to incorporate a reference catalog into its authorization flow but is + // still trying to perform operations on the (nonexistence) reference catalog. + diagnostics.checkNotNull(catalogName, "null_catalog_name_for_resolved_reference_catalog"); + EntityCacheEntry resolvedCachedCatalog = primaryResolver.getResolvedReferenceCatalog(); + if (resolvedCachedCatalog == null) { + return null; + } + return new PolarisResolvedPathWrapper( + List.of(new ResolvedPolarisEntity(resolvedCachedCatalog))); } /** @@ -163,26 +113,11 @@ public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity() { * "optional" */ @Override - public PolarisResolvedPathWrapper getResolvedPath(Object key) { - return getResolvedPath(key, false); - } - - /** - * @return null if the path resolved for {@code key} isn't fully-resolved when specified as - * "optional", or if it was resolved but the subType doesn't match the specified subType. - */ - @Override - public PolarisResolvedPathWrapper getResolvedPath(Object key, PolarisEntitySubType subType) { - return getResolvedPath(key, subType, false); + public PolarisResolvedPathWrapper getPassthroughResolvedPath(Namespace key) { + return getPassthroughResolvedPathInternal(key); } - /** - * @param key the key associated with the path to retrieve that was specified in addPath - * @return null if the path resolved for {@code key} isn't fully-resolved when specified as - * "optional" - */ - @Override - public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { + private PolarisResolvedPathWrapper getPassthroughResolvedPathInternal(Object key) { diagnostics.check( passthroughPaths.containsKey(key), "invalid_key_for_passthrough_resolved_path", @@ -192,13 +127,12 @@ public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { ResolverPath requestedPath = passthroughPaths.get(key); // Run a single-use Resolver for this path. - Resolver passthroughResolver = - entityManager.prepareResolver(callContext, authenticatedPrincipal, catalogName); - passthroughResolver.addPath(requestedPath); - ResolverStatus status = passthroughResolver.resolveAll(); - - if (status.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { - LOGGER.debug("Returning null for key {} due to resolver status {}", key, status.getStatus()); + Resolver passthroughResolver; + try { + passthroughResolver = resolverSupplier.get().addPath(requestedPath).buildResolved(); + } catch (ResolverException e) { + LOGGER.debug( + "Returning null for key {} due to resolver status {}", key, e.getClass().getSimpleName()); return null; } @@ -230,8 +164,8 @@ public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { */ @Override public PolarisResolvedPathWrapper getPassthroughResolvedPath( - Object key, PolarisEntitySubType subType) { - PolarisResolvedPathWrapper resolvedPath = getPassthroughResolvedPath(key); + TableIdentifier key, PolarisEntitySubType subType) { + PolarisResolvedPathWrapper resolvedPath = getPassthroughResolvedPathInternal(key); if (resolvedPath == null) { return null; } @@ -243,6 +177,7 @@ public PolarisResolvedPathWrapper getPassthroughResolvedPath( return resolvedPath; } + @Override public Set getAllActivatedCatalogRoleAndPrincipalRoles() { Set activatedRoles = new HashSet<>(); primaryResolver.getResolvedCallerPrincipalRoles().stream() @@ -256,6 +191,7 @@ public Set getAllActivatedCatalogRoleAndPrincipalRoles() { return activatedRoles; } + @Override public Set getAllActivatedPrincipalRoleEntities() { Set activatedEntities = new HashSet<>(); primaryResolver.getResolvedCallerPrincipalRoles().stream() @@ -264,61 +200,31 @@ public Set getAllActivatedPrincipalRoleEntities() { return activatedEntities; } - public void setSimulatedResolvedRootContainerEntity( - ResolvedPolarisEntity simulatedResolvedRootContainerEntity) { - this.simulatedResolvedRootContainerEntity = simulatedResolvedRootContainerEntity; - } - private ResolvedPolarisEntity getResolvedRootContainerEntity() { - if (primaryResolverStatus.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { - return null; - } EntityCacheEntry resolvedCacheEntry = primaryResolver.getResolvedEntity( PolarisEntityType.ROOT, PolarisEntityConstants.getRootContainerName()); if (resolvedCacheEntry == null) { LOGGER.debug("Failed to find rootContainer, so using simulated rootContainer instead."); - return simulatedResolvedRootContainerEntity; + return rootContainerEntity; } return new ResolvedPolarisEntity(resolvedCacheEntry); } + @Override public PolarisResolvedPathWrapper getResolvedRootContainerEntityAsPath() { return new PolarisResolvedPathWrapper(List.of(getResolvedRootContainerEntity())); } - public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity( - boolean prependRootContainer) { - // This is a server error instead of being able to legitimately return null, since this means - // a callsite failed to incorporate a reference catalog into its authorization flow but is - // still trying to perform operations on the (nonexistence) reference catalog. - diagnostics.checkNotNull(catalogName, "null_catalog_name_for_resolved_reference_catalog"); - EntityCacheEntry resolvedCachedCatalog = primaryResolver.getResolvedReferenceCatalog(); - if (resolvedCachedCatalog == null) { - return null; - } - if (prependRootContainer) { - // Operations directly on Catalogs also consider the root container to be a parent of its - // authorization chain. - // TODO: Throw appropriate Catalog NOT_FOUND exception before any call to - // getResolvedReferenceCatalogEntity(). - return new PolarisResolvedPathWrapper( - List.of( - getResolvedRootContainerEntity(), new ResolvedPolarisEntity(resolvedCachedCatalog))); - } else { - return new PolarisResolvedPathWrapper( - List.of(new ResolvedPolarisEntity(resolvedCachedCatalog))); - } - } - - public PolarisEntitySubType getLeafSubType(Object key) { + @Override + public PolarisEntitySubType getLeafSubType(TableIdentifier tableIdentifier) { diagnostics.check( - pathLookup.containsKey(key), + pathLookup.containsKey(tableIdentifier), "never_registered_key_for_resolved_path", "key={} pathLookup={}", - key, + tableIdentifier, pathLookup); - int index = pathLookup.get(key); + int index = pathLookup.get(tableIdentifier); List resolved = primaryResolver.getResolvedPaths().get(index); if (resolved.isEmpty()) { return PolarisEntitySubType.NULL_SUBTYPE; @@ -326,14 +232,31 @@ public PolarisEntitySubType getLeafSubType(Object key) { return resolved.get(resolved.size() - 1).getEntity().getSubType(); } - /** - * @param key the key associated with the path to retrieve that was specified in addPath - * @param prependRootContainer if true, also includes the rootContainer as the first element of - * the path; otherwise, the first element begins with the referenceCatalog. - * @return null if the path resolved for {@code key} isn't fully-resolved when specified as - * "optional" - */ - public PolarisResolvedPathWrapper getResolvedPath(Object key, boolean prependRootContainer) { + @Override + public PolarisResolvedPathWrapper getResolvedPath( + Namespace namespace, boolean prependRootContainer) { + return getResolvedPathInternal(namespace, prependRootContainer); + } + + @Override + public PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, boolean prependRootContainer) { + return getResolvedPathInternal(tableIdentifier, prependRootContainer); + } + + @Override + public PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, PolarisEntitySubType subType, boolean prependRootContainer) { + return getResolvedPathInternal(tableIdentifier, prependRootContainer); + } + + @Override + public PolarisResolvedPathWrapper getResolvedPath(String name, boolean prependRootContainer) { + return getResolvedPathInternal(name, prependRootContainer); + } + + private PolarisResolvedPathWrapper getResolvedPathInternal( + Object key, boolean prependRootContainer) { diagnostics.check( pathLookup.containsKey(key), "never_registered_key_for_resolved_path", @@ -341,9 +264,6 @@ public PolarisResolvedPathWrapper getResolvedPath(Object key, boolean prependRoo key, pathLookup); - if (primaryResolverStatus.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { - return null; - } int index = pathLookup.get(key); // Return null for a partially-resolved "optional" path. @@ -364,24 +284,7 @@ public PolarisResolvedPathWrapper getResolvedPath(Object key, boolean prependRoo return new PolarisResolvedPathWrapper(resolvedEntities); } - /** - * @return null if the path resolved for {@code key} isn't fully-resolved when specified as - * "optional", or if it was resolved but the subType doesn't match the specified subType. - */ - public PolarisResolvedPathWrapper getResolvedPath( - Object key, PolarisEntitySubType subType, boolean prependRootContainer) { - PolarisResolvedPathWrapper resolvedPath = getResolvedPath(key, prependRootContainer); - if (resolvedPath == null) { - return null; - } - if (resolvedPath.getRawLeafEntity() != null - && subType != PolarisEntitySubType.ANY_SUBTYPE - && resolvedPath.getRawLeafEntity().getSubType() != subType) { - return null; - } - return resolvedPath; - } - + @Override public PolarisResolvedPathWrapper getResolvedTopLevelEntity( String entityName, PolarisEntityType entityType) { // For now, all top-level entities will have the root container prepended so we don't have @@ -394,10 +297,6 @@ public PolarisResolvedPathWrapper getResolvedTopLevelEntity( entityType, addedTopLevelNames); - if (primaryResolverStatus.getStatus() != ResolverStatus.StatusEnum.SUCCESS) { - return null; - } - EntityCacheEntry resolvedCacheEntry = primaryResolver.getResolvedEntity(entityType, entityName); if (resolvedCacheEntry == null) { return null; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisResolutionManifestBuilder.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisResolutionManifestBuilder.java new file mode 100644 index 000000000..93e1dde2f --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/PolarisResolutionManifestBuilder.java @@ -0,0 +1,191 @@ +/* + * 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.impl; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.entity.PolarisEntityConstants; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.persistence.EntityNotFoundException; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; +import org.apache.polaris.core.persistence.resolution.ResolutionManifestBuilder; +import org.apache.polaris.core.persistence.resolution.ResolvedPolarisEntity; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverException; +import org.apache.polaris.core.persistence.resolver.ResolverPath; + +final class PolarisResolutionManifestBuilder implements ResolutionManifestBuilder { + private Function notFoundExceptionMapper = nf -> nf; + + private final Supplier resolverSupplier; + private final AuthenticatedPolarisPrincipal authenticatedPrincipal; + private final String catalogName; + private final ResolverBuilder primaryResolver; + private final PolarisDiagnostics diagnostics; + + private final Map pathLookup = new HashMap<>(); + private final List addedPaths = new ArrayList<>(); + private final Multimap addedTopLevelNames = HashMultimap.create(); + private final Map passthroughPaths = new HashMap<>(); + + private int currentPathIndex = 0; + private ResolvedPolarisEntity rootContainerEntity; + + PolarisResolutionManifestBuilder( + AuthenticatedPolarisPrincipal authenticatedPrincipal, + String catalogName, + Supplier resolverSupplier, + PolarisDiagnostics diagnostics) { + this.authenticatedPrincipal = authenticatedPrincipal; + this.catalogName = catalogName; + this.resolverSupplier = resolverSupplier; + this.primaryResolver = resolverSupplier.get(); + this.diagnostics = diagnostics; + + // TODO: Make the rootContainer lookup no longer optional in the persistence store. + // For now, we'll try to resolve the rootContainer as "optional", and only if we fail to find + // it, we'll use the "simulated" rootContainer entity. + addTopLevelName(PolarisEntityConstants.getRootContainerName(), PolarisEntityType.ROOT, true); + } + + @Override + public ResolutionManifestBuilder withRootContainerEntity( + ResolvedPolarisEntity rootContainerEntity) { + this.rootContainerEntity = rootContainerEntity; + return this; + } + + @Override + public ResolutionManifestBuilder addTopLevelName( + String entityName, PolarisEntityType entityType, boolean optional) { + addedTopLevelNames.put(entityName, entityType); + if (optional) { + primaryResolver.addOptionalEntityByName(entityType, entityName); + } else { + primaryResolver.addEntityByName(entityType, entityName); + } + return this; + } + + @Override + public ResolutionManifestBuilder addPath(ResolverPath resolverPath, String catalogRoleName) { + return addPathInternal(resolverPath, catalogRoleName); + } + + @Override + public ResolutionManifestBuilder addPath(ResolverPath resolverPath, Namespace namespace) { + return addPathInternal(resolverPath, namespace); + } + + @Override + public ResolutionManifestBuilder addPath( + ResolverPath resolverPath, TableIdentifier tableIdentifier) { + return addPathInternal(resolverPath, tableIdentifier); + } + + private ResolutionManifestBuilder addPathInternal(ResolverPath path, Object key) { + primaryResolver.addPath(path); + pathLookup.put(key, currentPathIndex); + addedPaths.add(path); + ++currentPathIndex; + return this; + } + + @Override + public ResolutionManifestBuilder addPassthroughPath( + ResolverPath resolverPath, TableIdentifier tableIdentifier) { + return addPassthroughPathInternal(resolverPath, tableIdentifier); + } + + @Override + public ResolutionManifestBuilder addPassthroughPath( + ResolverPath resolverPath, Namespace namespace) { + return addPassthroughPathInternal(resolverPath, namespace); + } + + private ResolutionManifestBuilder addPassthroughPathInternal( + ResolverPath resolverPath, Object key) { + addPathInternal(resolverPath, key); + passthroughPaths.put(key, resolverPath); + return this; + } + + @Override + public ResolutionManifestBuilder notFoundExceptionMapper( + Function notFoundExceptionMapper) { + this.notFoundExceptionMapper = notFoundExceptionMapper; + return this; + } + + @Override + public ResolutionManifest buildResolved() { + return buildResolved(null); + } + + @Override + public ResolutionManifest buildResolved(PolarisEntitySubType subType) { + + try { + var resolver = primaryResolver.buildResolved(); + + List activatedPrincipalRoles = + resolver.getResolvedCallerPrincipalRoles().stream() + .map(ce -> PrincipalRoleEntity.of(ce.getEntity())) + .collect(Collectors.toList()); + this.authenticatedPrincipal.setActivatedPrincipalRoles(activatedPrincipalRoles); + + return new PolarisResolutionManifest( + resolverSupplier, + catalogName, + resolver, + diagnostics, + pathLookup, + addedPaths, + addedTopLevelNames, + passthroughPaths, + rootContainerEntity); + } catch (ResolverException.EntityNotResolvedException e) { + throw notFoundExceptionMapper.apply( + new EntityNotFoundException( + e.failedToResolvedEntityType(), + Optional.ofNullable(subType), + e.failedToResolvedEntityName())); + } catch (ResolverException.PathNotFullyResolvedException e) { + var path = e.failedToResolvePath(); + var names = path.getEntityNames(); + throw notFoundExceptionMapper.apply( + new EntityNotFoundException( + path.getLastEntityType(), Optional.ofNullable(subType), String.join(".", names))); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/TransactionWorkspaceMetaStoreManager.java similarity index 94% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/TransactionWorkspaceMetaStoreManager.java index 44134751b..53582f755 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/impl/TransactionWorkspaceMetaStoreManager.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.impl; import com.google.common.collect.ImmutableList; import jakarta.annotation.Nonnull; @@ -25,7 +25,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntityCore; @@ -33,6 +36,10 @@ import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrivilege; +import org.apache.polaris.core.persistence.BaseResult; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.resolution.ResolutionManifestBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; import org.apache.polaris.core.storage.PolarisStorageActions; /** @@ -369,4 +376,13 @@ public CachedEntryResult refreshCachedEntity( .fail("illegal_method_in_transaction_workspace", "refreshCachedEntity"); return null; } + + @Override + public @Nonnull ResolutionManifestBuilder newResolutionManifestBuilder( + @Nonnull CallContext callCtx, + @Nonnull AuthenticatedPolarisPrincipal authenticatedPrincipal, + @Nonnull Supplier resolverSupplier, + @Nullable String referenceCatalogName) { + throw new RuntimeException("illegal_method_in_transaction_workspace"); + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/local/LocalPolarisMetaStoreManagerFactory.java similarity index 96% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/local/LocalPolarisMetaStoreManagerFactory.java index 25bea896f..56e838228 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/LocalPolarisMetaStoreManagerFactory.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/local/LocalPolarisMetaStoreManagerFactory.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.local; import jakarta.annotation.Nonnull; import java.util.HashMap; @@ -34,6 +34,11 @@ import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; +import org.apache.polaris.core.persistence.MetaStoreManagerFactory; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.PolarisMetaStoreSession; +import org.apache.polaris.core.persistence.PrincipalSecretsGenerator; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.storage.cache.StorageCredentialCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapMetaStoreSessionImpl.java similarity index 98% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapMetaStoreSessionImpl.java index bea205415..290769b3a 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreSessionImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapMetaStoreSessionImpl.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.local.inmem; import com.google.common.base.Predicates; import jakarta.annotation.Nonnull; @@ -36,6 +36,9 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; +import org.apache.polaris.core.persistence.PolarisMetaStoreSession; +import org.apache.polaris.core.persistence.PrincipalSecretsGenerator; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; import org.apache.polaris.core.storage.PolarisStorageIntegration; import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapStore.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapStore.java similarity index 99% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapStore.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapStore.java index 544bcf0ff..b193e9066 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisTreeMapStore.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapStore.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.local.inmem; import jakarta.annotation.Nonnull; import java.util.ArrayList; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisResolvedPathWrapper.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/PolarisResolvedPathWrapper.java similarity index 97% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisResolvedPathWrapper.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/PolarisResolvedPathWrapper.java index 6b09598c4..4e9dd8679 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/PolarisResolvedPathWrapper.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/PolarisResolvedPathWrapper.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.resolution; import java.util.List; import java.util.stream.Collectors; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifest.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifest.java new file mode 100644 index 000000000..99cc27b06 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifest.java @@ -0,0 +1,82 @@ +/* + * 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.resolution; + +import java.util.Set; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; + +/** Holds already resolved "name paths" to entities. */ +public interface ResolutionManifest { + PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity(); + + default PolarisResolvedPathWrapper getResolvedPath(Namespace namespace) { + return getResolvedPath(namespace, false); + } + + PolarisResolvedPathWrapper getResolvedPath(Namespace namespace, boolean prependRootContainer); + + default PolarisResolvedPathWrapper getResolvedPath(TableIdentifier tableIdentifier) { + return getResolvedPath(tableIdentifier, false); + } + + /** + * @param tableIdentifier the key associated with the path to retrieve that was specified in + * addPath + * @param prependRootContainer if true, also includes the rootContainer as the first element of + * the path; otherwise, the first element begins with the referenceCatalog. + * @return null if the path resolved for {@code key} isn't fully-resolved when specified as + * "optional" + */ + PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, boolean prependRootContainer); + + default PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, PolarisEntitySubType subType) { + return getResolvedPath(tableIdentifier, subType, false); + } + + PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, PolarisEntitySubType subType, boolean prependRootContainer); + + default PolarisResolvedPathWrapper getResolvedPath(String name) { + return getResolvedPath(name, false); + } + + PolarisResolvedPathWrapper getResolvedPath(String name, boolean prependRootContainer); + + PolarisResolvedPathWrapper getResolvedRootContainerEntityAsPath(); + + PolarisResolvedPathWrapper getPassthroughResolvedPath(Namespace namespace); + + PolarisResolvedPathWrapper getPassthroughResolvedPath( + TableIdentifier tableIdentifier, PolarisEntitySubType subType); + + PolarisResolvedPathWrapper getResolvedTopLevelEntity( + String name, PolarisEntityType polarisEntityType); + + Set getAllActivatedPrincipalRoleEntities(); + + Set getAllActivatedCatalogRoleAndPrincipalRoles(); + + PolarisEntitySubType getLeafSubType(TableIdentifier tableIdentifier); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifestBuilder.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifestBuilder.java new file mode 100644 index 000000000..79c833469 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolutionManifestBuilder.java @@ -0,0 +1,94 @@ +/* + * 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.resolution; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.function.Function; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.EntityNotFoundException; +import org.apache.polaris.core.persistence.resolver.ResolverPath; + +public interface ResolutionManifestBuilder { + @CanIgnoreReturnValue + ResolutionManifestBuilder withRootContainerEntity( + ResolvedPolarisEntity simulatedResolvedRootContainerEntity); + + /** Adds a name of a top-level entity (Catalog, Principal, PrincipalRole) to be resolved. */ + @CanIgnoreReturnValue + ResolutionManifestBuilder addTopLevelName( + String topLevelEntityName, PolarisEntityType entityType, boolean optional); + + /** + * 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 + * resolved. + * + * @param catalogRoleName the friendly lookup key for retrieving resolvedPaths after resolveAll() + */ + @CanIgnoreReturnValue + ResolutionManifestBuilder addPath(ResolverPath resolverPath, String catalogRoleName); + + /** + * 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 + * resolved. + * + * @param namespace the friendly lookup key for retrieving resolvedPaths after resolveAll() + */ + @CanIgnoreReturnValue + ResolutionManifestBuilder addPath(ResolverPath resolverPath, Namespace namespace); + + /** + * 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 + * resolved. + * + * @param tableIdentifier the friendly lookup key for retrieving resolvedPaths after resolveAll() + */ + @CanIgnoreReturnValue + ResolutionManifestBuilder addPath(ResolverPath resolverPath, TableIdentifier tableIdentifier); + + /** + * Adds a path that is allowed to be dynamically resolved with a new Resolver when + * getPassthroughResolvedPath is called. These paths are also included in the primary static + * resolution set resolved during resolveAll(). + */ + @CanIgnoreReturnValue + ResolutionManifestBuilder addPassthroughPath( + ResolverPath resolverPath, TableIdentifier tableIdentifier); + + /** + * Adds a path that is allowed to be dynamically resolved with a new Resolver when + * getPassthroughResolvedPath is called. These paths are also included in the primary static + * resolution set resolved during resolveAll(). + */ + @CanIgnoreReturnValue + ResolutionManifestBuilder addPassthroughPath(ResolverPath resolverPath, Namespace namespace); + + @CanIgnoreReturnValue + ResolutionManifestBuilder notFoundExceptionMapper( + Function notFoundExceptionMapper); + + ResolutionManifest buildResolved(PolarisEntitySubType subType); + + ResolutionManifest buildResolved(); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/ResolvedPolarisEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolvedPolarisEntity.java similarity index 98% rename from polaris-core/src/main/java/org/apache/polaris/core/persistence/ResolvedPolarisEntity.java rename to polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolvedPolarisEntity.java index a091362ac..eed685328 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/ResolvedPolarisEntity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolution/ResolvedPolarisEntity.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.resolution; import com.google.common.collect.ImmutableList; import jakarta.annotation.Nonnull; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java deleted file mode 100644 index 21e16f575..000000000 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/PolarisResolutionManifestCatalogView.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.core.persistence.resolver; - -import org.apache.polaris.core.entity.PolarisEntitySubType; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; - -/** - * Defines the methods by which a Catalog is expected to access resolved catalog-path entities, - * typically backed by a PolarisResolutionManifest. - */ -public interface PolarisResolutionManifestCatalogView { - PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity(); - - PolarisResolvedPathWrapper getResolvedPath(Object key); - - PolarisResolvedPathWrapper getResolvedPath(Object key, PolarisEntitySubType subType); - - PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key); - - PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key, PolarisEntitySubType subType); -} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java index ebefa1582..61f4a4f34 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/Resolver.java @@ -20,291 +20,34 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import java.util.AbstractSet; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import org.apache.polaris.core.PolarisCallContext; -import org.apache.polaris.core.PolarisDiagnostics; -import org.apache.polaris.core.entity.PolarisBaseEntity; -import org.apache.polaris.core.entity.PolarisChangeTrackingVersions; -import org.apache.polaris.core.entity.PolarisEntityConstants; -import org.apache.polaris.core.entity.PolarisEntityId; 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.cache.EntityCache; -import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey; import org.apache.polaris.core.persistence.cache.EntityCacheEntry; -import org.apache.polaris.core.persistence.cache.EntityCacheLookupResult; -import org.apache.polaris.core.persistence.cache.PolarisRemoteCache; -import org.apache.polaris.core.persistence.cache.PolarisRemoteCache.ChangeTrackingResult; /** * REST request resolver, allows to resolve all entities referenced directly or indirectly by in * incoming rest request, Once resolved, the request can be authorized. */ -public class Resolver { - - // we stash the Polaris call context here - private final @Nonnull PolarisCallContext polarisCallContext; - - // the diagnostic services - private final @Nonnull PolarisDiagnostics diagnostics; - - // the polaris metastore manager - private final @Nonnull PolarisRemoteCache polarisRemoteCache; - - // the cache of entities - private final @Nonnull EntityCache cache; - - // the id of the principal making the call or 0 if unknown - private final long callerPrincipalId; - - // the name of the principal making the call or null if unknown. If 0, the principal name will be - // not null - private final String callerPrincipalName; - - // reference catalog name for name resolution - private final String referenceCatalogName; - - // if not null, subset of principal roles to activate - private final @Nullable Set callerPrincipalRoleNamesScope; - - // set of entities to resolve given their name. This does not include namespaces or table_like - // entities which are - // part of a path - private final AbstractSet entitiesToResolve; - - // list of paths to resolve - private final List pathsToResolve; - - // caller principal - private EntityCacheEntry resolvedCallerPrincipal; - - // all principal roles which have been resolved - private List resolvedCallerPrincipalRoles; - - // catalog to use as the reference catalog for role activation - private EntityCacheEntry resolvedReferenceCatalog; - - // all catalog roles which have been activated - private final Map resolvedCatalogRoles; - - // all resolved paths - private List> resolvedPaths; - - // all entities which have been successfully resolved, by name - private final Map resolvedEntriesByName; - - // all entities which have been fully resolved, by id - private final Map resolvedEntriesById; - - private ResolverStatus resolverStatus; - - /** - * Constructor, effectively starts an entity resolver session - * - * @param polarisCallContext the polaris call context - * @param polarisRemoteCache meta store manager - * @param callerPrincipalId if not 0, the id of the principal calling the service - * @param callerPrincipalName if callerPrincipalId is 0, the name of the principal calling the - * service - * @param callerPrincipalRoleNamesScope if not null, scope principal roles - * @param cache shared entity cache - * @param referenceCatalogName if not null, specifies the name of the reference catalog. The - * reference catalog is the catalog used to resolve catalog roles and catalog path. Also, if a - * catalog reference is added, we will determine all catalog roles which are activated by the - * caller. Note that when a catalog name needs to be resolved because the principal creates or - * drop a catalog, it should not be specified here. Instead, it should be resolved by calling - * {@link #addEntityByName(PolarisEntityType, String)}. Generally, any DDL executed as a - * service admin should use null for that parameter. - */ - public Resolver( - @Nonnull PolarisCallContext polarisCallContext, - @Nonnull PolarisRemoteCache polarisRemoteCache, - long callerPrincipalId, - @Nullable String callerPrincipalName, - @Nullable Set callerPrincipalRoleNamesScope, - @Nonnull EntityCache cache, - @Nullable String referenceCatalogName) { - this.polarisCallContext = polarisCallContext; - this.diagnostics = polarisCallContext.getDiagServices(); - this.polarisRemoteCache = polarisRemoteCache; - this.cache = cache; - this.callerPrincipalName = callerPrincipalName; - this.callerPrincipalId = callerPrincipalId; - this.referenceCatalogName = referenceCatalogName; - - // scoped principal role names - this.callerPrincipalRoleNamesScope = callerPrincipalRoleNamesScope; - - // validate inputs - this.diagnostics.checkNotNull(polarisRemoteCache, "unexpected_null_polarisRemoteCache"); - this.diagnostics.checkNotNull(cache, "unexpected_null_cache"); - this.diagnostics.check( - callerPrincipalId != 0 || callerPrincipalName != null, "principal_must_be_specified"); - - // paths to resolve - this.pathsToResolve = new ArrayList<>(); - this.resolvedPaths = new ArrayList<>(); - - // all entities we need to resolve by name - this.entitiesToResolve = new HashSet<>(); - - // will contain all principal roles which we were able to resolve - this.resolvedCallerPrincipalRoles = new ArrayList<>(); - - // remember if a reference catalog name was specified - if (referenceCatalogName != null) { - this.resolvedCatalogRoles = new HashMap<>(); - } else { - this.resolvedCatalogRoles = null; - } - - // all resolved entities, by name and by if - this.resolvedEntriesByName = new HashMap<>(); - resolvedEntriesById = new HashMap<>(); - - // the resolver has not yet been called - this.resolverStatus = null; - } - - /** - * Add a top-level entity to resolve. If the entity type is a catalog role, we also expect that a - * reference catalog entity was specified at creation time, else we will assert. That catalog role - * entity will be resolved from there. We will fail the entire resolution process if that entity - * cannot be resolved. If this is not expected, use addOptionalEntityByName() instead. - * - * @param entityType the type of the entity, either a principal, a principal role, a catalog or a - * catalog role. - * @param entityName the name of the entity - */ - public void addEntityByName(@Nonnull PolarisEntityType entityType, @Nonnull String entityName) { - diagnostics.checkNotNull(entityType, "entity_type_is_null"); - diagnostics.checkNotNull(entityName, "entity_name_is_null"); - // can only be called if the resolver has not yet been called - this.diagnostics.check(resolverStatus == null, "resolver_called"); - this.addEntityByName(entityType, entityName, false); - } - - /** - * Add an optional top-level entity to resolve. If the entity type is a catalog role, we also - * expect that a reference catalog entity was specified at creation time, else we will assert. - * That catalog role entity will be resolved from there. If the entity cannot be resolved, we will - * not fail the resolution process - * - * @param entityType the type of the entity, either a principal, a principal role, a catalog or a - * catalog role. - * @param entityName the name of the entity - */ - public void addOptionalEntityByName( - @Nonnull PolarisEntityType entityType, @Nonnull String entityName) { - diagnostics.checkNotNull(entityType, "entity_type_is_null"); - diagnostics.checkNotNull(entityName, "entity_name_is_null"); - // can only be called if the resolver has not yet been called - this.diagnostics.check(resolverStatus == null, "resolver_called"); - this.addEntityByName(entityType, entityName, true); - } - - /** - * Add a path to resolve - * - * @param path path to resolve - */ - public void addPath(@Nonnull ResolverPath path) { - // can only be called if the resolver has not yet been called - this.diagnostics.check(resolverStatus == null, "resolver_called"); - diagnostics.checkNotNull(path, "unexpected_null_entity_path"); - this.pathsToResolve.add(path); - } - - /** - * Run the resolution process and return the status, either an error or success - * - *

-   * resolution might be working using multiple passes when using the cache since anything we find in the cache might
-   * have changed in the backend store.
-   * For each pass we will
-   *    -  go over all entities and call EntityCache.getOrLoad...() on these entities, including all paths.
-   *    -  split these entities into 3 groups:
-   *          - dropped or purged. We will return an error for these.
-   *          - to be validated entities, they were found in the cache. For those we need to ensure that the
-   *            entity id, its name and parent id has not changed. If yes we need to perform another pass.
-   *          - reloaded from backend, so the entity is validated. Validated entities will not be validated again
-   * 
- * - * @return the status of the resolver. If success, all entities have been resolved and the - * getResolvedXYZ() method can be called. - */ - public ResolverStatus resolveAll() { - // can only be called if the resolver has not yet been called - this.diagnostics.check(resolverStatus == null, "resolver_called"); - - // retry until a pass terminates, or we reached the maximum iteration count. Note that we should - // finish normally in no more than few passes so the 1000 limit is really to avoid spinning - // forever if there is a bug. - int count = 0; - ResolverStatus status; - do { - status = runResolvePass(); - count++; - } while (status == null && ++count < 1000); - - // assert if status is null - this.diagnostics.checkNotNull(status, "cannot_resolve_all_entities"); - - // remember the resolver status - this.resolverStatus = status; - - // all has been resolved - return status; - } - +public interface Resolver { /** * @return the principal we resolved */ - public @Nonnull EntityCacheEntry getResolvedCallerPrincipal() { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - - return resolvedCallerPrincipal; - } + @Nonnull + EntityCacheEntry getResolvedCallerPrincipal(); /** * @return all principal roles which were activated. The list can be empty */ - public @Nonnull List getResolvedCallerPrincipalRoles() { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - - return resolvedCallerPrincipalRoles; - } + @Nonnull + List getResolvedCallerPrincipalRoles(); /** * @return the reference catalog which has been resolved. Will be null if null was passed in for * the parameter referenceCatalogName when the Resolver was constructed. */ - public @Nullable EntityCacheEntry getResolvedReferenceCatalog() { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - - return resolvedReferenceCatalog; - } + @Nullable + EntityCacheEntry getResolvedReferenceCatalog(); /** * Empty map if no catalog was resolved. Else the list of catalog roles which are activated by the @@ -312,15 +55,8 @@ public ResolverStatus resolveAll() { * * @return map of activated catalog roles or null if no referenceCatalogName was specified */ - public @Nullable Map getResolvedCatalogRoles() { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - - return resolvedCatalogRoles; - } + @Nullable + Map getResolvedCatalogRoles(); /** * Get path which has been resolved, should be used only when a single path was added to the @@ -329,32 +65,16 @@ public ResolverStatus resolveAll() { * * @return single resolved path */ - public @Nonnull List getResolvedPath() { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - this.diagnostics.check(this.resolvedPaths.size() == 1, "only_if_single"); - - return resolvedPaths.get(0); - } + @Nonnull + List getResolvedPath(); /** * One of more resolved path, in the order they were added to the resolver. * * @return list of resolved path */ - public @Nonnull List> getResolvedPaths() { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - this.diagnostics.check(!this.resolvedPaths.isEmpty(), "no_path_resolved"); - - return resolvedPaths; - } + @Nonnull + List> getResolvedPaths(); /** * Get resolved entity associated to the specified type and name or null if not found @@ -365,623 +85,7 @@ public ResolverStatus resolveAll() { * @param entityName name of the entity. * @return the entity which has been resolved or null if that entity does not exist */ - public @Nullable EntityCacheEntry getResolvedEntity( - @Nonnull PolarisEntityType entityType, @Nonnull String entityName) { - // can only be called if the resolver has been called and was success - this.diagnostics.checkNotNull(resolverStatus, "resolver_must_be_called_first"); - this.diagnostics.check( - resolverStatus.getStatus() == ResolverStatus.StatusEnum.SUCCESS, - "resolver_must_be_successful"); - - // validate input - diagnostics.check( - entityType != PolarisEntityType.NAMESPACE && entityType != PolarisEntityType.TABLE_LIKE, - "cannot_be_path"); - diagnostics.check( - entityType.isTopLevel() || this.referenceCatalogName != null, "reference_catalog_expected"); - - if (entityType.isTopLevel()) { - return this.resolvedEntriesByName.get(new EntityCacheByNameKey(entityType, entityName)); - } else { - long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); - return this.resolvedEntriesByName.get( - new EntityCacheByNameKey(catalogId, catalogId, entityType, entityName)); - } - } - - /** - * Execute one resolve pass on all entities - * - * @return status of the resolve pass - */ - private ResolverStatus runResolvePass() { - - // we will resolve those again - this.resolvedCallerPrincipal = null; - this.resolvedReferenceCatalog = null; - if (this.resolvedCatalogRoles != null) { - this.resolvedCatalogRoles.clear(); - } - this.resolvedCallerPrincipalRoles.clear(); - this.resolvedPaths.clear(); - - // all entries we found in the cache but that we need to validate since they might be stale - List toValidate = new ArrayList<>(); - - // first resolve the principal and determine the set of activated principal roles - ResolverStatus status = - this.resolveCallerPrincipalAndPrincipalRoles( - toValidate, - this.callerPrincipalId, - this.callerPrincipalName, - this.callerPrincipalRoleNamesScope); - - // if success, continue resolving - if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { - // then resolve the reference catalog if one was specified - if (this.referenceCatalogName != null) { - status = this.resolveReferenceCatalog(toValidate, this.referenceCatalogName); - } - - // if success, continue resolving - if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { - // then resolve all the additional entities we were asked to resolve - status = this.resolveEntities(toValidate, this.entitiesToResolve); - - // if success, continue resolving - if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS - && this.referenceCatalogName != null) { - // finally, resolve all paths we need to resolve - status = this.resolvePaths(toValidate, this.pathsToResolve); - } - } - } - - // all the above resolution was optimistic i.e. when we probe the cache and find an entity, we - // don't validate if this entity has been changed in the backend. So validate now all these - // entities in one single - // go, - boolean validationSuccess = this.bulkValidate(toValidate); - - if (validationSuccess) { - this.updateResolved(); - } - - // if success, we are done, simply return the status. - return validationSuccess ? status : null; - } - - /** - * Update all entities which have been resolved since after validation, some might have changed - */ - private void updateResolved() { - - // if success, we need to get the validated entries - // we will resolve those again - this.resolvedCallerPrincipal = this.getResolved(this.resolvedCallerPrincipal); - - // update all principal roles with latest - if (!this.resolvedCallerPrincipalRoles.isEmpty()) { - List refreshedResolvedCallerPrincipalRoles = - new ArrayList<>(this.resolvedCallerPrincipalRoles.size()); - this.resolvedCallerPrincipalRoles.forEach( - ce -> refreshedResolvedCallerPrincipalRoles.add(this.getResolved(ce))); - this.resolvedCallerPrincipalRoles = refreshedResolvedCallerPrincipalRoles; - } - - // update referenced catalog - this.resolvedReferenceCatalog = this.getResolved(this.resolvedReferenceCatalog); - - // update all resolved catalog roles - if (this.resolvedCatalogRoles != null) { - for (EntityCacheEntry catalogCacheEntry : this.resolvedCatalogRoles.values()) { - this.resolvedCatalogRoles.put( - catalogCacheEntry.getEntity().getId(), this.getResolved(catalogCacheEntry)); - } - } - - // update all resolved paths - if (!this.resolvedPaths.isEmpty()) { - List> refreshedResolvedPaths = - new ArrayList<>(this.resolvedPaths.size()); - this.resolvedPaths.forEach( - rp -> { - List refreshedRp = new ArrayList<>(rp.size()); - rp.forEach(ce -> refreshedRp.add(this.getResolved(ce))); - refreshedResolvedPaths.add(refreshedRp); - }); - this.resolvedPaths = refreshedResolvedPaths; - } - } - - /** - * Get the fully resolved cache entry for the specified cache entry - * - * @param cacheEntry input cache entry - * @return the fully resolved cached entry which will often be the same - */ - private EntityCacheEntry getResolved(EntityCacheEntry cacheEntry) { - final EntityCacheEntry refreshedEntry; - if (cacheEntry == null) { - refreshedEntry = null; - } else { - // the latest refreshed entry - refreshedEntry = this.resolvedEntriesById.get(cacheEntry.getEntity().getId()); - this.diagnostics.checkNotNull( - refreshedEntry, "cache_entry_should_be_resolved", "entity={}", cacheEntry.getEntity()); - } - return refreshedEntry; - } - - /** - * Bulk validate now the set of entities we didn't validate when we were accessing the entity - * cache - * - * @param toValidate entities to validate - * @return true if none of the entities in the cache has changed - */ - private boolean bulkValidate(List toValidate) { - // assume everything is good - boolean validationStatus = true; - - // bulk validate - if (!toValidate.isEmpty()) { - List entityIds = - toValidate.stream() - .map( - cacheEntry -> - new PolarisEntityId( - cacheEntry.getEntity().getCatalogId(), cacheEntry.getEntity().getId())) - .collect(Collectors.toList()); - - // now get the current backend versions of all these entities - ChangeTrackingResult changeTrackingResult = - this.polarisRemoteCache.loadEntitiesChangeTracking(this.polarisCallContext, entityIds); - - // refresh any entity which is not fresh. If an entity is missing, reload it - Iterator entityIterator = toValidate.iterator(); - Iterator versionIterator = - changeTrackingResult.getChangeTrackingVersions().iterator(); - - // determine the ones we need to reload or refresh and the ones which are up-to-date - while (entityIterator.hasNext()) { - // get cache entry and associated versions - EntityCacheEntry cacheEntry = entityIterator.next(); - PolarisChangeTrackingVersions versions = versionIterator.next(); - - // entity we found in the cache - PolarisBaseEntity entity = cacheEntry.getEntity(); - - // refresh cache entry if the entity or grant records version is different - final EntityCacheEntry refreshedCacheEntry; - if (versions == null - || entity.getEntityVersion() != versions.getEntityVersion() - || entity.getGrantRecordsVersion() != versions.getGrantRecordsVersion()) { - // if null version we need to invalidate the cached entry since it has probably been - // dropped - if (versions == null) { - this.cache.removeCacheEntry(cacheEntry); - refreshedCacheEntry = null; - } else { - // refresh that entity. If versions is null, it has been dropped - refreshedCacheEntry = - this.cache.getAndRefreshIfNeeded( - this.polarisCallContext, - entity, - versions.getEntityVersion(), - versions.getGrantRecordsVersion()); - } - - // get the refreshed entity - PolarisBaseEntity refreshedEntity = - (refreshedCacheEntry == null) ? null : refreshedCacheEntry.getEntity(); - - // if the entity has been removed, or its name has changed, or it was re-parented, or it - // was dropped, we will have to perform another pass - if (refreshedEntity == null - || refreshedEntity.getParentId() != entity.getParentId() - || refreshedEntity.isDropped() != entity.isDropped() - || !refreshedEntity.getName().equals(entity.getName())) { - validationStatus = false; - } - - // special cases: the set of principal roles or catalog roles which have been - // activated might change if usage grants to a principal or a principal role have - // changed. Hence, force another pass if we are in that scenario - if (entity.getTypeCode() == PolarisEntityType.PRINCIPAL.getCode() - || entity.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode()) { - validationStatus = false; - } - } else { - // no need to refresh, it is up-to-date - refreshedCacheEntry = cacheEntry; - } - - // if it was found, it has been resolved, so if there is another pass, we will not have to - // resolve it again - if (refreshedCacheEntry != null) { - this.addToResolved(refreshedCacheEntry); - } - } - } - - // done, return final validation status - return validationStatus; - } - - /** - * Resolve a set of top-level service or catalog entities - * - * @param toValidate all entities we have resolved from the cache, hence we will have to verify - * that these entities have not changed in the backend - * @param entitiesToResolve the set of entities to resolve - * @return the status of resolution - */ - private ResolverStatus resolveEntities( - List toValidate, AbstractSet entitiesToResolve) { - // resolve each - for (ResolverEntityName entityName : entitiesToResolve) { - // resolve that entity - EntityCacheEntry resolvedEntity = - this.resolveByName(toValidate, entityName.getEntityType(), entityName.getEntityName()); - - // if not found, we can exit unless the entity is optional - if (!entityName.isOptional() - && (resolvedEntity == null || resolvedEntity.getEntity().isDropped())) { - return new ResolverStatus(entityName.getEntityType(), entityName.getEntityName()); - } - } - - // complete success - return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); - } - - /** - * Resolve a set of path inside the referenced catalog - * - * @param toValidate all entities we have resolved from the cache, hence we will have to verify - * that these entities have not changed in the backend - * @param pathsToResolve the set of paths to resolve - * @return the status of resolution - */ - private ResolverStatus resolvePaths( - List toValidate, List pathsToResolve) { - - // id of the catalog for all these paths - final long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); - - // resolve each path - for (ResolverPath path : pathsToResolve) { - - // path we are resolving - List resolvedPath = new ArrayList<>(); - - // initial parent id is the catalog itself - long parentId = catalogId; - - // resolve each segment - Iterator pathIt = path.getEntityNames().iterator(); - for (int segmentIndex = 0; segmentIndex < path.getEntityNames().size(); segmentIndex++) { - // get segment name - String segmentName = pathIt.next(); - - // determine the segment type - PolarisEntityType segmentType = - pathIt.hasNext() ? PolarisEntityType.NAMESPACE : path.getLastEntityType(); - - // resolve that entity - EntityCacheEntry segment = - this.resolveByName(toValidate, catalogId, segmentType, parentId, segmentName); - - // if not found, abort - if (segment == null || segment.getEntity().isDropped()) { - if (path.isOptional()) { - // we have resolved as much as what we could have - break; - } else { - return new ResolverStatus(path, segmentIndex); - } - } - - // this is the parent of the next segment - parentId = segment.getEntity().getId(); - - // add it to the path we are resolving - resolvedPath.add(segment); - } - - // one more path has been resolved - this.resolvedPaths.add(resolvedPath); - } - - // complete success - return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); - } - - /** - * Resolve the principal and determine which principal roles are activated. Resolved those. - * - * @param toValidate all entities we have resolved from the cache, hence we will have to verify - * that these entities have not changed in the backend - * @param callerPrincipalId the id of the principal which made the call - * @param callerPrincipalRoleNamesScope if not null, subset of roles activated by this call - * @return the status of resolution - */ - private ResolverStatus resolveCallerPrincipalAndPrincipalRoles( - List toValidate, - long callerPrincipalId, - String callerPrincipalName, - Set callerPrincipalRoleNamesScope) { - - // resolve the principal, by name or id - this.resolvedCallerPrincipal = - (callerPrincipalId != PolarisEntityConstants.getNullId()) - ? this.resolveById( - toValidate, - PolarisEntityType.PRINCIPAL, - PolarisEntityConstants.getNullId(), - callerPrincipalId) - : this.resolveByName(toValidate, PolarisEntityType.PRINCIPAL, callerPrincipalName); - - // if the principal was not found, we can end right there - if (this.resolvedCallerPrincipal == null - || this.resolvedCallerPrincipal.getEntity().isDropped()) { - return new ResolverStatus(ResolverStatus.StatusEnum.CALLER_PRINCIPAL_DOES_NOT_EXIST); - } - - // activate all principal roles which still exist - for (PolarisGrantRecord grantRecord : this.resolvedCallerPrincipal.getGrantRecordsAsGrantee()) { - if (grantRecord.getPrivilegeCode() == PolarisPrivilege.PRINCIPAL_ROLE_USAGE.getCode()) { - - // resolve principal role granted to that principal - EntityCacheEntry principalRole = - this.resolveById( - toValidate, - PolarisEntityType.PRINCIPAL_ROLE, - PolarisEntityConstants.getNullId(), - grantRecord.getSecurableId()); - - // skip if purged or has been dropped - if (principalRole != null && !principalRole.getEntity().isDropped()) { - // add it to the activated list if no scoped principal role or this principal role is - // activated - if (callerPrincipalRoleNamesScope == null - || callerPrincipalRoleNamesScope.contains(principalRole.getEntity().getName())) { - // this principal role is activated - this.resolvedCallerPrincipalRoles.add(principalRole); - } - } - } - } - - // total success - return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); - } - - /** - * Resolve the reference catalog and determine all activated role. The principal and principal - * roles should have already been resolved - * - * @param toValidate all entities we have resolved from the cache, hence we will have to verify - * that these entities have not changed in the backend - * @param referenceCatalogName name of the reference catalog to resolve, along with all catalog - * roles which are activated - * @return the status of resolution - */ - private ResolverStatus resolveReferenceCatalog( - @Nonnull List toValidate, @Nonnull String referenceCatalogName) { - // resolve the catalog - this.resolvedReferenceCatalog = - this.resolveByName(toValidate, PolarisEntityType.CATALOG, referenceCatalogName); - - // error out if we couldn't find it - if (this.resolvedReferenceCatalog == null - || this.resolvedReferenceCatalog.getEntity().isDropped()) { - return new ResolverStatus(PolarisEntityType.CATALOG, this.referenceCatalogName); - } - - // determine the set of catalog roles which have been activated - long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); - for (EntityCacheEntry principalRole : resolvedCallerPrincipalRoles) { - for (PolarisGrantRecord grantRecord : principalRole.getGrantRecordsAsGrantee()) { - // the securable is a catalog role belonging to - if (grantRecord.getPrivilegeCode() == PolarisPrivilege.CATALOG_ROLE_USAGE.getCode() - && grantRecord.getSecurableCatalogId() == catalogId) { - // the id of the catalog role - long catalogRoleId = grantRecord.getSecurableId(); - - // skip if it has already been added - if (!this.resolvedCatalogRoles.containsKey(catalogRoleId)) { - // see if this catalog can be resolved - EntityCacheEntry catalogRole = - this.resolveById( - toValidate, PolarisEntityType.CATALOG_ROLE, catalogId, catalogRoleId); - - // if found and not dropped, add it to the list of activated catalog roles - if (catalogRole != null && !catalogRole.getEntity().isDropped()) { - this.resolvedCatalogRoles.put(catalogRoleId, catalogRole); - } - } - } - } - } - - // all good - return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); - } - - /** - * Add a cache entry to the set of resolved entities - * - * @param refreshedCacheEntry refreshed cache entry - */ - private void addToResolved(EntityCacheEntry refreshedCacheEntry) { - // underlying entity - PolarisBaseEntity entity = refreshedCacheEntry.getEntity(); - - // add it by ID - this.resolvedEntriesById.put(entity.getId(), refreshedCacheEntry); - - // in the by name map, only add it if it has not been dropped - if (!entity.isDropped()) { - this.resolvedEntriesByName.put( - new EntityCacheByNameKey( - entity.getCatalogId(), entity.getParentId(), entity.getType(), entity.getName()), - refreshedCacheEntry); - } - } - - /** - * Add a top-level entity to resolve. If the entity type is a catalog role, we also expect that a - * reference catalog entity was specified at creation time, else we will assert. That catalog role - * entity will be resolved from there. We will fail the entire resolution process if that entity - * cannot be resolved. If this is not expected, use addOptionalEntityByName() instead. - * - * @param entityType the type of the entity, either a principal, a principal role, a catalog or a - * catalog role. - * @param entityName the name of the entity - * @param optional if true, the entity is optional - */ - private void addEntityByName( - @Nonnull PolarisEntityType entityType, @Nonnull String entityName, boolean optional) { - - // can only be called if the resolver has not yet been called - this.diagnostics.check(resolverStatus == null, "resolver_called"); - - // ensure everything was specified - diagnostics.checkNotNull(entityType, "unexpected_null_entity_type"); - diagnostics.checkNotNull(entityName, "unexpected_null_entity_name"); - - // ensure that a reference catalog has been specified if this entity is a catalog role - diagnostics.check( - entityType != PolarisEntityType.CATALOG_ROLE || this.referenceCatalogName != null, - "reference_catalog_must_be_specified"); - - // one more to resolve - this.entitiesToResolve.add(new ResolverEntityName(entityType, entityName, optional)); - } - - /** - * Resolve a top-level entity by name - * - * @param toValidate set of entries we will have to validate - * @param entityType entity type - * @param entityName name of the entity to resolve - * @return cache entry created for that entity - */ - private EntityCacheEntry resolveByName( - List toValidate, PolarisEntityType entityType, String entityName) { - if (entityType.isTopLevel()) { - return this.resolveByName( - toValidate, - PolarisEntityConstants.getNullId(), - entityType, - PolarisEntityConstants.getNullId(), - entityName); - } else { - // only top-level catalog entity - long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); - this.diagnostics.check(entityType == PolarisEntityType.CATALOG_ROLE, "catalog_role_expected"); - return this.resolveByName(toValidate, catalogId, entityType, catalogId, entityName); - } - } - - /** - * Resolve a top-level entity by name - * - * @param toValidate (IN/OUT) list of entities we will have to validate - * @param entityType entity type - * @param entityName name of the entity to resolve - * @return the resolve entity. Potentially update the toValidate list if we will have to validate - * that this entity is up-to-date - */ - private EntityCacheEntry resolveByName( - @Nonnull List toValidate, - long catalogId, - @Nonnull PolarisEntityType entityType, - long parentId, - @Nonnull String entityName) { - - // key for that entity - EntityCacheByNameKey nameKey = - new EntityCacheByNameKey(catalogId, parentId, entityType, entityName); - - // first check if this entity has not yet been resolved - EntityCacheEntry cacheEntry = this.resolvedEntriesByName.get(nameKey); - if (cacheEntry != null) { - return cacheEntry; - } - - // then check if it does not exist in the toValidate list. The same entity might be resolved - // several times with multi-path resolution - for (EntityCacheEntry ce : toValidate) { - PolarisBaseEntity entity = ce.getEntity(); - if (entity.getCatalogId() == catalogId - && entity.getParentId() == parentId - && entity.getType() == entityType - && entity.getName().equals(entityName)) { - return ce; - } - } - - // get or load by name - EntityCacheLookupResult lookupResult = - this.cache.getOrLoadEntityByName( - this.polarisCallContext, - new EntityCacheByNameKey(catalogId, parentId, entityType, entityName)); - - // if not found - if (lookupResult == null) { - // not found - return null; - } else if (lookupResult.isCacheHit()) { - // found in the cache, we will have to validate this entity - toValidate.add(lookupResult.getCacheEntry()); - } else { - // entry cannot be null - this.diagnostics.checkNotNull(lookupResult.getCacheEntry(), "cache_entry_is_null"); - // if not found in cache, it was loaded from backend, hence it has been resolved - this.addToResolved(lookupResult.getCacheEntry()); - } - - // return the cache entry - return lookupResult.getCacheEntry(); - } - - /** - * Resolve an entity by id - * - * @param toValidate (IN/OUT) list of entities we will have to validate - * @param entityType type of the entity to resolve - * @param catalogId entity catalog id - * @param entityId entity id - * @return the resolve entity. Potentially update the toValidate list if we will have to validate - * that this entity is up-to-date - */ - private EntityCacheEntry resolveById( - @Nonnull List toValidate, - @Nonnull PolarisEntityType entityType, - long catalogId, - long entityId) { - // get or load by name - EntityCacheLookupResult lookupResult = - this.cache.getOrLoadEntityById(this.polarisCallContext, catalogId, entityId); - - // if not found, return null - if (lookupResult == null) { - return null; - } else if (lookupResult.isCacheHit()) { - // found in the cache, we will have to validate this entity - toValidate.add(lookupResult.getCacheEntry()); - } else { - // entry cannot be null - this.diagnostics.checkNotNull(lookupResult.getCacheEntry(), "cache_entry_is_null"); - - // if not found in cache, it was loaded from backend, hence it has been resolved - this.addToResolved(lookupResult.getCacheEntry()); - } - - // return the cache entry - return lookupResult.getCacheEntry(); - } + @Nullable + EntityCacheEntry getResolvedEntity( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilder.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilder.java new file mode 100644 index 000000000..44f102a3c --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilder.java @@ -0,0 +1,92 @@ +/* + * 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; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import jakarta.annotation.Nonnull; +import org.apache.polaris.core.entity.PolarisEntityType; + +/** + * REST request resolver, allows to resolve all entities referenced directly or indirectly by in + * incoming rest request, Once resolved, the request can be authorized. + */ +public interface ResolverBuilder { + /** + * Add a top-level entity to resolve. If the entity type is a catalog role, we also expect that a + * reference catalog entity was specified at creation time, else we will assert. That catalog role + * entity will be resolved from there. We will fail the entire resolution process if that entity + * cannot be resolved. If this is not expected, use addOptionalEntityByName() instead. + * + * @param entityType the type of the entity, either a principal, a principal role, a catalog or a + * catalog role. + * @param entityName the name of the entity + */ + @CanIgnoreReturnValue + ResolverBuilder addEntityByName( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName); + + /** + * Add an optional top-level entity to resolve. If the entity type is a catalog role, we also + * expect that a reference catalog entity was specified at creation time, else we will assert. + * That catalog role entity will be resolved from there. If the entity cannot be resolved, we will + * not fail the resolution process + * + * @param entityType the type of the entity, either a principal, a principal role, a catalog or a + * catalog role. + * @param entityName the name of the entity + */ + @CanIgnoreReturnValue + ResolverBuilder addOptionalEntityByName( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName); + + /** + * Add a path to resolve + * + * @param path path to resolve + */ + @CanIgnoreReturnValue + ResolverBuilder addPath(@Nonnull ResolverPath path); + + /** + * Run the resolution process and return the status, either an error or success + * + *

Resolution might be working using multiple passes when using the cache since anything we + * find in the cache might have changed in the backend store. + * + *

For each pass we will + * + *

    + *
  • go over all entities and call EntityCache.getOrLoad...() on these entities, including all + * paths. + *
  • split these entities into 3 groups: + *
      + *
    • dropped or purged. We will return an error for these. + *
    • to be validated entities, they were found in the cache. For those we need to ensure + * that the entity id, its name and parent id has not changed. If yes we need to + * perform another pass. + *
    • reloaded from backend, so the entity is validated. Validated entities will not be + * validated again + *
    + *
+ * + * @return the resolver instance. If successfull, all entities have been resolved and the + * getResolvedXYZ() method can be called. + */ + Resolver buildResolved(); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilderImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilderImpl.java new file mode 100644 index 000000000..59103d68a --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverBuilderImpl.java @@ -0,0 +1,797 @@ +/* + * 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; + +import static com.google.common.base.Preconditions.checkState; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisChangeTrackingVersions; +import org.apache.polaris.core.entity.PolarisEntityConstants; +import org.apache.polaris.core.entity.PolarisEntityId; +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.cache.EntityCache; +import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey; +import org.apache.polaris.core.persistence.cache.EntityCacheEntry; +import org.apache.polaris.core.persistence.cache.EntityCacheLookupResult; +import org.apache.polaris.core.persistence.cache.PolarisRemoteCache; +import org.apache.polaris.core.persistence.cache.PolarisRemoteCache.ChangeTrackingResult; + +public final class ResolverBuilderImpl implements ResolverBuilder { + + // we stash the Polaris call context here + private final @Nonnull PolarisCallContext polarisCallContext; + + // the diagnostic services + private final @Nonnull PolarisDiagnostics diagnostics; + + // the polaris metastore manager + private final @Nonnull PolarisRemoteCache polarisRemoteCache; + + // the cache of entities + private final @Nonnull EntityCache cache; + + // the id of the principal making the call or 0 if unknown + private final long callerPrincipalId; + + // the name of the principal making the call or null if unknown. If 0, the principal name will be + // not null + private final String callerPrincipalName; + + // reference catalog name for name resolution + private final String referenceCatalogName; + + // if not null, subset of principal roles to activate + private final @Nullable Set callerPrincipalRoleNamesScope; + + // set of entities to resolve given their name. This does not include namespaces or table_like + // entities which are + // part of a path + private final AbstractSet entitiesToResolve; + + // list of paths to resolve + private final List pathsToResolve; + + // caller principal + private EntityCacheEntry resolvedCallerPrincipal; + + // all principal roles which have been resolved + private List resolvedCallerPrincipalRoles; + + // catalog to use as the reference catalog for role activation + private EntityCacheEntry resolvedReferenceCatalog; + + // all catalog roles which have been activated + private final Map resolvedCatalogRoles; + + // all resolved paths + private List> resolvedPaths; + + // all entities which have been successfully resolved, by name + private final Map resolvedEntriesByName; + + // all entities which have been fully resolved, by id + private final Map resolvedEntriesById; + + private boolean resolved; + + /** + * Constructor, effectively starts an entity resolver session + * + * @param polarisCallContext the polaris call context + * @param polarisRemoteCache meta store manager + * @param callerPrincipalId if not 0, the id of the principal calling the service + * @param callerPrincipalName if callerPrincipalId is 0, the name of the principal calling the + * service + * @param callerPrincipalRoleNamesScope if not null, scope principal roles + * @param cache shared entity cache + * @param referenceCatalogName if not null, specifies the name of the reference catalog. The + * reference catalog is the catalog used to resolve catalog roles and catalog path. Also, if a + * catalog reference is added, we will determine all catalog roles which are activated by the + * caller. Note that when a catalog name needs to be resolved because the principal creates or + * drop a catalog, it should not be specified here. Instead, it should be resolved by calling + * {@link #addEntityByName(PolarisEntityType, String)}. Generally, any DDL executed as a + * service admin should use null for that parameter. + */ + public ResolverBuilderImpl( + @Nonnull PolarisCallContext polarisCallContext, + @Nonnull PolarisRemoteCache polarisRemoteCache, + long callerPrincipalId, + @Nullable String callerPrincipalName, + @Nullable Set callerPrincipalRoleNamesScope, + @Nonnull EntityCache cache, + @Nullable String referenceCatalogName) { + this.polarisCallContext = polarisCallContext; + this.diagnostics = polarisCallContext.getDiagServices(); + this.polarisRemoteCache = polarisRemoteCache; + this.cache = cache; + this.callerPrincipalName = callerPrincipalName; + this.callerPrincipalId = callerPrincipalId; + this.referenceCatalogName = referenceCatalogName; + + // scoped principal role names + this.callerPrincipalRoleNamesScope = callerPrincipalRoleNamesScope; + + // validate inputs + this.diagnostics.checkNotNull(polarisRemoteCache, "unexpected_null_polarisRemoteCache"); + this.diagnostics.checkNotNull(cache, "unexpected_null_cache"); + this.diagnostics.check( + callerPrincipalId != 0 || callerPrincipalName != null, "principal_must_be_specified"); + + // paths to resolve + this.pathsToResolve = new ArrayList<>(); + this.resolvedPaths = new ArrayList<>(); + + // all entities we need to resolve by name + this.entitiesToResolve = new HashSet<>(); + + // will contain all principal roles which we were able to resolve + this.resolvedCallerPrincipalRoles = new ArrayList<>(); + + // remember if a reference catalog name was specified + if (referenceCatalogName != null) { + this.resolvedCatalogRoles = new HashMap<>(); + } else { + this.resolvedCatalogRoles = null; + } + + // all resolved entities, by name and by if + this.resolvedEntriesByName = new HashMap<>(); + resolvedEntriesById = new HashMap<>(); + } + + @Override + public ResolverBuilder addEntityByName( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName) { + checkState(!resolved, "resolver_called"); + diagnostics.checkNotNull(entityType, "entity_type_is_null"); + diagnostics.checkNotNull(entityName, "entity_name_is_null"); + this.addEntityByName(entityType, entityName, false); + return this; + } + + @Override + public ResolverBuilder addOptionalEntityByName( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName) { + checkState(!resolved, "resolver_called"); + diagnostics.checkNotNull(entityType, "entity_type_is_null"); + diagnostics.checkNotNull(entityName, "entity_name_is_null"); + this.addEntityByName(entityType, entityName, true); + return this; + } + + @Override + public ResolverBuilder addPath(@Nonnull ResolverPath path) { + checkState(!resolved, "resolver_called"); + diagnostics.checkNotNull(path, "unexpected_null_entity_path"); + this.pathsToResolve.add(path); + return this; + } + + @Override + public Resolver buildResolved() { + checkState(!resolved, "resolver_called"); + + resolved = true; + + // retry until a pass terminates, or we reached the maximum iteration count. Note that we should + // finish normally in no more than few passes so the 1000 limit is really to avoid spinning + // forever if there is a bug. + // + // TODO Note: this for-i-1000-loop looks suspicious (code smell) and at best just very expensive + for (int count = 0; ; count++) { + try { + runResolvePass(); + break; + } catch (ResolverException ex) { + if (count < 1000) { + continue; + } + throw ex; + } + } + ; + + // all has been resolved + return new ResolverImpl( + referenceCatalogName, + diagnostics, + resolvedCallerPrincipal, + resolvedCallerPrincipalRoles, + resolvedReferenceCatalog, + resolvedCatalogRoles, + resolvedPaths, + resolvedEntriesByName); + } + + /** Execute one resolve pass on all entities */ + private void runResolvePass() { + // we will resolve those again + this.resolvedCallerPrincipal = null; + this.resolvedReferenceCatalog = null; + if (this.resolvedCatalogRoles != null) { + this.resolvedCatalogRoles.clear(); + } + this.resolvedCallerPrincipalRoles.clear(); + this.resolvedPaths.clear(); + + // all entries we found in the cache but that we need to validate since they might be stale + List toValidate = new ArrayList<>(); + + // first resolve the principal and determine the set of activated principal roles + resolveCallerPrincipalAndPrincipalRoles( + toValidate, + this.callerPrincipalId, + this.callerPrincipalName, + this.callerPrincipalRoleNamesScope); + + // then resolve the reference catalog if one was specified + if (this.referenceCatalogName != null) { + resolveReferenceCatalog(toValidate, this.referenceCatalogName); + } + + // continue resolving all the additional entities we were asked to resolve + resolveEntities(toValidate, this.entitiesToResolve); + + // if success, continue resolving + if (this.referenceCatalogName != null) { + // finally, resolve all paths we need to resolve + resolvePaths(toValidate, this.pathsToResolve); + } + + // all the above resolution was optimistic i.e. when we probe the cache and find an entity, we + // don't validate if this entity has been changed in the backend. So validate now all these + // entities in one single go, + boolean validationSuccess = this.bulkValidate(toValidate); + + if (!validationSuccess) { + throw new ResolverException("bulk validation failed") {}; + } + + this.updateResolved(); + } + + /** + * Update all entities which have been resolved since after validation, some might have changed + */ + private void updateResolved() { + // if success, we need to get the validated entries + // we will resolve those again + this.resolvedCallerPrincipal = this.getResolved(this.resolvedCallerPrincipal); + + // update all principal roles with latest + if (!this.resolvedCallerPrincipalRoles.isEmpty()) { + List refreshedResolvedCallerPrincipalRoles = + new ArrayList<>(this.resolvedCallerPrincipalRoles.size()); + this.resolvedCallerPrincipalRoles.forEach( + ce -> refreshedResolvedCallerPrincipalRoles.add(this.getResolved(ce))); + this.resolvedCallerPrincipalRoles = refreshedResolvedCallerPrincipalRoles; + } + + // update referenced catalog + this.resolvedReferenceCatalog = this.getResolved(this.resolvedReferenceCatalog); + + // update all resolved catalog roles + if (this.resolvedCatalogRoles != null) { + for (EntityCacheEntry catalogCacheEntry : this.resolvedCatalogRoles.values()) { + this.resolvedCatalogRoles.put( + catalogCacheEntry.getEntity().getId(), this.getResolved(catalogCacheEntry)); + } + } + + // update all resolved paths + if (!this.resolvedPaths.isEmpty()) { + List> refreshedResolvedPaths = + new ArrayList<>(this.resolvedPaths.size()); + this.resolvedPaths.forEach( + rp -> { + List refreshedRp = new ArrayList<>(rp.size()); + rp.forEach(ce -> refreshedRp.add(this.getResolved(ce))); + refreshedResolvedPaths.add(refreshedRp); + }); + this.resolvedPaths = refreshedResolvedPaths; + } + } + + /** + * Get the fully resolved cache entry for the specified cache entry + * + * @param cacheEntry input cache entry + * @return the fully resolved cached entry which will often be the same + */ + private EntityCacheEntry getResolved(EntityCacheEntry cacheEntry) { + final EntityCacheEntry refreshedEntry; + if (cacheEntry == null) { + refreshedEntry = null; + } else { + // the latest refreshed entry + refreshedEntry = this.resolvedEntriesById.get(cacheEntry.getEntity().getId()); + this.diagnostics.checkNotNull( + refreshedEntry, "cache_entry_should_be_resolved", "entity={}", cacheEntry.getEntity()); + } + return refreshedEntry; + } + + /** + * Bulk validate now the set of entities we didn't validate when we were accessing the entity + * cache + * + * @param toValidate entities to validate + * @return true if none of the entities in the cache has changed + */ + private boolean bulkValidate(List toValidate) { + // assume everything is good + boolean validationStatus = true; + + // bulk validate + if (!toValidate.isEmpty()) { + List entityIds = + toValidate.stream() + .map( + cacheEntry -> + new PolarisEntityId( + cacheEntry.getEntity().getCatalogId(), cacheEntry.getEntity().getId())) + .collect(Collectors.toList()); + + // now get the current backend versions of all these entities + ChangeTrackingResult changeTrackingResult = + this.polarisRemoteCache.loadEntitiesChangeTracking(this.polarisCallContext, entityIds); + + // refresh any entity which is not fresh. If an entity is missing, reload it + Iterator entityIterator = toValidate.iterator(); + Iterator versionIterator = + changeTrackingResult.getChangeTrackingVersions().iterator(); + + // determine the ones we need to reload or refresh and the ones which are up-to-date + while (entityIterator.hasNext()) { + // get cache entry and associated versions + EntityCacheEntry cacheEntry = entityIterator.next(); + PolarisChangeTrackingVersions versions = versionIterator.next(); + + // entity we found in the cache + PolarisBaseEntity entity = cacheEntry.getEntity(); + + // refresh cache entry if the entity or grant records version is different + final EntityCacheEntry refreshedCacheEntry; + if (versions == null + || entity.getEntityVersion() != versions.getEntityVersion() + || entity.getGrantRecordsVersion() != versions.getGrantRecordsVersion()) { + // if null version we need to invalidate the cached entry since it has probably been + // dropped + if (versions == null) { + this.cache.removeCacheEntry(cacheEntry); + refreshedCacheEntry = null; + } else { + // refresh that entity. If versions is null, it has been dropped + refreshedCacheEntry = + this.cache.getAndRefreshIfNeeded( + this.polarisCallContext, + entity, + versions.getEntityVersion(), + versions.getGrantRecordsVersion()); + } + + // get the refreshed entity + PolarisBaseEntity refreshedEntity = + (refreshedCacheEntry == null) ? null : refreshedCacheEntry.getEntity(); + + // if the entity has been removed, or its name has changed, or it was re-parented, or it + // was dropped, we will have to perform another pass + if (refreshedEntity == null + || refreshedEntity.getParentId() != entity.getParentId() + || refreshedEntity.isDropped() != entity.isDropped() + || !refreshedEntity.getName().equals(entity.getName())) { + validationStatus = false; + } + + // special cases: the set of principal roles or catalog roles which have been + // activated might change if usage grants to a principal or a principal role have + // changed. Hence, force another pass if we are in that scenario + if (entity.getTypeCode() == PolarisEntityType.PRINCIPAL.getCode() + || entity.getTypeCode() == PolarisEntityType.PRINCIPAL_ROLE.getCode()) { + validationStatus = false; + } + } else { + // no need to refresh, it is up-to-date + refreshedCacheEntry = cacheEntry; + } + + // if it was found, it has been resolved, so if there is another pass, we will not have to + // resolve it again + if (refreshedCacheEntry != null) { + this.addToResolved(refreshedCacheEntry); + } + } + } + + // done, return final validation status + return validationStatus; + } + + /** + * Resolve a set of top-level service or catalog entities + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param entitiesToResolve the set of entities to resolve + */ + private void resolveEntities( + List toValidate, AbstractSet entitiesToResolve) { + // resolve each + for (ResolverEntityName entityName : entitiesToResolve) { + // resolve that entity + EntityCacheEntry resolvedEntity = + this.resolveByName(toValidate, entityName.getEntityType(), entityName.getEntityName()); + + // if not found, we can exit unless the entity is optional + if (!entityName.isOptional() + && (resolvedEntity == null || resolvedEntity.getEntity().isDropped())) { + throw new ResolverException.EntityNotResolvedException( + entityName.getEntityType(), entityName.getEntityName()); + } + } + } + + /** + * Resolve a set of path inside the referenced catalog + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param pathsToResolve the set of paths to resolve + */ + private void resolvePaths(List toValidate, List pathsToResolve) { + + // id of the catalog for all these paths + final long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + + // resolve each path + for (ResolverPath path : pathsToResolve) { + + // path we are resolving + List resolvedPath = new ArrayList<>(); + + // initial parent id is the catalog itself + long parentId = catalogId; + + // resolve each segment + Iterator pathIt = path.getEntityNames().iterator(); + for (int segmentIndex = 0; segmentIndex < path.getEntityNames().size(); segmentIndex++) { + // get segment name + String segmentName = pathIt.next(); + + // determine the segment type + PolarisEntityType segmentType = + pathIt.hasNext() ? PolarisEntityType.NAMESPACE : path.getLastEntityType(); + + // resolve that entity + EntityCacheEntry segment = + this.resolveByName(toValidate, catalogId, segmentType, parentId, segmentName); + + // if not found, abort + if (segment == null || segment.getEntity().isDropped()) { + if (path.isOptional()) { + // we have resolved as much as what we could have + break; + } else { + throw new ResolverException.PathNotFullyResolvedException(path, segmentIndex); + } + } + + // this is the parent of the next segment + parentId = segment.getEntity().getId(); + + // add it to the path we are resolving + resolvedPath.add(segment); + } + + // one more path has been resolved + this.resolvedPaths.add(resolvedPath); + } + } + + /** + * Resolve the principal and determine which principal roles are activated. Resolved those. + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param callerPrincipalId the id of the principal which made the call + * @param callerPrincipalRoleNamesScope if not null, subset of roles activated by this call + */ + private void resolveCallerPrincipalAndPrincipalRoles( + List toValidate, + long callerPrincipalId, + String callerPrincipalName, + Set callerPrincipalRoleNamesScope) { + + // resolve the principal, by name or id + this.resolvedCallerPrincipal = + (callerPrincipalId != PolarisEntityConstants.getNullId()) + ? this.resolveById( + toValidate, + PolarisEntityType.PRINCIPAL, + PolarisEntityConstants.getNullId(), + callerPrincipalId) + : this.resolveByName(toValidate, PolarisEntityType.PRINCIPAL, callerPrincipalName); + + // if the principal was not found, we can end right there + if (this.resolvedCallerPrincipal == null + || this.resolvedCallerPrincipal.getEntity().isDropped()) { + throw new ResolverException.CallerPrincipalNotFoundException(); + } + + // activate all principal roles which still exist + for (PolarisGrantRecord grantRecord : this.resolvedCallerPrincipal.getGrantRecordsAsGrantee()) { + if (grantRecord.getPrivilegeCode() == PolarisPrivilege.PRINCIPAL_ROLE_USAGE.getCode()) { + + // resolve principal role granted to that principal + EntityCacheEntry principalRole = + this.resolveById( + toValidate, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntityConstants.getNullId(), + grantRecord.getSecurableId()); + + // skip if purged or has been dropped + if (principalRole != null && !principalRole.getEntity().isDropped()) { + // add it to the activated list if no scoped principal role or this principal role is + // activated + if (callerPrincipalRoleNamesScope == null + || callerPrincipalRoleNamesScope.contains(principalRole.getEntity().getName())) { + // this principal role is activated + this.resolvedCallerPrincipalRoles.add(principalRole); + } + } + } + } + } + + /** + * Resolve the reference catalog and determine all activated role. The principal and principal + * roles should have already been resolved + * + * @param toValidate all entities we have resolved from the cache, hence we will have to verify + * that these entities have not changed in the backend + * @param referenceCatalogName name of the reference catalog to resolve, along with all catalog + * roles which are activated + */ + private void resolveReferenceCatalog( + @Nonnull List toValidate, @Nonnull String referenceCatalogName) { + // resolve the catalog + this.resolvedReferenceCatalog = + this.resolveByName(toValidate, PolarisEntityType.CATALOG, referenceCatalogName); + + // error out if we couldn't find it + if (this.resolvedReferenceCatalog == null + || this.resolvedReferenceCatalog.getEntity().isDropped()) { + throw new ResolverException.EntityNotResolvedException( + PolarisEntityType.CATALOG, this.referenceCatalogName); + } + + // determine the set of catalog roles which have been activated + long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + for (EntityCacheEntry principalRole : resolvedCallerPrincipalRoles) { + for (PolarisGrantRecord grantRecord : principalRole.getGrantRecordsAsGrantee()) { + // the securable is a catalog role belonging to + if (grantRecord.getPrivilegeCode() == PolarisPrivilege.CATALOG_ROLE_USAGE.getCode() + && grantRecord.getSecurableCatalogId() == catalogId) { + // the id of the catalog role + long catalogRoleId = grantRecord.getSecurableId(); + + // skip if it has already been added + if (!this.resolvedCatalogRoles.containsKey(catalogRoleId)) { + // see if this catalog can be resolved + EntityCacheEntry catalogRole = + this.resolveById( + toValidate, PolarisEntityType.CATALOG_ROLE, catalogId, catalogRoleId); + + // if found and not dropped, add it to the list of activated catalog roles + if (catalogRole != null && !catalogRole.getEntity().isDropped()) { + this.resolvedCatalogRoles.put(catalogRoleId, catalogRole); + } + } + } + } + } + } + + /** + * Add a cache entry to the set of resolved entities + * + * @param refreshedCacheEntry refreshed cache entry + */ + private void addToResolved(EntityCacheEntry refreshedCacheEntry) { + // underlying entity + PolarisBaseEntity entity = refreshedCacheEntry.getEntity(); + + // add it by ID + this.resolvedEntriesById.put(entity.getId(), refreshedCacheEntry); + + // in the by name map, only add it if it has not been dropped + if (!entity.isDropped()) { + this.resolvedEntriesByName.put( + new EntityCacheByNameKey( + entity.getCatalogId(), entity.getParentId(), entity.getType(), entity.getName()), + refreshedCacheEntry); + } + } + + /** + * Add a top-level entity to resolve. If the entity type is a catalog role, we also expect that a + * reference catalog entity was specified at creation time, else we will assert. That catalog role + * entity will be resolved from there. We will fail the entire resolution process if that entity + * cannot be resolved. If this is not expected, use addOptionalEntityByName() instead. + * + * @param entityType the type of the entity, either a principal, a principal role, a catalog or a + * catalog role. + * @param entityName the name of the entity + * @param optional if true, the entity is optional + */ + private void addEntityByName( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName, boolean optional) { + // ensure everything was specified + diagnostics.checkNotNull(entityType, "unexpected_null_entity_type"); + diagnostics.checkNotNull(entityName, "unexpected_null_entity_name"); + + // ensure that a reference catalog has been specified if this entity is a catalog role + diagnostics.check( + entityType != PolarisEntityType.CATALOG_ROLE || this.referenceCatalogName != null, + "reference_catalog_must_be_specified"); + + // one more to resolve + this.entitiesToResolve.add(new ResolverEntityName(entityType, entityName, optional)); + } + + /** + * Resolve a top-level entity by name + * + * @param toValidate set of entries we will have to validate + * @param entityType entity type + * @param entityName name of the entity to resolve + * @return cache entry created for that entity + */ + private EntityCacheEntry resolveByName( + List toValidate, PolarisEntityType entityType, String entityName) { + if (entityType.isTopLevel()) { + return this.resolveByName( + toValidate, + PolarisEntityConstants.getNullId(), + entityType, + PolarisEntityConstants.getNullId(), + entityName); + } else { + // only top-level catalog entity + long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + this.diagnostics.check(entityType == PolarisEntityType.CATALOG_ROLE, "catalog_role_expected"); + return this.resolveByName(toValidate, catalogId, entityType, catalogId, entityName); + } + } + + /** + * Resolve a top-level entity by name + * + * @param toValidate (IN/OUT) list of entities we will have to validate + * @param entityType entity type + * @param entityName name of the entity to resolve + * @return the resolve entity. Potentially update the toValidate list if we will have to validate + * that this entity is up-to-date + */ + private EntityCacheEntry resolveByName( + @Nonnull List toValidate, + long catalogId, + @Nonnull PolarisEntityType entityType, + long parentId, + @Nonnull String entityName) { + + // key for that entity + EntityCacheByNameKey nameKey = + new EntityCacheByNameKey(catalogId, parentId, entityType, entityName); + + // first check if this entity has not yet been resolved + EntityCacheEntry cacheEntry = this.resolvedEntriesByName.get(nameKey); + if (cacheEntry != null) { + return cacheEntry; + } + + // then check if it does not exist in the toValidate list. The same entity might be resolved + // several times with multi-path resolution + for (EntityCacheEntry ce : toValidate) { + PolarisBaseEntity entity = ce.getEntity(); + if (entity.getCatalogId() == catalogId + && entity.getParentId() == parentId + && entity.getType() == entityType + && entity.getName().equals(entityName)) { + return ce; + } + } + + // get or load by name + EntityCacheLookupResult lookupResult = + this.cache.getOrLoadEntityByName( + this.polarisCallContext, + new EntityCacheByNameKey(catalogId, parentId, entityType, entityName)); + + // if not found + if (lookupResult == null) { + // not found + return null; + } else if (lookupResult.isCacheHit()) { + // found in the cache, we will have to validate this entity + toValidate.add(lookupResult.getCacheEntry()); + } else { + // entry cannot be null + this.diagnostics.checkNotNull(lookupResult.getCacheEntry(), "cache_entry_is_null"); + // if not found in cache, it was loaded from backend, hence it has been resolved + this.addToResolved(lookupResult.getCacheEntry()); + } + + // return the cache entry + return lookupResult.getCacheEntry(); + } + + /** + * Resolve an entity by id + * + * @param toValidate (IN/OUT) list of entities we will have to validate + * @param entityType type of the entity to resolve + * @param catalogId entity catalog id + * @param entityId entity id + * @return the resolve entity. Potentially update the toValidate list if we will have to validate + * that this entity is up-to-date + */ + private EntityCacheEntry resolveById( + @Nonnull List toValidate, + @Nonnull PolarisEntityType entityType, + long catalogId, + long entityId) { + // get or load by name + EntityCacheLookupResult lookupResult = + this.cache.getOrLoadEntityById(this.polarisCallContext, catalogId, entityId); + + // if not found, return null + if (lookupResult == null) { + return null; + } else if (lookupResult.isCacheHit()) { + // found in the cache, we will have to validate this entity + toValidate.add(lookupResult.getCacheEntry()); + } else { + // entry cannot be null + this.diagnostics.checkNotNull(lookupResult.getCacheEntry(), "cache_entry_is_null"); + + // if not found in cache, it was loaded from backend, hence it has been resolved + this.addToResolved(lookupResult.getCacheEntry()); + } + + // return the cache entry + return lookupResult.getCacheEntry(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverException.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverException.java new file mode 100644 index 000000000..84d2f0a46 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverException.java @@ -0,0 +1,71 @@ +/* + * 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; + +import org.apache.polaris.core.entity.PolarisEntityType; + +public abstract class ResolverException extends RuntimeException { + protected ResolverException() { + super(); + } + + protected ResolverException(String message) { + super(message); + } + + public static final class CallerPrincipalNotFoundException extends ResolverException {} + + public static final class PathNotFullyResolvedException extends ResolverException { + private final ResolverPath failedToResolvePath; + private final int failedToResolvedEntityIndex; + + public PathNotFullyResolvedException( + ResolverPath failedToResolvePath, int failedToResolvedEntityIndex) { + this.failedToResolvePath = failedToResolvePath; + this.failedToResolvedEntityIndex = failedToResolvedEntityIndex; + } + + public ResolverPath failedToResolvePath() { + return failedToResolvePath; + } + + public int failedToResolvedEntityIndex() { + return failedToResolvedEntityIndex; + } + } + + public static final class EntityNotResolvedException extends ResolverException { + private final PolarisEntityType failedToResolvedEntityType; + private final String failedToResolvedEntityName; + + public EntityNotResolvedException( + PolarisEntityType failedToResolvedEntityType, String failedToResolvedEntityName) { + this.failedToResolvedEntityType = failedToResolvedEntityType; + this.failedToResolvedEntityName = failedToResolvedEntityName; + } + + public PolarisEntityType failedToResolvedEntityType() { + return failedToResolvedEntityType; + } + + public String failedToResolvedEntityName() { + return failedToResolvedEntityName; + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverImpl.java new file mode 100644 index 000000000..193799cb8 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverImpl.java @@ -0,0 +1,118 @@ +/* + * 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; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Map; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey; +import org.apache.polaris.core.persistence.cache.EntityCacheEntry; + +final class ResolverImpl implements Resolver { + private final String referenceCatalogName; + private final @Nonnull PolarisDiagnostics diagnostics; + private final EntityCacheEntry resolvedCallerPrincipal; + private final List resolvedCallerPrincipalRoles; + private final EntityCacheEntry resolvedReferenceCatalog; + private final Map resolvedCatalogRoles; + private final List> resolvedPaths; + private final Map resolvedEntriesByName; + + ResolverImpl( + String referenceCatalogName, + @Nonnull PolarisDiagnostics diagnostics, + EntityCacheEntry resolvedCallerPrincipal, + List resolvedCallerPrincipalRoles, + EntityCacheEntry resolvedReferenceCatalog, + Map resolvedCatalogRoles, + List> resolvedPaths, + Map resolvedEntriesByName) { + this.referenceCatalogName = referenceCatalogName; + this.diagnostics = diagnostics; + this.resolvedCallerPrincipal = resolvedCallerPrincipal; + this.resolvedCallerPrincipalRoles = resolvedCallerPrincipalRoles; + this.resolvedReferenceCatalog = resolvedReferenceCatalog; + this.resolvedCatalogRoles = resolvedCatalogRoles; + this.resolvedPaths = resolvedPaths; + this.resolvedEntriesByName = resolvedEntriesByName; + } + + @Override + @Nonnull + public EntityCacheEntry getResolvedCallerPrincipal() { + return resolvedCallerPrincipal; + } + + @Override + @Nonnull + public List getResolvedCallerPrincipalRoles() { + return resolvedCallerPrincipalRoles; + } + + @Override + @Nullable + public EntityCacheEntry getResolvedReferenceCatalog() { + return resolvedReferenceCatalog; + } + + @Override + @Nullable + public Map getResolvedCatalogRoles() { + return resolvedCatalogRoles; + } + + @Override + @Nonnull + public List getResolvedPath() { + this.diagnostics.check(this.resolvedPaths.size() == 1, "only_if_single"); + + return resolvedPaths.get(0); + } + + @Override + @Nonnull + public List> getResolvedPaths() { + this.diagnostics.check(!this.resolvedPaths.isEmpty(), "no_path_resolved"); + + return resolvedPaths; + } + + @Override + @Nullable + public EntityCacheEntry getResolvedEntity( + @Nonnull PolarisEntityType entityType, @Nonnull String entityName) { + // validate input + diagnostics.check( + entityType != PolarisEntityType.NAMESPACE && entityType != PolarisEntityType.TABLE_LIKE, + "cannot_be_path"); + diagnostics.check( + entityType.isTopLevel() || this.referenceCatalogName != null, "reference_catalog_expected"); + + if (entityType.isTopLevel()) { + return this.resolvedEntriesByName.get(new EntityCacheByNameKey(entityType, entityName)); + } else { + long catalogId = this.resolvedReferenceCatalog.getEntity().getId(); + return this.resolvedEntriesByName.get( + new EntityCacheByNameKey(catalogId, catalogId, entityType, entityName)); + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverStatus.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverStatus.java deleted file mode 100644 index 49fff120b..000000000 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/resolver/ResolverStatus.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.core.persistence.resolver; - -import org.apache.polaris.core.entity.PolarisEntityType; - -public class ResolverStatus { - - /** - * Status code for the caller to know if all entities were resolved successfully or if resolution - * failed. Anything but success is a failure - */ - public enum StatusEnum { - // success - SUCCESS, - - // error, principal making the call does not exist - CALLER_PRINCIPAL_DOES_NOT_EXIST, - - // error, the path could not be resolved. The payload of the status will provide the path and - // the index in that - // path for the segment of the path which could not be resolved - PATH_COULD_NOT_BE_FULLY_RESOLVED, - - // error, an entity could not be resolved - ENTITY_COULD_NOT_BE_RESOLVED, - } - - private final StatusEnum status; - - // if status is ENTITY_COULD_NOT_BE_RESOLVED, will be set to the entity type which couldn't be - // resolved - private final PolarisEntityType failedToResolvedEntityType; - - // if status is ENTITY_COULD_NOT_BE_RESOLVED, will be set to the entity name which couldn't be - // resolved - private final String failedToResolvedEntityName; - - // if status is PATH_COULD_NOT_BE_FULLY_RESOLVED, path which we failed to resolve - private final ResolverPath failedToResolvePath; - - // if status is PATH_COULD_NOT_BE_FULLY_RESOLVED, index in the path which we failed to - // resolve - private final int failedToResolvedEntityIndex; - - public ResolverStatus(StatusEnum status) { - this.status = status; - this.failedToResolvedEntityType = null; - this.failedToResolvedEntityName = null; - this.failedToResolvePath = null; - this.failedToResolvedEntityIndex = 0; - } - - public ResolverStatus( - PolarisEntityType failedToResolvedEntityType, String failedToResolvedEntityName) { - this.status = StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED; - this.failedToResolvedEntityType = failedToResolvedEntityType; - this.failedToResolvedEntityName = failedToResolvedEntityName; - this.failedToResolvePath = null; - this.failedToResolvedEntityIndex = 0; - } - - public ResolverStatus(ResolverPath failedToResolvePath, int failedToResolvedEntityIndex) { - this.status = StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED; - this.failedToResolvedEntityType = null; - this.failedToResolvedEntityName = null; - this.failedToResolvePath = failedToResolvePath; - this.failedToResolvedEntityIndex = failedToResolvedEntityIndex; - } - - public StatusEnum getStatus() { - return status; - } - - public PolarisEntityType getFailedToResolvedEntityType() { - return failedToResolvedEntityType; - } - - public String getFailedToResolvedEntityName() { - return failedToResolvedEntityName; - } - - public ResolverPath getFailedToResolvePath() { - return failedToResolvePath; - } - - public int getFailedToResolvedEntityIndex() { - return failedToResolvedEntityIndex; - } -} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java index c47367769..cdd0aef6b 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/EntityCacheTest.java @@ -34,6 +34,9 @@ import org.apache.polaris.core.persistence.cache.EntityCacheByNameKey; import org.apache.polaris.core.persistence.cache.EntityCacheEntry; import org.apache.polaris.core.persistence.cache.EntityCacheLookupResult; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapMetaStoreSessionImpl; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapStore; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java index f05cd7895..02321864e 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.persistence; import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -40,9 +41,14 @@ import org.apache.polaris.core.persistence.cache.EntityCache; import org.apache.polaris.core.persistence.cache.EntityCacheEntry; import org.apache.polaris.core.persistence.cache.PolarisRemoteCache.CachedEntryResult; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapMetaStoreSessionImpl; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapStore; import org.apache.polaris.core.persistence.resolver.Resolver; +import org.apache.polaris.core.persistence.resolver.ResolverBuilder; +import org.apache.polaris.core.persistence.resolver.ResolverBuilderImpl; +import org.apache.polaris.core.persistence.resolver.ResolverException; import org.apache.polaris.core.persistence.resolver.ResolverPath; -import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -119,7 +125,7 @@ void testResolvePrincipal() { // resolve same principal but now make it non optional, so should fail this.resolveDriver( - null, null, "P3", false, null, ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + null, null, "P3", false, null, ResolverException.EntityNotResolvedException.class); // then resolve a principal which does exist this.resolveDriver(null, null, "P2", false, null, null); @@ -207,21 +213,13 @@ void testResolvePath() { ResolverPath N5_N6_T8 = new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE); this.resolveDriver( - this.cache, - "test", - N5_N6_T8, - null, - ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + this.cache, "test", N5_N6_T8, null, ResolverException.PathNotFullyResolvedException.class); // Error scenarios: N8/N6/T8 which does not exists ResolverPath N8_N6_T8 = new ResolverPath(List.of("N8", "N6", "T8"), PolarisEntityType.TABLE_LIKE); this.resolveDriver( - this.cache, - "test", - N8_N6_T8, - null, - ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + this.cache, "test", N8_N6_T8, null, ResolverException.PathNotFullyResolvedException.class); // now test multiple paths this.resolveDriver( @@ -231,7 +229,7 @@ void testResolvePath() { "test", null, List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), - ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + ResolverException.PathNotFullyResolvedException.class); // except if the optional flag is specified N5_N6_T8 = new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE, true); @@ -262,18 +260,13 @@ void testConsistency() { // now resolve it again. Should fail because the entity was dropped this.resolveDriver( - this.cache, - null, - "P2", - false, - null, - ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + this.cache, null, "P2", false, null, ResolverException.EntityNotResolvedException.class); // recreate P2 this.tm.createPrincipal("P2"); // now resolve it again. Should succeed because the entity has been re-created - this.resolveDriver(this.cache, null, "P2", false, null, ResolverStatus.StatusEnum.SUCCESS); + this.resolveDriver(this.cache, null, "P2", false, null, null); // resolve existing grants on catalog this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); @@ -347,7 +340,7 @@ void testPathConsistency() { "test", N1_N2_T1_PATH, null, - ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + ResolverException.PathNotFullyResolvedException.class); // but we should be able to resolve it under N1/N3 ResolverPath N1_N3_T1_PATH = @@ -368,7 +361,7 @@ void testResolveCatalogRole() { // failure scenario this.resolveDriver( - this.cache, "test", "R5", ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + this.cache, "test", "R5", ResolverException.EntityNotResolvedException.class); } /** @@ -378,7 +371,7 @@ void testResolveCatalogRole() { * @return new resolver to test with */ @Nonnull - private Resolver allocateResolver() { + private ResolverBuilder allocateResolver() { return this.allocateResolver(null, null); } @@ -390,7 +383,7 @@ private Resolver allocateResolver() { * @return new resolver to test with */ @Nonnull - private Resolver allocateResolver(@Nullable String referenceCatalogName) { + private ResolverBuilder allocateResolver(@Nullable String referenceCatalogName) { return this.allocateResolver(null, referenceCatalogName); } @@ -402,7 +395,7 @@ private Resolver allocateResolver(@Nullable String referenceCatalogName) { * @return new resolver to test with */ @Nonnull - private Resolver allocateResolver(@Nullable EntityCache cache) { + private ResolverBuilder allocateResolver(@Nullable EntityCache cache) { return this.allocateResolver(cache, null); } @@ -415,7 +408,7 @@ private Resolver allocateResolver(@Nullable EntityCache cache) { * @return new resolver to test with */ @Nonnull - private Resolver allocateResolver( + private ResolverBuilder allocateResolver( @Nullable EntityCache cache, @Nullable String referenceCatalogName) { return this.allocateResolver(cache, null, referenceCatalogName); } @@ -430,7 +423,7 @@ private Resolver allocateResolver( * @return new resolver to test with */ @Nonnull - private Resolver allocateResolver( + private ResolverBuilder allocateResolver( @Nullable EntityCache cache, Set principalRolesScope, @Nullable String referenceCatalogName) { @@ -439,7 +432,7 @@ private Resolver allocateResolver( if (cache == null) { this.cache = new EntityCache(this.metaStoreManager); } - return new Resolver( + return new ResolverBuilderImpl( this.callCtx, this.metaStoreManager, this.P1.getId(), @@ -459,22 +452,19 @@ private Resolver allocateResolver( */ private void resolvePrincipalAndPrincipalRole( EntityCache cache, String principalName, boolean exists, String principalRoleName) { - Resolver resolver = allocateResolver(cache); + var resolverBuilder = allocateResolver(cache); // for a principal creation, we simply want to test if the principal we are creating exists // or not - resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); + resolverBuilder.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); // add principal role if one passed-in if (principalRoleName != null) { - resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + resolverBuilder.addOptionalEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); } // done, run resolve - ResolverStatus status = resolver.resolveAll(); - - // we expect success - Assertions.assertThat(status.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS); + var resolver = resolverBuilder.buildResolved(); // the principal does not exist, check that this is the case if (exists) { @@ -532,7 +522,7 @@ private Resolver resolveDriver( String principalName, boolean isPrincipalNameOptional, String principalRoleName, - ResolverStatus.StatusEnum expectedStatus) { + Class expectedStatus) { return this.resolveDriver( cache, principalRolesScope, @@ -562,7 +552,7 @@ private Resolver resolveDriver( String catalogName, ResolverPath path, List paths, - ResolverStatus.StatusEnum expectedStatus) { + Class expectedStatus) { return this.resolveDriver( cache, null, null, false, null, catalogName, null, path, paths, expectedStatus, null); } @@ -609,7 +599,7 @@ private Resolver resolveDriver( EntityCache cache, String catalogName, String catalogRoleName, - ResolverStatus.StatusEnum expectedStatus) { + Class expectedStatus) { return this.resolveDriver( cache, null, @@ -651,189 +641,181 @@ private Resolver resolveDriver( String catalogRoleName, ResolverPath path, List paths, - ResolverStatus.StatusEnum expectedStatus, + Class expectedStatus, Set expectedActivatedCatalogRoles) { - // if null we expect success - if (expectedStatus == null) { - expectedStatus = ResolverStatus.StatusEnum.SUCCESS; - } - // allocate resolver - Resolver resolver = allocateResolver(cache, principalRolesScope, catalogName); + var resolverBuilder = allocateResolver(cache, principalRolesScope, catalogName); // principal name? if (principalName != null) { if (isPrincipalNameOptional) { - resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); + resolverBuilder.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); } else { - resolver.addEntityByName(PolarisEntityType.PRINCIPAL, principalName); + resolverBuilder.addEntityByName(PolarisEntityType.PRINCIPAL, principalName); } } // add principal role if one passed-in if (principalRoleName != null) { - resolver.addEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + resolverBuilder.addEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); } // add catalog role if one passed-in if (catalogRoleName != null) { - resolver.addEntityByName(PolarisEntityType.CATALOG_ROLE, catalogRoleName); + resolverBuilder.addEntityByName(PolarisEntityType.CATALOG_ROLE, catalogRoleName); } // add all paths if (path != null) { - resolver.addPath(path); + resolverBuilder.addPath(path); } else if (paths != null) { - paths.forEach(resolver::addPath); + paths.forEach(resolverBuilder::addPath); } // done, run resolve - ResolverStatus status = resolver.resolveAll(); - - // we expect success unless a status - Assertions.assertThat(status).isNotNull(); - Assertions.assertThat(status.getStatus()).isEqualTo(expectedStatus); - - // validate if status is success - if (status.getStatus() == ResolverStatus.StatusEnum.SUCCESS) { - - // the principal does not exist, check that this is the case - if (principalName != null) { - // see if the principal exists - PolarisMetaStoreManager.EntityResult result = - this.metaStoreManager.readEntityByName( - this.callCtx, - null, - PolarisEntityType.PRINCIPAL, - PolarisEntitySubType.NULL_SUBTYPE, - principalName); - // if found, ensure properly resolved - if (result.getEntity() != null) { - // the principal exist, check that this is the case - this.ensureResolved( - resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName), + if (expectedStatus != null) { + assertThatThrownBy(resolverBuilder::buildResolved).isInstanceOf(expectedStatus); + return null; + } + + var resolver = resolverBuilder.buildResolved(); + + // the principal does not exist, check that this is the case + if (principalName != null) { + // see if the principal exists + PolarisMetaStoreManager.EntityResult result = + this.metaStoreManager.readEntityByName( + this.callCtx, + null, PolarisEntityType.PRINCIPAL, + PolarisEntitySubType.NULL_SUBTYPE, principalName); - } else { - // principal was optional - Assertions.assertThat(isPrincipalNameOptional).isTrue(); - // not found - Assertions.assertThat( - resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)) - .isNull(); - } + // if found, ensure properly resolved + if (result.getEntity() != null) { + // the principal exist, check that this is the case + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName), + PolarisEntityType.PRINCIPAL, + principalName); + } else { + // principal was optional + Assertions.assertThat(isPrincipalNameOptional).isTrue(); + // not found + Assertions.assertThat( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)) + .isNull(); } + } + + // validate that we were able to resolve the caller principal + this.ensureResolved(resolver.getResolvedCallerPrincipal(), PolarisEntityType.PRINCIPAL, "P1"); + + // validate that the correct set if principal roles have been activated + List principalRolesResolved = resolver.getResolvedCallerPrincipalRoles(); + principalRolesResolved.sort(Comparator.comparing(p -> p.getEntity().getName())); - // validate that we were able to resolve the caller principal - this.ensureResolved(resolver.getResolvedCallerPrincipal(), PolarisEntityType.PRINCIPAL, "P1"); - - // validate that the correct set if principal roles have been activated - List principalRolesResolved = resolver.getResolvedCallerPrincipalRoles(); - principalRolesResolved.sort(Comparator.comparing(p -> p.getEntity().getName())); - - // expect two principal roles if not scoped - int expectedSize; - if (principalRolesScope != null) { - expectedSize = 0; - for (String pr : principalRolesScope) { - if (pr.equals("PR1") || pr.equals("PR2")) { - expectedSize++; - } + // expect two principal roles if not scoped + int expectedSize; + if (principalRolesScope != null) { + expectedSize = 0; + for (String pr : principalRolesScope) { + if (pr.equals("PR1") || pr.equals("PR2")) { + expectedSize++; } - } else { - // both PR1 and PR2 - expectedSize = 2; } + } else { + // both PR1 and PR2 + expectedSize = 2; + } - // ensure the right set of principal roles were activated - Assertions.assertThat(principalRolesResolved).hasSize(expectedSize); + // ensure the right set of principal roles were activated + Assertions.assertThat(principalRolesResolved).hasSize(expectedSize); - // expect either PR1 and PR2 - for (EntityCacheEntry principalRoleResolved : principalRolesResolved) { - Assertions.assertThat(principalRoleResolved).isNotNull(); - Assertions.assertThat(principalRoleResolved.getEntity()).isNotNull(); - String roleName = principalRoleResolved.getEntity().getName(); + // expect either PR1 and PR2 + for (EntityCacheEntry principalRoleResolved : principalRolesResolved) { + Assertions.assertThat(principalRoleResolved).isNotNull(); + Assertions.assertThat(principalRoleResolved.getEntity()).isNotNull(); + String roleName = principalRoleResolved.getEntity().getName(); - // should be either PR1 or PR2 - Assertions.assertThat(roleName.equals("PR1") || roleName.equals("PR2")).isTrue(); + // should be either PR1 or PR2 + Assertions.assertThat(roleName.equals("PR1") || roleName.equals("PR2")).isTrue(); - // ensure they are PR1 and PR2 - this.ensureResolved(principalRoleResolved, PolarisEntityType.PRINCIPAL_ROLE, roleName); - } + // ensure they are PR1 and PR2 + this.ensureResolved(principalRoleResolved, PolarisEntityType.PRINCIPAL_ROLE, roleName); + } + + // if a principal role was passed-in, ensure it exists + if (principalRoleName != null) { + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName), + PolarisEntityType.PRINCIPAL_ROLE, + principalRoleName); + } - // if a principal role was passed-in, ensure it exists - if (principalRoleName != null) { + // if a catalog was passed-in, ensure it exists + if (catalogName != null) { + EntityCacheEntry catalogEntry = + resolver.getResolvedEntity(PolarisEntityType.CATALOG, catalogName); + Assertions.assertThat(catalogEntry).isNotNull(); + this.ensureResolved(catalogEntry, PolarisEntityType.CATALOG, catalogName); + + // if a catalog role was passed-in, ensure that it was properly resolved + if (catalogRoleName != null) { + EntityCacheEntry catalogRoleEntry = + resolver.getResolvedEntity(PolarisEntityType.CATALOG_ROLE, catalogRoleName); this.ensureResolved( - resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName), - PolarisEntityType.PRINCIPAL_ROLE, - principalRoleName); + catalogRoleEntry, + List.of(catalogEntry.getEntity()), + PolarisEntityType.CATALOG_ROLE, + catalogRoleName); } - // if a catalog was passed-in, ensure it exists - if (catalogName != null) { - EntityCacheEntry catalogEntry = - resolver.getResolvedEntity(PolarisEntityType.CATALOG, catalogName); - Assertions.assertThat(catalogEntry).isNotNull(); - this.ensureResolved(catalogEntry, PolarisEntityType.CATALOG, catalogName); - - // if a catalog role was passed-in, ensure that it was properly resolved - if (catalogRoleName != null) { - EntityCacheEntry catalogRoleEntry = - resolver.getResolvedEntity(PolarisEntityType.CATALOG_ROLE, catalogRoleName); - this.ensureResolved( - catalogRoleEntry, - List.of(catalogEntry.getEntity()), - PolarisEntityType.CATALOG_ROLE, - catalogRoleName); - } - - // validate activated catalog roles - Map activatedCatalogs = resolver.getResolvedCatalogRoles(); + // validate activated catalog roles + Map activatedCatalogs = resolver.getResolvedCatalogRoles(); - // if there is an expected set, ensure we have the same set - if (expectedActivatedCatalogRoles != null) { - Assertions.assertThat(activatedCatalogs).hasSameSizeAs(expectedActivatedCatalogRoles); - } + // if there is an expected set, ensure we have the same set + if (expectedActivatedCatalogRoles != null) { + Assertions.assertThat(activatedCatalogs).hasSameSizeAs(expectedActivatedCatalogRoles); + } - // process each of those - for (EntityCacheEntry resolvedActivatedCatalogEntry : activatedCatalogs.values()) { - // must be in the expected list - Assertions.assertThat(resolvedActivatedCatalogEntry).isNotNull(); - PolarisBaseEntity activatedCatalogRole = resolvedActivatedCatalogEntry.getEntity(); - Assertions.assertThat(activatedCatalogRole).isNotNull(); - // ensure well resolved - this.ensureResolved( - resolvedActivatedCatalogEntry, - List.of(catalogEntry.getEntity()), - PolarisEntityType.CATALOG_ROLE, - activatedCatalogRole.getName()); - - // in the set of expected catalog roles - Assertions.assertThat( - expectedActivatedCatalogRoles == null - || expectedActivatedCatalogRoles.contains(activatedCatalogRole.getName())) - .isTrue(); - } + // process each of those + for (EntityCacheEntry resolvedActivatedCatalogEntry : activatedCatalogs.values()) { + // must be in the expected list + Assertions.assertThat(resolvedActivatedCatalogEntry).isNotNull(); + PolarisBaseEntity activatedCatalogRole = resolvedActivatedCatalogEntry.getEntity(); + Assertions.assertThat(activatedCatalogRole).isNotNull(); + // ensure well resolved + this.ensureResolved( + resolvedActivatedCatalogEntry, + List.of(catalogEntry.getEntity()), + PolarisEntityType.CATALOG_ROLE, + activatedCatalogRole.getName()); + + // in the set of expected catalog roles + Assertions.assertThat( + expectedActivatedCatalogRoles == null + || expectedActivatedCatalogRoles.contains(activatedCatalogRole.getName())) + .isTrue(); + } - // resolve each path - if (path != null || paths != null) { - // path to validate - List allPathsToCheck = (paths == null) ? List.of(path) : paths; + // resolve each path + if (path != null || paths != null) { + // path to validate + List allPathsToCheck = (paths == null) ? List.of(path) : paths; - // all resolved path - List> allResolvedPaths = resolver.getResolvedPaths(); + // all resolved path + List> allResolvedPaths = resolver.getResolvedPaths(); - // same size - Assertions.assertThat(allResolvedPaths).hasSameSizeAs(allPathsToCheck); + // same size + Assertions.assertThat(allResolvedPaths).hasSameSizeAs(allPathsToCheck); - // check that each path was properly resolved - int pathCount = 0; - Iterator allPathsToCheckIt = allPathsToCheck.iterator(); - for (List resolvedPath : allResolvedPaths) { - this.ensurePathResolved( - pathCount++, catalogEntry.getEntity(), allPathsToCheckIt.next(), resolvedPath); - } + // check that each path was properly resolved + int pathCount = 0; + Iterator allPathsToCheckIt = allPathsToCheck.iterator(); + for (List resolvedPath : allResolvedPaths) { + this.ensurePathResolved( + pathCount++, catalogEntry.getEntity(), allPathsToCheckIt.next(), resolvedPath); } } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/impl/PolarisObjectMapperUtilTest.java similarity index 98% rename from polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java rename to polaris-core/src/test/java/org/apache/polaris/core/persistence/impl/PolarisObjectMapperUtilTest.java index c22a4ad8e..8e054e508 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/impl/PolarisObjectMapperUtilTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.impl; import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapMetaStoreManagerTest.java similarity index 86% rename from polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java rename to polaris-core/src/test/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapMetaStoreManagerTest.java index e44b45577..9f2881d6e 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisTreeMapMetaStoreManagerTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/local/inmem/PolarisTreeMapMetaStoreManagerTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.persistence; +package org.apache.polaris.core.persistence.local.inmem; import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS; @@ -25,6 +25,9 @@ import org.apache.polaris.core.PolarisConfigurationStore; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.persistence.BasePolarisMetaStoreManagerTest; +import org.apache.polaris.core.persistence.PolarisTestMetaStoreManager; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; import org.mockito.Mockito; public class PolarisTreeMapMetaStoreManagerTest extends BasePolarisMetaStoreManagerTest { diff --git a/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java b/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java index 5e276030d..2e3fc109b 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java @@ -39,11 +39,11 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.BaseResult; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; -import org.apache.polaris.core.persistence.PolarisMetaStoreManagerImpl; import org.apache.polaris.core.persistence.PolarisMetaStoreSession; -import org.apache.polaris.core.persistence.PolarisObjectMapperUtil; -import org.apache.polaris.core.persistence.PolarisTreeMapMetaStoreSessionImpl; -import org.apache.polaris.core.persistence.PolarisTreeMapStore; +import org.apache.polaris.core.persistence.impl.PolarisMetaStoreManagerImpl; +import org.apache.polaris.core.persistence.impl.PolarisObjectMapperUtil; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapMetaStoreSessionImpl; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapStore; import org.apache.polaris.core.storage.PolarisCredentialProperty; import org.apache.polaris.core.storage.PolarisCredentialVendor.ScopedCredentialsResult; import org.assertj.core.api.Assertions; diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java index 1b5483afe..d2dc2b73e 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java @@ -43,6 +43,7 @@ import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.TaskEntity; +import org.apache.polaris.core.persistence.impl.PolarisObjectMapperUtil; import org.assertj.core.api.Assertions; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java index 11ee4a87f..25bd95c67 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java @@ -44,6 +44,7 @@ import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.entity.PolarisTaskConstants; import org.apache.polaris.core.persistence.cache.PolarisRemoteCache.CachedEntryResult; +import org.apache.polaris.core.persistence.impl.PolarisObjectMapperUtil; import org.assertj.core.api.Assertions; /** Test the Polaris persistence layer */ diff --git a/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index a80460018..860ff6069 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -34,9 +34,6 @@ import org.apache.iceberg.exceptions.AlreadyExistsException; import org.apache.iceberg.exceptions.BadRequestException; import org.apache.iceberg.exceptions.CommitFailedException; -import org.apache.iceberg.exceptions.NoSuchNamespaceException; -import org.apache.iceberg.exceptions.NoSuchTableException; -import org.apache.iceberg.exceptions.NoSuchViewException; import org.apache.iceberg.exceptions.NotFoundException; import org.apache.iceberg.exceptions.ValidationException; import org.apache.polaris.core.PolarisCallContext; @@ -75,12 +72,12 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.entity.TableLikeEntity; +import org.apache.polaris.core.persistence.EntityNotFoundException; import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolution.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolverPath; -import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; import org.apache.polaris.core.storage.StorageLocation; import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo; @@ -108,7 +105,7 @@ public class PolarisAdminService { private final PolarisMetaStoreManager metaStoreManager; // Initialized in the authorize methods. - private PolarisResolutionManifest resolutionManifest = null; + private ResolutionManifest resolutionManifest = null; public PolarisAdminService( CallContext callContext, @@ -151,9 +148,10 @@ private Optional findCatalogRoleByName(String catalogName, St private void authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation op) { resolutionManifest = - entityManager.prepareResolutionManifest( - callContext, authenticatedPrincipal, null /* referenceCatalogName */); - resolutionManifest.resolveAll(); + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, null) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .buildResolved(); PolarisResolvedPathWrapper rootContainerWrapper = resolutionManifest.getResolvedRootContainerEntityAsPath(); authorizer.authorizeOrThrow( @@ -178,14 +176,11 @@ private void authorizeBasicTopLevelEntityOperationOrThrow( PolarisEntityType entityType, @Nullable String referenceCatalogName) { resolutionManifest = - entityManager.prepareResolutionManifest( - callContext, authenticatedPrincipal, referenceCatalogName); - resolutionManifest.addTopLevelName(topLevelEntityName, entityType, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); - if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { - throw new NotFoundException( - "TopLevelEntity of type %s does not exist: %s", entityType, topLevelEntityName); - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, referenceCatalogName) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addTopLevelName(topLevelEntityName, entityType, false /* isOptional */) + .buildResolved(); PolarisResolvedPathWrapper topLevelEntityWrapper = resolutionManifest.getResolvedTopLevelEntity(topLevelEntityName, entityType); @@ -212,11 +207,13 @@ private void authorizeBasicTopLevelEntityOperationOrThrow( private void authorizeBasicCatalogRoleOperationOrThrow( PolarisAuthorizableOperation op, String catalogName, String catalogRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addPath( - new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - resolutionManifest.resolveAll(); + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName) + .buildResolved(); PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(catalogRoleName, true); if (target == null) { throw new NotFoundException("CatalogRole does not exist: %s", catalogRoleName); @@ -232,16 +229,12 @@ private void authorizeBasicCatalogRoleOperationOrThrow( private void authorizeGrantOnRootContainerToPrincipalRoleOperationOrThrow( PolarisAuthorizableOperation op, String principalRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, null); - resolutionManifest.addTopLevelName( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); - - if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { - throw new NotFoundException( - "Entity %s not found when trying to grant on root to %s", - status.getFailedToResolvedEntityName(), principalRoleName); - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, null) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */) + .buildResolved(); // TODO: Merge this method into authorizeGrantOnTopLevelEntityToPrincipalRoleOperationOrThrow // once we remove any special handling logic for the rootContainer. @@ -265,21 +258,13 @@ private void authorizeGrantOnTopLevelEntityToPrincipalRoleOperationOrThrow( PolarisEntityType topLevelEntityType, String principalRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, null); - resolutionManifest.addTopLevelName( - topLevelEntityName, topLevelEntityType, false /* isOptional */); - resolutionManifest.addTopLevelName( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); - - if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { - throw new NotFoundException( - "Entity %s not found when trying to assign %s of type %s to %s", - status.getFailedToResolvedEntityName(), - topLevelEntityName, - topLevelEntityType, - principalRoleName); - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, null) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addTopLevelName(topLevelEntityName, topLevelEntityType, false /* isOptional */) + .addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */) + .buildResolved(); PolarisResolvedPathWrapper topLevelEntityWrapper = resolutionManifest.getResolvedTopLevelEntity(topLevelEntityName, topLevelEntityType); @@ -298,18 +283,13 @@ private void authorizeGrantOnTopLevelEntityToPrincipalRoleOperationOrThrow( private void authorizeGrantOnPrincipalRoleToPrincipalOperationOrThrow( PolarisAuthorizableOperation op, String principalRoleName, String principalName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, null); - resolutionManifest.addTopLevelName( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); - resolutionManifest.addTopLevelName( - principalName, PolarisEntityType.PRINCIPAL, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); - - if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { - throw new NotFoundException( - "Entity %s not found when trying to assign %s to %s", - status.getFailedToResolvedEntityName(), principalRoleName, principalName); - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, null) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */) + .addTopLevelName(principalName, PolarisEntityType.PRINCIPAL, false /* isOptional */) + .buildResolved(); PolarisResolvedPathWrapper principalRoleWrapper = resolutionManifest.getResolvedTopLevelEntity( @@ -331,23 +311,15 @@ private void authorizeGrantOnCatalogRoleToPrincipalRoleOperationOrThrow( String catalogRoleName, String principalRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addPath( - new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - resolutionManifest.addTopLevelName( - principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */); - ResolverStatus status = resolutionManifest.resolveAll(); - - if (status.getStatus() == ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) { - throw new NotFoundException( - "Entity %s not found when trying to assign %s.%s to %s", - status.getFailedToResolvedEntityName(), catalogName, catalogRoleName, principalRoleName); - } else if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { - throw new NotFoundException( - "Entity %s not found when trying to assign %s.%s to %s", - status.getFailedToResolvePath(), catalogName, catalogRoleName, principalRoleName); - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName) + .addTopLevelName( + principalRoleName, PolarisEntityType.PRINCIPAL_ROLE, false /* isOptional */) + .buildResolved(); PolarisResolvedPathWrapper principalRoleWrapper = resolutionManifest.getResolvedTopLevelEntity( @@ -366,19 +338,14 @@ private void authorizeGrantOnCatalogRoleToPrincipalRoleOperationOrThrow( private void authorizeGrantOnCatalogOperationOrThrow( PolarisAuthorizableOperation op, String catalogName, String catalogRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addTopLevelName( - catalogName, PolarisEntityType.CATALOG, false /* isOptional */); - resolutionManifest.addPath( - new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); - - 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) { - throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addTopLevelName(catalogName, PolarisEntityType.CATALOG, false /* isOptional */) + .addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName) + .buildResolved(); PolarisResolvedPathWrapper catalogWrapper = resolutionManifest.getResolvedTopLevelEntity(catalogName, PolarisEntityType.CATALOG); @@ -398,25 +365,16 @@ private void authorizeGrantOnNamespaceOperationOrThrow( Namespace namespace, String catalogRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); - resolutionManifest.addPath( - new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); - - 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) { - if (status.getFailedToResolvePath().getLastEntityType() == PolarisEntityType.NAMESPACE) { - throw new NoSuchNamespaceException( - "Namespace does not exist: %s", status.getFailedToResolvePath().getEntityNames()); - } else { - throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); - } - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace) + .addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName) + .buildResolved(); PolarisResolvedPathWrapper namespaceWrapper = resolutionManifest.getResolvedPath(namespace, true); @@ -438,29 +396,18 @@ private void authorizeGrantOnTableLikeOperationOrThrow( TableIdentifier identifier, String catalogRoleName) { resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE), - identifier); - resolutionManifest.addPath( - new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), - catalogRoleName); - ResolverStatus status = resolutionManifest.resolveAll(); - - 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) { - if (status.getFailedToResolvePath().getLastEntityType() == PolarisEntityType.TABLE_LIKE) { - if (subType == PolarisEntitySubType.TABLE) { - throw new NoSuchTableException("Table does not exist: %s", identifier); - } else { - throw new NoSuchViewException("View does not exist: %s", identifier); - } - } else { - throw new NotFoundException("CatalogRole not found: %s.%s", catalogName, catalogRoleName); - } - } + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asGenericIcebergNotFoundException) + .addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE), + identifier) + .addPath( + new ResolverPath(List.of(catalogRoleName), PolarisEntityType.CATALOG_ROLE), + catalogRoleName) + .buildResolved(); PolarisResolvedPathWrapper tableLikeWrapper = resolutionManifest.getResolvedPath(identifier, subType, true); diff --git a/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java b/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java index a70a2538d..0191f887f 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java @@ -87,13 +87,12 @@ import org.apache.polaris.core.entity.PolarisTaskConstants; import org.apache.polaris.core.entity.TableLikeEntity; import org.apache.polaris.core.persistence.BaseResult; +import org.apache.polaris.core.persistence.EntityNotFoundException; import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView; +import org.apache.polaris.core.persistence.resolution.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolverPath; -import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.storage.InMemoryStorageIntegration; import org.apache.polaris.core.storage.PolarisCredentialVendor; import org.apache.polaris.core.storage.PolarisStorageActions; @@ -160,7 +159,7 @@ public class BasePolarisCatalog extends BaseMetastoreViewCatalog private final PolarisEntityManager entityManager; private final CallContext callContext; - private final PolarisResolutionManifestCatalogView resolvedEntityView; + private final ResolutionManifest resolutionManifest; private final CatalogEntity catalogEntity; private final TaskExecutor taskExecutor; private final AuthenticatedPolarisPrincipal authenticatedPrincipal; @@ -179,7 +178,7 @@ public class BasePolarisCatalog extends BaseMetastoreViewCatalog * @param entityManager provides handle to underlying PolarisMetaStoreManager with which to * perform mutations on entities. * @param callContext the current CallContext - * @param resolvedEntityView accessor to resolved entity paths that have been pre-vetted to ensure + * @param resolutionManifest accessor to resolved entity paths that have been pre-vetted to ensure * this catalog instance only interacts with authorized resolved paths. * @param taskExecutor Executor we use to register cleanup task handlers */ @@ -187,15 +186,15 @@ public BasePolarisCatalog( PolarisEntityManager entityManager, PolarisMetaStoreManager metaStoreManager, CallContext callContext, - PolarisResolutionManifestCatalogView resolvedEntityView, + ResolutionManifest resolutionManifest, AuthenticatedPolarisPrincipal authenticatedPrincipal, TaskExecutor taskExecutor, FileIOFactory fileIOFactory) { this.entityManager = entityManager; this.callContext = callContext; - this.resolvedEntityView = resolvedEntityView; + this.resolutionManifest = resolutionManifest; this.catalogEntity = - CatalogEntity.of(resolvedEntityView.getResolvedReferenceCatalogEntity().getRawLeafEntity()); + CatalogEntity.of(resolutionManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity()); this.authenticatedPrincipal = authenticatedPrincipal; this.taskExecutor = taskExecutor; this.catalogId = catalogEntity.getId(); @@ -314,7 +313,7 @@ public Table registerTable(TableIdentifier identifier, String metadataFileLocati TableOperations ops = newTableOps(identifier); PolarisResolvedPathWrapper resolvedParent = - resolvedEntityView.getResolvedPath(identifier.namespace()); + resolutionManifest.getResolvedPath(identifier.namespace()); if (resolvedParent == null) { // Illegal state because the namespace should've already been in the static resolution set. throw new IllegalStateException( @@ -357,7 +356,7 @@ protected String defaultWarehouseLocation(TableIdentifier tableIdentifier) { defaultNamespaceLocation(tableIdentifier.namespace()), tableIdentifier.name()); } else { PolarisResolvedPathWrapper resolvedNamespace = - resolvedEntityView.getResolvedPath(tableIdentifier.namespace()); + resolutionManifest.getResolvedPath(tableIdentifier.namespace()); if (resolvedNamespace == null) { throw new NoSuchNamespaceException( "Namespace does not exist: %s", tableIdentifier.namespace()); @@ -489,7 +488,7 @@ public void createNamespace(Namespace namespace, Map metadata) { // TODO: These should really be helpers in core Iceberg Namespace. Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(namespace); - PolarisResolvedPathWrapper resolvedParent = resolvedEntityView.getResolvedPath(parentNamespace); + PolarisResolvedPathWrapper resolvedParent = resolutionManifest.getResolvedPath(parentNamespace); if (resolvedParent == null) { throw new NoSuchNamespaceException( "Cannot create namespace %s. Parent namespace does not exist.", namespace); @@ -543,7 +542,7 @@ private String resolveNamespaceLocation(Namespace namespace, Map List parentPath = namespace.length() > 1 ? getResolvedParentNamespace(namespace).getRawFullPath() - : List.of(resolvedEntityView.getResolvedReferenceCatalogEntity().getRawLeafEntity()); + : List.of(resolutionManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity()); String parentLocation = resolveLocationForPath(parentPath); @@ -596,21 +595,21 @@ private static String stripLeadingTrailingSlash(String location) { private PolarisResolvedPathWrapper getResolvedParentNamespace(Namespace namespace) { Namespace parentNamespace = Namespace.of(Arrays.copyOf(namespace.levels(), namespace.length() - 1)); - PolarisResolvedPathWrapper resolvedParent = resolvedEntityView.getResolvedPath(parentNamespace); + PolarisResolvedPathWrapper resolvedParent = resolutionManifest.getResolvedPath(parentNamespace); if (resolvedParent == null) { - return resolvedEntityView.getPassthroughResolvedPath(parentNamespace); + return resolutionManifest.getPassthroughResolvedPath(parentNamespace); } return resolvedParent; } @Override public boolean namespaceExists(Namespace namespace) { - return resolvedEntityView.getResolvedPath(namespace) != null; + return resolutionManifest.getResolvedPath(namespace) != null; } @Override public boolean dropNamespace(Namespace namespace) throws NamespaceNotEmptyException { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(namespace); if (resolvedEntities == null) { return false; } @@ -643,7 +642,7 @@ public boolean dropNamespace(Namespace namespace) throws NamespaceNotEmptyExcept @Override public boolean setProperties(Namespace namespace, Map properties) throws NoSuchNamespaceException { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(namespace); if (resolvedEntities == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } @@ -690,7 +689,7 @@ public boolean setProperties(Namespace namespace, Map properties @Override public boolean removeProperties(Namespace namespace, Set properties) throws NoSuchNamespaceException { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(namespace); if (resolvedEntities == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } @@ -722,7 +721,7 @@ public boolean removeProperties(Namespace namespace, Set properties) @Override public Map loadNamespaceMetadata(Namespace namespace) throws NoSuchNamespaceException { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(namespace); if (resolvedEntities == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } @@ -743,7 +742,7 @@ public List listNamespaces() { @Override public List listNamespaces(Namespace namespace) throws NoSuchNamespaceException { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(namespace); if (resolvedEntities == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); } @@ -849,11 +848,11 @@ public String transformTableLikeLocation(String specifiedTableLikeLocation) { private @Nonnull Optional findStorageInfo(TableIdentifier tableIdentifier) { PolarisResolvedPathWrapper resolvedTableEntities = - resolvedEntityView.getResolvedPath(tableIdentifier, PolarisEntitySubType.TABLE); + resolutionManifest.getResolvedPath(tableIdentifier, PolarisEntitySubType.TABLE); PolarisResolvedPathWrapper resolvedStorageEntity = resolvedTableEntities == null - ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()) + ? resolutionManifest.getResolvedPath(tableIdentifier.namespace()) : resolvedTableEntities; return findStorageInfoFromHierarchy(resolvedStorageEntity); @@ -919,12 +918,12 @@ private Map refreshCredentials( */ private void validateLocationForTableLike(TableIdentifier identifier, String location) { PolarisResolvedPathWrapper resolvedStorageEntity = - resolvedEntityView.getResolvedPath(identifier, PolarisEntitySubType.ANY_SUBTYPE); + resolutionManifest.getResolvedPath(identifier, PolarisEntitySubType.ANY_SUBTYPE); if (resolvedStorageEntity == null) { - resolvedStorageEntity = resolvedEntityView.getResolvedPath(identifier.namespace()); + resolvedStorageEntity = resolutionManifest.getResolvedPath(identifier.namespace()); } if (resolvedStorageEntity == null) { - resolvedStorageEntity = resolvedEntityView.getPassthroughResolvedPath(identifier.namespace()); + resolvedStorageEntity = resolutionManifest.getPassthroughResolvedPath(identifier.namespace()); } validateLocationForTableLike(identifier, location, resolvedStorageEntity); @@ -1113,25 +1112,30 @@ private void validateNoLocationOverlap( LOGGER.debug( "Resolving {} sibling entities to validate location", siblingTables.size() + siblingNamespaces.size()); - PolarisResolutionManifest resolutionManifest = - new PolarisResolutionManifest( - callContext, entityManager, authenticatedPrincipal, parentPath.getFirst().getName()); + + var referenceCatalogName = parentPath.getFirst().getName(); + var resolutionManifestBuilder = + metaStoreManager + .newResolutionManifestBuilder( + callContext, + authenticatedPrincipal, + () -> + entityManager.prepareResolver( + callContext, authenticatedPrincipal, referenceCatalogName), + referenceCatalogName) + .notFoundExceptionMapper( + EntityNotFoundException::asSpecializedIcebergNotFoundException); siblingTables.forEach( tbl -> - resolutionManifest.addPath( + resolutionManifestBuilder.addPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(tbl), PolarisEntityType.TABLE_LIKE), tbl)); siblingNamespaces.forEach( ns -> - resolutionManifest.addPath( + resolutionManifestBuilder.addPath( new ResolverPath(Arrays.asList(ns.levels()), PolarisEntityType.NAMESPACE), ns)); - ResolverStatus status = resolutionManifest.resolveAll(); - if (!status.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { - throw new IllegalStateException( - "Unable to resolve sibling entities to validate location - could not resolve" - + status.getFailedToResolvedEntityName()); - } + var resolutionManifest = resolutionManifestBuilder.buildResolved(); StorageLocation targetLocation = StorageLocation.of(location); Stream.concat( @@ -1212,7 +1216,7 @@ public void doRefresh() { // While doing refresh/commit protocols, we must fetch the fresh "passthrough" resolved // table entity instead of the statically-resolved authz resolution set. PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getPassthroughResolvedPath( + resolutionManifest.getPassthroughResolvedPath( tableIdentifier, PolarisEntitySubType.TABLE); TableLikeEntity entity = null; @@ -1266,7 +1270,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { } PolarisResolvedPathWrapper resolvedTableEntities = - resolvedEntityView.getPassthroughResolvedPath( + resolutionManifest.getPassthroughResolvedPath( tableIdentifier, PolarisEntitySubType.TABLE); // Fetch credentials for the resolved entity. The entity could be the table itself (if it has @@ -1274,7 +1278,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { // table's namespace or catalog. PolarisResolvedPathWrapper resolvedStorageEntity = resolvedTableEntities == null - ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()) + ? resolutionManifest.getResolvedPath(tableIdentifier.namespace()) : resolvedTableEntities; // refresh credentials because we need to read the metadata file to validate its location @@ -1288,7 +1292,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { List resolvedNamespace = resolvedTableEntities == null - ? resolvedEntityView.getResolvedPath(tableIdentifier.namespace()).getRawFullPath() + ? resolutionManifest.getResolvedPath(tableIdentifier.namespace()).getRawFullPath() : resolvedTableEntities.getRawParentPath(); CatalogEntity catalog = CatalogEntity.of(resolvedNamespace.getFirst()); @@ -1329,7 +1333,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { String oldLocation = base == null ? null : base.metadataFileLocation(); PolarisResolvedPathWrapper resolvedView = - resolvedEntityView.getPassthroughResolvedPath(tableIdentifier, PolarisEntitySubType.VIEW); + resolutionManifest.getPassthroughResolvedPath(tableIdentifier, PolarisEntitySubType.VIEW); if (resolvedView != null) { throw new AlreadyExistsException("View with same name already exists: %s", tableIdentifier); } @@ -1341,7 +1345,7 @@ public void doCommit(TableMetadata base, TableMetadata metadata) { // modification between our checking of unchanged metadataLocation here and actual // persistence-layer commit). PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getPassthroughResolvedPath( + resolutionManifest.getPassthroughResolvedPath( tableIdentifier, PolarisEntitySubType.TABLE); TableLikeEntity entity = TableLikeEntity.of(resolvedEntities == null ? null : resolvedEntities.getRawLeafEntity()); @@ -1450,7 +1454,7 @@ private class BasePolarisViewOperations extends BaseViewOperations { @Override public void doRefresh() { PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getPassthroughResolvedPath(identifier, PolarisEntitySubType.VIEW); + resolutionManifest.getPassthroughResolvedPath(identifier, PolarisEntitySubType.VIEW); TableLikeEntity entity = null; if (resolvedEntities != null) { @@ -1504,25 +1508,25 @@ public void doCommit(ViewMetadata base, ViewMetadata metadata) { } PolarisResolvedPathWrapper resolvedTable = - resolvedEntityView.getPassthroughResolvedPath(identifier, PolarisEntitySubType.TABLE); + resolutionManifest.getPassthroughResolvedPath(identifier, PolarisEntitySubType.TABLE); if (resolvedTable != null) { throw new AlreadyExistsException("Table with same name already exists: %s", identifier); } PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getPassthroughResolvedPath(identifier, PolarisEntitySubType.VIEW); + resolutionManifest.getPassthroughResolvedPath(identifier, PolarisEntitySubType.VIEW); // Fetch credentials for the resolved entity. The entity could be the view itself (if it has // already been stored and credentials have been configured directly) or it could be the // table's namespace or catalog. PolarisResolvedPathWrapper resolvedStorageEntity = resolvedEntities == null - ? resolvedEntityView.getResolvedPath(identifier.namespace()) + ? resolutionManifest.getResolvedPath(identifier.namespace()) : resolvedEntities; List resolvedNamespace = resolvedEntities == null - ? resolvedEntityView.getResolvedPath(identifier.namespace()).getRawFullPath() + ? resolutionManifest.getResolvedPath(identifier.namespace()).getRawFullPath() : resolvedEntities.getRawParentPath(); if (base == null || !metadata.location().equals(base.location())) { // If location is changing then we must validate that the requested location is valid @@ -1649,7 +1653,7 @@ long getCatalogId() { private void renameTableLike( PolarisEntitySubType subType, TableIdentifier from, TableIdentifier to) { LOGGER.debug("Renaming tableLike from {} to {}", from, to); - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(from, subType); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(from, subType); if (resolvedEntities == null) { if (subType == PolarisEntitySubType.VIEW) { throw new NoSuchViewException("Cannot rename %s to %s. View does not exist", from, to); @@ -1663,7 +1667,7 @@ private void renameTableLike( List newCatalogPath = null; if (!from.namespace().equals(to.namespace())) { PolarisResolvedPathWrapper resolvedNewParentEntities = - resolvedEntityView.getResolvedPath(to.namespace()); + resolutionManifest.getResolvedPath(to.namespace()); if (resolvedNewParentEntities == null) { throw new NoSuchNamespaceException( "Cannot rename %s to %s. Namespace does not exist: %s", from, to, to.namespace()); @@ -1706,19 +1710,11 @@ private void renameTableLike( { PolarisEntitySubType existingEntitySubType = returnedEntityResult.getAlreadyExistsEntitySubType(); - if (existingEntitySubType == null) { - // this code path is unexpected - throw new AlreadyExistsException( - "Cannot rename %s to %s. Object already exists", from, to); - } else if (existingEntitySubType == PolarisEntitySubType.TABLE) { - throw new AlreadyExistsException( - "Cannot rename %s to %s. Table already exists", from, to); - } else if (existingEntitySubType == PolarisEntitySubType.VIEW) { - throw new AlreadyExistsException( - "Cannot rename %s to %s. View already exists", from, to); - } - throw new IllegalStateException( - String.format("Unexpected entity type '%s'", existingEntitySubType)); + throw new AlreadyExistsException( + "Cannot rename %s to %s. %s already exists", + from, + to, + existingEntitySubType != null ? existingEntitySubType.readableName() : "Object"); } case BaseResult.ReturnStatus.ENTITY_NOT_FOUND: @@ -1765,7 +1761,7 @@ private void renameTableLike( */ private void createTableLike(TableIdentifier identifier, PolarisEntity entity) { PolarisResolvedPathWrapper resolvedParent = - resolvedEntityView.getResolvedPath(identifier.namespace()); + resolutionManifest.getResolvedPath(identifier.namespace()); if (resolvedParent == null) { // Illegal state because the namespace should've already been in the static resolution set. throw new IllegalStateException( @@ -1806,7 +1802,7 @@ private void createTableLike( private void updateTableLike(TableIdentifier identifier, PolarisEntity entity) { PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getResolvedPath(identifier, entity.getSubType()); + resolutionManifest.getResolvedPath(identifier, entity.getSubType()); if (resolvedEntities == null) { // Illegal state because the identifier should've already been in the static resolution set. throw new IllegalStateException( @@ -1838,7 +1834,7 @@ private void updateTableLike(TableIdentifier identifier, PolarisEntity entity) { Map storageProperties, boolean purge) { PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getResolvedPath(identifier, subType); + resolutionManifest.getResolvedPath(identifier, subType); if (resolvedEntities == null) { // TODO: Error? return new PolarisMetaStoreManager.DropEntityResult( @@ -1883,7 +1879,7 @@ private boolean sendNotificationForTableLike( LOGGER.debug( "Handling notification request {} for tableIdentifier {}", request, tableIdentifier); PolarisResolvedPathWrapper resolvedEntities = - resolvedEntityView.getPassthroughResolvedPath(tableIdentifier, subType); + resolutionManifest.getPassthroughResolvedPath(tableIdentifier, subType); NotificationType notificationType = request.getNotificationType(); @@ -1907,7 +1903,7 @@ private boolean sendNotificationForTableLike( Arrays.stream(tableIdentifier.namespace().levels()) .limit(i) .toArray(String[]::new)); - resolvedStorageEntity = resolvedEntityView.getResolvedPath(nsLevel); + resolvedStorageEntity = resolutionManifest.getResolvedPath(nsLevel); if (resolvedStorageEntity != null) { storageInfoEntity = findStorageInfoFromHierarchy(resolvedStorageEntity); break; @@ -1943,7 +1939,7 @@ private boolean sendNotificationForTableLike( Namespace ns = tableIdentifier.namespace(); createNonExistingNamespaces(ns); - PolarisResolvedPathWrapper resolvedParent = resolvedEntityView.getPassthroughResolvedPath(ns); + PolarisResolvedPathWrapper resolvedParent = resolutionManifest.getPassthroughResolvedPath(ns); TableLikeEntity entity = TableLikeEntity.of(resolvedEntities == null ? null : resolvedEntities.getRawLeafEntity()); @@ -2023,17 +2019,17 @@ private void createNonExistingNamespaces(Namespace namespace) { for (int i = 1; i <= namespace.length(); i++) { Namespace nsLevel = Namespace.of(Arrays.stream(namespace.levels()).limit(i).toArray(String[]::new)); - if (resolvedEntityView.getPassthroughResolvedPath(nsLevel) == null) { + if (resolutionManifest.getPassthroughResolvedPath(nsLevel) == null) { Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(nsLevel); PolarisResolvedPathWrapper resolvedParent = - resolvedEntityView.getPassthroughResolvedPath(parentNamespace); + resolutionManifest.getPassthroughResolvedPath(parentNamespace); createNamespaceInternal(nsLevel, Collections.emptyMap(), resolvedParent); } } } private List listTableLike(PolarisEntitySubType subType, Namespace namespace) { - PolarisResolvedPathWrapper resolvedEntities = resolvedEntityView.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedEntities = resolutionManifest.getResolvedPath(namespace); if (resolvedEntities == null) { // Illegal state because the namespace should've already been in the static resolution set. throw new IllegalStateException( diff --git a/polaris-service/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java b/polaris-service/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java index 926fcfe81..751776ff0 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/catalog/IcebergCatalogAdapter.java @@ -60,7 +60,7 @@ import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.cache.EntityCacheEntry; import org.apache.polaris.core.persistence.resolver.Resolver; -import org.apache.polaris.core.persistence.resolver.ResolverStatus; +import org.apache.polaris.core.persistence.resolver.ResolverException; import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService; import org.apache.polaris.service.catalog.api.IcebergRestConfigurationApiService; import org.apache.polaris.service.config.RealmEntityManagerFactory; @@ -490,11 +490,13 @@ public Response getConfig(String warehouse, SecurityContext securityContext) { if (warehouse == null) { throw new BadRequestException("Please specify a warehouse"); } - Resolver resolver = - entityManager.prepareResolver( - CallContext.getCurrentContext(), authenticatedPrincipal, warehouse); - ResolverStatus resolverStatus = resolver.resolveAll(); - if (!resolverStatus.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) { + Resolver resolver; + try { + resolver = + entityManager + .prepareResolver(CallContext.getCurrentContext(), authenticatedPrincipal, warehouse) + .buildResolved(); + } catch (ResolverException.EntityNotResolvedException e) { throw new NotFoundException("Unable to find warehouse %s", warehouse); } EntityCacheEntry resolvedReferenceCatalog = resolver.getResolvedReferenceCatalog(); diff --git a/polaris-service/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java b/polaris-service/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java index 3d12f75a7..acebf0c77 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapper.java @@ -80,13 +80,13 @@ import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.EntityNotFoundException; import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; -import org.apache.polaris.core.persistence.TransactionWorkspaceMetaStoreManager; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.impl.TransactionWorkspaceMetaStoreManager; +import org.apache.polaris.core.persistence.resolution.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolverPath; -import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.storage.PolarisStorageActions; import org.apache.polaris.service.context.CallContextCatalogFactory; import org.apache.polaris.service.types.NotificationRequest; @@ -120,7 +120,7 @@ public class PolarisCatalogHandlerWrapper { private final CallContextCatalogFactory catalogFactory; // Initialized in the authorize methods. - private PolarisResolutionManifest resolutionManifest = null; + private ResolutionManifest resolutionManifest = null; // Catalog instance will be initialized after authorizing resolver successfully resolves // the catalog entity. @@ -185,15 +185,17 @@ private void authorizeBasicNamespaceOperationOrThrow( Namespace namespace, List extraPassthroughNamespaces, List extraPassthroughTableLikes) { - resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); + var resolutionManifestBuilder = + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asSpecializedIcebergNotFoundException) + .addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace); if (extraPassthroughNamespaces != null) { for (Namespace ns : extraPassthroughNamespaces) { - resolutionManifest.addPassthroughPath( + resolutionManifestBuilder.addPassthroughPath( new ResolverPath( Arrays.asList(ns.levels()), PolarisEntityType.NAMESPACE, true /* optional */), ns); @@ -201,7 +203,7 @@ private void authorizeBasicNamespaceOperationOrThrow( } if (extraPassthroughTableLikes != null) { for (TableIdentifier id : extraPassthroughTableLikes) { - resolutionManifest.addPassthroughPath( + resolutionManifestBuilder.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(id), PolarisEntityType.TABLE_LIKE, @@ -209,7 +211,8 @@ private void authorizeBasicNamespaceOperationOrThrow( id); } } - resolutionManifest.resolveAll(); + this.resolutionManifest = resolutionManifestBuilder.buildResolved(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); @@ -226,23 +229,28 @@ private void authorizeBasicNamespaceOperationOrThrow( private void authorizeCreateNamespaceUnderNamespaceOperationOrThrow( PolarisAuthorizableOperation op, Namespace namespace) { - resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - Namespace parentNamespace = PolarisCatalogHelpers.getParentNamespace(namespace); - resolutionManifest.addPath( - new ResolverPath(Arrays.asList(parentNamespace.levels()), PolarisEntityType.NAMESPACE), - 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 - // path to indicate that the underlying catalog is "allowed" to check the creation path for - // a conflicting entity. - resolutionManifest.addPassthroughPath( - new ResolverPath( - Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE, true /* optional */), - namespace); - resolutionManifest.resolveAll(); + + this.resolutionManifest = + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asSpecializedIcebergNotFoundException) + .addPath( + new ResolverPath( + Arrays.asList(parentNamespace.levels()), PolarisEntityType.NAMESPACE), + 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 path to indicate that the underlying catalog is "allowed" to + // check the creation path for a conflicting entity. + .addPassthroughPath( + new ResolverPath( + Arrays.asList(namespace.levels()), + PolarisEntityType.NAMESPACE, + true /* optional */), + namespace) + .buildResolved(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(parentNamespace, true); if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", parentNamespace); @@ -261,25 +269,25 @@ private void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( PolarisAuthorizableOperation op, TableIdentifier identifier) { Namespace namespace = identifier.namespace(); - resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - resolutionManifest.addPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); - - // When creating an entity under a namespace, the authz target is the namespace, but we must - // also - // add the actual path that will be created as an "optional" passthrough resolution path to - // indicate that the underlying catalog is "allowed" to check the creation path for a - // conflicting - // entity. - resolutionManifest.addPassthroughPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), - PolarisEntityType.TABLE_LIKE, - true /* optional */), - identifier); - resolutionManifest.resolveAll(); + this.resolutionManifest = + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asSpecializedIcebergNotFoundException) + .addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace) + // When creating an entity under a namespace, the authz target is the namespace, but we + // must also add the actual path that will be created as an "optional" passthrough + // resolution path to indicate that the underlying catalog is "allowed" to check the + // creation path for a conflicting entity. + .addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + identifier) + .buildResolved(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); if (target == null) { throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); @@ -296,17 +304,20 @@ private void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( private void authorizeBasicTableLikeOperationOrThrow( PolarisAuthorizableOperation op, PolarisEntitySubType subType, TableIdentifier identifier) { - resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - - // The underlying Catalog is also allowed to fetch "fresh" versions of the target entity. - resolutionManifest.addPassthroughPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), - PolarisEntityType.TABLE_LIKE, - true /* optional */), - identifier); - resolutionManifest.resolveAll(); + this.resolutionManifest = + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asSpecializedIcebergNotFoundException) + // The underlying Catalog is also allowed to fetch "fresh" versions of the target + // entity. + .addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(identifier), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + identifier) + .buildResolved(); + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(identifier, subType, true); if (target == null) { @@ -330,30 +341,20 @@ private void authorizeCollectionOfTableLikeOperationOrThrow( PolarisAuthorizableOperation op, final PolarisEntitySubType subType, List ids) { - resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + var resolutionManifestBuilder = + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper( + EntityNotFoundException::asSpecializedIcebergNotFoundException); ids.forEach( identifier -> - resolutionManifest.addPassthroughPath( + resolutionManifestBuilder.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE), identifier)); - ResolverStatus status = resolutionManifest.resolveAll(); - - // If one of the paths failed to resolve, throw exception based on the one that - // we first failed to resolve. - if (status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) { - TableIdentifier identifier = - PolarisCatalogHelpers.listToTableIdentifier( - status.getFailedToResolvePath().getEntityNames()); - if (subType == PolarisEntitySubType.TABLE) { - throw new NoSuchTableException("Table does not exist: %s", identifier); - } else { - throw new NoSuchViewException("View does not exist: %s", identifier); - } - } + this.resolutionManifest = resolutionManifestBuilder.buildResolved(subType); List targets = ids.stream() @@ -384,33 +385,26 @@ private void authorizeRenameTableLikeOperationOrThrow( PolarisEntitySubType subType, TableIdentifier src, TableIdentifier dst) { - resolutionManifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - // Add src, dstParent, and dst(optional) - resolutionManifest.addPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(src), PolarisEntityType.TABLE_LIKE), - src); - resolutionManifest.addPath( - new ResolverPath(Arrays.asList(dst.namespace().levels()), PolarisEntityType.NAMESPACE), - dst.namespace()); - resolutionManifest.addPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(dst), - PolarisEntityType.TABLE_LIKE, - true /* optional */), - dst); - ResolverStatus status = resolutionManifest.resolveAll(); - 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, subType) == null) { - if (subType == PolarisEntitySubType.TABLE) { - throw new NoSuchTableException("Table does not exist: %s", src); - } else { - throw new NoSuchViewException("View does not exist: %s", src); - } - } + this.resolutionManifest = + entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .notFoundExceptionMapper(EntityNotFoundException::asSpecializedIcebergNotFoundException) + // Add src, dstParent, and dst(optional) + .addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(src), PolarisEntityType.TABLE_LIKE), + src) + .addPath( + new ResolverPath( + Arrays.asList(dst.namespace().levels()), PolarisEntityType.NAMESPACE), + dst.namespace()) + .addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(dst), + PolarisEntityType.TABLE_LIKE, + true /* optional */), + dst) + .buildResolved(subType); // Normally, since we added the dst as an optional path, we'd expect it to only get resolved // up to its parent namespace, and for there to be no TABLE_LIKE already in the dst in which diff --git a/polaris-service/src/main/java/org/apache/polaris/service/context/CallContextCatalogFactory.java b/polaris-service/src/main/java/org/apache/polaris/service/context/CallContextCatalogFactory.java index 24551a2d0..4d0f86d50 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/context/CallContextCatalogFactory.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/context/CallContextCatalogFactory.java @@ -21,11 +21,11 @@ import org.apache.iceberg.catalog.Catalog; import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; import org.apache.polaris.core.context.CallContext; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; public interface CallContextCatalogFactory { Catalog createCallContextCatalog( CallContext context, AuthenticatedPolarisPrincipal authenticatedPrincipal, - PolarisResolutionManifest resolvedManifest); + ResolutionManifest resolutionManifest); } diff --git a/polaris-service/src/main/java/org/apache/polaris/service/context/PolarisCallContextCatalogFactory.java b/polaris-service/src/main/java/org/apache/polaris/service/context/PolarisCallContextCatalogFactory.java index a72f71431..73670bed2 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/context/PolarisCallContextCatalogFactory.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/context/PolarisCallContextCatalogFactory.java @@ -31,7 +31,7 @@ import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisEntityManager; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.service.catalog.BasePolarisCatalog; import org.apache.polaris.service.catalog.io.FileIOFactory; import org.apache.polaris.service.config.RealmEntityManagerFactory; @@ -67,9 +67,9 @@ public PolarisCallContextCatalogFactory( public Catalog createCallContextCatalog( CallContext context, AuthenticatedPolarisPrincipal authenticatedPrincipal, - final PolarisResolutionManifest resolvedManifest) { + ResolutionManifest resolutionManifest) { PolarisBaseEntity baseCatalogEntity = - resolvedManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity(); + resolutionManifest.getResolvedReferenceCatalogEntity().getRawLeafEntity(); String catalogName = baseCatalogEntity.getName(); String realm = context.getRealmContext().getRealmIdentifier(); @@ -84,7 +84,7 @@ public Catalog createCallContextCatalog( entityManager, metaStoreManagerFactory.getOrCreateMetaStoreManager(context.getRealmContext()), context, - resolvedManifest, + resolutionManifest, authenticatedPrincipal, taskExecutor, fileIOFactory); diff --git a/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java b/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java index 7714fc3df..3e7b4ee33 100644 --- a/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java +++ b/polaris-service/src/main/java/org/apache/polaris/service/persistence/InMemoryPolarisMetaStoreManagerFactory.java @@ -30,11 +30,11 @@ import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.auth.PolarisSecretsManager.PrincipalSecretsResult; import org.apache.polaris.core.context.RealmContext; -import org.apache.polaris.core.persistence.LocalPolarisMetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisMetaStoreSession; -import org.apache.polaris.core.persistence.PolarisTreeMapMetaStoreSessionImpl; -import org.apache.polaris.core.persistence.PolarisTreeMapStore; +import org.apache.polaris.core.persistence.local.LocalPolarisMetaStoreManagerFactory; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapMetaStoreSessionImpl; +import org.apache.polaris.core.persistence.local.inmem.PolarisTreeMapStore; import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider; @Identifier("in-memory") diff --git a/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index ecc9a01fb..ecc8229a5 100644 --- a/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/polaris-service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -65,7 +65,7 @@ import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.cache.EntityCache; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.core.storage.cache.StorageCredentialCache; import org.apache.polaris.service.catalog.BasePolarisCatalog; import org.apache.polaris.service.catalog.PolarisPassthroughResolutionView; @@ -385,7 +385,7 @@ public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext) public Catalog createCallContextCatalog( CallContext context, AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, - final PolarisResolutionManifest resolvedManifest) { + ResolutionManifest resolvedManifest) { // This depends on the BasePolarisCatalog allowing calling initialize multiple times // to override the previous config. Catalog catalog = diff --git a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java index 091bff21a..3d1f462ed 100644 --- a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java +++ b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisCatalogHandlerWrapperAuthzTest.java @@ -59,7 +59,7 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.service.admin.PolarisAuthzTestBase; import org.apache.polaris.service.catalog.io.DefaultFileIOFactory; import org.apache.polaris.service.config.RealmEntityManagerFactory; @@ -1688,7 +1688,7 @@ public PolarisEntityManager getOrCreateEntityManager(RealmContext realmContext) public Catalog createCallContextCatalog( CallContext context, AuthenticatedPolarisPrincipal authenticatedPolarisPrincipal, - PolarisResolutionManifest resolvedManifest) { + ResolutionManifest resolvedManifest) { Catalog catalog = super.createCallContextCatalog( context, authenticatedPolarisPrincipal, resolvedManifest); diff --git a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java index 313e0265f..c05e00786 100644 --- a/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java +++ b/polaris-service/src/test/java/org/apache/polaris/service/catalog/PolarisPassthroughResolutionView.java @@ -19,17 +19,19 @@ package org.apache.polaris.service.catalog; import java.util.Arrays; +import java.util.Set; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.entity.PolarisBaseEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.EntityNotFoundException; import org.apache.polaris.core.persistence.PolarisEntityManager; -import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; -import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView; +import org.apache.polaris.core.persistence.resolution.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolution.ResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolverPath; /** @@ -39,7 +41,7 @@ * new single-use PolarisResolutionManifests for each desired resolved path without defining a fixed * set of resolved entities that need to be checked against authorizable operations. */ -public class PolarisPassthroughResolutionView implements PolarisResolutionManifestCatalogView { +public class PolarisPassthroughResolutionView implements ResolutionManifest { private final PolarisEntityManager entityManager; private final CallContext callContext; private final AuthenticatedPolarisPrincipal authenticatedPrincipal; @@ -58,86 +60,125 @@ public PolarisPassthroughResolutionView( @Override public PolarisResolvedPathWrapper getResolvedReferenceCatalogEntity() { - PolarisResolutionManifest manifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - manifest.resolveAll(); - return manifest.getResolvedReferenceCatalogEntity(); + return entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .buildResolved() + .getResolvedReferenceCatalogEntity(); } @Override - public PolarisResolvedPathWrapper getResolvedPath(Object key) { - PolarisResolutionManifest manifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - - if (key instanceof Namespace namespace) { - manifest.addPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); - manifest.resolveAll(); - return manifest.getResolvedPath(namespace); - } else { - throw new IllegalStateException( - String.format( - "Trying to getResolvedPath(key) for %s with class %s", key, key.getClass())); + public PolarisResolvedPathWrapper getResolvedPath( + Namespace namespace, boolean prependRootContainer) { + try { + return entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .addPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace) + .buildResolved() + .getResolvedPath(namespace, prependRootContainer); + } catch (EntityNotFoundException nf) { + return null; } } @Override - public PolarisResolvedPathWrapper getResolvedPath(Object key, PolarisEntitySubType subType) { - PolarisResolutionManifest manifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - - if (key instanceof TableIdentifier identifier) { - manifest.addPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), - PolarisEntityType.TABLE_LIKE), - identifier); - manifest.resolveAll(); - return manifest.getResolvedPath(identifier, subType); - } else { - throw new IllegalStateException( - String.format( - "Trying to getResolvedPath(key, subType) for %s with class %s and subType %s", - key, key.getClass(), subType)); + public PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, boolean prependRootContainer) { + try { + return entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(tableIdentifier), + PolarisEntityType.TABLE_LIKE), + tableIdentifier) + .buildResolved() + .getResolvedPath(tableIdentifier, prependRootContainer); + } catch (EntityNotFoundException nf) { + return null; } } @Override - public PolarisResolvedPathWrapper getPassthroughResolvedPath(Object key) { - PolarisResolutionManifest manifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); + public PolarisResolvedPathWrapper getResolvedPath( + TableIdentifier tableIdentifier, PolarisEntitySubType subType, boolean prependRootContainer) { + try { + return entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(tableIdentifier), + PolarisEntityType.TABLE_LIKE), + tableIdentifier) + .buildResolved() + .getResolvedPath(tableIdentifier, subType, prependRootContainer); + } catch (EntityNotFoundException nf) { + return null; + } + } - if (key instanceof Namespace namespace) { - manifest.addPassthroughPath( - new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), - namespace); - return manifest.getPassthroughResolvedPath(namespace); - } else { - throw new IllegalStateException( - String.format( - "Trying to getResolvedPath(key) for %s with class %s", key, key.getClass())); + @Override + public PolarisResolvedPathWrapper getPassthroughResolvedPath(Namespace namespace) { + try { + return entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .addPassthroughPath( + new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), + namespace) + .buildResolved() + .getPassthroughResolvedPath(namespace); + } catch (EntityNotFoundException nf) { + return null; } } @Override public PolarisResolvedPathWrapper getPassthroughResolvedPath( - Object key, PolarisEntitySubType subType) { - PolarisResolutionManifest manifest = - entityManager.prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName); - - if (key instanceof TableIdentifier identifier) { - manifest.addPassthroughPath( - new ResolverPath( - PolarisCatalogHelpers.tableIdentifierToList(identifier), - PolarisEntityType.TABLE_LIKE), - identifier); - return manifest.getPassthroughResolvedPath(identifier, subType); - } else { - throw new IllegalStateException( - String.format( - "Trying to getResolvedPath(key, subType) for %s with class %s and subType %s", - key, key.getClass(), subType)); + TableIdentifier tableIdentifier, PolarisEntitySubType subType) { + try { + return entityManager + .prepareResolutionManifest(callContext, authenticatedPrincipal, catalogName) + .addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(tableIdentifier), + PolarisEntityType.TABLE_LIKE), + tableIdentifier) + .buildResolved(subType) + .getPassthroughResolvedPath(tableIdentifier, subType); + } catch (EntityNotFoundException nf) { + return null; } } + + @Override + public PolarisResolvedPathWrapper getResolvedPath(String name, boolean prependRootContainer) { + throw new UnsupportedOperationException(); + } + + @Override + public PolarisResolvedPathWrapper getResolvedTopLevelEntity( + String name, PolarisEntityType polarisEntityType) { + throw new UnsupportedOperationException(); + } + + @Override + public PolarisResolvedPathWrapper getResolvedRootContainerEntityAsPath() { + throw new UnsupportedOperationException(); + } + + @Override + public PolarisEntitySubType getLeafSubType(TableIdentifier tableIdentifier) { + throw new UnsupportedOperationException(); + } + + @Override + public Set getAllActivatedPrincipalRoleEntities() { + throw new UnsupportedOperationException(); + } + + @Override + public Set getAllActivatedCatalogRoleAndPrincipalRoles() { + throw new UnsupportedOperationException(); + } }