This is an automated email from the ASF dual-hosted git repository. dimas pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push: new 364b53c9f Fix Namespace resolution on grant/revoke privilege operations (#2170) 364b53c9f is described below commit 364b53c9f80f0880e428f796d17a0376bfb8cbcc Author: Pooja Nilangekar <poo...@umd.edu> AuthorDate: Mon Jul 28 19:03:59 2025 -0400 Fix Namespace resolution on grant/revoke privilege operations (#2170) * Fix Namespace resolution on grant/revoke privilege operations * Move isFullyResolvedNamespace to PolarisResolvedPathWrapper --- .../persistence/PolarisResolvedPathWrapper.java | 41 +++ .../PolarisResolvedPathWrapperTest.java | 146 +++++++++++ .../polaris/service/admin/PolarisAdminService.java | 6 +- .../service/admin/PolarisAdminServiceTest.java | 285 +++++++++++++++++++++ 4 files changed, 476 insertions(+), 2 deletions(-) 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/PolarisResolvedPathWrapper.java index 6b09598c4..41f65a243 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/PolarisResolvedPathWrapper.java @@ -18,9 +18,13 @@ */ package org.apache.polaris.core.persistence; +import jakarta.annotation.Nonnull; +import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; +import org.apache.iceberg.catalog.Namespace; import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; /** * Holds fully-resolved path of PolarisEntities representing the targetEntity with all its grants @@ -77,6 +81,43 @@ public class PolarisResolvedPathWrapper { .collect(Collectors.toList()); } + /** + * Checks if a namespace is fully resolved. + * + * @param catalogName the name of the catalog + * @param namespace the namespace we're trying to resolve + * @return true if the namespace is considered fully resolved for the given catalog type + */ + public boolean isFullyResolvedNamespace( + @Nonnull String catalogName, @Nonnull Namespace namespace) { + if (resolvedPath == null) { + return false; + } + + List<PolarisEntity> fullPath = getRawFullPath(); + int expectedPathLength = 1 + namespace.levels().length; + if (fullPath.size() != expectedPathLength) { + return false; + } + + if (!fullPath.get(0).getName().equals(catalogName)) { + return false; + } + + String[] namespaceLevels = namespace.levels(); + int levelsLength = namespaceLevels.length; + Iterator<PolarisEntity> fullPathIterator = fullPath.iterator(); + fullPathIterator.next(); + for (int i = 0; i < levelsLength; i++) { + PolarisEntity entity = fullPathIterator.next(); + if (!entity.getName().equals(namespaceLevels[i]) + || entity.getType() != PolarisEntityType.NAMESPACE) { + return false; + } + } + return true; + } + @Override public String toString() { return "resolvedPath:" + resolvedPath; diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisResolvedPathWrapperTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisResolvedPathWrapperTest.java new file mode 100644 index 000000000..466aba17e --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisResolvedPathWrapperTest.java @@ -0,0 +1,146 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.apache.iceberg.catalog.Namespace; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.junit.jupiter.api.Test; + +public class PolarisResolvedPathWrapperTest { + + @Test + void testIsFullyResolvedNamespace_NullResolvedPath() { + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(null); + + assertFalse(wrapper.isFullyResolvedNamespace("test-catalog", Namespace.of("ns1"))); + } + + @Test + void testIsFullyResolvedNamespace_InsufficientPathLength() { + String catalogName = "test-catalog"; + Namespace namespace = Namespace.of("ns1", "ns2"); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + List<ResolvedPolarisEntity> shortPath = List.of(createResolvedEntity(catalogEntity)); + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(shortPath); + + assertFalse(wrapper.isFullyResolvedNamespace(catalogName, namespace)); + } + + @Test + void testIsFullyResolvedNamespace_WrongCatalogName() { + String catalogName = "test-catalog"; + String wrongCatalogName = "wrong-catalog"; + Namespace namespace = Namespace.of("ns1"); + + PolarisEntity wrongCatalogEntity = createEntity(wrongCatalogName, PolarisEntityType.CATALOG); + PolarisEntity namespaceEntity = createEntity("ns1", PolarisEntityType.NAMESPACE); + List<ResolvedPolarisEntity> path = + List.of(createResolvedEntity(wrongCatalogEntity), createResolvedEntity(namespaceEntity)); + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(path); + + assertFalse(wrapper.isFullyResolvedNamespace(catalogName, namespace)); + } + + @Test + void testIsFullyResolvedNamespace_WrongNamespaceNames() { + String catalogName = "test-catalog"; + Namespace namespace = Namespace.of("ns1", "ns2"); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisEntity namespace1Entity = createEntity("ns1", PolarisEntityType.NAMESPACE); + PolarisEntity namespace2WrongEntity = createEntity("wrong-ns2", PolarisEntityType.NAMESPACE); + List<ResolvedPolarisEntity> path = + List.of( + createResolvedEntity(catalogEntity), + createResolvedEntity(namespace1Entity), + createResolvedEntity(namespace2WrongEntity)); + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(path); + + assertFalse(wrapper.isFullyResolvedNamespace(catalogName, namespace)); + } + + @Test + void testIsFullyResolvedNamespace_CorrectPath() { + String catalogName = "test-catalog"; + Namespace namespace = Namespace.of("ns1", "ns2"); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisEntity namespace1Entity = createEntity("ns1", PolarisEntityType.NAMESPACE); + PolarisEntity namespace2Entity = createEntity("ns2", PolarisEntityType.NAMESPACE); + List<ResolvedPolarisEntity> path = + List.of( + createResolvedEntity(catalogEntity), + createResolvedEntity(namespace1Entity), + createResolvedEntity(namespace2Entity)); + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(path); + + assertTrue(wrapper.isFullyResolvedNamespace(catalogName, namespace)); + } + + @Test + void testIsFullyResolvedNamespace_WrongEntityType() { + String catalogName = "test-catalog"; + Namespace namespace = Namespace.of("ns1", "ns2"); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisEntity correctEntityType = createEntity("ns1", PolarisEntityType.NAMESPACE); + PolarisEntity wrongEntityType = createEntity("ns2", PolarisEntityType.TABLE_LIKE); + List<ResolvedPolarisEntity> path = + List.of( + createResolvedEntity(catalogEntity), + createResolvedEntity(correctEntityType), + createResolvedEntity(wrongEntityType)); + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(path); + + assertFalse(wrapper.isFullyResolvedNamespace(catalogName, namespace)); + } + + @Test + void testIsFullyResolvedNamespace_EmptyNamespace() { + String catalogName = "test-catalog"; + Namespace namespace = Namespace.empty(); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + List<ResolvedPolarisEntity> path = List.of(createResolvedEntity(catalogEntity)); + PolarisResolvedPathWrapper wrapper = new PolarisResolvedPathWrapper(path); + + assertTrue(wrapper.isFullyResolvedNamespace(catalogName, namespace)); + } + + private PolarisEntity createEntity(String name, PolarisEntityType type) { + return new PolarisEntity.Builder() + .setName(name) + .setType(type) + .setId(1L) + .setCatalogId(1L) + .setParentId(1L) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + } + + private ResolvedPolarisEntity createResolvedEntity(PolarisEntity entity) { + return new ResolvedPolarisEntity(entity, null, null); + } +} diff --git a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 9ef57701d..5003c78a2 100644 --- a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -1684,7 +1684,8 @@ public class PolarisAdminService { .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); - if (resolvedPathWrapper == null) { + if (resolvedPathWrapper == null + || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { throw new NotFoundException("Namespace %s not found", namespace); } List<PolarisEntity> catalogPath = resolvedPathWrapper.getRawParentPath(); @@ -1712,7 +1713,8 @@ public class PolarisAdminService { .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); - if (resolvedPathWrapper == null) { + if (resolvedPathWrapper == null + || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { throw new NotFoundException("Namespace %s not found", namespace); } List<PolarisEntity> catalogPath = resolvedPathWrapper.getRawParentPath(); diff --git a/service/common/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java b/service/common/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java new file mode 100644 index 000000000..58b7a0c6d --- /dev/null +++ b/service/common/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java @@ -0,0 +1,285 @@ +/* + * 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.service.admin; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import jakarta.ws.rs.core.SecurityContext; +import java.util.List; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.exceptions.NotFoundException; +import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.entity.PolarisPrivilege; +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.dao.entity.PrivilegeResult; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.core.persistence.resolver.ResolverStatus; +import org.apache.polaris.core.secrets.UserSecretsManager; +import org.apache.polaris.service.config.ReservedProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class PolarisAdminServiceTest { + + @Mock private CallContext callContext; + @Mock private PolarisCallContext polarisCallContext; + @Mock private PolarisDiagnostics polarisDiagnostics; + @Mock private PolarisEntityManager entityManager; + @Mock private PolarisMetaStoreManager metaStoreManager; + @Mock private UserSecretsManager userSecretsManager; + @Mock private SecurityContext securityContext; + @Mock private PolarisAuthorizer authorizer; + @Mock private ReservedProperties reservedProperties; + @Mock private AuthenticatedPolarisPrincipal authenticatedPrincipal; + @Mock private PolarisResolutionManifest resolutionManifest; + @Mock private PolarisResolvedPathWrapper resolvedPathWrapper; + + private PolarisAdminService adminService; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + when(securityContext.getUserPrincipal()).thenReturn(authenticatedPrincipal); + when(callContext.getPolarisCallContext()).thenReturn(polarisCallContext); + when(polarisCallContext.getDiagServices()).thenReturn(polarisDiagnostics); + + adminService = + new PolarisAdminService( + callContext, + entityManager, + metaStoreManager, + userSecretsManager, + securityContext, + authorizer, + reservedProperties); + } + + @Test + void testGrantPrivilegeOnNamespaceToRole() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("existing-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + setupSuccessfulNamespaceResolution(catalogName, catalogRoleName, namespace); + + PrivilegeResult successResult = mock(PrivilegeResult.class); + when(successResult.isSuccess()).thenReturn(true); + when(metaStoreManager.grantPrivilegeOnSecurableToRole(any(), any(), any(), any(), any())) + .thenReturn(successResult); + + boolean result = + adminService.grantPrivilegeOnNamespaceToRole( + catalogName, catalogRoleName, namespace, privilege); + + assertThat(result).isTrue(); + } + + @Test + void testGrantPrivilegeOnNamespaceToRole_ThrowsNamespaceNotFoundException() { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("non-existent-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + when(entityManager.prepareResolutionManifest(any(), any(), eq(catalogName))) + .thenReturn(resolutionManifest); + when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(null); + + assertThatThrownBy( + () -> + adminService.grantPrivilegeOnNamespaceToRole( + catalogName, catalogRoleName, namespace, privilege)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Namespace " + namespace + " not found"); + } + + @Test + void testGrantPrivilegeOnNamespaceToRole_IncompleteNamespaceThrowsNamespaceNotFoundException() + throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("complete-ns", "incomplete-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + when(entityManager.prepareResolutionManifest(any(), any(), eq(catalogName))) + .thenReturn(resolutionManifest); + when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); + when(resolvedPathWrapper.getRawFullPath()) + .thenReturn( + List.of( + createEntity("test-catalog", PolarisEntityType.CATALOG), + createEntity("complete-ns", PolarisEntityType.NAMESPACE))); + when(resolvedPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) + .thenReturn(false); + + assertThatThrownBy( + () -> + adminService.grantPrivilegeOnNamespaceToRole( + catalogName, catalogRoleName, namespace, privilege)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Namespace " + namespace + " not found"); + } + + @Test + void testRevokePrivilegeOnNamespaceFromRole() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("existing-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + setupSuccessfulNamespaceResolution(catalogName, catalogRoleName, namespace); + + PrivilegeResult successResult = mock(PrivilegeResult.class); + when(successResult.isSuccess()).thenReturn(true); + when(metaStoreManager.revokePrivilegeOnSecurableFromRole(any(), any(), any(), any(), any())) + .thenReturn(successResult); + + boolean result = + adminService.revokePrivilegeOnNamespaceFromRole( + catalogName, catalogRoleName, namespace, privilege); + + assertThat(result).isTrue(); + } + + @Test + void testRevokePrivilegeOnNamespaceFromRole_ThrowsNamespaceNotFoundException() { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("non-existent-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + when(entityManager.prepareResolutionManifest(any(), any(), eq(catalogName))) + .thenReturn(resolutionManifest); + when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(null); + + assertThatThrownBy( + () -> + adminService.revokePrivilegeOnNamespaceFromRole( + catalogName, catalogRoleName, namespace, privilege)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Namespace " + namespace + " not found"); + } + + @Test + void testRevokePrivilegeOnNamespaceFromRole_IncompletelNamespaceThrowsNamespaceNotFoundException() + throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("incomplete-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + when(entityManager.prepareResolutionManifest(any(), any(), eq(catalogName))) + .thenReturn(resolutionManifest); + when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); + when(resolvedPathWrapper.getRawFullPath()) + .thenReturn(List.of(createEntity("wrong-catalog", PolarisEntityType.CATALOG))); + when(resolvedPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) + .thenReturn(false); + + assertThatThrownBy( + () -> + adminService.revokePrivilegeOnNamespaceFromRole( + catalogName, catalogRoleName, namespace, privilege)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Namespace " + namespace + " not found"); + } + + private PolarisEntity createEntity(String name, PolarisEntityType type) { + return new PolarisEntity.Builder() + .setName(name) + .setType(type) + .setId(1L) + .setCatalogId(1L) + .setParentId(1L) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + } + + private ResolverStatus createSuccessfulResolverStatus() { + return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); + } + + private void setupSuccessfulNamespaceResolution( + String catalogName, String catalogRoleName, Namespace namespace) throws Exception { + + when(entityManager.prepareResolutionManifest(any(), any(), eq(catalogName))) + .thenReturn(resolutionManifest); + when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisEntity namespaceEntity = + createEntity(namespace.levels()[0], PolarisEntityType.NAMESPACE); + List<PolarisEntity> fullPath = List.of(catalogEntity, namespaceEntity); + when(resolvedPathWrapper.getRawFullPath()).thenReturn(fullPath); + when(resolvedPathWrapper.getRawParentPath()).thenReturn(List.of(catalogEntity)); + when(resolvedPathWrapper.getRawLeafEntity()).thenReturn(namespaceEntity); + when(resolvedPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) + .thenReturn(true); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + } +}