13
13
import org .elasticsearch .common .bytes .BytesReference ;
14
14
import org .elasticsearch .common .settings .SecureString ;
15
15
import org .elasticsearch .common .settings .Settings ;
16
+ import org .elasticsearch .common .unit .TimeValue ;
16
17
import org .elasticsearch .common .util .concurrent .ThreadContext ;
17
18
import org .elasticsearch .common .xcontent .ToXContent ;
18
19
import org .elasticsearch .common .xcontent .XContentBuilder ;
41
42
import org .elasticsearch .xpack .security .test .SecurityMocks ;
42
43
import org .junit .After ;
43
44
import org .junit .Before ;
45
+ import org .mockito .Mockito ;
44
46
45
47
import java .io .IOException ;
46
48
import java .nio .charset .StandardCharsets ;
54
56
import java .util .Collections ;
55
57
import java .util .HashMap ;
56
58
import java .util .Map ;
59
+ import java .util .concurrent .Semaphore ;
60
+ import java .util .concurrent .atomic .AtomicInteger ;
57
61
58
62
import static org .elasticsearch .xpack .core .security .authz .store .ReservedRolesStore .SUPERUSER_ROLE_DESCRIPTOR ;
59
63
import static org .hamcrest .Matchers .arrayContaining ;
@@ -431,16 +435,7 @@ public void testApiKeyCache() {
431
435
Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
432
436
final char [] hash = hasher .hash (new SecureString (apiKey .toCharArray ()));
433
437
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 );
444
439
445
440
ApiKeyService service = createApiKeyService (Settings .EMPTY );
446
441
ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
@@ -488,6 +483,64 @@ public void testApiKeyCache() {
488
483
assertThat (service .getFromCache (creds .getId ()).success , is (true ));
489
484
}
490
485
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
+
491
544
public void testApiKeyCacheDisabled () {
492
545
final String apiKey = randomAlphaOfLength (16 );
493
546
Hasher hasher = randomFrom (Hasher .PBKDF2 , Hasher .BCRYPT4 , Hasher .BCRYPT );
@@ -496,16 +549,7 @@ public void testApiKeyCacheDisabled() {
496
549
.put (ApiKeyService .CACHE_TTL_SETTING .getKey (), "0s" )
497
550
.build ();
498
551
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 );
509
553
510
554
ApiKeyService service = createApiKeyService (settings );
511
555
ApiKeyCredentials creds = new ApiKeyCredentials (randomAlphaOfLength (12 ), new SecureString (apiKey .toCharArray ()));
@@ -517,10 +561,40 @@ public void testApiKeyCacheDisabled() {
517
561
assertNull (cachedApiKeyHashResult );
518
562
}
519
563
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 ();
521
569
return new ApiKeyService (settings , Clock .systemUTC (), client , licenseState , securityIndex ,
522
570
ClusterServiceUtils .createClusterService (threadPool ), threadPool );
523
571
}
524
572
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
+ }
525
599
526
600
}
0 commit comments