3434import org .elasticsearch .xpack .core .security .action .rolemapping .PutRoleMappingAction ;
3535import org .elasticsearch .xpack .core .security .action .rolemapping .PutRoleMappingRequest ;
3636import org .elasticsearch .xpack .core .security .action .rolemapping .PutRoleMappingRequestBuilder ;
37+ import org .elasticsearch .xpack .core .security .action .rolemapping .PutRoleMappingResponse ;
3738import org .elasticsearch .xpack .core .security .authc .RealmConfig ;
3839import org .elasticsearch .xpack .core .security .authc .support .UserRoleMapper ;
3940import org .elasticsearch .xpack .core .security .authc .support .mapper .ExpressionRoleMapping ;
5859import static org .elasticsearch .indices .recovery .RecoverySettings .INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING ;
5960import static org .elasticsearch .xcontent .XContentType .JSON ;
6061import static org .elasticsearch .xpack .core .security .test .TestRestrictedIndices .INTERNAL_SECURITY_MAIN_INDEX_7 ;
62+ import static org .elasticsearch .xpack .security .authc .support .mapper .ClusterStateRoleMapper .RESERVED_ROLE_MAPPING_SUFFIX ;
6163import static org .hamcrest .Matchers .allOf ;
62- import static org .hamcrest .Matchers .contains ;
6364import static org .hamcrest .Matchers .containsInAnyOrder ;
6465import static org .hamcrest .Matchers .containsString ;
6566import static org .hamcrest .Matchers .empty ;
66- import static org .hamcrest .Matchers .emptyArray ;
6767import static org .hamcrest .Matchers .equalTo ;
6868import static org .hamcrest .Matchers .hasSize ;
6969import static org .hamcrest .Matchers .notNullValue ;
@@ -270,21 +270,28 @@ private void assertRoleMappingsSaveOK(CountDownLatch savedClusterState, AtomicLo
270270 assertThat (resolveRolesFuture .get (), containsInAnyOrder ("kibana_user" , "fleet_user" ));
271271 }
272272
273- // the role mappings are not retrievable by the role mapping action (which only accesses "native" i.e. index-based role mappings)
274- var request = new GetRoleMappingsRequest ();
275- request .setNames ("everyone_kibana" , "everyone_fleet" );
276- var response = client ().execute (GetRoleMappingsAction .INSTANCE , request ).get ();
277- assertFalse (response .hasMappings ());
278- assertThat (response .mappings (), emptyArray ());
279-
280- // role mappings (with the same names) can also be stored in the "native" store
281- var putRoleMappingResponse = client ().execute (PutRoleMappingAction .INSTANCE , sampleRestRequest ("everyone_kibana" )).actionGet ();
282- assertTrue (putRoleMappingResponse .isCreated ());
283- putRoleMappingResponse = client ().execute (PutRoleMappingAction .INSTANCE , sampleRestRequest ("everyone_fleet" )).actionGet ();
284- assertTrue (putRoleMappingResponse .isCreated ());
273+ // the role mappings are retrievable by the role mapping action for BWC
274+ assertGetResponseHasMappings (true , "everyone_kibana" , "everyone_fleet" );
275+
276+ // role mappings (with the same names) can be stored in the "native" store
277+ {
278+ PutRoleMappingResponse response = client ().execute (PutRoleMappingAction .INSTANCE , sampleRestRequest ("everyone_kibana" ))
279+ .actionGet ();
280+ assertTrue (response .isCreated ());
281+ response = client ().execute (PutRoleMappingAction .INSTANCE , sampleRestRequest ("everyone_fleet" )).actionGet ();
282+ assertTrue (response .isCreated ());
283+ }
284+ {
285+ // deleting role mappings that exist in the native store and in cluster-state should result in success
286+ var response = client ().execute (DeleteRoleMappingAction .INSTANCE , deleteRequest ("everyone_kibana" )).actionGet ();
287+ assertTrue (response .isFound ());
288+ response = client ().execute (DeleteRoleMappingAction .INSTANCE , deleteRequest ("everyone_fleet" )).actionGet ();
289+ assertTrue (response .isFound ());
290+ }
291+
285292 }
286293
287- public void testRoleMappingsApplied () throws Exception {
294+ public void testClusterStateRoleMappingsAddedThenDeleted () throws Exception {
288295 ensureGreen ();
289296
290297 var savedClusterState = setupClusterStateListener (internalCluster ().getMasterName (), "everyone_kibana" );
@@ -293,6 +300,12 @@ public void testRoleMappingsApplied() throws Exception {
293300 assertRoleMappingsSaveOK (savedClusterState .v1 (), savedClusterState .v2 ());
294301 logger .info ("---> cleanup cluster settings..." );
295302
303+ {
304+ // Deleting non-existent native role mappings returns not found even if they exist in config file
305+ var response = client ().execute (DeleteRoleMappingAction .INSTANCE , deleteRequest ("everyone_kibana" )).get ();
306+ assertFalse (response .isFound ());
307+ }
308+
296309 savedClusterState = setupClusterStateListenerForCleanup (internalCluster ().getMasterName ());
297310
298311 writeJSONFile (internalCluster ().getMasterName (), emptyJSON , logger , versionCounter );
@@ -307,48 +320,85 @@ public void testRoleMappingsApplied() throws Exception {
307320 clusterStateResponse .getState ().metadata ().persistentSettings ().get (INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING .getKey ())
308321 );
309322
310- // native role mappings are not affected by the removal of the cluster-state based ones
323+ // cluster-state role mapping was removed and is not returned in the API anymore
311324 {
312325 var request = new GetRoleMappingsRequest ();
313326 request .setNames ("everyone_kibana" , "everyone_fleet" );
314327 var response = client ().execute (GetRoleMappingsAction .INSTANCE , request ).get ();
315- assertTrue (response .hasMappings ());
316- assertThat (
317- Arrays .stream (response .mappings ()).map (ExpressionRoleMapping ::getName ).toList (),
318- containsInAnyOrder ("everyone_kibana" , "everyone_fleet" )
319- );
328+ assertFalse (response .hasMappings ());
320329 }
321330
322- // and roles are resolved based on the native role mappings
331+ // no role mappings means no roles are resolved
323332 for (UserRoleMapper userRoleMapper : internalCluster ().getInstances (UserRoleMapper .class )) {
324333 PlainActionFuture <Set <String >> resolveRolesFuture = new PlainActionFuture <>();
325334 userRoleMapper .resolveRoles (
326335 new UserRoleMapper .UserData ("anyUsername" , null , List .of (), Map .of (), mock (RealmConfig .class )),
327336 resolveRolesFuture
328337 );
329- assertThat (resolveRolesFuture .get (), contains ( "kibana_user_native" ));
338+ assertThat (resolveRolesFuture .get (), empty ( ));
330339 }
340+ }
331341
332- {
333- var request = new DeleteRoleMappingRequest ();
334- request .setName ("everyone_kibana" );
335- var response = client ().execute (DeleteRoleMappingAction .INSTANCE , request ).get ();
336- assertTrue (response .isFound ());
337- request = new DeleteRoleMappingRequest ();
338- request .setName ("everyone_fleet" );
339- response = client ().execute (DeleteRoleMappingAction .INSTANCE , request ).get ();
340- assertTrue (response .isFound ());
342+ public void testGetRoleMappings () throws Exception {
343+ ensureGreen ();
344+
345+ final List <String > nativeMappings = List .of ("everyone_kibana" , "_everyone_kibana" , "zzz_mapping" , "123_mapping" );
346+ for (var mapping : nativeMappings ) {
347+ client ().execute (PutRoleMappingAction .INSTANCE , sampleRestRequest (mapping )).actionGet ();
341348 }
342349
343- // no roles are resolved now, because both native and cluster-state based stores have been cleared
344- for (UserRoleMapper userRoleMapper : internalCluster ().getInstances (UserRoleMapper .class )) {
345- PlainActionFuture <Set <String >> resolveRolesFuture = new PlainActionFuture <>();
346- userRoleMapper .resolveRoles (
347- new UserRoleMapper .UserData ("anyUsername" , null , List .of (), Map .of (), mock (RealmConfig .class )),
348- resolveRolesFuture
349- );
350- assertThat (resolveRolesFuture .get (), empty ());
350+ var savedClusterState = setupClusterStateListener (internalCluster ().getMasterName (), "everyone_kibana" );
351+ writeJSONFile (internalCluster ().getMasterName (), testJSON , logger , versionCounter );
352+ boolean awaitSuccessful = savedClusterState .v1 ().await (20 , TimeUnit .SECONDS );
353+ assertTrue (awaitSuccessful );
354+
355+ var request = new GetRoleMappingsRequest ();
356+ var response = client ().execute (GetRoleMappingsAction .INSTANCE , request ).get ();
357+ assertTrue (response .hasMappings ());
358+ assertThat (
359+ Arrays .stream (response .mappings ()).map (ExpressionRoleMapping ::getName ).toList (),
360+ containsInAnyOrder (
361+ "everyone_kibana" ,
362+ "everyone_kibana " + RESERVED_ROLE_MAPPING_SUFFIX ,
363+ "_everyone_kibana" ,
364+ "everyone_fleet " + RESERVED_ROLE_MAPPING_SUFFIX ,
365+ "zzz_mapping" ,
366+ "123_mapping"
367+ )
368+ );
369+
370+ int readOnlyCount = 0 ;
371+ // assert that cluster-state role mappings come last
372+ for (ExpressionRoleMapping mapping : response .mappings ()) {
373+ readOnlyCount = mapping .getName ().endsWith (RESERVED_ROLE_MAPPING_SUFFIX ) ? readOnlyCount + 1 : readOnlyCount ;
351374 }
375+ // Two sourced from cluster-state
376+ assertEquals (readOnlyCount , 2 );
377+
378+ // it's possible to delete overlapping native role mapping
379+ assertTrue (client ().execute (DeleteRoleMappingAction .INSTANCE , deleteRequest ("everyone_kibana" )).actionGet ().isFound ());
380+
381+ savedClusterState = setupClusterStateListenerForCleanup (internalCluster ().getMasterName ());
382+ writeJSONFile (internalCluster ().getMasterName (), emptyJSON , logger , versionCounter );
383+ awaitSuccessful = savedClusterState .v1 ().await (20 , TimeUnit .SECONDS );
384+ assertTrue (awaitSuccessful );
385+
386+ final ClusterStateResponse clusterStateResponse = clusterAdmin ().state (
387+ new ClusterStateRequest (TEST_REQUEST_TIMEOUT ).waitForMetadataVersion (savedClusterState .v2 ().get ())
388+ ).get ();
389+
390+ assertNull (
391+ clusterStateResponse .getState ().metadata ().persistentSettings ().get (INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING .getKey ())
392+ );
393+
394+ // Make sure remaining native mappings can still be fetched
395+ request = new GetRoleMappingsRequest ();
396+ response = client ().execute (GetRoleMappingsAction .INSTANCE , request ).get ();
397+ assertTrue (response .hasMappings ());
398+ assertThat (
399+ Arrays .stream (response .mappings ()).map (ExpressionRoleMapping ::getName ).toList (),
400+ containsInAnyOrder ("_everyone_kibana" , "zzz_mapping" , "123_mapping" )
401+ );
352402 }
353403
354404 public static Tuple <CountDownLatch , AtomicLong > setupClusterStateListenerForError (
@@ -433,11 +483,8 @@ public void testRoleMappingApplyWithSecurityIndexClosed() throws Exception {
433483 boolean awaitSuccessful = savedClusterState .v1 ().await (20 , TimeUnit .SECONDS );
434484 assertTrue (awaitSuccessful );
435485
436- // no native role mappings exist
437- var request = new GetRoleMappingsRequest ();
438- request .setNames ("everyone_kibana" , "everyone_fleet" );
439- var response = client ().execute (GetRoleMappingsAction .INSTANCE , request ).get ();
440- assertFalse (response .hasMappings ());
486+ // even if index is closed, cluster-state role mappings are still returned
487+ assertGetResponseHasMappings (true , "everyone_kibana" , "everyone_fleet" );
441488
442489 // cluster state settings are also applied
443490 var clusterStateResponse = clusterAdmin ().state (
@@ -476,6 +523,12 @@ public void testRoleMappingApplyWithSecurityIndexClosed() throws Exception {
476523 }
477524 }
478525
526+ private DeleteRoleMappingRequest deleteRequest (String name ) {
527+ var request = new DeleteRoleMappingRequest ();
528+ request .setName (name );
529+ return request ;
530+ }
531+
479532 private PutRoleMappingRequest sampleRestRequest (String name ) throws Exception {
480533 var json = """
481534 {
@@ -494,4 +547,19 @@ private PutRoleMappingRequest sampleRestRequest(String name) throws Exception {
494547 return new PutRoleMappingRequestBuilder (null ).source (name , parser ).request ();
495548 }
496549 }
550+
551+ private static void assertGetResponseHasMappings (boolean readOnly , String ... mappings ) throws InterruptedException , ExecutionException {
552+ var request = new GetRoleMappingsRequest ();
553+ request .setNames (mappings );
554+ var response = client ().execute (GetRoleMappingsAction .INSTANCE , request ).get ();
555+ assertTrue (response .hasMappings ());
556+ assertThat (
557+ Arrays .stream (response .mappings ()).map (ExpressionRoleMapping ::getName ).toList (),
558+ containsInAnyOrder (
559+ Arrays .stream (mappings )
560+ .map (mapping -> mapping + (readOnly ? " " + RESERVED_ROLE_MAPPING_SUFFIX : "" ))
561+ .toArray (String []::new )
562+ )
563+ );
564+ }
497565}
0 commit comments