diff --git a/polaris-core/build.gradle.kts b/polaris-core/build.gradle.kts index 29803dd8b5..5e47ef5e8f 100644 --- a/polaris-core/build.gradle.kts +++ b/polaris-core/build.gradle.kts @@ -103,6 +103,7 @@ dependencies { testFixturesApi(platform(libs.jackson.bom)) testRuntimeOnly("org.junit.platform:junit-platform-launcher") testFixturesApi(libs.jakarta.annotation.api) + testFixturesApi(libs.jakarta.ws.rs.api) compileOnly(libs.jakarta.annotation.api) } 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 51ec06a05d..d4c75240d8 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 @@ -20,1020 +20,45 @@ import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; -import jakarta.ws.rs.core.SecurityContext; -import java.security.Principal; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; -import org.apache.polaris.core.PolarisDiagnostics; -import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; -import org.apache.polaris.core.entity.PolarisBaseEntity; -import org.apache.polaris.core.entity.PolarisEntityCore; -import org.apache.polaris.core.entity.PolarisEntitySubType; -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.entity.PrincipalEntity; -import org.apache.polaris.core.entity.PrincipalRoleEntity; -import org.apache.polaris.core.persistence.cache.EntityCache; -import org.apache.polaris.core.persistence.dao.entity.EntityResult; -import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult; -import org.apache.polaris.core.persistence.resolver.Resolver; -import org.apache.polaris.core.persistence.resolver.ResolverPath; -import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.persistence.transactional.TransactionalMetaStoreManagerImpl; -import org.apache.polaris.core.persistence.transactional.TransactionalPersistence; import org.apache.polaris.core.persistence.transactional.TreeMapMetaStore; import org.apache.polaris.core.persistence.transactional.TreeMapTransactionalPersistenceImpl; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -public class ResolverTest { +public class ResolverTest extends BaseResolverTest { - // diag services - private final PolarisDiagnostics diagServices; + private PolarisCallContext callCtx; + private PolarisTestMetaStoreManager tm; + private TransactionalMetaStoreManagerImpl metaStoreManager; - // the entity store, use treemap implementation - private final TreeMapMetaStore store; - - // to interact with the metastore - private final TransactionalPersistence metaStore; - - // polaris call context - private final PolarisCallContext callCtx; - - // utility to bootstrap the mata store - private final PolarisTestMetaStoreManager tm; - - // the meta store manager - private final PolarisMetaStoreManager metaStoreManager; - - // Principal P1 - private final PolarisBaseEntity P1; - - // cache we are using - private EntityCache cache; - - // whenever constructing a new Resolver instance, if false, disable cache for that Resolver - // instance by giving it a null cache regardless of the current state of the test-level - // cache instance; use a boolean for this instead of just modifying the test member 'cache' - // so that we can potentially alternate between using cache and not using cache - private boolean shouldUseCache; - - /** - * Initialize and create the test metadata - * - *
-   * - test
-   * - (N1/N2/T1)
-   * - (N1/N2/T2)
-   * - (N1/N2/V1)
-   * - (N1/N3/T3)
-   * - (N1/N3/V2)
-   * - (N1/T4)
-   * - (N1/N4)
-   * - N5/N6/T5
-   * - N5/N6/T6
-   * - N7/N8/POL1
-   * - N7/N8/POL2
-   * - N7/POL3
-   * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N2, TABLE_DROP on N5/N6/T5)
-   * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C)
-   * - PR1(R1, R2)
-   * - PR2(R2)
-   * - P1(PR1, PR2)
-   * - P2(PR1)
-   * 
- */ - public ResolverTest() { - diagServices = new PolarisDefaultDiagServiceImpl(); - store = new TreeMapMetaStore(diagServices); - metaStore = new TreeMapTransactionalPersistenceImpl(store, Mockito.mock(), RANDOM_SECRETS); - callCtx = new PolarisCallContext(metaStore, diagServices); - metaStoreManager = new TransactionalMetaStoreManagerImpl(); - - // bootstrap the mata store with our test schema - tm = new PolarisTestMetaStoreManager(metaStoreManager, callCtx); - tm.testCreateTestCatalog(); - - // principal P1 - this.P1 = tm.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); - } - - /** This test resolver for a create-principal scenario */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testResolvePrincipal(boolean useCache) { - this.shouldUseCache = useCache; - - // resolve a principal which does not exist, but make it optional so will succeed - this.resolveDriver(null, null, "P3", true, null, null); - - // 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); - - // then resolve a principal which does exist - this.resolveDriver(null, null, "P2", false, null, null); - - // do it again, but this time using the primed cache - this.resolveDriver(this.cache, null, "P2", false, null, null); - - // now add a principal roles - this.resolveDriver(this.cache, null, "P2", false, "PR1", null); - - // do it again, everything in the cache - this.resolveDriver(this.cache, null, "P2", false, "PR1", null); - - // do it again on a cold cache - this.resolveDriver(this.cache, null, "P2", false, "PR1", null); - } - - /** Test that we can specify a subset of principal role names */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testScopedPrincipalRole(boolean useCache) { - this.shouldUseCache = useCache; - - // start without a scope - this.resolveDriver(null, null, "P2", false, "PR1", null); - - // specify various scopes - this.resolveDriver(this.cache, Set.of("PR1"), "P2", false, "PR1", null); - this.resolveDriver(this.cache, Set.of("PR2"), "P2", false, "PR1", null); - this.resolveDriver(this.cache, Set.of("PR2", "PR3"), "P2", false, "PR1", null); - this.resolveDriver(null, Set.of("PR2", "PR3"), "P2", false, "PR1", null); - this.resolveDriver(null, Set.of("PR3"), "P2", false, "PR1", null); - this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "P2", false, "PR1", null); - } - - /** - * Test that the set of catalog roles being activated is correctly inferred, based of a set of - * principal roles - */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testCatalogRolesActivation(boolean useCache) { - this.shouldUseCache = useCache; - - // start simple, with both PR1 and PR2, you get R1 and R2 - this.resolveDriver(null, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); - - // PR1 itself is enough to activate both R1 and R2 - this.resolveDriver(this.cache, Set.of("PR1"), "test", Set.of("R1", "R2")); - - // PR2 only activates R2 - this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2")); - - // With a non-existing principal roles, nothing gets activated - this.resolveDriver(this.cache, Set.of("NOT_EXISTING"), "test", Set.of()); - } - - /** Test that paths, one or more, are properly resolved */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testResolvePath(boolean useCache) { - this.shouldUseCache = useCache; - - // N1 which exists - ResolverPath N1 = new ResolverPath(List.of("N1"), PolarisEntityType.NAMESPACE); - this.resolveDriver(null, "test", N1, null, null); - - // N1/N2 which exists - ResolverPath N1_N2 = new ResolverPath(List.of("N1", "N2"), PolarisEntityType.NAMESPACE); - this.resolveDriver(null, "test", N1_N2, null, null); - - // N1/N2/T1 which exists - ResolverPath N1_N2_T1 = - new ResolverPath(List.of("N1", "N2", "T1"), PolarisEntityType.TABLE_LIKE); - this.resolveDriver(this.cache, "test", N1_N2_T1, null, null); - - // N1/N2/T1 which exists - ResolverPath N1_N2_V1 = - new ResolverPath(List.of("N1", "N2", "V1"), PolarisEntityType.TABLE_LIKE); - this.resolveDriver(this.cache, "test", N1_N2_V1, null, null); - - // N5/N6 which exists - ResolverPath N5_N6 = new ResolverPath(List.of("N5", "N6"), PolarisEntityType.NAMESPACE); - this.resolveDriver(this.cache, "test", N5_N6, null, null); - - // N5/N6/T5 which exists - ResolverPath N5_N6_T5 = - new ResolverPath(List.of("N5", "N6", "T5"), PolarisEntityType.TABLE_LIKE); - this.resolveDriver(this.cache, "test", N5_N6_T5, null, null); - - // N7/N8 which exists - ResolverPath N7_N8 = new ResolverPath(List.of("N7", "N8"), PolarisEntityType.NAMESPACE); - this.resolveDriver(this.cache, "test", N7_N8, null, null); - - // N7/N8/POL1 which exists - ResolverPath N7_N8_POL1 = - new ResolverPath(List.of("N7", "N8", "POL1"), PolarisEntityType.POLICY); - this.resolveDriver(this.cache, "test", N7_N8_POL1, null, null); - - // N7/POL3 which exists - ResolverPath N7_POL3 = new ResolverPath(List.of("N7", "POL3"), PolarisEntityType.POLICY); - this.resolveDriver(this.cache, "test", N7_POL3, null, null); - - // Error scenarios: N5/N6/T8 which does not exists - 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); - - // 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); - - // now test multiple paths - this.resolveDriver( - this.cache, "test", null, List.of(N1, N5_N6, N1, N1_N2, N5_N6_T5, N1_N2), null); - this.resolveDriver( - this.cache, - "test", - null, - List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), - ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); - - // except if the optional flag is specified - N5_N6_T8 = new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE, true); - Resolver resolver = - this.resolveDriver(this.cache, "test", null, List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), null); - // get all the resolved paths - List> resolvedPath = resolver.getResolvedPaths(); - Assertions.assertThat(resolvedPath.get(0)).hasSize(1); - Assertions.assertThat(resolvedPath.get(1)).hasSize(2); - Assertions.assertThat(resolvedPath.get(2)).hasSize(3); - Assertions.assertThat(resolvedPath.get(3)).hasSize(2); - } - - /** - * Ensure that if data changes while entities are cached, we will always resolve to the latest - * version - */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testConsistency(boolean useCache) { - this.shouldUseCache = useCache; - - // resolve principal "P2" - this.resolveDriver(null, null, "P2", false, null, null); - this.resolveDriver(this.cache, null, "P2", false, null, null); - - // now drop this principal. It is still cached - PolarisBaseEntity P2 = this.tm.ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P2"); - this.tm.dropEntity(null, P2); - - // 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); - - // 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); - - // resolve existing grants on catalog - this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); - - // with only PR2, we will only activate R2 - Resolver resolver = this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2")); - - // Now add a new catalog role and see if the changes are reflected - Assertions.assertThat(resolver.getResolvedReferenceCatalog()).isNotNull(); - PolarisBaseEntity TEST = resolver.getResolvedReferenceCatalog().getEntity(); - PolarisBaseEntity R3 = - this.tm.createEntity(List.of(TEST), PolarisEntityType.CATALOG_ROLE, "R3"); - - // now grant R3 to PR2 - Assertions.assertThat(resolver.getResolvedCallerPrincipalRoles()).hasSize(1); - PolarisBaseEntity PR2 = resolver.getResolvedCallerPrincipalRoles().get(0).getEntity(); - this.tm.grantToGrantee(TEST, R3, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); - - // now resolve again with only PR2 activated, should see the new catalog role R3 - this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2", "R3")); - - // now drop that role and then recreate it. The new incarnation should be used - this.tm.dropEntity(List.of(TEST), R3); - PolarisBaseEntity R3_NEW = - this.tm.createEntity(List.of(TEST), PolarisEntityType.CATALOG_ROLE, "R3"); - - // now grant R3_NEW to PR2 and resolve it again - this.tm.grantToGrantee(TEST, R3_NEW, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); - resolver = this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2", "R3")); - - // ensure that the correct catalog role was resolved - Assertions.assertThat(resolver.getResolvedCatalogRoles()).containsKey(R3_NEW.getId()); - } - - /** Check resolve paths when cache is inconsistent */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testPathConsistency(boolean useCache) { - this.shouldUseCache = useCache; - - // resolve few paths path - ResolverPath N1_PATH = new ResolverPath(List.of("N1"), PolarisEntityType.NAMESPACE); - this.resolveDriver(null, "test", N1_PATH, null, null); - ResolverPath N1_N2_PATH = new ResolverPath(List.of("N1", "N2"), PolarisEntityType.NAMESPACE); - this.resolveDriver(this.cache, "test", N1_N2_PATH, null, null); - ResolverPath N1_N2_T1_PATH = - new ResolverPath(List.of("N1", "N2", "T1"), PolarisEntityType.TABLE_LIKE); - Resolver resolver = this.resolveDriver(this.cache, "test", N1_N2_T1_PATH, null, null); - - // get the catalog - Assertions.assertThat(resolver.getResolvedReferenceCatalog()).isNotNull(); - PolarisBaseEntity TEST = resolver.getResolvedReferenceCatalog().getEntity(); - - // get the various entities in the path - Assertions.assertThat(resolver.getResolvedPath()).isNotNull(); - Assertions.assertThat(resolver.getResolvedPath()).hasSize(3); - PolarisBaseEntity N1 = resolver.getResolvedPath().get(0).getEntity(); - PolarisBaseEntity N2 = resolver.getResolvedPath().get(1).getEntity(); - PolarisBaseEntity T1 = resolver.getResolvedPath().get(2).getEntity(); - - // resolve N3 - ResolverPath N1_N3_PATH = new ResolverPath(List.of("N1", "N3"), PolarisEntityType.NAMESPACE); - resolver = this.resolveDriver(this.cache, "test", N1_N3_PATH, null, null); - Assertions.assertThat(resolver.getResolvedPath()).isNotNull(); - Assertions.assertThat(resolver.getResolvedPath()).hasSize(2); - PolarisBaseEntity N3 = resolver.getResolvedPath().get(1).getEntity(); - - // now re-parent T1 under N3, keeping the same name - this.tm.renameEntity(List.of(TEST, N1, N2), T1, List.of(TEST, N1, N3), "T1"); - - // now expect to fail resolving T1 under N1/N2 - this.resolveDriver( - this.cache, - "test", - N1_N2_T1_PATH, - null, - ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); - - // but we should be able to resolve it under N1/N3 - ResolverPath N1_N3_T1_PATH = - new ResolverPath(List.of("N1", "N3", "T1"), PolarisEntityType.TABLE_LIKE); - this.resolveDriver(this.cache, "test", N1_N3_T1_PATH, null, null); - } - - /** Resolve catalog roles */ - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testResolveCatalogRole(boolean useCache) { - this.shouldUseCache = useCache; - - // resolve catalog role - this.resolveDriver(null, "test", "R1", null); - - // do it again - this.resolveDriver(this.cache, "test", "R1", null); - this.resolveDriver(this.cache, "test", "R1", null); - - // failure scenario - this.resolveDriver( - this.cache, "test", "R5", ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); - } - - /** - * Create a simple resolver without a reference catalog, any principal roles sub-scope and using - * P1 as the caller principal - * - * @return new resolver to test with - */ - @Nonnull - private Resolver allocateResolver() { - return this.allocateResolver(null, null); - } - - /** - * Create a simple resolver without any principal roles sub-scope and using P1 as the caller - * principal - * - * @param referenceCatalogName the reference e catalog name, can be null - * @return new resolver to test with - */ - @Nonnull - private Resolver allocateResolver(@Nullable String referenceCatalogName) { - return this.allocateResolver(null, referenceCatalogName); - } - - /** - * Create a simple resolver without any principal roles sub-scope and using P1 as the caller - * principal - * - * @param cache if not null, cache to use, else one will be created - * @return new resolver to test with - */ - @Nonnull - private Resolver allocateResolver(@Nullable EntityCache cache) { - return this.allocateResolver(cache, null); - } - - /** - * Create a simple resolver without any principal roles sub-scope and using P1 as the caller - * principal - * - * @param cache if not null, cache to use, else one will be created - * @param referenceCatalogName the reference e catalog name, can be null - * @return new resolver to test with - */ - @Nonnull - private Resolver allocateResolver( - @Nullable EntityCache cache, @Nullable String referenceCatalogName) { - return this.allocateResolver(cache, null, referenceCatalogName); - } - - /** - * Create a simple resolver without any principal roles sub-scope and using P1 as the caller - * principal - * - * @param cache if not null, cache to use, else one will be created - * @param principalRolesScope if not null, scoped principal roles - * @param referenceCatalogName the reference e catalog name, can be null - * @return new resolver to test with - */ - @Nonnull - private Resolver allocateResolver( - @Nullable EntityCache cache, - Set principalRolesScope, - @Nullable String referenceCatalogName) { - - // create a new cache if needs be - if (cache == null) { - this.cache = new EntityCache(this.metaStoreManager); - } - boolean allRoles = principalRolesScope == null; - Optional> roleEntities = - Optional.ofNullable(principalRolesScope) - .map( - scopes -> - scopes.stream() - .map( - role -> - metaStoreManager.readEntityByName( - callCtx, - null, - PolarisEntityType.PRINCIPAL_ROLE, - PolarisEntitySubType.NULL_SUBTYPE, - role)) - .filter(EntityResult::isSuccess) - .map(EntityResult::getEntity) - .map(PrincipalRoleEntity::of) - .collect(Collectors.toList())); - AuthenticatedPolarisPrincipal authenticatedPrincipal = - new AuthenticatedPolarisPrincipal( - PrincipalEntity.of(P1), Optional.ofNullable(principalRolesScope).orElse(Set.of())); - return new Resolver( - this.callCtx, - metaStoreManager, - new SecurityContext() { - @Override - public Principal getUserPrincipal() { - return authenticatedPrincipal; - } - - @Override - public boolean isUserInRole(String role) { - return roleEntities - .map(l -> l.stream().map(PrincipalRoleEntity::getName).anyMatch(role::equals)) - .orElse(allRoles); - } - - @Override - public boolean isSecure() { - return false; - } - - @Override - public String getAuthenticationScheme() { - return ""; - } - }, - this.shouldUseCache ? this.cache : null, - referenceCatalogName); - } - - /** - * Resolve a principal and optionally a principal role - * - * @param cache if not null, cache to use - * @param principalName name of the principal name being created - * @param exists true if this principal already exists - * @param principalRoleName name of the principal role, should exist - */ - private void resolvePrincipalAndPrincipalRole( - EntityCache cache, String principalName, boolean exists, String principalRoleName) { - Resolver resolver = 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); - - // add principal role if one passed-in - if (principalRoleName != null) { - resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); - } - - // done, run resolve - ResolverStatus status = resolver.resolveAll(); - - // we expect success - Assertions.assertThat(status.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS); - - // the principal does not exist, check that this is the case - if (exists) { - // the principal exist, check that this is the case - this.ensureResolved( - resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName), - PolarisEntityType.PRINCIPAL, - principalName); - } else { - // not found - Assertions.assertThat(resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)) - .isNull(); - } - - // validate that we were able to resolve the principal and the two principal roles - this.ensureResolved(resolver.getResolvedCallerPrincipal(), PolarisEntityType.PRINCIPAL, "P1"); - - // validate that the two principal roles have been activated - List principalRolesResolved = resolver.getResolvedCallerPrincipalRoles(); - - // expect two principal roles - Assertions.assertThat(principalRolesResolved).hasSize(2); - principalRolesResolved.sort(Comparator.comparing(p -> p.getEntity().getName())); - - // ensure they are PR1 and PR2 - this.ensureResolved(principalRolesResolved.get(0), PolarisEntityType.PRINCIPAL_ROLE, "PR1"); - this.ensureResolved( - principalRolesResolved.get(principalRolesResolved.size() - 1), - PolarisEntityType.PRINCIPAL_ROLE, - "PR2"); - - // 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); - } - } - - /** - * Main resolve driver - * - * @param cache if not null, cache we can use - * @param principalRolesScope if not null, scoped roles - * @param principalName if not null, name of the principal to resolve - * @param isPrincipalNameOptional if true, the name of the principal is optional - * @param principalRoleName if not null, name of the principal role to resolve - * @param expectedStatus the expected status if not success - * @return resolver we created and which has been validated. - */ - private Resolver resolveDriver( - EntityCache cache, - Set principalRolesScope, - String principalName, - boolean isPrincipalNameOptional, - String principalRoleName, - ResolverStatus.StatusEnum expectedStatus) { - return this.resolveDriver( - cache, - principalRolesScope, - principalName, - isPrincipalNameOptional, - principalRoleName, - null, - null, - null, - null, - expectedStatus, - null); - } - - /** - * Main resolve driver - * - * @param cache if not null, cache we can use - * @param catalogName if not null, name of the catalog to resolve - * @param path if not null, single path in that catalog - * @param paths if not null, set of path in that catalog. Path and paths are mutually exclusive - * @param expectedStatus the expected status if not success activated - * @return resolver we created and which has been validated. - */ - private Resolver resolveDriver( - EntityCache cache, - String catalogName, - ResolverPath path, - List paths, - ResolverStatus.StatusEnum expectedStatus) { - return this.resolveDriver( - cache, null, null, false, null, catalogName, null, path, paths, expectedStatus, null); - } - - /** - * Main resolve driver for testing catalog role activation - * - * @param cache if not null, cache we can use - * @param principalRolesScope if not null, scoped roles - * @param catalogName if not null, name of the catalog to resolve - * @param expectedActivatedCatalogRoles set of catalog role names the caller expects to be - * activated - * @return resolver we created and which has been validated. - */ - private Resolver resolveDriver( - EntityCache cache, - Set principalRolesScope, - String catalogName, - Set expectedActivatedCatalogRoles) { - return this.resolveDriver( - cache, - principalRolesScope, - null, - false, - null, - catalogName, - null, - null, - null, - null, - expectedActivatedCatalogRoles); - } - - /** - * Main resolve driver for resolving catalog roles - * - * @param cache if not null, cache we can use - * @param catalogName if not null, name of the catalog to resolve - * @param catalogRoleName if not null, name of catalog role name to resolve - * @param expectedStatus the expected status if not success - * @return resolver we created and which has been validated. - */ - private Resolver resolveDriver( - EntityCache cache, - String catalogName, - String catalogRoleName, - ResolverStatus.StatusEnum expectedStatus) { - return this.resolveDriver( - cache, - null, - null, - false, - null, - catalogName, - catalogRoleName, - null, - null, - expectedStatus, - null); - } - - /** - * Main resolve driver - * - * @param cache if not null, cache we can use - * @param principalRolesScope if not null, scoped roles - * @param principalName if not null, name of the principal to resolve - * @param isPrincipalNameOptional if true, the name of the principal is optional - * @param principalRoleName if not null, name of the principal role to resolve - * @param catalogName if not null, name of the catalog to resolve - * @param catalogRoleName if not null, name of catalog role name to resolve - * @param path if not null, single path in that catalog - * @param paths if not null, set of path in that catalog. Path and paths are mutually exclusive - * @param expectedStatus the expected status if not success - * @param expectedActivatedCatalogRoles set of catalog role names the caller expects to be - * activated - * @return resolver we created and which has been validated. - */ - private Resolver resolveDriver( - EntityCache cache, - Set principalRolesScope, - String principalName, - boolean isPrincipalNameOptional, - String principalRoleName, - String catalogName, - String catalogRoleName, - ResolverPath path, - List paths, - ResolverStatus.StatusEnum expectedStatus, - Set expectedActivatedCatalogRoles) { - - // if null we expect success - if (expectedStatus == null) { - expectedStatus = ResolverStatus.StatusEnum.SUCCESS; - } - - // allocate resolver - Resolver resolver = allocateResolver(cache, principalRolesScope, catalogName); - - // principal name? - if (principalName != null) { - if (isPrincipalNameOptional) { - resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); - } else { - resolver.addEntityByName(PolarisEntityType.PRINCIPAL, principalName); - } - } - - // add principal role if one passed-in - if (principalRoleName != null) { - resolver.addEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); - } - - // add catalog role if one passed-in - if (catalogRoleName != null) { - resolver.addEntityByName(PolarisEntityType.CATALOG_ROLE, catalogRoleName); - } - - // add all paths - if (path != null) { - resolver.addPath(path); - } else if (paths != null) { - paths.forEach(resolver::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 - 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), - PolarisEntityType.PRINCIPAL, - principalName); - } else { - // principal was optional - Assertions.assertThat(isPrincipalNameOptional).isTrue(); - // not found - Assertions.assertThat( - resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)) - .isNull(); - } - } - - // 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++; - } - } - } else { - // both PR1 and PR2 - expectedSize = 2; - } - - // ensure the right set of principal roles were activated - Assertions.assertThat(principalRolesResolved).hasSize(expectedSize); - - // expect either PR1 and PR2 - for (ResolvedPolarisEntity 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(); - - // 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 catalog was passed-in, ensure it exists - if (catalogName != null) { - ResolvedPolarisEntity 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) { - ResolvedPolarisEntity 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(); - - // 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 (ResolvedPolarisEntity 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; - - // all resolved path - List> allResolvedPaths = resolver.getResolvedPaths(); - - // 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); - } - } - } + @Override + protected PolarisCallContext callCtx() { + if (callCtx == null) { + PolarisDefaultDiagServiceImpl diagServices = new PolarisDefaultDiagServiceImpl(); + TreeMapMetaStore store = new TreeMapMetaStore(diagServices); + TreeMapTransactionalPersistenceImpl metaStore = + new TreeMapTransactionalPersistenceImpl(store, Mockito.mock(), RANDOM_SECRETS); + callCtx = new PolarisCallContext(metaStore, diagServices); } - return resolver; + return callCtx; } - /** - * Ensure a path has been properly resolved - * - * @param pathCount pathCount - * @param catalog catalog - * @param pathToResolve the path to resolve - * @param resolvedPath resolved path - */ - private void ensurePathResolved( - int pathCount, - PolarisBaseEntity catalog, - ResolverPath pathToResolve, - List resolvedPath) { - - // ensure same cardinality - if (!pathToResolve.isOptional()) { - Assertions.assertThat(resolvedPath).hasSameSizeAs(pathToResolve.getEntityNames()); - } - - // catalog path - List catalogPath = new ArrayList<>(); - catalogPath.add(catalog); - - // loop and validate each element - for (int index = 0; index < resolvedPath.size(); index++) { - ResolvedPolarisEntity cacheEntry = resolvedPath.get(index); - String entityName = pathToResolve.getEntityNames().get(index); - PolarisEntityType entityType = - (index == pathToResolve.getEntityNames().size() - 1) - ? pathToResolve.getLastEntityType() - : PolarisEntityType.NAMESPACE; - - // ensure that this entity has been properly resolved - this.ensureResolved(cacheEntry, catalogPath, entityType, entityName); - - // add to the path under construction - catalogPath.add(cacheEntry.getEntity()); + @Override + protected PolarisMetaStoreManager metaStoreManager() { + if (metaStoreManager == null) { + metaStoreManager = new TransactionalMetaStoreManagerImpl(); } + return metaStoreManager; } - /** - * Ensure that an entity has been properly resolved - * - * @param cacheEntry the entity as resolved by the resolver - * @param catalogPath path to that entity, can be null for top-level entities - * @param entityType entity type - * @param entityName entity name - */ - private void ensureResolved( - ResolvedPolarisEntity cacheEntry, - List catalogPath, - PolarisEntityType entityType, - String entityName) { - // everything was resolved - Assertions.assertThat(cacheEntry).isNotNull(); - PolarisBaseEntity entity = cacheEntry.getEntity(); - Assertions.assertThat(entity).isNotNull(); - List grantRecords = cacheEntry.getAllGrantRecords(); - Assertions.assertThat(grantRecords).isNotNull(); - - // reference entity cannot be null - PolarisBaseEntity refEntity = - this.tm.ensureExistsByName( - catalogPath, entityType, PolarisEntitySubType.ANY_SUBTYPE, entityName); - Assertions.assertThat(refEntity).isNotNull(); - - // reload the cached entry from the backend - ResolvedEntityResult refResolvedEntity = - this.metaStoreManager.loadResolvedEntityById( - this.callCtx, refEntity.getCatalogId(), refEntity.getId(), refEntity.getType()); - - // should exist - Assertions.assertThat(refResolvedEntity).isNotNull(); - - // ensure same entity - refEntity = refResolvedEntity.getEntity(); - List refGrantRecords = refResolvedEntity.getEntityGrantRecords(); - Assertions.assertThat(refEntity).isNotNull(); - Assertions.assertThat(refGrantRecords).isNotNull(); - Assertions.assertThat(entity).isEqualTo(refEntity); - Assertions.assertThat(entity.getEntityVersion()).isEqualTo(refEntity.getEntityVersion()); - - // ensure it has not been dropped - Assertions.assertThat(entity.getDropTimestamp()).isZero(); - - // same number of grants - Assertions.assertThat(grantRecords).hasSameSizeAs(refGrantRecords); - - // ensure same grant records. The order in the list should be deterministic - Iterator refGrantRecordsIt = refGrantRecords.iterator(); - for (PolarisGrantRecord grantRecord : grantRecords) { - PolarisGrantRecord refGrantRecord = refGrantRecordsIt.next(); - Assertions.assertThat(grantRecord).isEqualTo(refGrantRecord); + @Override + protected PolarisTestMetaStoreManager tm() { + if (tm == null) { + // bootstrap the mata store with our test schema + tm = new PolarisTestMetaStoreManager(metaStoreManager(), callCtx()); } - } - - /** - * Ensure that an entity has been properly resolved - * - * @param cacheEntry the entity as resolved by the resolver - * @param entityType entity type - * @param entityName entity name - */ - private void ensureResolved( - ResolvedPolarisEntity cacheEntry, PolarisEntityType entityType, String entityName) { - this.ensureResolved(cacheEntry, null, entityType, entityName); + return tm; } } 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 0f9824fcd0..b37a574644 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 @@ -76,32 +76,32 @@ public void setupPolariMetaStoreManager() { /** validate that the root catalog was properly constructed */ @Test - void validateBootstrap() { + protected void validateBootstrap() { // allocate test driver polarisTestMetaStoreManager.validateBootstrap(); } @Test - void testCreateTestCatalog() { + protected void testCreateTestCatalog() { // allocate test driver polarisTestMetaStoreManager.testCreateTestCatalog(); } @Test - void testCreateTestCatalogWithRetry() { + protected void testCreateTestCatalogWithRetry() { // allocate test driver polarisTestMetaStoreManager.forceRetry(); polarisTestMetaStoreManager.testCreateTestCatalog(); } @Test - void testBrowse() { + protected void testBrowse() { // allocate test driver polarisTestMetaStoreManager.testBrowse(); } @Test - void testCreateEntities() { + protected void testCreateEntities() { PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; try (CallContext callCtx = CallContext.of(() -> "testRealm", polarisTestMetaStoreManager.polarisCallContext)) { @@ -152,7 +152,7 @@ void testCreateEntities() { } @Test - void testCreateEntitiesAlreadyExisting() { + protected void testCreateEntitiesAlreadyExisting() { PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; try (CallContext callCtx = CallContext.of(() -> "testRealm", polarisTestMetaStoreManager.polarisCallContext)) { @@ -196,7 +196,7 @@ void testCreateEntitiesAlreadyExisting() { } @Test - void testCreateEntitiesWithConflict() { + protected void testCreateEntitiesWithConflict() { PolarisMetaStoreManager metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager; try (CallContext callCtx = CallContext.of(() -> "testRealm", polarisTestMetaStoreManager.polarisCallContext)) { @@ -247,47 +247,47 @@ private static TaskEntity createTask(String taskName, long id) { /** Test that entity updates works well */ @Test - void testUpdateEntities() { + protected void testUpdateEntities() { // allocate test driver polarisTestMetaStoreManager.testUpdateEntities(); } /** Test that entity drop works well */ @Test - void testDropEntities() { + protected void testDropEntities() { // allocate test driver polarisTestMetaStoreManager.testDropEntities(); } /** Test that granting/revoking privileges works well */ @Test - void testPrivileges() { + protected void testPrivileges() { // allocate test driver polarisTestMetaStoreManager.testPrivileges(); } /** test entity rename */ @Test - void testRename() { + protected void testRename() { // allocate test driver polarisTestMetaStoreManager.testRename(); } /** Test the set of functions for the entity cache */ @Test - void testEntityCache() { + protected void testEntityCache() { // allocate test driver polarisTestMetaStoreManager.testEntityCache(); } /** Test that attaching/detaching policies works well */ @Test - void testPolicyMapping() { + protected void testPolicyMapping() { polarisTestMetaStoreManager.testPolicyMapping(); } @Test - void testLoadTasks() { + protected void testLoadTasks() { for (int i = 0; i < 20; i++) { polarisTestMetaStoreManager.createEntity( null, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, "task_" + i); @@ -373,7 +373,7 @@ void testLoadTasks() { } @Test - void testLoadTasksInParallel() throws Exception { + protected void testLoadTasksInParallel() throws Exception { for (int i = 0; i < 100; i++) { polarisTestMetaStoreManager.createEntity( null, PolarisEntityType.TASK, PolarisEntitySubType.NULL_SUBTYPE, "task_" + i); @@ -433,7 +433,7 @@ void testLoadTasksInParallel() throws Exception { /** Test generateNewEntityId() function that generates unique ids by creating Tasks in parallel */ @Test - void testCreateTasksInParallel() throws Exception { + protected void testCreateTasksInParallel() throws Exception { List>> futureList = new ArrayList<>(); Random rand = new Random(); ExecutorService executorService = Executors.newCachedThreadPool(); diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java new file mode 100644 index 0000000000..108db5a6d7 --- /dev/null +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java @@ -0,0 +1,1028 @@ +/* + * 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 jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.ws.rs.core.SecurityContext; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntityCore; +import org.apache.polaris.core.entity.PolarisEntitySubType; +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.entity.PrincipalEntity; +import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.persistence.cache.EntityCache; +import org.apache.polaris.core.persistence.dao.entity.EntityResult; +import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult; +import org.apache.polaris.core.persistence.resolver.Resolver; +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.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public abstract class BaseResolverTest { + + // Principal P1 + protected PolarisBaseEntity P1; + + // cache we are using + protected EntityCache cache; + + // whenever constructing a new Resolver instance, if false, disable cache for that Resolver + // instance by giving it a null cache regardless of the current state of the test-level + // cache instance; use a boolean for this instead of just modifying the test member 'cache' + // so that we can potentially alternate between using cache and not using cache + protected boolean shouldUseCache; + + /** + * Initialize and create the test metadata + * + *
+   * - test
+   * - (N1/N2/T1)
+   * - (N1/N2/T2)
+   * - (N1/N2/V1)
+   * - (N1/N3/T3)
+   * - (N1/N3/V2)
+   * - (N1/T4)
+   * - (N1/N4)
+   * - N5/N6/T5
+   * - N5/N6/T6
+   * - N7/N8/POL1
+   * - N7/N8/POL2
+   * - N7/POL3
+   * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N2, TABLE_DROP on N5/N6/T5)
+   * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C)
+   * - PR1(R1, R2)
+   * - PR2(R2)
+   * - P1(PR1, PR2)
+   * - P2(PR1)
+   * 
+ */ + @BeforeEach + public void setupTest() { + tm().testCreateTestCatalog(); + + // principal P1 + this.P1 = tm().ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P1"); + } + + // polaris call context + protected abstract PolarisCallContext callCtx(); + + // utility to bootstrap the mata store + protected abstract PolarisTestMetaStoreManager tm(); + + // the meta store manager + protected abstract PolarisMetaStoreManager metaStoreManager(); + + protected static boolean supportEntityCache = true; + + protected static List useCacheValueSource() { + return supportEntityCache ? List.of(Boolean.TRUE, Boolean.FALSE) : List.of(Boolean.FALSE); + } + + /** This test resolver for a create-principal scenario */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testResolvePrincipal(boolean useCache) { + this.shouldUseCache = useCache; + + // resolve a principal which does not exist, but make it optional so will succeed + this.resolveDriver(null, null, "P3", true, null, null); + + // 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); + + // then resolve a principal which does exist + this.resolveDriver(null, null, "P2", false, null, null); + + // do it again, but this time using the primed cache + this.resolveDriver(this.cache, null, "P2", false, null, null); + + // now add a principal roles + this.resolveDriver(this.cache, null, "P2", false, "PR1", null); + + // do it again, everything in the cache + this.resolveDriver(this.cache, null, "P2", false, "PR1", null); + + // do it again on a cold cache + this.resolveDriver(this.cache, null, "P2", false, "PR1", null); + } + + /** Test that we can specify a subset of principal role names */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testScopedPrincipalRole(boolean useCache) { + this.shouldUseCache = useCache; + + // start without a scope + this.resolveDriver(null, null, "P2", false, "PR1", null); + + // specify various scopes + this.resolveDriver(this.cache, Set.of("PR1"), "P2", false, "PR1", null); + this.resolveDriver(this.cache, Set.of("PR2"), "P2", false, "PR1", null); + this.resolveDriver(this.cache, Set.of("PR2", "PR3"), "P2", false, "PR1", null); + this.resolveDriver(null, Set.of("PR2", "PR3"), "P2", false, "PR1", null); + this.resolveDriver(null, Set.of("PR3"), "P2", false, "PR1", null); + this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "P2", false, "PR1", null); + } + + /** + * Test that the set of catalog roles being activated is correctly inferred, based of a set of + * principal roles + */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testCatalogRolesActivation(boolean useCache) { + this.shouldUseCache = useCache; + + // start simple, with both PR1 and PR2, you get R1 and R2 + this.resolveDriver(null, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); + + // PR1 itself is enough to activate both R1 and R2 + this.resolveDriver(this.cache, Set.of("PR1"), "test", Set.of("R1", "R2")); + + // PR2 only activates R2 + this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2")); + + // With a non-existing principal roles, nothing gets activated + this.resolveDriver(this.cache, Set.of("NOT_EXISTING"), "test", Set.of()); + } + + /** Test that paths, one or more, are properly resolved */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testResolvePath(boolean useCache) { + this.shouldUseCache = useCache; + + // N1 which exists + ResolverPath N1 = new ResolverPath(List.of("N1"), PolarisEntityType.NAMESPACE); + this.resolveDriver(null, "test", N1, null, null); + + // N1/N2 which exists + ResolverPath N1_N2 = new ResolverPath(List.of("N1", "N2"), PolarisEntityType.NAMESPACE); + this.resolveDriver(null, "test", N1_N2, null, null); + + // N1/N2/T1 which exists + ResolverPath N1_N2_T1 = + new ResolverPath(List.of("N1", "N2", "T1"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N1_N2_T1, null, null); + + // N1/N2/T1 which exists + ResolverPath N1_N2_V1 = + new ResolverPath(List.of("N1", "N2", "V1"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N1_N2_V1, null, null); + + // N5/N6 which exists + ResolverPath N5_N6 = new ResolverPath(List.of("N5", "N6"), PolarisEntityType.NAMESPACE); + this.resolveDriver(this.cache, "test", N5_N6, null, null); + + // N5/N6/T5 which exists + ResolverPath N5_N6_T5 = + new ResolverPath(List.of("N5", "N6", "T5"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N5_N6_T5, null, null); + + // N7/N8 which exists + ResolverPath N7_N8 = new ResolverPath(List.of("N7", "N8"), PolarisEntityType.NAMESPACE); + this.resolveDriver(this.cache, "test", N7_N8, null, null); + + // N7/N8/POL1 which exists + ResolverPath N7_N8_POL1 = + new ResolverPath(List.of("N7", "N8", "POL1"), PolarisEntityType.POLICY); + this.resolveDriver(this.cache, "test", N7_N8_POL1, null, null); + + // N7/POL3 which exists + ResolverPath N7_POL3 = new ResolverPath(List.of("N7", "POL3"), PolarisEntityType.POLICY); + this.resolveDriver(this.cache, "test", N7_POL3, null, null); + + // Error scenarios: N5/N6/T8 which does not exists + 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); + + // 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); + + // now test multiple paths + this.resolveDriver( + this.cache, "test", null, List.of(N1, N5_N6, N1, N1_N2, N5_N6_T5, N1_N2), null); + this.resolveDriver( + this.cache, + "test", + null, + List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), + ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + + // except if the optional flag is specified + N5_N6_T8 = new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE, true); + Resolver resolver = + this.resolveDriver(this.cache, "test", null, List.of(N1, N5_N6_T8, N5_N6_T5, N1_N2), null); + // get all the resolved paths + List> resolvedPath = resolver.getResolvedPaths(); + Assertions.assertThat(resolvedPath.get(0)).hasSize(1); + Assertions.assertThat(resolvedPath.get(1)).hasSize(2); + Assertions.assertThat(resolvedPath.get(2)).hasSize(3); + Assertions.assertThat(resolvedPath.get(3)).hasSize(2); + } + + /** + * Ensure that if data changes while entities are cached, we will always resolve to the latest + * version + */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testConsistency(boolean useCache) { + this.shouldUseCache = useCache; + + // resolve principal "P2" + this.resolveDriver(null, null, "P2", false, null, null); + this.resolveDriver(this.cache, null, "P2", false, null, null); + + // now drop this principal. It is still cached + PolarisBaseEntity P2 = tm().ensureExistsByName(null, PolarisEntityType.PRINCIPAL, "P2"); + tm().dropEntity(null, P2); + + // 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); + + // recreate P2 + 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); + + // resolve existing grants on catalog + this.resolveDriver(this.cache, Set.of("PR1", "PR2"), "test", Set.of("R1", "R2")); + + // with only PR2, we will only activate R2 + Resolver resolver = this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2")); + + // Now add a new catalog role and see if the changes are reflected + Assertions.assertThat(resolver.getResolvedReferenceCatalog()).isNotNull(); + PolarisBaseEntity TEST = resolver.getResolvedReferenceCatalog().getEntity(); + PolarisBaseEntity R3 = tm().createEntity(List.of(TEST), PolarisEntityType.CATALOG_ROLE, "R3"); + + // now grant R3 to PR2 + Assertions.assertThat(resolver.getResolvedCallerPrincipalRoles()).hasSize(1); + PolarisBaseEntity PR2 = resolver.getResolvedCallerPrincipalRoles().get(0).getEntity(); + tm().grantToGrantee(TEST, R3, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); + + // now resolve again with only PR2 activated, should see the new catalog role R3 + this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2", "R3")); + + // now drop that role and then recreate it. The new incarnation should be used + tm().dropEntity(List.of(TEST), R3); + PolarisBaseEntity R3_NEW = + tm().createEntity(List.of(TEST), PolarisEntityType.CATALOG_ROLE, "R3"); + + // now grant R3_NEW to PR2 and resolve it again + tm().grantToGrantee(TEST, R3_NEW, PR2, PolarisPrivilege.CATALOG_ROLE_USAGE); + resolver = this.resolveDriver(this.cache, Set.of("PR2"), "test", Set.of("R2", "R3")); + + // ensure that the correct catalog role was resolved + Assertions.assertThat(resolver.getResolvedCatalogRoles()).containsKey(R3_NEW.getId()); + } + + /** Check resolve paths when cache is inconsistent */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testPathConsistency(boolean useCache) { + this.shouldUseCache = useCache; + + // resolve few paths path + ResolverPath N1_PATH = new ResolverPath(List.of("N1"), PolarisEntityType.NAMESPACE); + this.resolveDriver(null, "test", N1_PATH, null, null); + ResolverPath N1_N2_PATH = new ResolverPath(List.of("N1", "N2"), PolarisEntityType.NAMESPACE); + this.resolveDriver(this.cache, "test", N1_N2_PATH, null, null); + ResolverPath N1_N2_T1_PATH = + new ResolverPath(List.of("N1", "N2", "T1"), PolarisEntityType.TABLE_LIKE); + Resolver resolver = this.resolveDriver(this.cache, "test", N1_N2_T1_PATH, null, null); + + // get the catalog + Assertions.assertThat(resolver.getResolvedReferenceCatalog()).isNotNull(); + PolarisBaseEntity TEST = resolver.getResolvedReferenceCatalog().getEntity(); + + // get the various entities in the path + Assertions.assertThat(resolver.getResolvedPath()).isNotNull(); + Assertions.assertThat(resolver.getResolvedPath()).hasSize(3); + PolarisBaseEntity N1 = resolver.getResolvedPath().get(0).getEntity(); + PolarisBaseEntity N2 = resolver.getResolvedPath().get(1).getEntity(); + PolarisBaseEntity T1 = resolver.getResolvedPath().get(2).getEntity(); + + // resolve N3 + ResolverPath N1_N3_PATH = new ResolverPath(List.of("N1", "N3"), PolarisEntityType.NAMESPACE); + resolver = this.resolveDriver(this.cache, "test", N1_N3_PATH, null, null); + Assertions.assertThat(resolver.getResolvedPath()).isNotNull(); + Assertions.assertThat(resolver.getResolvedPath()).hasSize(2); + PolarisBaseEntity N3 = resolver.getResolvedPath().get(1).getEntity(); + + // now re-parent T1 under N3, keeping the same name + tm().renameEntity(List.of(TEST, N1, N2), T1, List.of(TEST, N1, N3), "T1"); + + // now expect to fail resolving T1 under N1/N2 + this.resolveDriver( + this.cache, + "test", + N1_N2_T1_PATH, + null, + ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED); + + // but we should be able to resolve it under N1/N3 + ResolverPath N1_N3_T1_PATH = + new ResolverPath(List.of("N1", "N3", "T1"), PolarisEntityType.TABLE_LIKE); + this.resolveDriver(this.cache, "test", N1_N3_T1_PATH, null, null); + } + + /** Resolve catalog roles */ + @ParameterizedTest + @MethodSource("useCacheValueSource") + protected void testResolveCatalogRole(boolean useCache) { + this.shouldUseCache = useCache; + + // resolve catalog role + this.resolveDriver(null, "test", "R1", null); + + // do it again + this.resolveDriver(this.cache, "test", "R1", null); + this.resolveDriver(this.cache, "test", "R1", null); + + // failure scenario + this.resolveDriver( + this.cache, "test", "R5", ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED); + } + + /** + * Create a simple resolver without a reference catalog, any principal roles sub-scope and using + * P1 as the caller principal + * + * @return new resolver to test with + */ + @Nonnull + private Resolver allocateResolver() { + return this.allocateResolver(null, null); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param referenceCatalogName the reference e catalog name, can be null + * @return new resolver to test with + */ + @Nonnull + private Resolver allocateResolver(@Nullable String referenceCatalogName) { + return this.allocateResolver(null, referenceCatalogName); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param cache if not null, cache to use, else one will be created + * @return new resolver to test with + */ + @Nonnull + private Resolver allocateResolver(@Nullable EntityCache cache) { + return this.allocateResolver(cache, null); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param cache if not null, cache to use, else one will be created + * @param referenceCatalogName the reference e catalog name, can be null + * @return new resolver to test with + */ + @Nonnull + private Resolver allocateResolver( + @Nullable EntityCache cache, @Nullable String referenceCatalogName) { + return this.allocateResolver(cache, null, referenceCatalogName); + } + + /** + * Create a simple resolver without any principal roles sub-scope and using P1 as the caller + * principal + * + * @param cache if not null, cache to use, else one will be created + * @param principalRolesScope if not null, scoped principal roles + * @param referenceCatalogName the reference e catalog name, can be null + * @return new resolver to test with + */ + @Nonnull + private Resolver allocateResolver( + @Nullable EntityCache cache, + Set principalRolesScope, + @Nullable String referenceCatalogName) { + + // create a new cache if needs be + if (cache == null) { + this.cache = new EntityCache(metaStoreManager()); + } + boolean allRoles = principalRolesScope == null; + Optional> roleEntities = + Optional.ofNullable(principalRolesScope) + .map( + scopes -> + scopes.stream() + .map( + role -> + metaStoreManager() + .readEntityByName( + callCtx(), + null, + PolarisEntityType.PRINCIPAL_ROLE, + PolarisEntitySubType.NULL_SUBTYPE, + role)) + .filter(EntityResult::isSuccess) + .map(EntityResult::getEntity) + .map(PrincipalRoleEntity::of) + .collect(Collectors.toList())); + AuthenticatedPolarisPrincipal authenticatedPrincipal = + new AuthenticatedPolarisPrincipal( + PrincipalEntity.of(P1), Optional.ofNullable(principalRolesScope).orElse(Set.of())); + return new Resolver( + callCtx(), + metaStoreManager(), + new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return authenticatedPrincipal; + } + + @Override + public boolean isUserInRole(String role) { + return roleEntities + .map(l -> l.stream().map(PrincipalRoleEntity::getName).anyMatch(role::equals)) + .orElse(allRoles); + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String getAuthenticationScheme() { + return ""; + } + }, + this.shouldUseCache ? this.cache : null, + referenceCatalogName); + } + + /** + * Resolve a principal and optionally a principal role + * + * @param cache if not null, cache to use + * @param principalName name of the principal name being created + * @param exists true if this principal already exists + * @param principalRoleName name of the principal role, should exist + */ + private void resolvePrincipalAndPrincipalRole( + EntityCache cache, String principalName, boolean exists, String principalRoleName) { + Resolver resolver = 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); + + // add principal role if one passed-in + if (principalRoleName != null) { + resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + } + + // done, run resolve + ResolverStatus status = resolver.resolveAll(); + + // we expect success + Assertions.assertThat(status.getStatus()).isEqualTo(ResolverStatus.StatusEnum.SUCCESS); + + // the principal does not exist, check that this is the case + if (exists) { + // the principal exist, check that this is the case + this.ensureResolved( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName), + PolarisEntityType.PRINCIPAL, + principalName); + } else { + // not found + Assertions.assertThat(resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)) + .isNull(); + } + + // validate that we were able to resolve the principal and the two principal roles + this.ensureResolved(resolver.getResolvedCallerPrincipal(), PolarisEntityType.PRINCIPAL, "P1"); + + // validate that the two principal roles have been activated + List principalRolesResolved = resolver.getResolvedCallerPrincipalRoles(); + + // expect two principal roles + Assertions.assertThat(principalRolesResolved).hasSize(2); + principalRolesResolved.sort(Comparator.comparing(p -> p.getEntity().getName())); + + // ensure they are PR1 and PR2 + this.ensureResolved(principalRolesResolved.get(0), PolarisEntityType.PRINCIPAL_ROLE, "PR1"); + this.ensureResolved( + principalRolesResolved.get(principalRolesResolved.size() - 1), + PolarisEntityType.PRINCIPAL_ROLE, + "PR2"); + + // 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); + } + } + + /** + * Main resolve driver + * + * @param cache if not null, cache we can use + * @param principalRolesScope if not null, scoped roles + * @param principalName if not null, name of the principal to resolve + * @param isPrincipalNameOptional if true, the name of the principal is optional + * @param principalRoleName if not null, name of the principal role to resolve + * @param expectedStatus the expected status if not success + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + Set principalRolesScope, + String principalName, + boolean isPrincipalNameOptional, + String principalRoleName, + ResolverStatus.StatusEnum expectedStatus) { + return this.resolveDriver( + cache, + principalRolesScope, + principalName, + isPrincipalNameOptional, + principalRoleName, + null, + null, + null, + null, + expectedStatus, + null); + } + + /** + * Main resolve driver + * + * @param cache if not null, cache we can use + * @param catalogName if not null, name of the catalog to resolve + * @param path if not null, single path in that catalog + * @param paths if not null, set of path in that catalog. Path and paths are mutually exclusive + * @param expectedStatus the expected status if not success activated + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + String catalogName, + ResolverPath path, + List paths, + ResolverStatus.StatusEnum expectedStatus) { + return this.resolveDriver( + cache, null, null, false, null, catalogName, null, path, paths, expectedStatus, null); + } + + /** + * Main resolve driver for testing catalog role activation + * + * @param cache if not null, cache we can use + * @param principalRolesScope if not null, scoped roles + * @param catalogName if not null, name of the catalog to resolve + * @param expectedActivatedCatalogRoles set of catalog role names the caller expects to be + * activated + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + Set principalRolesScope, + String catalogName, + Set expectedActivatedCatalogRoles) { + return this.resolveDriver( + cache, + principalRolesScope, + null, + false, + null, + catalogName, + null, + null, + null, + null, + expectedActivatedCatalogRoles); + } + + /** + * Main resolve driver for resolving catalog roles + * + * @param cache if not null, cache we can use + * @param catalogName if not null, name of the catalog to resolve + * @param catalogRoleName if not null, name of catalog role name to resolve + * @param expectedStatus the expected status if not success + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + String catalogName, + String catalogRoleName, + ResolverStatus.StatusEnum expectedStatus) { + return this.resolveDriver( + cache, + null, + null, + false, + null, + catalogName, + catalogRoleName, + null, + null, + expectedStatus, + null); + } + + /** + * Main resolve driver + * + * @param cache if not null, cache we can use + * @param principalRolesScope if not null, scoped roles + * @param principalName if not null, name of the principal to resolve + * @param isPrincipalNameOptional if true, the name of the principal is optional + * @param principalRoleName if not null, name of the principal role to resolve + * @param catalogName if not null, name of the catalog to resolve + * @param catalogRoleName if not null, name of catalog role name to resolve + * @param path if not null, single path in that catalog + * @param paths if not null, set of path in that catalog. Path and paths are mutually exclusive + * @param expectedStatus the expected status if not success + * @param expectedActivatedCatalogRoles set of catalog role names the caller expects to be + * activated + * @return resolver we created and which has been validated. + */ + private Resolver resolveDriver( + EntityCache cache, + Set principalRolesScope, + String principalName, + boolean isPrincipalNameOptional, + String principalRoleName, + String catalogName, + String catalogRoleName, + ResolverPath path, + List paths, + ResolverStatus.StatusEnum expectedStatus, + Set expectedActivatedCatalogRoles) { + + // if null we expect success + if (expectedStatus == null) { + expectedStatus = ResolverStatus.StatusEnum.SUCCESS; + } + + // allocate resolver + Resolver resolver = allocateResolver(cache, principalRolesScope, catalogName); + + // principal name? + if (principalName != null) { + if (isPrincipalNameOptional) { + resolver.addOptionalEntityByName(PolarisEntityType.PRINCIPAL, principalName); + } else { + resolver.addEntityByName(PolarisEntityType.PRINCIPAL, principalName); + } + } + + // add principal role if one passed-in + if (principalRoleName != null) { + resolver.addEntityByName(PolarisEntityType.PRINCIPAL_ROLE, principalRoleName); + } + + // add catalog role if one passed-in + if (catalogRoleName != null) { + resolver.addEntityByName(PolarisEntityType.CATALOG_ROLE, catalogRoleName); + } + + // add all paths + if (path != null) { + resolver.addPath(path); + } else if (paths != null) { + paths.forEach(resolver::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 + EntityResult result = + metaStoreManager() + .readEntityByName( + 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), + PolarisEntityType.PRINCIPAL, + principalName); + } else { + // principal was optional + Assertions.assertThat(isPrincipalNameOptional).isTrue(); + // not found + Assertions.assertThat( + resolver.getResolvedEntity(PolarisEntityType.PRINCIPAL, principalName)) + .isNull(); + } + } + + // 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++; + } + } + } else { + // both PR1 and PR2 + expectedSize = 2; + } + + // ensure the right set of principal roles were activated + Assertions.assertThat(principalRolesResolved).hasSize(expectedSize); + + // expect either PR1 and PR2 + for (ResolvedPolarisEntity 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(); + + // 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 catalog was passed-in, ensure it exists + if (catalogName != null) { + ResolvedPolarisEntity 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) { + ResolvedPolarisEntity 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(); + + // 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 (ResolvedPolarisEntity 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; + + // all resolved path + List> allResolvedPaths = resolver.getResolvedPaths(); + + // 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); + } + } + } + } + return resolver; + } + + /** + * Ensure a path has been properly resolved + * + * @param pathCount pathCount + * @param catalog catalog + * @param pathToResolve the path to resolve + * @param resolvedPath resolved path + */ + private void ensurePathResolved( + int pathCount, + PolarisBaseEntity catalog, + ResolverPath pathToResolve, + List resolvedPath) { + + // ensure same cardinality + if (!pathToResolve.isOptional()) { + Assertions.assertThat(resolvedPath).hasSameSizeAs(pathToResolve.getEntityNames()); + } + + // catalog path + List catalogPath = new ArrayList<>(); + catalogPath.add(catalog); + + // loop and validate each element + for (int index = 0; index < resolvedPath.size(); index++) { + ResolvedPolarisEntity cacheEntry = resolvedPath.get(index); + String entityName = pathToResolve.getEntityNames().get(index); + PolarisEntityType entityType = + (index == pathToResolve.getEntityNames().size() - 1) + ? pathToResolve.getLastEntityType() + : PolarisEntityType.NAMESPACE; + + // ensure that this entity has been properly resolved + this.ensureResolved(cacheEntry, catalogPath, entityType, entityName); + + // add to the path under construction + catalogPath.add(cacheEntry.getEntity()); + } + } + + /** + * Ensure that an entity has been properly resolved + * + * @param cacheEntry the entity as resolved by the resolver + * @param catalogPath path to that entity, can be null for top-level entities + * @param entityType entity type + * @param entityName entity name + */ + private void ensureResolved( + ResolvedPolarisEntity cacheEntry, + List catalogPath, + PolarisEntityType entityType, + String entityName) { + // everything was resolved + Assertions.assertThat(cacheEntry).isNotNull(); + PolarisBaseEntity entity = cacheEntry.getEntity(); + Assertions.assertThat(entity).isNotNull(); + List grantRecords = cacheEntry.getAllGrantRecords(); + Assertions.assertThat(grantRecords).isNotNull(); + + // reference entity cannot be null + PolarisBaseEntity refEntity = + tm().ensureExistsByName( + catalogPath, entityType, PolarisEntitySubType.ANY_SUBTYPE, entityName); + Assertions.assertThat(refEntity).isNotNull(); + + // reload the cached entry from the backend + ResolvedEntityResult refResolvedEntity = + metaStoreManager() + .loadResolvedEntityById( + callCtx(), refEntity.getCatalogId(), refEntity.getId(), refEntity.getType()); + + // should exist + Assertions.assertThat(refResolvedEntity).isNotNull(); + + // ensure same entity + refEntity = refResolvedEntity.getEntity(); + List refGrantRecords = refResolvedEntity.getEntityGrantRecords(); + Assertions.assertThat(refEntity).isNotNull(); + Assertions.assertThat(refGrantRecords).isNotNull(); + Assertions.assertThat(entity).isEqualTo(refEntity); + Assertions.assertThat(entity.getEntityVersion()).isEqualTo(refEntity.getEntityVersion()); + + // ensure it has not been dropped + Assertions.assertThat(entity.getDropTimestamp()).isZero(); + + checkRefGrantRecords(grantRecords, refGrantRecords); + } + + protected void checkRefGrantRecords( + List grantRecords, List refGrantRecords) { + // same number of grants + Assertions.assertThat(grantRecords).hasSameSizeAs(refGrantRecords); + + // ensure same grant records. The order in the list should be deterministic + Iterator refGrantRecordsIt = refGrantRecords.iterator(); + for (PolarisGrantRecord grantRecord : grantRecords) { + PolarisGrantRecord refGrantRecord = refGrantRecordsIt.next(); + Assertions.assertThat(grantRecord).isEqualTo(refGrantRecord); + } + } + + /** + * Ensure that an entity has been properly resolved + * + * @param cacheEntry the entity as resolved by the resolver + * @param entityType entity type + * @param entityName entity name + */ + private void ensureResolved( + ResolvedPolarisEntity cacheEntry, PolarisEntityType entityType, String entityName) { + this.ensureResolved(cacheEntry, null, entityType, entityName); + } +} 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 3e7a0fb2ac..8bcbe431f4 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 @@ -67,24 +67,36 @@ public class PolarisTestMetaStoreManager { final PolarisMetaStoreManager polarisMetaStoreManager; // the start time - private final long testStartTime = System.currentTimeMillis(); + private final long testStartTime; private final ObjectMapper objectMapper = new ObjectMapper(); + private final boolean supportsChangeTracking; + // if true, simulate retries by client private boolean doRetry; // initialize the test public PolarisTestMetaStoreManager( PolarisMetaStoreManager polarisMetaStoreManager, PolarisCallContext polarisCallContext) { - this.polarisCallContext = polarisCallContext; - this.polarisMetaStoreManager = polarisMetaStoreManager; - this.doRetry = false; + this(polarisMetaStoreManager, polarisCallContext, System.currentTimeMillis(), true); // bootstrap the Polaris service polarisMetaStoreManager.purge(polarisCallContext); polarisMetaStoreManager.bootstrapPolarisService(polarisCallContext); } + public PolarisTestMetaStoreManager( + PolarisMetaStoreManager polarisMetaStoreManager, + PolarisCallContext polarisCallContext, + long testStartTime, + boolean supportsChangeTracking) { + this.testStartTime = testStartTime; + this.polarisCallContext = polarisCallContext; + this.polarisMetaStoreManager = polarisMetaStoreManager; + this.supportsChangeTracking = supportsChangeTracking; + this.doRetry = false; + } + public void forceRetry() { this.doRetry = true; } @@ -1443,16 +1455,18 @@ PolarisBaseEntity updateEntity( .isEqualTo(jsonNode(entity.getInternalProperties())); // lookup the tracking slice to verify this has been updated too - List versions = - polarisMetaStoreManager - .loadEntitiesChangeTracking( - this.polarisCallContext, List.of(new PolarisEntityId(catalogId, entity.getId()))) - .getChangeTrackingVersions(); - Assertions.assertThat(versions).hasSize(1); - Assertions.assertThat(versions.get(0).getEntityVersion()) - .isEqualTo(updatedEntity.getEntityVersion()); - Assertions.assertThat(versions.get(0).getGrantRecordsVersion()) - .isEqualTo(updatedEntity.getGrantRecordsVersion()); + if (supportsChangeTracking) { + List versions = + polarisMetaStoreManager + .loadEntitiesChangeTracking( + this.polarisCallContext, List.of(new PolarisEntityId(catalogId, entity.getId()))) + .getChangeTrackingVersions(); + Assertions.assertThat(versions).hasSize(1); + Assertions.assertThat(versions.get(0).getEntityVersion()) + .isEqualTo(updatedEntity.getEntityVersion()); + Assertions.assertThat(versions.get(0).getGrantRecordsVersion()) + .isEqualTo(updatedEntity.getGrantRecordsVersion()); + } return updatedEntity; } @@ -2430,12 +2444,12 @@ void renameEntity( // the renamed entity PolarisEntity renamedEntityInput = new PolarisEntity(entity); renamedEntityInput.setName(newName); - String updatedInternalPropertiesString = "{\"key1\": \"updatedDataForInternalProperties1234\"}"; - String updatedPropertiesString = "{\"key1\": \"updatedDataForProperties9876\"}"; + var updatedInternalProperties = Map.of("key1", "updatedDataForInternalProperties1234"); + var updatedProperties = Map.of("key1", "updatedDataForProperties9876"); // this is to test that properties are also updated during the rename operation - renamedEntityInput.setInternalProperties(updatedInternalPropertiesString); - renamedEntityInput.setProperties(updatedPropertiesString); + renamedEntityInput.setInternalPropertiesAsMap(updatedInternalProperties); + renamedEntityInput.setPropertiesAsMap(updatedProperties); // check to see if we would have a name conflict EntityResult newNameLookup = @@ -2468,9 +2482,10 @@ void renameEntity( Assertions.assertThat(renamedEntity).isEqualTo(renamedEntityOut); // ensure properties have been updated - Assertions.assertThat(renamedEntityOut.getInternalProperties()) - .isEqualTo(updatedInternalPropertiesString); - Assertions.assertThat(renamedEntityOut.getProperties()).isEqualTo(updatedPropertiesString); + Assertions.assertThat(renamedEntityOut.getInternalPropertiesAsMap()) + .containsAllEntriesOf(updatedInternalProperties); + Assertions.assertThat(renamedEntityOut.getPropertiesAsMap()) + .containsAllEntriesOf(updatedProperties); // ensure the old one is gone EntityResult res =