70
70
import java .util .HashMap ;
71
71
import java .util .Map ;
72
72
import java .util .concurrent .ExecutionException ;
73
+ import java .util .concurrent .Semaphore ;
74
+ import java .util .concurrent .atomic .AtomicInteger ;
73
75
74
76
import static org .elasticsearch .xpack .core .security .authz .store .ReservedRolesStore .SUPERUSER_ROLE_DESCRIPTOR ;
75
77
import static org .hamcrest .Matchers .arrayContaining ;
@@ -508,16 +510,7 @@ public void testApiKeyCache() {
508
510
Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
509
511
final char [] hash = hasher .hash (new SecureString (apiKey .toCharArray ()));
510
512
511
- Map <String , Object > sourceMap = new HashMap <>();
512
- sourceMap .put ("doc_type" , "api_key" );
513
- sourceMap .put ("api_key_hash" , new String (hash ));
514
- sourceMap .put ("role_descriptors" , Collections .singletonMap ("a role" , Collections .singletonMap ("cluster" , "all" )));
515
- sourceMap .put ("limited_by_role_descriptors" , Collections .singletonMap ("limited role" , Collections .singletonMap ("cluster" , "all" )));
516
- Map <String , Object > creatorMap = new HashMap <>();
517
- creatorMap .put ("principal" , "test_user" );
518
- creatorMap .put ("metadata" , Collections .emptyMap ());
519
- sourceMap .put ("creator" , creatorMap );
520
- sourceMap .put ("api_key_invalidated" , false );
513
+ Map <String , Object > sourceMap = buildApiKeySourceDoc (hash );
521
514
522
515
ApiKeyService service = createApiKeyService (Settings .EMPTY );
523
516
ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
@@ -565,6 +558,64 @@ public void testApiKeyCache() {
565
558
assertThat (service .getFromCache (creds .getId ()).success , is (true ));
566
559
}
567
560
561
+ public void testAuthenticateWhileCacheBeingPopulated () throws Exception {
562
+ final String apiKey = randomAlphaOfLength (16 );
563
+ Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
564
+ final char [] hash = hasher .hash (new SecureString (apiKey .toCharArray ()));
565
+
566
+ Map <String , Object > sourceMap = buildApiKeySourceDoc (hash );
567
+
568
+ ApiKeyService realService = createApiKeyService (Settings .EMPTY );
569
+ ApiKeyService service = Mockito .spy (realService );
570
+
571
+ // Used to block the hashing of the first api-key secret so that we can guarantee
572
+ // that a second api key authentication takes place while hashing is "in progress".
573
+ final Semaphore hashWait = new Semaphore (0 );
574
+ final AtomicInteger hashCounter = new AtomicInteger (0 );
575
+ doAnswer (invocationOnMock -> {
576
+ hashCounter .incrementAndGet ();
577
+ hashWait .acquire ();
578
+ return invocationOnMock .callRealMethod ();
579
+ }).when (service ).verifyKeyAgainstHash (any (String .class ), any (ApiKeyCredentials .class ));
580
+
581
+ final ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
582
+ final PlainActionFuture <AuthenticationResult > future1 = new PlainActionFuture <>();
583
+
584
+ // Call the top level authenticate... method because it has been known to be buggy in async situations
585
+ writeCredentialsToThreadContext (creds );
586
+ mockSourceDocument (creds .getId (), sourceMap );
587
+
588
+ // 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
589
+ this .threadPool .generic ().execute (() -> service .authenticateWithApiKeyIfPresent (threadPool .getThreadContext (), future1 ));
590
+
591
+ // Wait for the first credential validation to get to the blocked state
592
+ assertBusy (() -> assertThat (hashCounter .get (), equalTo (1 )));
593
+ if (future1 .isDone ()) {
594
+ // We do this [ rather than assertFalse(isDone) ] so we can get a reasonable failure message
595
+ fail ("Expected authentication to be blocked, but was " + future1 .actionGet ());
596
+ }
597
+
598
+ // The second authentication should pass (but not immediately, but will not block)
599
+ PlainActionFuture <AuthenticationResult > future2 = new PlainActionFuture <>();
600
+
601
+ service .authenticateWithApiKeyIfPresent (threadPool .getThreadContext (), future2 );
602
+
603
+ assertThat (hashCounter .get (), equalTo (1 ));
604
+ if (future2 .isDone ()) {
605
+ // We do this [ rather than assertFalse(isDone) ] so we can get a reasonable failure message
606
+ fail ("Expected authentication to be blocked, but was " + future2 .actionGet ());
607
+ }
608
+
609
+ hashWait .release ();
610
+
611
+ assertThat (future1 .actionGet (TimeValue .timeValueSeconds (2 )).isAuthenticated (), is (true ));
612
+ assertThat (future2 .actionGet (TimeValue .timeValueMillis (100 )).isAuthenticated (), is (true ));
613
+
614
+ CachedApiKeyHashResult cachedApiKeyHashResult = service .getFromCache (creds .getId ());
615
+ assertNotNull (cachedApiKeyHashResult );
616
+ assertThat (cachedApiKeyHashResult .success , is (true ));
617
+ }
618
+
568
619
public void testApiKeyCacheDisabled () {
569
620
final String apiKey = randomAlphaOfLength (16 );
570
621
Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
@@ -573,16 +624,7 @@ public void testApiKeyCacheDisabled() {
573
624
.put (ApiKeyService .CACHE_TTL_SETTING .getKey (), "0s" )
574
625
.build ();
575
626
576
- Map <String , Object > sourceMap = new HashMap <>();
577
- sourceMap .put ("doc_type" , "api_key" );
578
- sourceMap .put ("api_key_hash" , new String (hash ));
579
- sourceMap .put ("role_descriptors" , Collections .singletonMap ("a role" , Collections .singletonMap ("cluster" , "all" )));
580
- sourceMap .put ("limited_by_role_descriptors" , Collections .singletonMap ("limited role" , Collections .singletonMap ("cluster" , "all" )));
581
- Map <String , Object > creatorMap = new HashMap <>();
582
- creatorMap .put ("principal" , "test_user" );
583
- creatorMap .put ("metadata" , Collections .emptyMap ());
584
- sourceMap .put ("creator" , creatorMap );
585
- sourceMap .put ("api_key_invalidated" , false );
627
+ Map <String , Object > sourceMap = buildApiKeySourceDoc (hash );
586
628
587
629
ApiKeyService service = createApiKeyService (settings );
588
630
ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
@@ -594,10 +636,40 @@ public void testApiKeyCacheDisabled() {
594
636
assertNull (cachedApiKeyHashResult );
595
637
}
596
638
597
- private ApiKeyService createApiKeyService (Settings settings ) {
639
+ private ApiKeyService createApiKeyService (Settings baseSettings ) {
640
+ final Settings settings = Settings .builder ()
641
+ .put (XPackSettings .API_KEY_SERVICE_ENABLED_SETTING .getKey (), true )
642
+ .put (baseSettings )
643
+ .build ();
598
644
return new ApiKeyService (settings , Clock .systemUTC (), client , licenseState , securityIndex ,
599
645
ClusterServiceUtils .createClusterService (threadPool ), threadPool );
600
646
}
601
647
648
+ private Map <String , Object > buildApiKeySourceDoc (char [] hash ) {
649
+ Map <String , Object > sourceMap = new HashMap <>();
650
+ sourceMap .put ("doc_type" , "api_key" );
651
+ sourceMap .put ("api_key_hash" , new String (hash ));
652
+ sourceMap .put ("role_descriptors" , Collections .singletonMap ("a role" , Collections .singletonMap ("cluster" , "all" )));
653
+ sourceMap .put ("limited_by_role_descriptors" , Collections .singletonMap ("limited role" , Collections .singletonMap ("cluster" , "all" )));
654
+ Map <String , Object > creatorMap = new HashMap <>();
655
+ creatorMap .put ("principal" , "test_user" );
656
+ creatorMap .put ("metadata" , Collections .emptyMap ());
657
+ sourceMap .put ("creator" , creatorMap );
658
+ sourceMap .put ("api_key_invalidated" , false );
659
+ return sourceMap ;
660
+ }
661
+
662
+ private void writeCredentialsToThreadContext (ApiKeyCredentials creds ) {
663
+ final String credentialString = creds .getId () + ":" + creds .getKey ();
664
+ this .threadPool .getThreadContext ().putHeader ("Authorization" ,
665
+ "ApiKey " + Base64 .getEncoder ().encodeToString (credentialString .getBytes (StandardCharsets .US_ASCII )));
666
+ }
667
+
668
+ private void mockSourceDocument (String id , Map <String , Object > sourceMap ) throws IOException {
669
+ try (XContentBuilder builder = JsonXContent .contentBuilder ()) {
670
+ builder .map (sourceMap );
671
+ SecurityMocks .mockGetRequest (client , id , BytesReference .bytes (builder ));
672
+ }
673
+ }
602
674
603
675
}
0 commit comments