|
9 | 9 |
|
10 | 10 | import org.elasticsearch.action.ActionListener; |
11 | 11 | import org.elasticsearch.action.DocWriteResponse; |
| 12 | +import org.elasticsearch.action.UnavailableShardsException; |
12 | 13 | import org.elasticsearch.action.support.WriteRequest; |
13 | 14 | import org.elasticsearch.cluster.ClusterChangedEvent; |
14 | 15 | import org.elasticsearch.cluster.ClusterName; |
|
45 | 46 |
|
46 | 47 | import java.util.ArrayList; |
47 | 48 | import java.util.Collection; |
| 49 | +import java.util.Collections; |
| 50 | +import java.util.HashSet; |
48 | 51 | import java.util.List; |
49 | 52 | import java.util.Set; |
| 53 | +import java.util.stream.Collectors; |
50 | 54 |
|
51 | 55 | import static org.elasticsearch.gateway.GatewayService.STATE_NOT_RECOVERED_BLOCK; |
52 | 56 | import static org.elasticsearch.xpack.security.support.QueryableBuiltInRolesSynchronizer.QUERYABLE_BUILT_IN_ROLES_FEATURE; |
@@ -164,6 +168,7 @@ public void testSuccessfulSync() { |
164 | 168 | verify(clusterService, times(3)).state(); |
165 | 169 | verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService); |
166 | 170 | assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 171 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(0)); |
167 | 172 | } |
168 | 173 |
|
169 | 174 | public void testNotMaster() { |
@@ -384,6 +389,151 @@ public void testSecurityIndexClosed() { |
384 | 389 | verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService); |
385 | 390 | } |
386 | 391 |
|
| 392 | + public void testUnexpectedSyncFailures() { |
| 393 | + assertInitialState(); |
| 394 | + |
| 395 | + ClusterState clusterState = markShardsAvailable(createClusterStateWithOpenSecurityIndex()).nodes(localNodeMaster()) |
| 396 | + .blocks(emptyClusterBlocks()) |
| 397 | + .build(); |
| 398 | + |
| 399 | + when(clusterService.state()).thenReturn(clusterState); |
| 400 | + when(featureService.clusterHasFeature(any(), eq(QUERYABLE_BUILT_IN_ROLES_FEATURE))).thenReturn(true); |
| 401 | + |
| 402 | + final Set<String> roles = randomReservedRoles(randomIntBetween(1, 10)); |
| 403 | + final QueryableBuiltInRoles builtInRoles = buildQueryableBuiltInRoles( |
| 404 | + roles.stream().map(ReservedRolesStore::roleDescriptor).collect(Collectors.toSet()) |
| 405 | + ); |
| 406 | + when(reservedRolesProvider.getRoles()).thenReturn(builtInRoles); |
| 407 | + mockNativeRolesStoreWithFailure(builtInRoles.roleDescriptors(), Set.of(), new IllegalStateException("unexpected failure")); |
| 408 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 409 | + |
| 410 | + for (int i = 1; i <= QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS + 5; i++) { |
| 411 | + synchronizer.clusterChanged(event(clusterState)); |
| 412 | + if (i < QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS) { |
| 413 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(i)); |
| 414 | + } else { |
| 415 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS)); |
| 416 | + } |
| 417 | + } |
| 418 | + |
| 419 | + verify(nativeRolesStore, times(QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS)).isEnabled(); |
| 420 | + verify(featureService, times(QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS)).clusterHasFeature( |
| 421 | + any(), |
| 422 | + eq(QUERYABLE_BUILT_IN_ROLES_FEATURE) |
| 423 | + ); |
| 424 | + verify(reservedRolesProvider, times(QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS)).getRoles(); |
| 425 | + verify(nativeRolesStore, times(QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS)).putRoles( |
| 426 | + eq(WriteRequest.RefreshPolicy.IMMEDIATE), |
| 427 | + eq(builtInRoles.roleDescriptors()), |
| 428 | + eq(false), |
| 429 | + any() |
| 430 | + ); |
| 431 | + verify(clusterService, times(QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS)).state(); |
| 432 | + verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService); |
| 433 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 434 | + } |
| 435 | + |
| 436 | + public void testFailedSyncAttemptsGetsResetAfterSuccessfulSync() { |
| 437 | + assertInitialState(); |
| 438 | + |
| 439 | + ClusterState clusterState = markShardsAvailable(createClusterStateWithOpenSecurityIndex()).nodes(localNodeMaster()) |
| 440 | + .blocks(emptyClusterBlocks()) |
| 441 | + .build(); |
| 442 | + |
| 443 | + when(clusterService.state()).thenReturn(clusterState); |
| 444 | + when(featureService.clusterHasFeature(any(), eq(QUERYABLE_BUILT_IN_ROLES_FEATURE))).thenReturn(true); |
| 445 | + |
| 446 | + final Set<String> roles = randomReservedRoles(randomIntBetween(1, 10)); |
| 447 | + final QueryableBuiltInRoles builtInRoles = buildQueryableBuiltInRoles( |
| 448 | + roles.stream().map(ReservedRolesStore::roleDescriptor).collect(Collectors.toSet()) |
| 449 | + ); |
| 450 | + when(reservedRolesProvider.getRoles()).thenReturn(builtInRoles); |
| 451 | + mockNativeRolesStoreWithFailure(builtInRoles.roleDescriptors(), Set.of(), new IllegalStateException("unexpected failure")); |
| 452 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 453 | + |
| 454 | + // assert failed sync attempts are counted |
| 455 | + int numOfSimulatedFailures = randomIntBetween(1, QueryableBuiltInRolesSynchronizer.MAX_FAILED_SYNC_ATTEMPTS - 1); |
| 456 | + for (int i = 0; i < numOfSimulatedFailures; i++) { |
| 457 | + synchronizer.clusterChanged(event(clusterState)); |
| 458 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(i + 1)); |
| 459 | + } |
| 460 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(numOfSimulatedFailures)); |
| 461 | + |
| 462 | + // assert successful sync resets the failed sync attempts |
| 463 | + mockEnabledNativeStore(builtInRoles.roleDescriptors(), Set.of()); |
| 464 | + synchronizer.clusterChanged(event(clusterState)); |
| 465 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(0)); |
| 466 | + |
| 467 | + verify(nativeRolesStore, times(numOfSimulatedFailures + 1)).isEnabled(); |
| 468 | + verify(featureService, times(numOfSimulatedFailures + 1)).clusterHasFeature(any(), eq(QUERYABLE_BUILT_IN_ROLES_FEATURE)); |
| 469 | + verify(reservedRolesProvider, times(numOfSimulatedFailures + 1)).getRoles(); |
| 470 | + verify(nativeRolesStore, times(numOfSimulatedFailures + 1)).putRoles( |
| 471 | + eq(WriteRequest.RefreshPolicy.IMMEDIATE), |
| 472 | + eq(builtInRoles.roleDescriptors()), |
| 473 | + eq(false), |
| 474 | + any() |
| 475 | + ); |
| 476 | + verify(taskQueue, times(1)).submitTask(any(), argThat(task -> task.getNewRoleDigests().equals(builtInRoles.rolesDigest())), any()); |
| 477 | + verify(clusterService, times(numOfSimulatedFailures + 3)).state(); |
| 478 | + verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService); |
| 479 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 480 | + } |
| 481 | + |
| 482 | + public void testExpectedSyncFailuresAreNotCounted() { |
| 483 | + assertInitialState(); |
| 484 | + |
| 485 | + ClusterState clusterState = markShardsAvailable(createClusterStateWithOpenSecurityIndex()).nodes(localNodeMaster()) |
| 486 | + .blocks(emptyClusterBlocks()) |
| 487 | + .build(); |
| 488 | + |
| 489 | + when(clusterService.state()).thenReturn(clusterState); |
| 490 | + when(featureService.clusterHasFeature(any(), eq(QUERYABLE_BUILT_IN_ROLES_FEATURE))).thenReturn(true); |
| 491 | + |
| 492 | + final Set<String> roles = randomReservedRoles(randomIntBetween(1, 10)); |
| 493 | + final QueryableBuiltInRoles builtInRoles = buildQueryableBuiltInRoles( |
| 494 | + roles.stream().map(ReservedRolesStore::roleDescriptor).collect(Collectors.toSet()) |
| 495 | + ); |
| 496 | + when(reservedRolesProvider.getRoles()).thenReturn(builtInRoles); |
| 497 | + mockNativeRolesStoreWithFailure(builtInRoles.roleDescriptors(), Set.of(), new UnavailableShardsException(null, "expected failure")); |
| 498 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 499 | + |
| 500 | + synchronizer.clusterChanged(event(clusterState)); |
| 501 | + |
| 502 | + assertThat(synchronizer.getFailedSyncAttempts(), equalTo(0)); |
| 503 | + |
| 504 | + verify(nativeRolesStore, times(1)).isEnabled(); |
| 505 | + verify(featureService, times(1)).clusterHasFeature(any(), eq(QUERYABLE_BUILT_IN_ROLES_FEATURE)); |
| 506 | + verify(reservedRolesProvider, times(1)).getRoles(); |
| 507 | + verify(nativeRolesStore, times(1)).putRoles( |
| 508 | + eq(WriteRequest.RefreshPolicy.IMMEDIATE), |
| 509 | + eq(builtInRoles.roleDescriptors()), |
| 510 | + eq(false), |
| 511 | + any() |
| 512 | + ); |
| 513 | + verify(clusterService, times(1)).state(); |
| 514 | + verifyNoMoreInteractions(nativeRolesStore, featureService, taskQueue, reservedRolesProvider, threadPool, clusterService); |
| 515 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(false)); |
| 516 | + } |
| 517 | + |
| 518 | + private Set<String> randomReservedRoles(int count) { |
| 519 | + assert count >= 0; |
| 520 | + if (count == 0) { |
| 521 | + return Set.of(); |
| 522 | + } |
| 523 | + if (count == 1) { |
| 524 | + return Set.of(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()); |
| 525 | + } |
| 526 | + |
| 527 | + final Set<String> reservedRoles = new HashSet<>(); |
| 528 | + reservedRoles.add(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()); |
| 529 | + final Set<String> allReservedRolesExceptSuperuser = ReservedRolesStore.names() |
| 530 | + .stream() |
| 531 | + .filter(role -> false == role.equals(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName())) |
| 532 | + .collect(Collectors.toSet()); |
| 533 | + reservedRoles.addAll(randomUnique(() -> randomFrom(allReservedRolesExceptSuperuser), count - 1)); |
| 534 | + return Collections.unmodifiableSet(reservedRoles); |
| 535 | + } |
| 536 | + |
387 | 537 | private static ClusterState.Builder markShardsAvailable(ClusterState.Builder clusterStateBuilder) { |
388 | 538 | final ClusterState cs = clusterStateBuilder.build(); |
389 | 539 | return ClusterState.builder(cs) |
@@ -508,6 +658,31 @@ private void mockEnabledNativeStore(final Collection<RoleDescriptor> rolesToUpse |
508 | 658 | .deleteRoles(eq(rolesToDelete), eq(WriteRequest.RefreshPolicy.IMMEDIATE), eq(false), any(ActionListener.class)); |
509 | 659 | } |
510 | 660 |
|
| 661 | + @SuppressWarnings({ "unchecked", "rawtypes" }) |
| 662 | + private void mockNativeRolesStoreWithFailure( |
| 663 | + final Collection<RoleDescriptor> rolesToUpsert, |
| 664 | + final Collection<String> rolesToDelete, |
| 665 | + Exception failure |
| 666 | + ) { |
| 667 | + when(nativeRolesStore.isEnabled()).thenReturn(true); |
| 668 | + doAnswer(i -> { |
| 669 | + assertThat(synchronizer.isSynchronizationInProgress(), equalTo(true)); |
| 670 | + ((ActionListener) i.getArgument(3)).onResponse( |
| 671 | + new BulkRolesResponse(rolesToUpsert.stream().map(role -> BulkRolesResponse.Item.failure(role.getName(), failure)).toList()) |
| 672 | + ); |
| 673 | + return null; |
| 674 | + }).when(nativeRolesStore) |
| 675 | + .putRoles(eq(WriteRequest.RefreshPolicy.IMMEDIATE), eq(rolesToUpsert), eq(false), any(ActionListener.class)); |
| 676 | + |
| 677 | + doAnswer(i -> { |
| 678 | + ((ActionListener) i.getArgument(3)).onResponse( |
| 679 | + new BulkRolesResponse(rolesToDelete.stream().map(role -> BulkRolesResponse.Item.failure(role, failure)).toList()) |
| 680 | + ); |
| 681 | + return null; |
| 682 | + }).when(nativeRolesStore) |
| 683 | + .deleteRoles(eq(rolesToDelete), eq(WriteRequest.RefreshPolicy.IMMEDIATE), eq(false), any(ActionListener.class)); |
| 684 | + } |
| 685 | + |
511 | 686 | private static ClusterState.Builder createClusterStateWithOpenSecurityIndex() { |
512 | 687 | return SecurityIndexManagerTests.createClusterState( |
513 | 688 | TestRestrictedIndices.INTERNAL_SECURITY_MAIN_INDEX_7, |
|
0 commit comments