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..df80d1770 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,41 @@ 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 { +public interface 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; - } + // TODO The contract of this interface (was a concrete class before) is not to deal with cache + // details, and callers almost always extract the `PolarisBaseEntity` from the + // `EntityCacheEntry`, or build a `ResolvedPolarisEntity` from it. The indirection via + // `EntityCacheEntry` leads to boilerplate code exposing internal details, and should really be + // avoided. /** * @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 +62,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 +72,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 +92,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(); + } }