1313import org .elasticsearch .common .bytes .BytesReference ;
1414import org .elasticsearch .common .settings .SecureString ;
1515import org .elasticsearch .common .settings .Settings ;
16+ import org .elasticsearch .common .unit .TimeValue ;
1617import org .elasticsearch .common .util .concurrent .ThreadContext ;
1718import org .elasticsearch .common .xcontent .ToXContent ;
1819import org .elasticsearch .common .xcontent .XContentBuilder ;
4142import org .elasticsearch .xpack .security .test .SecurityMocks ;
4243import org .junit .After ;
4344import org .junit .Before ;
45+ import org .mockito .Mockito ;
4446
4547import java .io .IOException ;
4648import java .nio .charset .StandardCharsets ;
5456import java .util .Collections ;
5557import java .util .HashMap ;
5658import java .util .Map ;
59+ import java .util .concurrent .Semaphore ;
60+ import java .util .concurrent .atomic .AtomicInteger ;
5761
5862import static org .elasticsearch .xpack .core .security .authz .store .ReservedRolesStore .SUPERUSER_ROLE_DESCRIPTOR ;
5963import static org .hamcrest .Matchers .arrayContaining ;
@@ -431,16 +435,7 @@ public void testApiKeyCache() {
431435 Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
432436 final char [] hash = hasher .hash (new SecureString (apiKey .toCharArray ()));
433437
434- Map <String , Object > sourceMap = new HashMap <>();
435- sourceMap .put ("doc_type" , "api_key" );
436- sourceMap .put ("api_key_hash" , new String (hash ));
437- sourceMap .put ("role_descriptors" , Collections .singletonMap ("a role" , Collections .singletonMap ("cluster" , "all" )));
438- sourceMap .put ("limited_by_role_descriptors" , Collections .singletonMap ("limited role" , Collections .singletonMap ("cluster" , "all" )));
439- Map <String , Object > creatorMap = new HashMap <>();
440- creatorMap .put ("principal" , "test_user" );
441- creatorMap .put ("metadata" , Collections .emptyMap ());
442- sourceMap .put ("creator" , creatorMap );
443- sourceMap .put ("api_key_invalidated" , false );
438+ Map <String , Object > sourceMap = buildApiKeySourceDoc (hash );
444439
445440 ApiKeyService service = createApiKeyService (Settings .EMPTY );
446441 ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
@@ -488,6 +483,64 @@ public void testApiKeyCache() {
488483 assertThat (service .getFromCache (creds .getId ()).success , is (true ));
489484 }
490485
486+ public void testAuthenticateWhileCacheBeingPopulated () throws Exception {
487+ final String apiKey = randomAlphaOfLength (16 );
488+ Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
489+ final char [] hash = hasher .hash (new SecureString (apiKey .toCharArray ()));
490+
491+ Map <String , Object > sourceMap = buildApiKeySourceDoc (hash );
492+
493+ ApiKeyService realService = createApiKeyService (Settings .EMPTY );
494+ ApiKeyService service = Mockito .spy (realService );
495+
496+ // Used to block the hashing of the first api-key secret so that we can guarantee
497+ // that a second api key authentication takes place while hashing is "in progress".
498+ final Semaphore hashWait = new Semaphore (0 );
499+ final AtomicInteger hashCounter = new AtomicInteger (0 );
500+ doAnswer (invocationOnMock -> {
501+ hashCounter .incrementAndGet ();
502+ hashWait .acquire ();
503+ return invocationOnMock .callRealMethod ();
504+ }).when (service ).verifyKeyAgainstHash (any (String .class ), any (ApiKeyCredentials .class ));
505+
506+ final ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
507+ final PlainActionFuture <AuthenticationResult > future1 = new PlainActionFuture <>();
508+
509+ // Call the top level authenticate... method because it has been known to be buggy in async situations
510+ writeCredentialsToThreadContext (creds );
511+ mockSourceDocument (creds .getId (), sourceMap );
512+
513+ // This needs to be done in another thread, because we need it to not complete until we say so, but it should not block this test
514+ this .threadPool .generic ().execute (() -> service .authenticateWithApiKeyIfPresent (threadPool .getThreadContext (), future1 ));
515+
516+ // Wait for the first credential validation to get to the blocked state
517+ assertBusy (() -> assertThat (hashCounter .get (), equalTo (1 )));
518+ if (future1 .isDone ()) {
519+ // We do this [ rather than assertFalse(isDone) ] so we can get a reasonable failure message
520+ fail ("Expected authentication to be blocked, but was " + future1 .actionGet ());
521+ }
522+
523+ // The second authentication should pass (but not immediately, but will not block)
524+ PlainActionFuture <AuthenticationResult > future2 = new PlainActionFuture <>();
525+
526+ service .authenticateWithApiKeyIfPresent (threadPool .getThreadContext (), future2 );
527+
528+ assertThat (hashCounter .get (), equalTo (1 ));
529+ if (future2 .isDone ()) {
530+ // We do this [ rather than assertFalse(isDone) ] so we can get a reasonable failure message
531+ fail ("Expected authentication to be blocked, but was " + future2 .actionGet ());
532+ }
533+
534+ hashWait .release ();
535+
536+ assertThat (future1 .actionGet (TimeValue .timeValueSeconds (2 )).isAuthenticated (), is (true ));
537+ assertThat (future2 .actionGet (TimeValue .timeValueMillis (100 )).isAuthenticated (), is (true ));
538+
539+ CachedApiKeyHashResult cachedApiKeyHashResult = service .getFromCache (creds .getId ());
540+ assertNotNull (cachedApiKeyHashResult );
541+ assertThat (cachedApiKeyHashResult .success , is (true ));
542+ }
543+
491544 public void testApiKeyCacheDisabled () {
492545 final String apiKey = randomAlphaOfLength (16 );
493546 Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
@@ -496,16 +549,7 @@ public void testApiKeyCacheDisabled() {
496549 .put (ApiKeyService .CACHE_TTL_SETTING .getKey (), "0s" )
497550 .build ();
498551
499- Map <String , Object > sourceMap = new HashMap <>();
500- sourceMap .put ("doc_type" , "api_key" );
501- sourceMap .put ("api_key_hash" , new String (hash ));
502- sourceMap .put ("role_descriptors" , Collections .singletonMap ("a role" , Collections .singletonMap ("cluster" , "all" )));
503- sourceMap .put ("limited_by_role_descriptors" , Collections .singletonMap ("limited role" , Collections .singletonMap ("cluster" , "all" )));
504- Map <String , Object > creatorMap = new HashMap <>();
505- creatorMap .put ("principal" , "test_user" );
506- creatorMap .put ("metadata" , Collections .emptyMap ());
507- sourceMap .put ("creator" , creatorMap );
508- sourceMap .put ("api_key_invalidated" , false );
552+ Map <String , Object > sourceMap = buildApiKeySourceDoc (hash );
509553
510554 ApiKeyService service = createApiKeyService (settings );
511555 ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
@@ -517,10 +561,40 @@ public void testApiKeyCacheDisabled() {
517561 assertNull (cachedApiKeyHashResult );
518562 }
519563
520- private ApiKeyService createApiKeyService (Settings settings ) {
564+ private ApiKeyService createApiKeyService (Settings baseSettings ) {
565+ final Settings settings = Settings .builder ()
566+ .put (XPackSettings .API_KEY_SERVICE_ENABLED_SETTING .getKey (), true )
567+ .put (baseSettings )
568+ .build ();
521569 return new ApiKeyService (settings , Clock .systemUTC (), client , licenseState , securityIndex ,
522570 ClusterServiceUtils .createClusterService (threadPool ), threadPool );
523571 }
524572
573+ private Map <String , Object > buildApiKeySourceDoc (char [] hash ) {
574+ Map <String , Object > sourceMap = new HashMap <>();
575+ sourceMap .put ("doc_type" , "api_key" );
576+ sourceMap .put ("api_key_hash" , new String (hash ));
577+ sourceMap .put ("role_descriptors" , Collections .singletonMap ("a role" , Collections .singletonMap ("cluster" , "all" )));
578+ sourceMap .put ("limited_by_role_descriptors" , Collections .singletonMap ("limited role" , Collections .singletonMap ("cluster" , "all" )));
579+ Map <String , Object > creatorMap = new HashMap <>();
580+ creatorMap .put ("principal" , "test_user" );
581+ creatorMap .put ("metadata" , Collections .emptyMap ());
582+ sourceMap .put ("creator" , creatorMap );
583+ sourceMap .put ("api_key_invalidated" , false );
584+ return sourceMap ;
585+ }
586+
587+ private void writeCredentialsToThreadContext (ApiKeyCredentials creds ) {
588+ final String credentialString = creds .getId () + ":" + creds .getKey ();
589+ this .threadPool .getThreadContext ().putHeader ("Authorization" ,
590+ "ApiKey " + Base64 .getEncoder ().encodeToString (credentialString .getBytes (StandardCharsets .US_ASCII )));
591+ }
592+
593+ private void mockSourceDocument (String id , Map <String , Object > sourceMap ) throws IOException {
594+ try (XContentBuilder builder = JsonXContent .contentBuilder ()) {
595+ builder .map (sourceMap );
596+ SecurityMocks .mockGetRequest (client , id , BytesReference .bytes (builder ));
597+ }
598+ }
525599
526600}
0 commit comments