Skip to content

Commit 430b04f

Browse files
HotSushisfc-gh-sraikar
authored andcommitted
fix: Filter null entities in listPrincipals and listPrincipalRoles to prevent NPE
1 parent d6ae2c8 commit 430b04f

File tree

2 files changed

+95
-0
lines changed

2 files changed

+95
-0
lines changed

runtime/service/src/test/java/org/apache/polaris/service/quarkus/admin/ManagementServiceTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,81 @@ public void testCatalogNotReturnedWhenDeletedAfterListBeforeGet() {
315315
assertThat(catalogs.size()).isEqualTo(1);
316316
assertThat(catalogs.getFirst().getId()).isEqualTo(catalog2.getCatalog().getId());
317317
}
318+
319+
/**
320+
* Simulates the case when a principal is dropped after being found while listing all principals.
321+
*/
322+
@Test
323+
public void testPrincipalNotReturnedWhenDeletedAfterListBeforeGet() {
324+
PolarisMetaStoreManager metaStoreManager = Mockito.spy(setupMetaStoreManager());
325+
PolarisCallContext callContext = setupCallContext();
326+
PolarisAdminService polarisAdminService =
327+
setupPolarisAdminService(metaStoreManager, callContext);
328+
329+
int initialPrincipalCount = polarisAdminService.listPrincipals().size();
330+
331+
PrincipalEntity principal1 = createPrincipal(metaStoreManager, callContext, "my-principal-1");
332+
metaStoreManager.createPrincipal(callContext, principal1);
333+
334+
PrincipalEntity principal2 = createPrincipal(metaStoreManager, callContext, "my-principal-2");
335+
metaStoreManager.createPrincipal(callContext, principal2);
336+
337+
Mockito.doAnswer(
338+
invocation -> {
339+
Object entityId = invocation.getArgument(2);
340+
if (entityId.equals(principal1.getId())) {
341+
return new EntityResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, "");
342+
}
343+
return invocation.callRealMethod();
344+
})
345+
.when(metaStoreManager)
346+
.loadEntity(Mockito.any(), Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
347+
348+
List<PolarisEntity> principals = polarisAdminService.listPrincipals();
349+
350+
assertThat(principals.size())
351+
.isEqualTo(initialPrincipalCount + 1); // initial + 2 created - 1 filtered
352+
assertThat(principals.stream().anyMatch(p -> p.getId() == principal2.getId()))
353+
.isTrue(); // principal2 survived
354+
assertThat(principals.stream().noneMatch(p -> p.getId() == principal1.getId()))
355+
.isTrue(); // principal1 filtered out
356+
}
357+
358+
/**
359+
* Simulates the case when a principal role is dropped after being found while listing all
360+
* principal roles.
361+
*/
362+
@Test
363+
public void testPrincipalRoleNotReturnedWhenDeletedAfterListBeforeGet() {
364+
PolarisMetaStoreManager metaStoreManager = Mockito.spy(setupMetaStoreManager());
365+
PolarisCallContext callContext = setupCallContext();
366+
PolarisAdminService polarisAdminService =
367+
setupPolarisAdminService(metaStoreManager, callContext);
368+
369+
int initialRoleCount = polarisAdminService.listPrincipalRoles().size();
370+
371+
PrincipalRoleEntity role1 = createRole(metaStoreManager, callContext, "my-role-1", false);
372+
EntityResult createResult1 = metaStoreManager.createEntityIfNotExists(callContext, null, role1);
373+
assertThat(createResult1.isSuccess()).isTrue();
374+
375+
PrincipalRoleEntity role2 = createRole(metaStoreManager, callContext, "my-role-2", false);
376+
EntityResult createResult2 = metaStoreManager.createEntityIfNotExists(callContext, null, role2);
377+
assertThat(createResult2.isSuccess()).isTrue();
378+
379+
Mockito.doAnswer(
380+
invocation -> {
381+
Object entityId = invocation.getArgument(2);
382+
if (entityId.equals(role1.getId())) {
383+
return new EntityResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, "");
384+
}
385+
return invocation.callRealMethod();
386+
})
387+
.when(metaStoreManager)
388+
.loadEntity(Mockito.any(), Mockito.anyLong(), Mockito.anyLong(), Mockito.any());
389+
390+
List<PolarisEntity> principalRoles = polarisAdminService.listPrincipalRoles();
391+
assertThat(principalRoles.size()).isEqualTo(initialRoleCount + 1);
392+
assertThat(principalRoles.stream().anyMatch(r -> r.getId() == role2.getId())).isTrue();
393+
assertThat(principalRoles.stream().noneMatch(r -> r.getId() == role1.getId())).isTrue();
394+
}
318395
}

service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,10 +1141,18 @@ public void deletePrincipal(String name) {
11411141
return rotateOrResetCredentialsHelper(principalName, true);
11421142
}
11431143

1144+
/**
1145+
* List all principals after checking for permission. Nulls due to non-atomic list-then-get are
1146+
* filtered out.
1147+
*/
11441148
public List<PolarisEntity> listPrincipals() {
11451149
PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_PRINCIPALS;
11461150
authorizeBasicRootOperationOrThrow(op);
11471151

1152+
// loadEntity may return null due to multiple non-atomic
1153+
// API calls to the persistence layer. Specifically, this can happen when a PolarisEntity is
1154+
// returned by listEntities, but cannot be loaded afterward because it was purged by another
1155+
// process before it could be loaded.
11481156
return metaStoreManager
11491157
.listEntities(
11501158
getCurrentPolarisContext(),
@@ -1159,6 +1167,7 @@ public List<PolarisEntity> listPrincipals() {
11591167
PolarisEntity.of(
11601168
metaStoreManager.loadEntity(
11611169
getCurrentPolarisContext(), 0, nameAndId.getId(), nameAndId.getType())))
1170+
.filter(Objects::nonNull)
11621171
.toList();
11631172
}
11641173

@@ -1254,10 +1263,18 @@ public void deletePrincipalRole(String name) {
12541263
return returnedEntity;
12551264
}
12561265

1266+
/**
1267+
* List all principal roles after checking for permission. Nulls due to non-atomic list-then-get
1268+
* are filtered out.
1269+
*/
12571270
public List<PolarisEntity> listPrincipalRoles() {
12581271
PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_PRINCIPAL_ROLES;
12591272
authorizeBasicRootOperationOrThrow(op);
12601273

1274+
// loadEntity may return null due to multiple non-atomic
1275+
// API calls to the persistence layer. Specifically, this can happen when a PolarisEntity is
1276+
// returned by listEntities, but cannot be loaded afterward because it was purged by another
1277+
// process before it could be loaded.
12611278
return metaStoreManager
12621279
.listEntities(
12631280
getCurrentPolarisContext(),
@@ -1272,6 +1289,7 @@ public List<PolarisEntity> listPrincipalRoles() {
12721289
PolarisEntity.of(
12731290
metaStoreManager.loadEntity(
12741291
getCurrentPolarisContext(), 0, nameAndId.getId(), nameAndId.getType())))
1292+
.filter(Objects::nonNull)
12751293
.toList();
12761294
}
12771295

0 commit comments

Comments
 (0)