3535import java .util .Arrays ;
3636import java .util .Collections ;
3737import java .util .HashMap ;
38- import java .util .HashSet ;
3938import java .util .LinkedHashMap ;
4039import java .util .List ;
4140import java .util .Map ;
4544import java .util .stream .Collectors ;
4645import java .util .stream .Stream ;
4746
47+ import com .google .common .collect .Streams ;
4848import com .fasterxml .jackson .databind .JsonNode ;
4949import com .fasterxml .jackson .databind .ObjectMapper ;
5050import com .fasterxml .jackson .dataformat .yaml .YAMLFactory ;
@@ -166,7 +166,9 @@ public TestSecurityConfig authz(AuthzDomain authzDomain) {
166166
167167 public TestSecurityConfig user (User user ) {
168168 this .internalUsers .put (user .name , user );
169- // The user's roles will be collected by aggregateRoles() when the configuration is written
169+ for (Role role : user .roles ) {
170+ this .roles .put (role .name , role );
171+ }
170172 return this ;
171173 }
172174
@@ -453,11 +455,8 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec
453455 String name ;
454456 private String password ;
455457 List <Role > roles = new ArrayList <>();
458+ List <Role > referencedRoles = new ArrayList <>();
456459 List <String > backendRoles = new ArrayList <>();
457- /**
458- * This will be initialized by aggregateRoles()
459- */
460- Set <String > roleNames ;
461460 String requestedTenant ;
462461 private Map <String , String > attributes = new HashMap <>();
463462 private Map <MetadataKey <?>, Object > matchers = new HashMap <>();
@@ -486,8 +485,31 @@ public User password(String password) {
486485 return this ;
487486 }
488487
488+ /**
489+ * Adds a user-specific role to this user. Internally, the role name will be scoped with the user name
490+ * to avoid accidental collisions between roles of different users.
491+ */
489492 public User roles (Role ... roles ) {
490- this .roles .addAll (Arrays .asList (roles ));
493+ // We scope the role names by user to keep tests free of potential side effects
494+ String roleNamePrefix = "user_" + this .getName () + "__" ;
495+
496+ for (Role role : roles ) {
497+ Role copy = role .clone ();
498+ if (!copy .name .startsWith (roleNamePrefix )) {
499+ copy .name = roleNamePrefix + role .name ;
500+ }
501+ this .roles .add (copy );
502+ }
503+
504+ return this ;
505+ }
506+
507+ /**
508+ * Adds references to roles which are already defined for the top-level SecurityTestConfig object.
509+ * This allows tests to share roles between users.
510+ */
511+ public User referencedRoles (Role ... roles ) {
512+ this .referencedRoles .addAll (Arrays .asList (roles ));
491513 return this ;
492514 }
493515
@@ -534,10 +556,7 @@ public String getPassword() {
534556 }
535557
536558 public Set <String > getRoleNames () {
537- if (roleNames == null ) {
538- this .aggregateRoles ();
539- }
540- return roleNames ;
559+ return Streams .concat (roles .stream (), referencedRoles .stream ()).map (Role ::getName ).collect (Collectors .toSet ());
541560 }
542561
543562 public String getDescription () {
@@ -640,25 +659,6 @@ public MetadataKey(String name, Class<T> type) {
640659 this .type = type ;
641660 }
642661 }
643-
644- void aggregateRoles () {
645- if (this .roleNames == null ) {
646- this .roleNames = new HashSet <>();
647- }
648-
649- for (Role role : this .roles ) {
650- if (role .addedIndependentlyOfUser ) {
651- // This is a globally defined role, we just use this
652- this .roleNames .add (role .name );
653- } else {
654- // This is role that is locally defined for the user; let's scope the name
655- if (!role .name .startsWith ("user_" + this .name )) {
656- role .name = "user_" + this .name + "__" + role .name ;
657- }
658- this .roleNames .add (role .name );
659- }
660- }
661- }
662662 }
663663
664664 public static class Role implements ToXContentObject {
@@ -1115,11 +1115,13 @@ public void initIndex(Client client) {
11151115 client .admin ().indices ().create (new CreateIndexRequest (indexName ).settings (settings )).actionGet ();
11161116
11171117 if (rawConfigurationDocuments == null ) {
1118+ checkReferencedRoles ();
1119+
11181120 writeSingleEntryConfigToIndex (client , CType .CONFIG , config );
11191121 if (auditConfiguration != null ) {
11201122 writeSingleEntryConfigToIndex (client , CType .AUDIT , "config" , auditConfiguration );
11211123 }
1122- writeConfigToIndex (client , CType .ROLES , aggregateRoles () );
1124+ writeConfigToIndex (client , CType .ROLES , roles );
11231125 writeConfigToIndex (client , CType .INTERNALUSERS , internalUsers );
11241126 writeConfigToIndex (client , CType .ROLESMAPPING , rolesMapping );
11251127 writeConfigToIndex (client , CType .ACTIONGROUPS , actionGroups );
@@ -1134,23 +1136,23 @@ public void initIndex(Client client) {
11341136 }
11351137
11361138 /**
1137- * Merges the globally defined roles with the roles defined by user. Roles defined by user will be scoped
1138- * so that user definitions cannot interfere with others.
1139+ * Does a sanity check on the user's referenced roles; these must actually match the globally defined roles.
11391140 */
1140- private Map <String , Role > aggregateRoles () {
1141- Map <String , Role > result = new HashMap <>(this .roles );
1142-
1141+ private void checkReferencedRoles () {
11431142 for (User user : this .internalUsers .values ()) {
1144- user .aggregateRoles ();
1145-
1146- for (Role role : user .roles ) {
1147- if (!role .addedIndependentlyOfUser ) {
1148- result .put (role .name , role );
1143+ for (Role role : user .referencedRoles ) {
1144+ if (this .roles .containsKey (role .name ) && !this .roles .get (role .name ).equals (role )) {
1145+ throw new RuntimeException (
1146+ "Referenced role '"
1147+ + role .name
1148+ + "' in user '"
1149+ + user .name
1150+ + "' does not match the definition in TestSecurityConfig: "
1151+ + this .roles .get (role .name )
1152+ );
11491153 }
11501154 }
11511155 }
1152-
1153- return result ;
11541156 }
11551157
11561158 public void updateInternalUsersConfiguration (Client client , List <User > users ) {
0 commit comments