From 1af4a5d571db4824398a52ae0d0d8f40b56fdf7a Mon Sep 17 00:00:00 2001 From: Pooja Nilangekar Date: Wed, 30 Jul 2025 17:53:07 -0700 Subject: [PATCH 1/9] Support Namespace/Table level RBAC for external passthrough catalogs --- .../service/admin/PolarisAdminService.java | 183 +++++++++- .../admin/PolarisAdminServiceTest.java | 319 +++++++++++++++++- 2 files changed, 484 insertions(+), 18 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 469bab0896..5338f888fb 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -509,7 +509,8 @@ private void authorizeGrantOnTableLikeOperationOrThrow( PolarisResolvedPathWrapper tableLikeWrapper = resolutionManifest.getResolvedPath( identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE, true); - if (!subTypes.contains(tableLikeWrapper.getRawLeafEntity().getSubType())) { + if (!resolutionManifest.getIsPassthroughFacade() + && !subTypes.contains(tableLikeWrapper.getRawLeafEntity().getSubType())) { CatalogHandler.throwNotFoundExceptionForTableLikeEntity(identifier, subTypes); } @@ -1687,6 +1688,9 @@ public PrivilegeResult grantPrivilegeOnNamespaceToRole( PolarisAuthorizableOperation.ADD_NAMESPACE_GRANT_TO_CATALOG_ROLE; authorizeGrantOnNamespaceOperationOrThrow(op, catalogName, namespace, catalogRoleName); + CatalogEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); PolarisEntity catalogRoleEntity = findCatalogRoleByName(catalogName, catalogRoleName) .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); @@ -1694,7 +1698,19 @@ public PrivilegeResult grantPrivilegeOnNamespaceToRole( PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); if (resolvedPathWrapper == null || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { - throw new NotFoundException("Namespace %s not found", namespace); + if (resolutionManifest.getIsPassthroughFacade()) { + resolvedPathWrapper = + createSyntheticNamespaceEntities(catalogEntity, namespace, resolvedPathWrapper); + if (resolvedPathWrapper == null + || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { + throw new RuntimeException( + String.format( + "Failed to create synthetic namespace entities for namespace %s in catalog %s", + namespace.toString(), catalogName)); + } + } else { + throw new NotFoundException("Namespace %s not found", namespace); + } } List catalogPath = resolvedPathWrapper.getRawParentPath(); PolarisEntity namespaceEntity = resolvedPathWrapper.getRawLeafEntity(); @@ -1734,6 +1750,80 @@ public PrivilegeResult revokePrivilegeOnNamespaceFromRole( privilege); } + /** + * Creates and persists the missing synthetic namespace entities for external catalogs. + * + * @param catalogEntity the external passthrough facade catalog entity. + * @param namespace the expected fully resolved namespace to be created. + * @param existingPath the partially resolved path currently stored in the metastore. + * @return the fully resolved path wrapper. + */ + private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( + CatalogEntity catalogEntity, Namespace namespace, PolarisResolvedPathWrapper existingPath) { + + if (existingPath == null) { + throw new IllegalStateException( + String.format("Catalog entity %s does not exist.", catalogEntity.getName())); + } + + List completePath = new ArrayList<>(existingPath.getRawFullPath()); + PolarisEntity currentParent = existingPath.getRawLeafEntity(); + + String[] allNamespaceLevels = namespace.levels(); + int matchingLevel = -1; + for (PolarisEntity entity : completePath.subList(1, completePath.size())) { + if (entity.getName().equals(allNamespaceLevels[matchingLevel + 1])) { + matchingLevel++; + } else { + break; + } + } + + for (int i = matchingLevel + 1; i < allNamespaceLevels.length; i++) { + String namespacePart = allNamespaceLevels[i]; + + // TODO: Instead of creating synthetic entitties, rely on external catalog mediated backfill. + PolarisEntity syntheticNamespace = + new PolarisEntity.Builder() + .setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId()) + .setCatalogId(catalogEntity.getId()) + .setParentId(currentParent.getId()) + .setType(PolarisEntityType.NAMESPACE) + .setName(namespacePart) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + + EntityResult result = + metaStoreManager.createEntityIfNotExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(completePath), + syntheticNamespace); + + if (result.isSuccess()) { + syntheticNamespace = PolarisEntity.of(result.getEntity()); + } else { + Namespace partialNamespace = Namespace.of(Arrays.copyOf(allNamespaceLevels, i + 1)); + PolarisResolvedPathWrapper partialPath = + resolutionManifest.getResolvedPath(partialNamespace); + PolarisEntity partialLeafEntity = partialPath.getRawLeafEntity(); + if (partialLeafEntity == null + || !(partialLeafEntity.getName().equals(namespacePart) + && partialLeafEntity.getType() == PolarisEntityType.NAMESPACE)) { + throw new RuntimeException( + String.format( + "Failed to create or find namespace entity '%s' in federated catalog '%s'", + namespacePart, catalogEntity.getName())); + } + syntheticNamespace = partialLeafEntity; + } + completePath.add(syntheticNamespace); + currentParent = syntheticNamespace; + } + PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); + return resolvedPathWrapper; + } + + public boolean grantPrivilegeOnTableToRole( public PrivilegeResult grantPrivilegeOnTableToRole( String catalogName, String catalogRoleName, @@ -2011,9 +2101,9 @@ private PrivilegeResult grantPrivilegeOnTableLikeToRole( TableIdentifier identifier, List subTypes, PolarisPrivilege privilege) { - if (findCatalogByName(catalogName).isEmpty()) { - throw new NotFoundException("Parent catalog %s not found", catalogName); - } + CatalogEntity catalogEntity = + findCatalogByName(catalogName) + .orElseThrow(() -> new NotFoundException("Parent catalog %s not found", catalogName)); PolarisEntity catalogRoleEntity = findCatalogRoleByName(catalogName, catalogRoleName) .orElseThrow(() -> new NotFoundException("CatalogRole %s not found", catalogRoleName)); @@ -2023,7 +2113,20 @@ private PrivilegeResult grantPrivilegeOnTableLikeToRole( identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE); if (resolvedPathWrapper == null || !subTypes.contains(resolvedPathWrapper.getRawLeafEntity().getSubType())) { - CatalogHandler.throwNotFoundExceptionForTableLikeEntity(identifier, subTypes); + if (resolutionManifest.getIsPassthroughFacade()) { + resolvedPathWrapper = + createSyntheticTableLikeEntities( + catalogEntity, identifier, subTypes, resolvedPathWrapper); + if (resolvedPathWrapper == null + || !subTypes.contains(resolvedPathWrapper.getRawLeafEntity().getSubType())) { + throw new RuntimeException( + String.format( + "Failed to create synthetic table-like entity for table %s in catalog %s", + identifier.name(), catalogEntity.getName())); + } + } else { + CatalogHandler.throwNotFoundExceptionForTableLikeEntity(identifier, subTypes); + } } List catalogPath = resolvedPathWrapper.getRawParentPath(); PolarisEntity tableLikeEntity = resolvedPathWrapper.getRawLeafEntity(); @@ -2036,6 +2139,74 @@ private PrivilegeResult grantPrivilegeOnTableLikeToRole( privilege); } + /** + * Creates and persists the missing synthetic table-like entity and its parent namespace entities + * for external catalogs. + * + * @param catalogEntity the external passthrough facade catalog entity. + * @param identifier the path of the table-like entity(including the namespace). + * @param subTypes the expected subtypes of the table-like entity + * @param existingPathWrapper the partially resolved path currently stored in the metastore. + * @return the resolved path wrapper + */ + private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( + CatalogEntity catalogEntity, + TableIdentifier identifier, + List subTypes, + PolarisResolvedPathWrapper existingPathWrapper) { + + Namespace namespace = identifier.namespace(); + PolarisResolvedPathWrapper resolvedNamespacePathWrapper = + !namespace.isEmpty() + ? createSyntheticNamespaceEntities(catalogEntity, namespace, existingPathWrapper) + : existingPathWrapper; + + if (resolvedNamespacePathWrapper == null + || (!namespace.isEmpty() + && !resolvedNamespacePathWrapper.isFullyResolvedNamespace( + catalogEntity.getName(), namespace))) { + throw new RuntimeException( + String.format( + "Failed to create synthetic namespace entities for namespace %s in catalog %s", + namespace.toString(), catalogEntity.getName())); + } + + // TODO: Instead of creating a synthetic table-like entity, rely on external catalog mediated + // backfill. + PolarisEntity parentNamespaceEntity = resolvedNamespacePathWrapper.getRawLeafEntity(); + PolarisEntity syntheticTableEntity = + new PolarisEntity.Builder() + .setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId()) + .setCatalogId(parentNamespaceEntity.getCatalogId()) + .setParentId(parentNamespaceEntity.getId()) + .setType(PolarisEntityType.TABLE_LIKE) + .setSubType(subTypes.get(0)) + .setName(identifier.name()) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + + EntityResult result = + metaStoreManager.createEntityIfNotExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(resolvedNamespacePathWrapper.getRawFullPath()), + syntheticTableEntity); + + if (result.isSuccess()) { + syntheticTableEntity = PolarisEntity.of(result.getEntity()); + } else { + PolarisResolvedPathWrapper tablePathWrapper = resolutionManifest.getResolvedPath(identifier); + PolarisEntity leafEntity = + tablePathWrapper != null ? tablePathWrapper.getRawLeafEntity() : null; + if (leafEntity == null || !subTypes.contains(leafEntity.getSubType())) { + throw new RuntimeException( + String.format( + "Failed to create or find table entity '%s' in federated catalog '%s'", + identifier.name(), catalogEntity.getName())); + } + } + return resolutionManifest.getResolvedPath(identifier); + } + /** * Removes a table-level or view-level grant on {@code identifier} from {@code catalogRoleName}. */ diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java index 60e0559426..7e0a89367c 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java @@ -27,6 +27,7 @@ import jakarta.ws.rs.core.SecurityContext; import java.util.List; import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.NotFoundException; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; @@ -35,11 +36,14 @@ import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.dao.entity.BaseResult; +import org.apache.polaris.core.persistence.dao.entity.EntityResult; +import org.apache.polaris.core.persistence.dao.entity.GenerateEntityIdResult; import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult; import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -74,6 +78,11 @@ void setUp() throws Exception { when(securityContext.getUserPrincipal()).thenReturn(authenticatedPrincipal); when(callContext.getPolarisCallContext()).thenReturn(polarisCallContext); + when(resolutionManifestFactory.createResolutionManifest(any(), any(), any())) + .thenReturn(resolutionManifest); + when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + when(resolutionManifest.getIsPassthroughFacade()).thenReturn(false); + adminService = new PolarisAdminService( diagnostics, @@ -122,8 +131,14 @@ void testGrantPrivilegeOnNamespaceToRole_ThrowsNamespaceNotFoundException() { .thenReturn(resolutionManifest); when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); - PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); @@ -149,8 +164,14 @@ void testGrantPrivilegeOnNamespaceToRole_IncompleteNamespaceThrowsNamespaceNotFo .thenReturn(resolutionManifest); when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG, 1L); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); - PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); @@ -159,7 +180,7 @@ void testGrantPrivilegeOnNamespaceToRole_IncompleteNamespaceThrowsNamespaceNotFo .thenReturn( List.of( createEntity("test-catalog", PolarisEntityType.CATALOG), - createEntity("complete-ns", PolarisEntityType.NAMESPACE))); + createEntity("complete-ns", PolarisEntityType.NAMESPACE, 3L, 1L))); when(resolvedPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) .thenReturn(false); @@ -204,7 +225,8 @@ void testRevokePrivilegeOnNamespaceFromRole_ThrowsNamespaceNotFoundException() { when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); - PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); @@ -231,7 +253,8 @@ void testRevokePrivilegeOnNamespaceFromRole_IncompletelNamespaceThrowsNamespaceN when(resolutionManifest.resolveAll()).thenReturn(createSuccessfulResolverStatus()); PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); - PolarisEntity catalogRoleEntity = createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); @@ -249,13 +272,279 @@ void testRevokePrivilegeOnNamespaceFromRole_IncompletelNamespaceThrowsNamespaceN .hasMessageContaining("Namespace " + namespace + " not found"); } + @Test + void testGrantPrivilegeOnNamespaceToRole_PassthroughFacade() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("org-ns", "team-ns", "project-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + when(resolutionManifest.getIsPassthroughFacade()).thenReturn(true); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + PolarisEntity orgNsEntity = createEntity("org-ns", PolarisEntityType.NAMESPACE, 3L, 1L); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); + when(resolvedPathWrapper.getRawFullPath()).thenReturn(List.of(catalogEntity, orgNsEntity)); + when(resolvedPathWrapper.getRawLeafEntity()).thenReturn(orgNsEntity); + + // Mock creation of team-ns. + GenerateEntityIdResult idResult = mock(GenerateEntityIdResult.class); + when(idResult.getId()).thenReturn(4L); + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); + EntityResult teamNsCreateResult = mock(EntityResult.class); + EntityResult projectNsCreateResult = mock(EntityResult.class); + when(teamNsCreateResult.isSuccess()).thenReturn(true); + when(projectNsCreateResult.isSuccess()).thenReturn(true); + + PolarisEntity teamNsEntity = createEntity("team-ns", PolarisEntityType.NAMESPACE, 4L, 3L); + when(teamNsCreateResult.getEntity()).thenReturn(teamNsEntity); + + // Mock creation of project-ns. + when(idResult.getId()).thenReturn(5L); + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); + PolarisEntity projectNsEntity = createEntity("project-ns", PolarisEntityType.NAMESPACE, 5L, 4L); + when(projectNsCreateResult.getEntity()).thenReturn(projectNsEntity); + + when(metaStoreManager.createEntityIfNotExists(any(), any(), any())) + .thenReturn(teamNsCreateResult, projectNsCreateResult); + + // Mock successful synthetic namespace resolution. + PolarisResolvedPathWrapper syntheticPathWrapper = mock(PolarisResolvedPathWrapper.class); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(syntheticPathWrapper); + when(syntheticPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) + .thenReturn(true); + + 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_SyntheticEntityCreationFails() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("org-ns", "team-ns", "project-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + when(resolutionManifest.getIsPassthroughFacade()).thenReturn(true); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + PolarisEntity orgNsEntity = createEntity("org-ns", PolarisEntityType.NAMESPACE, 3L, 1L); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); + when(resolvedPathWrapper.getRawFullPath()).thenReturn(List.of(catalogEntity, orgNsEntity)); + when(resolvedPathWrapper.getRawLeafEntity()).thenReturn(orgNsEntity); + + // Mock generateNewEntityId for team-ns + GenerateEntityIdResult idResult = mock(GenerateEntityIdResult.class); + when(idResult.getId()).thenReturn(4L); + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); + + // Mock createEntityIfNotExists to fail + EntityResult failedResult = mock(EntityResult.class); + when(failedResult.isSuccess()).thenReturn(false); + when(metaStoreManager.createEntityIfNotExists(any(), any(), any())).thenReturn(failedResult); + + // Mock getResolvedPath to return null for partial namespace + PolarisResolvedPathWrapper partialPathWrapper = mock(PolarisResolvedPathWrapper.class); + when(partialPathWrapper.getRawLeafEntity()).thenReturn(orgNsEntity); + when(resolutionManifest.getResolvedPath(eq(Namespace.of("org-ns", "team-ns")))) + .thenReturn(partialPathWrapper); + + assertThatThrownBy( + () -> + adminService.grantPrivilegeOnNamespaceToRole( + catalogName, catalogRoleName, namespace, privilege)) + .isInstanceOf(RuntimeException.class) + .hasMessage( + "Failed to create or find namespace entity 'team-ns' in federated catalog 'test-catalog'"); + } + + @Test + void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("org-ns", "team-ns", "project-ns"); + TableIdentifier identifier = TableIdentifier.of(namespace, "test-table"); + PolarisPrivilege privilege = PolarisPrivilege.TABLE_WRITE_DATA; + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + when(resolutionManifest.getIsPassthroughFacade()).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); + + PolarisEntity orgNsEntity = createEntity("org-ns", PolarisEntityType.NAMESPACE, 3L, 1L); + PolarisEntity teamNsEntity = createEntity("team-ns", PolarisEntityType.NAMESPACE, 4L, 3L); + + PolarisResolvedPathWrapper existingPathWrapper = mock(PolarisResolvedPathWrapper.class); + when(existingPathWrapper.getRawFullPath()) + .thenReturn(List.of(catalogEntity, orgNsEntity, teamNsEntity)); + when(existingPathWrapper.getRawLeafEntity()).thenReturn(teamNsEntity); + when(resolutionManifest.getResolvedPath( + identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE)) + .thenReturn(existingPathWrapper); + when(existingPathWrapper.getRawLeafEntity()).thenReturn(teamNsEntity); + + GenerateEntityIdResult idResult = mock(GenerateEntityIdResult.class); + when(idResult.getId()).thenReturn(5L); + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); + PolarisEntity projectNsEntity = createEntity("project-ns", PolarisEntityType.NAMESPACE, 5L, 4L); + EntityResult projectNsCreateResult = mock(EntityResult.class); + when(projectNsCreateResult.isSuccess()).thenReturn(true); + when(projectNsCreateResult.getEntity()).thenReturn(projectNsEntity); + when(metaStoreManager.createEntityIfNotExists(any(), any(), any())) + .thenReturn(projectNsCreateResult); + + PolarisResolvedPathWrapper syntheticPathWrapper = mock(PolarisResolvedPathWrapper.class); + when(syntheticPathWrapper.getRawFullPath()) + .thenReturn(List.of(catalogEntity, orgNsEntity, teamNsEntity, projectNsEntity)); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(syntheticPathWrapper); + when(syntheticPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) + .thenReturn(true); + when(syntheticPathWrapper.getRawLeafEntity()).thenReturn(projectNsEntity); + + when(idResult.getId()).thenReturn(6L); + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); + PolarisEntity tableEntity = + createEntity( + "test-table", PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE, 6L, 5L); + EntityResult tableCreateResult = mock(EntityResult.class); + when(tableCreateResult.isSuccess()).thenReturn(true); + when(tableCreateResult.getEntity()).thenReturn(tableEntity); + when(metaStoreManager.createEntityIfNotExists(any(), any(), any())) + .thenReturn(tableCreateResult); + + PolarisResolvedPathWrapper tablePathWrapper = mock(PolarisResolvedPathWrapper.class); + when(tablePathWrapper.getRawLeafEntity()).thenReturn(tableEntity); + when(resolutionManifest.getResolvedPath( + identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE)) + .thenReturn(tablePathWrapper); + + PrivilegeResult successResult = mock(PrivilegeResult.class); + when(successResult.isSuccess()).thenReturn(true); + when(metaStoreManager.grantPrivilegeOnSecurableToRole(any(), any(), any(), any(), any())) + .thenReturn(successResult); + + boolean result = + adminService.grantPrivilegeOnTableToRole( + catalogName, catalogRoleName, identifier, privilege); + assertThat(result).isTrue(); + } + + @Test + void testGrantPrivilegeOnTableLikeToRole_SyntheticEntityCreationFails() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + TableIdentifier identifier = TableIdentifier.of(Namespace.empty(), "test-table"); + PolarisPrivilege privilege = PolarisPrivilege.TABLE_WRITE_DATA; + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + when(resolutionManifest.getIsPassthroughFacade()).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); + + PolarisResolvedPathWrapper existingPathWrapper = mock(PolarisResolvedPathWrapper.class); + when(existingPathWrapper.getRawFullPath()).thenReturn(List.of(catalogEntity)); + when(resolutionManifest.getResolvedPath( + identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE)) + .thenReturn(existingPathWrapper); + when(existingPathWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + + GenerateEntityIdResult idResult = mock(GenerateEntityIdResult.class); + when(idResult.getId()).thenReturn(3L); + when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); + EntityResult tableCreateResult = mock(EntityResult.class); + when(metaStoreManager.createEntityIfNotExists(any(), any(), any())) + .thenReturn(tableCreateResult); + when(tableCreateResult.isSuccess()).thenReturn(false); + + when(resolutionManifest.getResolvedPath(identifier)).thenReturn(existingPathWrapper); + when(existingPathWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + + assertThatThrownBy( + () -> + adminService.grantPrivilegeOnTableToRole( + catalogName, catalogRoleName, identifier, privilege)) + .isInstanceOf(RuntimeException.class) + .hasMessage( + "Failed to create or find table entity 'test-table' in federated catalog 'test-catalog'"); + } + 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 PolarisEntity createEntity(String name, PolarisEntityType type, long id) { + return new PolarisEntity.Builder() + .setName(name) + .setType(type) + .setId(id) + .setCatalogId(1L) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + } + + private PolarisEntity createEntity(String name, PolarisEntityType type, long id, long parentId) { + return new PolarisEntity.Builder() + .setName(name) + .setType(type) + .setId(id) + .setCatalogId(1L) + .setParentId(parentId) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + } + + private PolarisEntity createEntity( + String name, PolarisEntityType type, PolarisEntitySubType subType, long id, long parentId) { + return new PolarisEntity.Builder() + .setName(name) + .setType(type) + .setSubType(subType) + .setId(id) + .setCatalogId(1L) + .setParentId(parentId) .setCreateTimestamp(System.currentTimeMillis()) .build(); } @@ -273,18 +562,24 @@ private void setupSuccessfulNamespaceResolution( when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + PolarisEntity namespaceEntity = - createEntity(namespace.levels()[0], PolarisEntityType.NAMESPACE); + createEntity(namespace.levels()[0], PolarisEntityType.NAMESPACE, 3L, 1L); List 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); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); } } From 99f866c79afdb2aa2c22693eb62254c3ea7bdcc3 Mon Sep 17 00:00:00 2001 From: Pooja Nilangekar Date: Tue, 5 Aug 2025 21:30:15 -0700 Subject: [PATCH 2/9] Address review comments --- .../core/config/FeatureConfiguration.java | 9 ++ .../service/admin/PolarisAdminService.java | 77 +++++++--- .../admin/PolarisAdminServiceTest.java | 141 ++++++++++++++++-- 3 files changed, 195 insertions(+), 32 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index ad91ad0240..545efa6c86 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -271,6 +271,15 @@ public static void enforceFeatureEnabledOrThrow( .defaultValue(false) .buildFeatureConfiguration(); + public static final FeatureConfiguration ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS = + PolarisConfiguration.builder() + .key("ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS") + .description( + "When enabled, allows RBAC operations to create synthetic entities for" + + " entities in federated catalogs that don't exist in the local metastore.") + .defaultValue(false) + .buildFeatureConfiguration(); + public static final FeatureConfiguration ENABLE_POLICY_STORE = PolarisConfiguration.builder() .key("ENABLE_POLICY_STORE") diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 5338f888fb..49ea97dbd9 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -509,7 +509,11 @@ private void authorizeGrantOnTableLikeOperationOrThrow( PolarisResolvedPathWrapper tableLikeWrapper = resolutionManifest.getResolvedPath( identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE, true); - if (!resolutionManifest.getIsPassthroughFacade() + boolean rbacForFederatedCatalogsEnabled = + getCurrentPolarisContext() + .getRealmConfig() + .getConfig(FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS); + if (!(resolutionManifest.getIsPassthroughFacade() && rbacForFederatedCatalogsEnabled) && !subTypes.contains(tableLikeWrapper.getRawLeafEntity().getSubType())) { CatalogHandler.throwNotFoundExceptionForTableLikeEntity(identifier, subTypes); } @@ -1698,7 +1702,11 @@ public PrivilegeResult grantPrivilegeOnNamespaceToRole( PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); if (resolvedPathWrapper == null || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { - if (resolutionManifest.getIsPassthroughFacade()) { + boolean rbacForFederatedCatalogsEnabled = + getCurrentPolarisContext() + .getRealmConfig() + .getConfig(FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS); + if (resolutionManifest.getIsPassthroughFacade() && rbacForFederatedCatalogsEnabled) { resolvedPathWrapper = createSyntheticNamespaceEntities(catalogEntity, namespace, resolvedPathWrapper); if (resolvedPathWrapper == null @@ -1770,26 +1778,25 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( PolarisEntity currentParent = existingPath.getRawLeafEntity(); String[] allNamespaceLevels = namespace.levels(); - int matchingLevel = -1; + int numMatchingLevels = 0; for (PolarisEntity entity : completePath.subList(1, completePath.size())) { - if (entity.getName().equals(allNamespaceLevels[matchingLevel + 1])) { - matchingLevel++; - } else { + if (!entity.getName().equals(allNamespaceLevels[numMatchingLevels])) { break; } + numMatchingLevels++; } - for (int i = matchingLevel + 1; i < allNamespaceLevels.length; i++) { - String namespacePart = allNamespaceLevels[i]; + for (int i = numMatchingLevels; i < allNamespaceLevels.length; i++) { + String[] namespacePart = Arrays.copyOfRange(allNamespaceLevels, 0, i + 1); + String leafNamespace = namespacePart[namespacePart.length - 1]; + Namespace currentNamespace = Namespace.of(namespacePart); // TODO: Instead of creating synthetic entitties, rely on external catalog mediated backfill. PolarisEntity syntheticNamespace = - new PolarisEntity.Builder() + new NamespaceEntity.Builder(currentNamespace) .setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId()) .setCatalogId(catalogEntity.getId()) .setParentId(currentParent.getId()) - .setType(PolarisEntityType.NAMESPACE) - .setName(namespacePart) .setCreateTimestamp(System.currentTimeMillis()) .build(); @@ -1807,12 +1814,12 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( resolutionManifest.getResolvedPath(partialNamespace); PolarisEntity partialLeafEntity = partialPath.getRawLeafEntity(); if (partialLeafEntity == null - || !(partialLeafEntity.getName().equals(namespacePart) + || !(partialLeafEntity.getName().equals(leafNamespace) && partialLeafEntity.getType() == PolarisEntityType.NAMESPACE)) { throw new RuntimeException( String.format( "Failed to create or find namespace entity '%s' in federated catalog '%s'", - namespacePart, catalogEntity.getName())); + leafNamespace, catalogEntity.getName())); } syntheticNamespace = partialLeafEntity; } @@ -2113,7 +2120,11 @@ private PrivilegeResult grantPrivilegeOnTableLikeToRole( identifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE); if (resolvedPathWrapper == null || !subTypes.contains(resolvedPathWrapper.getRawLeafEntity().getSubType())) { - if (resolutionManifest.getIsPassthroughFacade()) { + boolean rbacForFederatedCatalogsEnabled = + getCurrentPolarisContext() + .getRealmConfig() + .getConfig(FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS); + if (resolutionManifest.getIsPassthroughFacade() && rbacForFederatedCatalogsEnabled) { resolvedPathWrapper = createSyntheticTableLikeEntities( catalogEntity, identifier, subTypes, resolvedPathWrapper); @@ -2171,17 +2182,20 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( namespace.toString(), catalogEntity.getName())); } - // TODO: Instead of creating a synthetic table-like entity, rely on external catalog mediated - // backfill. PolarisEntity parentNamespaceEntity = resolvedNamespacePathWrapper.getRawLeafEntity(); + + // TODO: Once we support GENERIC_TABLE federation, select the intended type depending on the + // callsite; if it is instantiated via an Iceberg RESTCatalog factory or a different factory + // for GenericCatalogs. + PolarisEntitySubType syntheticEntitySubType = selectEntitySubType(subTypes); + + // TODO: Instead of creating a synthetic table-like entity, rely on external catalog mediated + // backfill and use the metadata location from the external catalog. PolarisEntity syntheticTableEntity = - new PolarisEntity.Builder() + new IcebergTableLikeEntity.Builder(identifier, "") .setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId()) .setCatalogId(parentNamespaceEntity.getCatalogId()) - .setParentId(parentNamespaceEntity.getId()) - .setType(PolarisEntityType.TABLE_LIKE) - .setSubType(subTypes.get(0)) - .setName(identifier.name()) + .setSubType(syntheticEntitySubType) .setCreateTimestamp(System.currentTimeMillis()) .build(); @@ -2296,4 +2310,25 @@ private PrivilegeResult revokePrivilegeOnPolicyEntityFromRole( policyEntity, privilege); } + + /** + * Selects the appropriate entity subtype for synthetic entities in external catalogs. + * + * @param subTypes list of candidate subtypes + * @return the selected subtype for the synthetic entity + * @throws IllegalStateException if no supported subtype is found + */ + private static PolarisEntitySubType selectEntitySubType(List subTypes) { + if (subTypes.contains(PolarisEntitySubType.ICEBERG_TABLE)) { + return PolarisEntitySubType.ICEBERG_TABLE; + } else if (subTypes.contains(PolarisEntitySubType.ICEBERG_VIEW)) { + return PolarisEntitySubType.ICEBERG_VIEW; + } else { + throw new IllegalStateException( + String.format( + "No supported subtype found in %s. Only ICEBERG_TABLE and ICEBERG_VIEW are" + + " supported for synthetic entities in external catalogs.", + subTypes)); + } + } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java index 7e0a89367c..a4e80c1b8a 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java @@ -28,17 +28,22 @@ import java.util.List; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.NotFoundException; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.config.FeatureConfiguration; +import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.entity.NamespaceEntity; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrivilege; +import org.apache.polaris.core.entity.table.IcebergTableLikeEntity; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -69,6 +74,7 @@ public class PolarisAdminServiceTest { @Mock private PolarisPrincipal authenticatedPrincipal; @Mock private PolarisResolutionManifest resolutionManifest; @Mock private PolarisResolvedPathWrapper resolvedPathWrapper; + @Mock private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -77,6 +83,7 @@ void setUp() throws Exception { MockitoAnnotations.openMocks(this); when(securityContext.getUserPrincipal()).thenReturn(authenticatedPrincipal); when(callContext.getPolarisCallContext()).thenReturn(polarisCallContext); + when(polarisCallContext.getDiagServices()).thenReturn(polarisDiagnostics); when(resolutionManifestFactory.createResolutionManifest(any(), any(), any())) .thenReturn(resolutionManifest); @@ -180,7 +187,7 @@ void testGrantPrivilegeOnNamespaceToRole_IncompleteNamespaceThrowsNamespaceNotFo .thenReturn( List.of( createEntity("test-catalog", PolarisEntityType.CATALOG), - createEntity("complete-ns", PolarisEntityType.NAMESPACE, 3L, 1L))); + createNamespaceEntity(Namespace.of("complete-ns"), 3L, 1L))); when(resolvedPathWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) .thenReturn(false); @@ -291,7 +298,7 @@ void testGrantPrivilegeOnNamespaceToRole_PassthroughFacade() throws Exception { when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); - PolarisEntity orgNsEntity = createEntity("org-ns", PolarisEntityType.NAMESPACE, 3L, 1L); + PolarisEntity orgNsEntity = createNamespaceEntity(Namespace.of("org-ns"), 3L, 1L); when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); when(resolvedPathWrapper.getRawFullPath()).thenReturn(List.of(catalogEntity, orgNsEntity)); when(resolvedPathWrapper.getRawLeafEntity()).thenReturn(orgNsEntity); @@ -305,13 +312,14 @@ void testGrantPrivilegeOnNamespaceToRole_PassthroughFacade() throws Exception { when(teamNsCreateResult.isSuccess()).thenReturn(true); when(projectNsCreateResult.isSuccess()).thenReturn(true); - PolarisEntity teamNsEntity = createEntity("team-ns", PolarisEntityType.NAMESPACE, 4L, 3L); + PolarisEntity teamNsEntity = createNamespaceEntity(Namespace.of("org-ns", "team-ns"), 4L, 3L); when(teamNsCreateResult.getEntity()).thenReturn(teamNsEntity); // Mock creation of project-ns. when(idResult.getId()).thenReturn(5L); when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); - PolarisEntity projectNsEntity = createEntity("project-ns", PolarisEntityType.NAMESPACE, 5L, 4L); + PolarisEntity projectNsEntity = + createNamespaceEntity(Namespace.of("org-ns", "team-ns", "project-ns"), 5L, 4L); when(projectNsCreateResult.getEntity()).thenReturn(projectNsEntity); when(metaStoreManager.createEntityIfNotExists(any(), any(), any())) @@ -334,6 +342,44 @@ void testGrantPrivilegeOnNamespaceToRole_PassthroughFacade() throws Exception { assertThat(result).isTrue(); } + @Test + void testGrantPrivilegeOnNamespaceToRole_PassthroughFacade_FeatureDisabled() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("org-ns", "team-ns", "project-ns"); + PolarisPrivilege privilege = PolarisPrivilege.NAMESPACE_FULL_METADATA; + + // Disable the feature configuration + when(realmConfig.getConfig(FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS)) + .thenReturn(false); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + when(resolutionManifest.getIsPassthroughFacade()).thenReturn(true); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + // Create a mock resolved path that returns null initially and is not fully resolved + PolarisResolvedPathWrapper unresolvedWrapper = mock(PolarisResolvedPathWrapper.class); + when(unresolvedWrapper.isFullyResolvedNamespace(eq(catalogName), eq(namespace))) + .thenReturn(false); + when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(unresolvedWrapper); + + // Should throw NotFoundException because feature is disabled and it's passthrough facade + assertThatThrownBy( + () -> + adminService.grantPrivilegeOnNamespaceToRole( + catalogName, catalogRoleName, namespace, privilege)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Namespace " + namespace + " not found"); + } + @Test void testGrantPrivilegeOnNamespaceToRole_SyntheticEntityCreationFails() throws Exception { String catalogName = "test-catalog"; @@ -353,7 +399,7 @@ void testGrantPrivilegeOnNamespaceToRole_SyntheticEntityCreationFails() throws E when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); - PolarisEntity orgNsEntity = createEntity("org-ns", PolarisEntityType.NAMESPACE, 3L, 1L); + PolarisEntity orgNsEntity = createNamespaceEntity(Namespace.of("org-ns"), 3L, 1L); when(resolutionManifest.getResolvedPath(eq(namespace))).thenReturn(resolvedPathWrapper); when(resolvedPathWrapper.getRawFullPath()).thenReturn(List.of(catalogEntity, orgNsEntity)); when(resolvedPathWrapper.getRawLeafEntity()).thenReturn(orgNsEntity); @@ -402,8 +448,8 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); - PolarisEntity orgNsEntity = createEntity("org-ns", PolarisEntityType.NAMESPACE, 3L, 1L); - PolarisEntity teamNsEntity = createEntity("team-ns", PolarisEntityType.NAMESPACE, 4L, 3L); + PolarisEntity orgNsEntity = createNamespaceEntity(Namespace.of("org-ns"), 3L, 1L); + PolarisEntity teamNsEntity = createNamespaceEntity(Namespace.of("org-ns", "team-ns"), 4L, 3L); PolarisResolvedPathWrapper existingPathWrapper = mock(PolarisResolvedPathWrapper.class); when(existingPathWrapper.getRawFullPath()) @@ -417,7 +463,8 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { GenerateEntityIdResult idResult = mock(GenerateEntityIdResult.class); when(idResult.getId()).thenReturn(5L); when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); - PolarisEntity projectNsEntity = createEntity("project-ns", PolarisEntityType.NAMESPACE, 5L, 4L); + PolarisEntity projectNsEntity = + createNamespaceEntity(Namespace.of("org-ns", "team-ns", "project-ns"), 5L, 4L); EntityResult projectNsCreateResult = mock(EntityResult.class); when(projectNsCreateResult.isSuccess()).thenReturn(true); when(projectNsCreateResult.getEntity()).thenReturn(projectNsEntity); @@ -435,8 +482,7 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { when(idResult.getId()).thenReturn(6L); when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); PolarisEntity tableEntity = - createEntity( - "test-table", PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE, 6L, 5L); + createTableEntity(identifier, PolarisEntitySubType.ICEBERG_TABLE, 6L, 5L); EntityResult tableCreateResult = mock(EntityResult.class); when(tableCreateResult.isSuccess()).thenReturn(true); when(tableCreateResult.getEntity()).thenReturn(tableEntity); @@ -460,6 +506,59 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { assertThat(result).isTrue(); } + @Test + void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade_FeatureDisabled() throws Exception { + String catalogName = "test-catalog"; + String catalogRoleName = "test-role"; + Namespace namespace = Namespace.of("org-ns", "team-ns", "project-ns"); + TableIdentifier identifier = TableIdentifier.of(namespace, "test-table"); + PolarisPrivilege privilege = PolarisPrivilege.TABLE_WRITE_DATA; + + // Disable the feature configuration + when(realmConfig.getConfig(FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS)) + .thenReturn(false); + + PolarisEntity catalogEntity = createEntity(catalogName, PolarisEntityType.CATALOG); + PolarisResolvedPathWrapper catalogWrapper = mock(PolarisResolvedPathWrapper.class); + when(catalogWrapper.getRawLeafEntity()).thenReturn(catalogEntity); + when(resolutionManifest.getResolvedReferenceCatalogEntity()).thenReturn(catalogWrapper); + when(resolutionManifest.getIsPassthroughFacade()).thenReturn(true); + + PolarisResolvedPathWrapper catalogRoleWrapper = mock(PolarisResolvedPathWrapper.class); + PolarisEntity catalogRoleEntity = + createEntity(catalogRoleName, PolarisEntityType.CATALOG_ROLE, 2L); + when(catalogRoleWrapper.getRawLeafEntity()).thenReturn(catalogRoleEntity); + when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); + + // Create a table entity for authorization but later it should not be found + PolarisEntity tableEntity = + createEntity( + "test-table", PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE, 5L, 4L); + PolarisResolvedPathWrapper tableWrapper = mock(PolarisResolvedPathWrapper.class); + when(tableWrapper.getRawLeafEntity()).thenReturn(tableEntity); + + // Mock authorization path with table + when(resolutionManifest.getResolvedPath( + eq(identifier), + eq(PolarisEntityType.TABLE_LIKE), + eq(PolarisEntitySubType.ANY_SUBTYPE), + eq(true))) + .thenReturn(tableWrapper); + + // Mock the main resolution to return null (table not found in main logic) + when(resolutionManifest.getResolvedPath( + eq(identifier), eq(PolarisEntityType.TABLE_LIKE), eq(PolarisEntitySubType.ANY_SUBTYPE))) + .thenReturn(null); + + // Should throw NoSuchTableException because feature is disabled + assertThatThrownBy( + () -> + adminService.grantPrivilegeOnTableToRole( + catalogName, catalogRoleName, identifier, privilege)) + .isInstanceOf(NoSuchTableException.class) + .hasMessageContaining("Table does not exist"); + } + @Test void testGrantPrivilegeOnTableLikeToRole_SyntheticEntityCreationFails() throws Exception { String catalogName = "test-catalog"; @@ -549,6 +648,26 @@ private PolarisEntity createEntity( .build(); } + private PolarisEntity createNamespaceEntity(Namespace namespace, long id, long parentId) { + return new NamespaceEntity.Builder(namespace) + .setId(id) + .setCatalogId(1L) + .setParentId(parentId) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + } + + private PolarisEntity createTableEntity( + TableIdentifier identifier, PolarisEntitySubType subType, long id, long parentId) { + return new IcebergTableLikeEntity.Builder(identifier, "") + .setId(id) + .setCatalogId(1L) + .setParentId(parentId) + .setSubType(subType) + .setCreateTimestamp(System.currentTimeMillis()) + .build(); + } + private ResolverStatus createSuccessfulResolverStatus() { return new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS); } @@ -573,7 +692,7 @@ private void setupSuccessfulNamespaceResolution( when(resolutionManifest.getResolvedPath(eq(catalogRoleName))).thenReturn(catalogRoleWrapper); PolarisEntity namespaceEntity = - createEntity(namespace.levels()[0], PolarisEntityType.NAMESPACE, 3L, 1L); + createNamespaceEntity(Namespace.of(namespace.levels()[0]), 3L, 1L); List fullPath = List.of(catalogEntity, namespaceEntity); when(resolvedPathWrapper.getRawFullPath()).thenReturn(fullPath); when(resolvedPathWrapper.getRawParentPath()).thenReturn(List.of(catalogEntity)); From 2c441ed327301402372ccd33fa243a6d22788a75 Mon Sep 17 00:00:00 2001 From: Pooja Nilangekar Date: Tue, 5 Aug 2025 21:54:02 -0700 Subject: [PATCH 3/9] Add a comment about the skipped CatalogEntity --- .../org/apache/polaris/service/admin/PolarisAdminService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 49ea97dbd9..0dd35d6033 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -1779,6 +1779,8 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( String[] allNamespaceLevels = namespace.levels(); int numMatchingLevels = 0; + // Find parts of the complete path that match the namespace levels. + // We skip index 0 because it is the CatalogEntity. for (PolarisEntity entity : completePath.subList(1, completePath.size())) { if (!entity.getName().equals(allNamespaceLevels[numMatchingLevels])) { break; From e07dd7d437c563e0b54c5600302ee317005703d8 Mon Sep 17 00:00:00 2001 From: Honah J Date: Mon, 22 Sep 2025 12:38:58 -0500 Subject: [PATCH 4/9] Fix merge conflict --- .../service/admin/PolarisAdminService.java | 4 +- .../admin/PolarisAdminServiceTest.java | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 0dd35d6033..9e71b116e3 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -1832,7 +1832,6 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( return resolvedPathWrapper; } - public boolean grantPrivilegeOnTableToRole( public PrivilegeResult grantPrivilegeOnTableToRole( String catalogName, String catalogRoleName, @@ -2194,10 +2193,9 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( // TODO: Instead of creating a synthetic table-like entity, rely on external catalog mediated // backfill and use the metadata location from the external catalog. PolarisEntity syntheticTableEntity = - new IcebergTableLikeEntity.Builder(identifier, "") + new IcebergTableLikeEntity.Builder(syntheticEntitySubType, identifier, "") .setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId()) .setCatalogId(parentNamespaceEntity.getCatalogId()) - .setSubType(syntheticEntitySubType) .setCreateTimestamp(System.currentTimeMillis()) .build(); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java index a4e80c1b8a..d44ec0cbff 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java @@ -18,6 +18,8 @@ */ package org.apache.polaris.service.admin; +import static org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_TABLE; +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; @@ -34,9 +36,9 @@ import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; -import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.NamespaceEntity; import org.apache.polaris.core.entity.PolarisEntity; @@ -83,7 +85,11 @@ void setUp() throws Exception { MockitoAnnotations.openMocks(this); when(securityContext.getUserPrincipal()).thenReturn(authenticatedPrincipal); when(callContext.getPolarisCallContext()).thenReturn(polarisCallContext); - when(polarisCallContext.getDiagServices()).thenReturn(polarisDiagnostics); + when(polarisCallContext.getRealmConfig()).thenReturn(realmConfig); + + // Default feature configuration - enabled by default + when(realmConfig.getConfig(FeatureConfiguration.ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS)) + .thenReturn(true); when(resolutionManifestFactory.createResolutionManifest(any(), any(), any())) .thenReturn(resolutionManifest); @@ -336,10 +342,10 @@ void testGrantPrivilegeOnNamespaceToRole_PassthroughFacade() throws Exception { when(metaStoreManager.grantPrivilegeOnSecurableToRole(any(), any(), any(), any(), any())) .thenReturn(successResult); - boolean result = + PrivilegeResult result = adminService.grantPrivilegeOnNamespaceToRole( catalogName, catalogRoleName, namespace, privilege); - assertThat(result).isTrue(); + assertThat(result.isSuccess()).isTrue(); } @Test @@ -481,8 +487,7 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { when(idResult.getId()).thenReturn(6L); when(metaStoreManager.generateNewEntityId(any())).thenReturn(idResult); - PolarisEntity tableEntity = - createTableEntity(identifier, PolarisEntitySubType.ICEBERG_TABLE, 6L, 5L); + PolarisEntity tableEntity = createTableEntity(identifier, ICEBERG_TABLE, 6L, 5L); EntityResult tableCreateResult = mock(EntityResult.class); when(tableCreateResult.isSuccess()).thenReturn(true); when(tableCreateResult.getEntity()).thenReturn(tableEntity); @@ -500,10 +505,10 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade() throws Exception { when(metaStoreManager.grantPrivilegeOnSecurableToRole(any(), any(), any(), any(), any())) .thenReturn(successResult); - boolean result = + PrivilegeResult result = adminService.grantPrivilegeOnTableToRole( catalogName, catalogRoleName, identifier, privilege); - assertThat(result).isTrue(); + assertThat(result.isSuccess()).isTrue(); } @Test @@ -532,8 +537,7 @@ void testGrantPrivilegeOnTableLikeToRole_PassthroughFacade_FeatureDisabled() thr // Create a table entity for authorization but later it should not be found PolarisEntity tableEntity = - createEntity( - "test-table", PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE, 5L, 4L); + createEntity("test-table", PolarisEntityType.TABLE_LIKE, ICEBERG_TABLE, 5L, 4L); PolarisResolvedPathWrapper tableWrapper = mock(PolarisResolvedPathWrapper.class); when(tableWrapper.getRawLeafEntity()).thenReturn(tableEntity); @@ -624,16 +628,17 @@ private PolarisEntity createEntity(String name, PolarisEntityType type, long id) .build(); } - private PolarisEntity createEntity(String name, PolarisEntityType type, long id, long parentId) { - return new PolarisEntity.Builder() - .setName(name) - .setType(type) - .setId(id) - .setCatalogId(1L) - .setParentId(parentId) - .setCreateTimestamp(System.currentTimeMillis()) - .build(); - } + // private PolarisEntity createEntity(String name, PolarisEntityType type, long id, long + // parentId) { + // return new PolarisEntity.Builder() + // .setName(name) + // .setType(type) + // .setId(id) + // .setCatalogId(1L) + // .setParentId(parentId) + // .setCreateTimestamp(System.currentTimeMillis()) + // .build(); + // } private PolarisEntity createEntity( String name, PolarisEntityType type, PolarisEntitySubType subType, long id, long parentId) { @@ -659,11 +664,10 @@ private PolarisEntity createNamespaceEntity(Namespace namespace, long id, long p private PolarisEntity createTableEntity( TableIdentifier identifier, PolarisEntitySubType subType, long id, long parentId) { - return new IcebergTableLikeEntity.Builder(identifier, "") + return new IcebergTableLikeEntity.Builder(subType, identifier, "") .setId(id) .setCatalogId(1L) .setParentId(parentId) - .setSubType(subType) .setCreateTimestamp(System.currentTimeMillis()) .build(); } From 4f52cddf294ff0352ebf37cc712e0d71d9846a6d Mon Sep 17 00:00:00 2001 From: Honah J Date: Tue, 23 Sep 2025 13:59:26 -0500 Subject: [PATCH 5/9] Add some test and fix issue --- .../service/admin/PolarisAdminService.java | 17 +++-- .../admin/PolarisAdminServiceAuthzTest.java | 35 +++++----- .../service/admin/PolarisAuthzTestBase.java | 67 +++++++++++++++++++ 3 files changed, 98 insertions(+), 21 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 9e71b116e3..632c33bd6b 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -446,7 +446,7 @@ private void authorizeGrantOnNamespaceOperationOrThrow( resolutionManifest = resolutionManifestFactory.createResolutionManifest( callContext, securityContext, catalogName); - resolutionManifest.addPath( + resolutionManifest.addPassthroughPath( new ResolverPath(Arrays.asList(namespace.levels()), PolarisEntityType.NAMESPACE), namespace); resolutionManifest.addPath( @@ -487,7 +487,11 @@ private void authorizeGrantOnTableLikeOperationOrThrow( resolutionManifest = resolutionManifestFactory.createResolutionManifest( callContext, securityContext, catalogName); - resolutionManifest.addPath( + resolutionManifest.addPassthroughPath( + new ResolverPath( + Arrays.asList(identifier.namespace().levels()), PolarisEntityType.NAMESPACE), + identifier.namespace()); + resolutionManifest.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE), identifier); @@ -1813,7 +1817,7 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( } else { Namespace partialNamespace = Namespace.of(Arrays.copyOf(allNamespaceLevels, i + 1)); PolarisResolvedPathWrapper partialPath = - resolutionManifest.getResolvedPath(partialNamespace); + resolutionManifest.getPassthroughResolvedPath(partialNamespace); PolarisEntity partialLeafEntity = partialPath.getRawLeafEntity(); if (partialLeafEntity == null || !(partialLeafEntity.getName().equals(leafNamespace) @@ -1828,7 +1832,7 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( completePath.add(syntheticNamespace); currentParent = syntheticNamespace; } - PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getPassthroughResolvedPath(namespace); return resolvedPathWrapper; } @@ -2194,6 +2198,7 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( // backfill and use the metadata location from the external catalog. PolarisEntity syntheticTableEntity = new IcebergTableLikeEntity.Builder(syntheticEntitySubType, identifier, "") + .setParentId(parentNamespaceEntity.getId()) .setId(metaStoreManager.generateNewEntityId(getCurrentPolarisContext()).getId()) .setCatalogId(parentNamespaceEntity.getCatalogId()) .setCreateTimestamp(System.currentTimeMillis()) @@ -2208,7 +2213,7 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( if (result.isSuccess()) { syntheticTableEntity = PolarisEntity.of(result.getEntity()); } else { - PolarisResolvedPathWrapper tablePathWrapper = resolutionManifest.getResolvedPath(identifier); + PolarisResolvedPathWrapper tablePathWrapper = resolutionManifest.getPassthroughResolvedPath(identifier); PolarisEntity leafEntity = tablePathWrapper != null ? tablePathWrapper.getRawLeafEntity() : null; if (leafEntity == null || !subTypes.contains(leafEntity.getSubType())) { @@ -2218,7 +2223,7 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( identifier.name(), catalogEntity.getName())); } } - return resolutionManifest.getResolvedPath(identifier); + return resolutionManifest.getPassthroughResolvedPath(identifier); } /** diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java index ad5fa0ce6a..bbb27ef003 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -37,6 +37,8 @@ import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; @QuarkusTest @TestProfile(PolarisAuthzTestBase.Profile.class) @@ -1498,8 +1500,9 @@ public void testRevokePrivilegeOnCatalogFromRoleInsufficientPrivileges() { adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); } - @Test - public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges() { + @ParameterizedTest(name = "{displayName}({0})") + @ValueSource(strings = {CATALOG_NAME, FEDERATED_CATALOG_NAME}) + public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges(String catalogName) { doTestSufficientPrivileges( List.of( PolarisPrivilege.CATALOG_MANAGE_ACCESS, @@ -1507,16 +1510,17 @@ public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges() { () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)) .grantPrivilegeOnNamespaceToRole( - CATALOG_NAME, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), + catalogName, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), null, // cleanupAction (privilege) -> - adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), (privilege) -> - adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); } - @Test - public void testGrantPrivilegeOnNamespaceToRoleInsufficientPrivileges() { + @ParameterizedTest(name = "{displayName}({0})") + @ValueSource(strings = {CATALOG_NAME, FEDERATED_CATALOG_NAME}) + public void testGrantPrivilegeOnNamespaceToRoleInsufficientPrivileges(String catalogName) { doTestInsufficientPrivileges( List.of( PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE, @@ -1543,11 +1547,11 @@ public void testGrantPrivilegeOnNamespaceToRoleInsufficientPrivileges() { () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)) .grantPrivilegeOnNamespaceToRole( - CATALOG_NAME, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), + catalogName, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), (privilege) -> - adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), (privilege) -> - adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); } @Test @@ -1606,8 +1610,9 @@ public void testRevokePrivilegeOnNamespaceFromRoleInsufficientPrivileges() { adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); } - @Test - public void testGrantPrivilegeOnTableToRoleSufficientPrivileges() { + @ParameterizedTest(name = "{displayName}({0})") + @ValueSource(strings = {CATALOG_NAME, FEDERATED_CATALOG_NAME}) + public void testGrantPrivilegeOnTableToRoleSufficientPrivileges(String catalogName) { doTestSufficientPrivileges( List.of( PolarisPrivilege.CATALOG_MANAGE_ACCESS, @@ -1615,15 +1620,15 @@ public void testGrantPrivilegeOnTableToRoleSufficientPrivileges() { () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)) .grantPrivilegeOnTableToRole( - CATALOG_NAME, + catalogName, CATALOG_ROLE2, TABLE_NS1_1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), null, // cleanupAction (privilege) -> - adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), (privilege) -> - adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); } @Test diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index 1140e3bf66..0f0ceb3dbc 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -19,6 +19,7 @@ package org.apache.polaris.service.admin; import static org.apache.iceberg.types.Types.NestedField.required; +import static org.apache.polaris.core.entity.CatalogEntity.DEFAULT_BASE_LOCATION_KEY; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; @@ -47,8 +48,13 @@ import org.apache.iceberg.types.Types; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.CreateCatalogRequest; +import org.apache.polaris.core.admin.model.ExternalCatalog; import org.apache.polaris.core.admin.model.FileStorageConfigInfo; +import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo; +import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters; import org.apache.polaris.core.admin.model.PrincipalWithCredentials; import org.apache.polaris.core.admin.model.PrincipalWithCredentialsCredentials; import org.apache.polaris.core.admin.model.StorageConfigInfo; @@ -121,11 +127,14 @@ public Map getConfigOverrides() { "true") .put("polaris.features.\"DROP_WITH_PURGE_ENABLED\"", "true") .put("polaris.behavior-changes.\"ALLOW_NAMESPACE_CUSTOM_LOCATION\"", "true") + .put("polaris.features.\"ENABLE_CATALOG_FEDERATION\"", "true") + .put("polaris.features.\"ENABLE_SUB_CATALOG_RBAC_FOR_FEDERATED_CATALOGS\"", "true") .build(); } } protected static final String CATALOG_NAME = "polaris-catalog"; + protected static final String FEDERATED_CATALOG_NAME = "federated-polaris-catalog"; protected static final String PRINCIPAL_NAME = "snowman"; // catalog_role1 will be assigned only to principal_role1 and @@ -202,6 +211,7 @@ public Map getConfigOverrides() { protected PolarisMetaStoreManager metaStoreManager; protected UserSecretsManager userSecretsManager; protected PolarisBaseEntity catalogEntity; + protected PolarisBaseEntity federatedCatalogEntity; protected PrincipalEntity principalEntity; protected CallContext callContext; protected RealmConfig realmConfig; @@ -259,11 +269,57 @@ public void before(TestInfo testInfo) { reservedProperties); String storageLocation = "file:///tmp/authz"; + String storageLocationForFederatedCatalog = "file:///tmp/authz_federated"; FileStorageConfigInfo storageConfigModel = FileStorageConfigInfo.builder() .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) .setAllowedLocations(List.of(storageLocation, "file:///tmp/authz")) .build(); + FileStorageConfigInfo storageConfigModelForFederatedCatalog = + FileStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations( + List.of(storageLocationForFederatedCatalog, "file:///tmp/authz_federated")) + .build(); + ConnectionConfigInfo connectionConfigInfo = + IcebergRestConnectionConfigInfo.builder( + ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setUri("https://myorg-my_account.snowflakecomputing.com/polaris/api/catalog") + .setRemoteCatalogName("my-remote-catalog") + .setAuthenticationParameters( + OAuthClientCredentialsParameters.builder( + AuthenticationParameters.AuthenticationTypeEnum.OAUTH) + .setClientId("my-client-id") + .setClientSecret("my-client-secret") + .setScopes(List.of("PRINCIPAL_ROLE:ALL")) + .build()) + .build(); + CatalogEntity externalCatalogEntity = + new CatalogEntity.Builder() + .setName(FEDERATED_CATALOG_NAME) + .setCatalogType("EXTERNAL") + .setDefaultBaseLocation(storageLocationForFederatedCatalog) + .setStorageConfigurationInfo( + realmConfig, + storageConfigModelForFederatedCatalog, + storageLocationForFederatedCatalog) + .build(); + ExternalCatalog externalCatalog = + ExternalCatalog.builder() + .setName(externalCatalogEntity.getName()) + .setType(ExternalCatalog.TypeEnum.EXTERNAL) + .setProperties( + org.apache.polaris.core.admin.model.CatalogProperties.builder( + externalCatalogEntity.getPropertiesAsMap().get(DEFAULT_BASE_LOCATION_KEY)) + .putAll(externalCatalogEntity.getPropertiesAsMap()) + .build()) + .setCreateTimestamp(externalCatalogEntity.getCreateTimestamp()) + .setLastUpdateTimestamp(externalCatalogEntity.getLastUpdateTimestamp()) + .setEntityVersion(externalCatalogEntity.getEntityVersion()) + .setStorageConfigInfo(storageConfigModelForFederatedCatalog) + .setConnectionConfigInfo(connectionConfigInfo) + .build(); + catalogEntity = adminService.createCatalog( new CreateCatalogRequest( @@ -274,6 +330,7 @@ public void before(TestInfo testInfo) { .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() .asCatalog())); + federatedCatalogEntity = adminService.createCatalog(new CreateCatalogRequest(externalCatalog)); initBaseCatalog(); @@ -296,6 +353,10 @@ public void before(TestInfo testInfo) { CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE2).build()); adminService.createCatalogRole( CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE_SHARED).build()); + adminService.createCatalogRole( + FEDERATED_CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE1).build()); + adminService.createCatalogRole( + FEDERATED_CATALOG_NAME, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE2).build()); assertSuccess(adminService.assignPrincipalRole(PRINCIPAL_NAME, PRINCIPAL_ROLE1)); assertSuccess(adminService.assignPrincipalRole(PRINCIPAL_NAME, PRINCIPAL_ROLE2)); @@ -312,6 +373,12 @@ public void before(TestInfo testInfo) { assertSuccess( adminService.assignCatalogRoleToPrincipalRole( PRINCIPAL_ROLE2, CATALOG_NAME, CATALOG_ROLE_SHARED)); + assertSuccess( + adminService.assignCatalogRoleToPrincipalRole( + PRINCIPAL_ROLE1, FEDERATED_CATALOG_NAME, CATALOG_ROLE1)); + assertSuccess( + adminService.assignCatalogRoleToPrincipalRole( + PRINCIPAL_ROLE2, FEDERATED_CATALOG_NAME, CATALOG_ROLE2)); // Do some shared setup with non-authz-aware baseCatalog. baseCatalog.createNamespace(NS1); From 97b0eb552f426a43dee8c940a1b2da5efeb3e036 Mon Sep 17 00:00:00 2001 From: Honah J Date: Wed, 24 Sep 2025 11:36:08 -0500 Subject: [PATCH 6/9] Finish adding authz test to be more real --- .../service/admin/PolarisAdminService.java | 5 ++--- .../admin/PolarisAdminServiceAuthzTest.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 632c33bd6b..b076b80c2c 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -1815,10 +1815,9 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( if (result.isSuccess()) { syntheticNamespace = PolarisEntity.of(result.getEntity()); } else { - Namespace partialNamespace = Namespace.of(Arrays.copyOf(allNamespaceLevels, i + 1)); PolarisResolvedPathWrapper partialPath = - resolutionManifest.getPassthroughResolvedPath(partialNamespace); - PolarisEntity partialLeafEntity = partialPath.getRawLeafEntity(); + resolutionManifest.getPassthroughResolvedPath(namespace); + PolarisEntity partialLeafEntity = partialPath != null ? partialPath.getRawLeafEntity() : null; if (partialLeafEntity == null || !(partialLeafEntity.getName().equals(leafNamespace) && partialLeafEntity.getType() == PolarisEntityType.NAMESPACE)) { diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java index bbb27ef003..6130fddbb8 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -1518,6 +1518,23 @@ public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges(String catal adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); } + @Test + public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges_FederationNestedNamespace() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .grantPrivilegeOnNamespaceToRole( + FEDERATED_CATALOG_NAME, CATALOG_ROLE2, NS1AA, PolarisPrivilege.CATALOG_MANAGE_ACCESS), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(FEDERATED_CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(FEDERATED_CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + @ParameterizedTest(name = "{displayName}({0})") @ValueSource(strings = {CATALOG_NAME, FEDERATED_CATALOG_NAME}) public void testGrantPrivilegeOnNamespaceToRoleInsufficientPrivileges(String catalogName) { From 63c61dd21fa8657fbd45e1333f4b7ee7678e0b5b Mon Sep 17 00:00:00 2001 From: Honah J Date: Wed, 24 Sep 2025 17:53:00 -0500 Subject: [PATCH 7/9] Fix style issue --- .../service/admin/PolarisAdminService.java | 15 ++++---- .../admin/PolarisAdminServiceAuthzTest.java | 35 +++++++++++-------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index b076b80c2c..9d1a68fab8 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -488,9 +488,9 @@ private void authorizeGrantOnTableLikeOperationOrThrow( resolutionManifestFactory.createResolutionManifest( callContext, securityContext, catalogName); resolutionManifest.addPassthroughPath( - new ResolverPath( - Arrays.asList(identifier.namespace().levels()), PolarisEntityType.NAMESPACE), - identifier.namespace()); + new ResolverPath( + Arrays.asList(identifier.namespace().levels()), PolarisEntityType.NAMESPACE), + identifier.namespace()); resolutionManifest.addPassthroughPath( new ResolverPath( PolarisCatalogHelpers.tableIdentifierToList(identifier), PolarisEntityType.TABLE_LIKE), @@ -1817,7 +1817,8 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( } else { PolarisResolvedPathWrapper partialPath = resolutionManifest.getPassthroughResolvedPath(namespace); - PolarisEntity partialLeafEntity = partialPath != null ? partialPath.getRawLeafEntity() : null; + PolarisEntity partialLeafEntity = + partialPath != null ? partialPath.getRawLeafEntity() : null; if (partialLeafEntity == null || !(partialLeafEntity.getName().equals(leafNamespace) && partialLeafEntity.getType() == PolarisEntityType.NAMESPACE)) { @@ -1831,7 +1832,8 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( completePath.add(syntheticNamespace); currentParent = syntheticNamespace; } - PolarisResolvedPathWrapper resolvedPathWrapper = resolutionManifest.getPassthroughResolvedPath(namespace); + PolarisResolvedPathWrapper resolvedPathWrapper = + resolutionManifest.getPassthroughResolvedPath(namespace); return resolvedPathWrapper; } @@ -2212,7 +2214,8 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( if (result.isSuccess()) { syntheticTableEntity = PolarisEntity.of(result.getEntity()); } else { - PolarisResolvedPathWrapper tablePathWrapper = resolutionManifest.getPassthroughResolvedPath(identifier); + PolarisResolvedPathWrapper tablePathWrapper = + resolutionManifest.getPassthroughResolvedPath(identifier); PolarisEntity leafEntity = tablePathWrapper != null ? tablePathWrapper.getRawLeafEntity() : null; if (leafEntity == null || !subTypes.contains(leafEntity.getSubType())) { diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java index 6130fddbb8..b60bcb0a92 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -1510,7 +1510,7 @@ public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges(String catal () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)) .grantPrivilegeOnNamespaceToRole( - catalogName, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), + catalogName, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), null, // cleanupAction (privilege) -> adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), @@ -1521,18 +1521,23 @@ public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges(String catal @Test public void testGrantPrivilegeOnNamespaceToRoleSufficientPrivileges_FederationNestedNamespace() { doTestSufficientPrivileges( - List.of( - PolarisPrivilege.CATALOG_MANAGE_ACCESS, - PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE), - () -> - newTestAdminService(Set.of(PRINCIPAL_ROLE1)) - .grantPrivilegeOnNamespaceToRole( - FEDERATED_CATALOG_NAME, CATALOG_ROLE2, NS1AA, PolarisPrivilege.CATALOG_MANAGE_ACCESS), - null, // cleanupAction - (privilege) -> - adminService.grantPrivilegeOnCatalogToRole(FEDERATED_CATALOG_NAME, CATALOG_ROLE1, privilege), - (privilege) -> - adminService.revokePrivilegeOnCatalogFromRole(FEDERATED_CATALOG_NAME, CATALOG_ROLE1, privilege)); + List.of( + PolarisPrivilege.CATALOG_MANAGE_ACCESS, + PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE), + () -> + newTestAdminService(Set.of(PRINCIPAL_ROLE1)) + .grantPrivilegeOnNamespaceToRole( + FEDERATED_CATALOG_NAME, + CATALOG_ROLE2, + NS1AA, + PolarisPrivilege.CATALOG_MANAGE_ACCESS), + null, // cleanupAction + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole( + FEDERATED_CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole( + FEDERATED_CATALOG_NAME, CATALOG_ROLE1, privilege)); } @ParameterizedTest(name = "{displayName}({0})") @@ -1564,7 +1569,7 @@ public void testGrantPrivilegeOnNamespaceToRoleInsufficientPrivileges(String cat () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)) .grantPrivilegeOnNamespaceToRole( - catalogName, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), + catalogName, CATALOG_ROLE2, NS1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), (privilege) -> adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), (privilege) -> @@ -1637,7 +1642,7 @@ public void testGrantPrivilegeOnTableToRoleSufficientPrivileges(String catalogNa () -> newTestAdminService(Set.of(PRINCIPAL_ROLE1)) .grantPrivilegeOnTableToRole( - catalogName, + catalogName, CATALOG_ROLE2, TABLE_NS1_1, PolarisPrivilege.CATALOG_MANAGE_ACCESS), From c09711febe506ab7d6d124e69e41004d9f6b04a8 Mon Sep 17 00:00:00 2001 From: Honah J Date: Wed, 24 Sep 2025 18:23:53 -0500 Subject: [PATCH 8/9] Add TODO for exception --- .../org/apache/polaris/service/admin/PolarisAdminService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 9d1a68fab8..9dbafbab45 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -1715,10 +1715,11 @@ public PrivilegeResult grantPrivilegeOnNamespaceToRole( createSyntheticNamespaceEntities(catalogEntity, namespace, resolvedPathWrapper); if (resolvedPathWrapper == null || !resolvedPathWrapper.isFullyResolvedNamespace(catalogName, namespace)) { + // TODO: update the exception thrown as we refine the possible retry scenarios throw new RuntimeException( String.format( "Failed to create synthetic namespace entities for namespace %s in catalog %s", - namespace.toString(), catalogName)); + namespace, catalogName)); } } else { throw new NotFoundException("Namespace %s not found", namespace); @@ -2136,6 +2137,7 @@ private PrivilegeResult grantPrivilegeOnTableLikeToRole( catalogEntity, identifier, subTypes, resolvedPathWrapper); if (resolvedPathWrapper == null || !subTypes.contains(resolvedPathWrapper.getRawLeafEntity().getSubType())) { + // TODO: update the exception thrown as we refine the possible retry scenarios throw new RuntimeException( String.format( "Failed to create synthetic table-like entity for table %s in catalog %s", From 9fe92fe989a1b26d045548c82997c3f9ae6ea18c Mon Sep 17 00:00:00 2001 From: Honah J Date: Wed, 24 Sep 2025 19:03:34 -0500 Subject: [PATCH 9/9] FIx bugs --- .../service/admin/PolarisAdminService.java | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 9dbafbab45..d39ebfae6b 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -102,6 +102,7 @@ import org.apache.polaris.core.exceptions.CommitConflictException; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.dao.entity.BaseResult; import org.apache.polaris.core.persistence.dao.entity.CreateCatalogResult; import org.apache.polaris.core.persistence.dao.entity.CreatePrincipalResult; import org.apache.polaris.core.persistence.dao.entity.DropEntityResult; @@ -1815,7 +1816,7 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( if (result.isSuccess()) { syntheticNamespace = PolarisEntity.of(result.getEntity()); - } else { + } else if (result.getReturnStatus() == BaseResult.ReturnStatus.ENTITY_ALREADY_EXISTS) { PolarisResolvedPathWrapper partialPath = resolutionManifest.getPassthroughResolvedPath(namespace); PolarisEntity partialLeafEntity = @@ -1829,6 +1830,11 @@ private PolarisResolvedPathWrapper createSyntheticNamespaceEntities( leafNamespace, catalogEntity.getName())); } syntheticNamespace = partialLeafEntity; + } else { + throw new RuntimeException( + String.format( + "Failed to create or find namespace entity '%s' in federated catalog '%s'", + leafNamespace, catalogEntity.getName())); } completePath.add(syntheticNamespace); currentParent = syntheticNamespace; @@ -2206,28 +2212,27 @@ private PolarisResolvedPathWrapper createSyntheticTableLikeEntities( .setCatalogId(parentNamespaceEntity.getCatalogId()) .setCreateTimestamp(System.currentTimeMillis()) .build(); - - EntityResult result = - metaStoreManager.createEntityIfNotExists( - getCurrentPolarisContext(), - PolarisEntity.toCoreList(resolvedNamespacePathWrapper.getRawFullPath()), - syntheticTableEntity); - - if (result.isSuccess()) { - syntheticTableEntity = PolarisEntity.of(result.getEntity()); - } else { - PolarisResolvedPathWrapper tablePathWrapper = - resolutionManifest.getPassthroughResolvedPath(identifier); - PolarisEntity leafEntity = - tablePathWrapper != null ? tablePathWrapper.getRawLeafEntity() : null; - if (leafEntity == null || !subTypes.contains(leafEntity.getSubType())) { - throw new RuntimeException( - String.format( - "Failed to create or find table entity '%s' in federated catalog '%s'", - identifier.name(), catalogEntity.getName())); - } + // We will re-resolve later anyway, so + metaStoreManager.createEntityIfNotExists( + getCurrentPolarisContext(), + PolarisEntity.toCoreList(resolvedNamespacePathWrapper.getRawFullPath()), + syntheticTableEntity); + + PolarisResolvedPathWrapper completePathWrapper = + resolutionManifest.getPassthroughResolvedPath(identifier); + PolarisEntity leafEntity = + completePathWrapper != null ? completePathWrapper.getRawLeafEntity() : null; + if (completePathWrapper == null + || leafEntity == null + || !(leafEntity.getType() == PolarisEntityType.TABLE_LIKE + && leafEntity.getSubType() == PolarisEntitySubType.ICEBERG_TABLE + && Objects.equals(leafEntity.getName(), identifier.name()))) { + throw new RuntimeException( + String.format( + "Failed to create or find table entity '%s' in federated catalog '%s'", + identifier.name(), catalogEntity.getName())); } - return resolutionManifest.getPassthroughResolvedPath(identifier); + return completePathWrapper; } /**