Updating Role REST v2 service
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/Permission.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/Permission.java
index daf5f65..3900bc5 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/Permission.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/Permission.java
@@ -60,6 +60,10 @@
         this.permanent = permission.isPermanent();
     }
 
+    public static Permission of( org.apache.archiva.redback.rbac.Permission perm )  {
+        return new Permission( perm );
+    }
+
     @Schema(name="name", description = "The identifier of the permission")
     public String getName()
     {
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/RoleInfo.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/RoleInfo.java
index 01a2698..11ead1a 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/RoleInfo.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/v2/RoleInfo.java
@@ -28,6 +28,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * Result object for role information.
@@ -36,7 +37,7 @@
  * @since 3.0
  */
 @XmlRootElement( name = "role" )
-@Schema(name="RoleInfo",description = "Information about role")
+@Schema(name="RoleInfo",description = "Information about role.")
 public class RoleInfo extends BaseRoleInfo
     implements Serializable
 {
@@ -57,26 +58,6 @@
      */
     private List<String> parentRoleIds = new ArrayList<>(0);
 
-    /**
-     * The ids of all the assigned users.
-     */
-    protected List<BaseUserInfo> assignedUsers = new ArrayList<>( 0 );
-
-    @Schema( description = "List of user ids that are assigned to this role.")
-    public List<BaseUserInfo> getAssignedUsers( )
-    {
-        return assignedUsers;
-    }
-
-    public void setAssignedUsers( List<BaseUserInfo> assignedUsers )
-    {
-        this.assignedUsers = assignedUsers;
-    }
-
-    public void addAssignedUser( BaseUserInfo id) {
-        this.assignedUsers.add( id );
-    }
-
     public RoleInfo()
     {
         // no op
@@ -85,6 +66,12 @@
 
     public static RoleInfo of( Role rbacRole) {
         RoleInfo role = BaseRoleInfo.of( rbacRole, new RoleInfo( ) );
+        if(rbacRole.getPermissions()!=null)
+        {
+            role.permissions = rbacRole.getPermissions( ).stream( ).map( rbacPerm ->
+                Permission.of( rbacPerm )
+            ).collect( Collectors.toList( ) );
+        }
         return role;
     }
 
@@ -151,7 +138,6 @@
         sb.append( ", childRoleNames=" ).append( childRoleIds );
         sb.append( ", permissions=" ).append( permissions );
         sb.append( ", parentRoleNames=" ).append( parentRoleIds );
-        sb.append( ", assignedUsers=" ).append( assignedUsers );
         sb.append( ", permanent=" ).append( isPermanent( ) );
         sb.append( ", modelId='" ).append( modelId ).append( '\'' );
         sb.append( ", resource='" ).append( resource ).append( '\'' );
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/RoleService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/RoleService.java
index c88bc61..3aec8ba 100644
--- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/RoleService.java
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/RoleService.java
@@ -33,6 +33,7 @@
 import org.apache.archiva.redback.rest.api.model.v2.Role;
 import org.apache.archiva.redback.rest.api.model.v2.RoleInfo;
 import org.apache.archiva.redback.rest.api.model.v2.RoleTemplate;
+import org.apache.archiva.redback.rest.api.model.v2.UserInfo;
 import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
 
 import javax.ws.rs.DELETE;
@@ -99,7 +100,7 @@
     @GET
     @Produces( { APPLICATION_JSON } )
     @RedbackAuthorization( permissions = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
-    @Operation( summary = "Returns information about a specific role. Use HTTP HEAD method for checking, if the resource exists.",
+    @Operation( summary = "Returns the definition about a specific role.",
         security = {
             @SecurityRequirement(
                 name = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION
@@ -124,7 +125,7 @@
     @HEAD
     @Produces( { APPLICATION_JSON } )
     @RedbackAuthorization( permissions = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
-    @Operation( summary = "Returns information about a specific role. Use HTTP HEAD method for checking, if the resource exists.",
+    @Operation( summary = "Checks, if the role with the given id exists.",
         security = {
             @SecurityRequirement(
                 name = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION
@@ -273,32 +274,6 @@
         throws RedbackServiceException;
 
 
-    /**
-     * Assigns the role indicated by the roleId to the given principal
-     *
-     * @param roleId
-     * @param userId
-     */
-    @Path( "{roleId}/user/{userId}" )
-    @PUT
-    @Produces( { APPLICATION_JSON } )
-    @RedbackAuthorization( permissions = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
-    @Operation( summary = "Assigns a role to a given user",
-        security = {
-            @SecurityRequirement( name = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
-        },
-        responses = {
-            @ApiResponse( responseCode = "200",
-                description = "If the role was assigned"
-            ),
-            @ApiResponse( responseCode = "404", description = "Role does not exist",
-                content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) ),
-            @ApiResponse( responseCode = "403", description = "The authenticated user has not the permission for role assignment.",
-                content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) )
-        }
-    )
-    RoleInfo assignRole( @PathParam( "roleId" ) String roleId, @PathParam( "userId" ) String userId )
-        throws RedbackServiceException;
 
     /**
      * Assigns the templated role indicated by the templateId
@@ -333,10 +308,37 @@
         throws RedbackServiceException;
 
     /**
-     * Unassigns the role indicated by the role id from the given principal
+     * Assigns the role indicated by the roleId to the given principal
      *
      * @param roleId
      * @param userId
+     */
+    @Path( "{roleId}/user/{userId}" )
+    @PUT
+    @Produces( { APPLICATION_JSON } )
+    @RedbackAuthorization( permissions = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
+    @Operation( summary = "Assigns a role to a given user",
+        security = {
+            @SecurityRequirement( name = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
+        },
+        responses = {
+            @ApiResponse( responseCode = "200",
+                description = "If the role was assigned"
+            ),
+            @ApiResponse( responseCode = "404", description = "Role does not exist",
+                content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) ),
+            @ApiResponse( responseCode = "403", description = "The authenticated user has not the permission for role assignment.",
+                content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) )
+        }
+    )
+    RoleInfo assignRole( @PathParam( "roleId" ) String roleId, @PathParam( "userId" ) String userId )
+        throws RedbackServiceException;
+
+    /**
+     * Deletes the assignment of a role to a user.
+     *
+     * @param roleId the role id
+     * @param userId the user id
      * @throws RedbackServiceException
      */
     @Path( "{roleId}/user/{userId}" )
@@ -357,9 +359,40 @@
                 content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) )
         }
     )
-    RoleInfo unassignRole( @PathParam( "roleId" ) String roleId, @PathParam( "userId" ) String userId )
+    RoleInfo deleteRoleAssignment( @PathParam( "roleId" ) String roleId, @PathParam( "userId" ) String userId )
         throws RedbackServiceException;
 
+    @Path("{roleId}/user")
+    @GET
+    @Produces({APPLICATION_JSON})
+    @RedbackAuthorization(permissions = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION)
+    @Operation( summary = "Returns the users assigned to the given role",
+        parameters = {
+            @Parameter(name = "q", description = "Search term"),
+            @Parameter(name = "offset", description = "The offset of the first element returned"),
+            @Parameter(name = "limit", description = "Maximum number of items to return in the response"),
+            @Parameter(name = "orderBy", description = "List of attribute used for sorting (user_id, fullName, email, created"),
+            @Parameter(name = "order", description = "The sort order. Either ascending (asc) or descending (desc)")
+        },
+        security = {
+            @SecurityRequirement( name = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
+        },
+        responses = {
+            @ApiResponse( responseCode = "200",
+                description = "If the users could be retrieved"
+            ),
+            @ApiResponse( responseCode = "404", description = "Role instance does not exist",
+                content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) ),
+            @ApiResponse( responseCode = "403", description = "The authenticated user has not the permission for role assignment.",
+                content = @Content(mediaType = APPLICATION_JSON, schema = @Schema(implementation = RedbackRestError.class )) )
+        }
+    )
+    PagedResult<UserInfo> getRoleUsers(@PathParam( "roleId" ) String roleId,
+                                       @QueryParam("q") @DefaultValue( "" ) String searchTerm,
+                                       @QueryParam( "offset" ) @DefaultValue( "0" ) Integer offset,
+                                       @QueryParam( "limit" ) @DefaultValue( value = DEFAULT_PAGE_LIMIT ) Integer limit,
+                                       @QueryParam( "orderBy") @DefaultValue( "id" ) List<String> orderBy,
+                                       @QueryParam("order") @DefaultValue( "asc" ) String order) throws RedbackServiceException;
 
     /**
      * Updates a role. Attributes that are empty or null will be ignored.
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
index 7553cf0..fb3e72e 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
@@ -127,7 +127,7 @@
             "origin, content-type, accept, authorization");
         responseContext.getHeaders().add(
             "Access-Control-Allow-Methods",
-            "GET, POST, PUT, DELETE, OPTIONS, HEAD");
+            "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH");
     }
 
     private class HeaderValidationInfo
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/BaseRedbackService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/BaseRedbackService.java
index 4dc9ab3..48de1bb 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/BaseRedbackService.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/BaseRedbackService.java
@@ -24,18 +24,26 @@
 import org.apache.archiva.redback.rest.api.MessageKeys;
 import org.apache.archiva.redback.rest.api.model.ErrorMessage;
 import org.apache.archiva.redback.rest.api.model.v2.BaseUserInfo;
+import org.apache.archiva.redback.rest.api.model.v2.PagedResult;
 import org.apache.archiva.redback.rest.api.model.v2.RoleInfo;
+import org.apache.archiva.redback.rest.api.model.v2.UserInfo;
 import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
 import org.apache.archiva.redback.users.User;
 import org.apache.archiva.redback.users.UserManager;
 import org.apache.archiva.redback.users.UserManagerException;
+import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Named;
 import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -44,8 +52,36 @@
  */
 public class BaseRedbackService
 {
+    protected static final String[] DEFAULT_SEARCH_FIELDS = {"user_id", "full_name", "email"};
+    protected static final Map<String, BiPredicate<String, User>> USER_FILTER_MAP = new HashMap<>( );
+    protected static final Map<String, Comparator<User>> USER_ORDER_MAP = new HashMap<>( );
+    protected static final QueryHelper<User> USER_QUERY_HELPER;
     private static final Logger log = LoggerFactory.getLogger( BaseRedbackService.class );
 
+
+    static
+    {
+        // The simple Comparator.comparing(attribute) is not null safe
+        // As there are attributes that may have a null value, we have to use a comparator with nullsLast(naturalOrder)
+        // and the wrapping Comparator.nullsLast(Comparator.comparing(attribute)) does not work, because the attribute is not checked by the nullsLast-Comparator
+        USER_ORDER_MAP.put( "id", Comparator.comparing( org.apache.archiva.redback.users.User::getId, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "user_id", Comparator.comparing( org.apache.archiva.redback.users.User::getUsername, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "full_name", Comparator.comparing( org.apache.archiva.redback.users.User::getFullName, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "email", Comparator.comparing( org.apache.archiva.redback.users.User::getEmail, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "created", Comparator.comparing( org.apache.archiva.redback.users.User::getAccountCreationDate, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "last_login", Comparator.comparing( org.apache.archiva.redback.users.User::getLastLoginDate, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "validated", Comparator.comparing( org.apache.archiva.redback.users.User::isValidated, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "locked", Comparator.comparing( org.apache.archiva.redback.users.User::isLocked, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "password_change_required", Comparator.comparing( org.apache.archiva.redback.users.User::isPasswordChangeRequired, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+        USER_ORDER_MAP.put( "last_password_change", Comparator.comparing( org.apache.archiva.redback.users.User::getLastPasswordChange, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
+
+        USER_FILTER_MAP.put( "user_id", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getUsername( ), q ) );
+        USER_FILTER_MAP.put( "full_name", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getFullName( ), q ) );
+        USER_FILTER_MAP.put( "email", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getEmail( ), q ) );
+
+        USER_QUERY_HELPER = new QueryHelper<>( USER_FILTER_MAP, USER_ORDER_MAP, DEFAULT_SEARCH_FIELDS );
+    }
+
     protected RBACManager rbacManager;
     protected UserManager userManager;
 
@@ -62,7 +98,6 @@
             RoleInfo role = RoleInfo.of( rbacRole );
             role.setParentRoleIds( getParentRoles( rbacRole ) );
             role.setChildRoleIds( getChildRoles( rbacRole ) );
-            role.setAssignedUsers( getAssignedUsersRecursive( rbacRole ) );
             return role;
         }
         catch ( RbacManagerException e )
@@ -72,6 +107,10 @@
         }
     }
 
+    protected boolean isAscending(String order) {
+        return !"desc".equals( order );
+    }
+
     protected List<String> getParentRoles( org.apache.archiva.redback.rbac.Role rbacRole ) throws RbacManagerException
     {
         return new ArrayList<>( rbacManager.getParentRoleIds( rbacRole ).keySet( ));
@@ -96,6 +135,33 @@
         }
     }
 
+    protected List<User> getAssignedRedbackUsersRecursive( org.apache.archiva.redback.rbac.Role rbacRole ) throws RbacManagerException
+    {
+        try
+        {
+            return rbacManager.getUserAssignmentsForRoles( recurseRoles( rbacRole ).map( role -> role.getId( ) ).collect( Collectors.toList( ) ) )
+                .stream( ).map( assignment -> getRedbackUser( assignment.getPrincipal( ) ) ).collect( Collectors.toList( ) );
+        }
+        catch ( RuntimeException e )
+        {
+            log.error( "Could not recurse roles for assignments {}", e.getMessage( ) );
+            throw new RbacManagerException( e.getCause( ) );
+        }
+    }
+
+    protected User getRedbackUser(String userId) throws RuntimeException {
+        try
+        {
+            return userManager.findUser( userId, true );
+        }
+        catch ( UserManagerException e )
+        {
+            throw new RuntimeException( e );
+        }
+    }
+
+
+
     private Stream<Role> recurseRoles( Role startRole )
     {
         return Stream.concat( Stream.of( startRole ), getParentRoleStream( startRole ).flatMap( this::recurseRoles ) ).distinct( );
@@ -133,7 +199,6 @@
             RoleInfo role = RoleInfo.of( rbacRole );
             role.setParentRoleIds( getParentRoles( rbacRole ) );
             role.setChildRoleIds( getChildRoles( rbacRole ) );
-            role.setAssignedUsers( getAssignedUsersRecursive( rbacRole ) );
             return Optional.of( role );
         }
         catch ( RbacManagerException e )
@@ -142,4 +207,25 @@
             return Optional.empty( );
         }
     }
+
+    protected UserInfo getRestUser( User user )
+    {
+        if ( user == null )
+        {
+            return null;
+        }
+        return new UserInfo( user );
+    }
+
+    protected PagedResult<UserInfo> getUserInfoPagedResult( List<? extends User> rawUsers, String q, Integer offset, Integer limit, List<String> orderBy, boolean ascending)
+    {
+        Predicate<User> filter = USER_QUERY_HELPER.getQueryFilter( q );
+        long size = rawUsers.stream( ).filter( filter ).count( );
+        List<UserInfo> users = rawUsers.stream( )
+            .filter( filter )
+            .sorted( USER_QUERY_HELPER.getComparator( orderBy, ascending ) ).skip( offset ).limit( limit )
+            .map( user -> getRestUser( user ) )
+            .collect( Collectors.toList( ) );
+        return new PagedResult<>( (int) size, offset, limit, users );
+    }
 }
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultRoleService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultRoleService.java
index 43adc13..ea0c34c 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultRoleService.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultRoleService.java
@@ -27,6 +27,7 @@
 import org.apache.archiva.redback.rest.api.model.v2.Role;
 import org.apache.archiva.redback.rest.api.model.v2.RoleInfo;
 import org.apache.archiva.redback.rest.api.model.v2.RoleTemplate;
+import org.apache.archiva.redback.rest.api.model.v2.UserInfo;
 import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
 import org.apache.archiva.redback.rest.api.services.v2.RoleService;
 import org.apache.archiva.redback.role.PermanentRoleDeletionInvalid;
@@ -34,9 +35,8 @@
 import org.apache.archiva.redback.role.RoleManager;
 import org.apache.archiva.redback.role.RoleManagerException;
 import org.apache.archiva.redback.role.RoleNotFoundException;
-import org.apache.archiva.redback.role.model.ModelApplication;
-import org.apache.archiva.redback.role.model.ModelTemplate;
 import org.apache.archiva.redback.role.util.RoleModelUtils;
+import org.apache.archiva.redback.users.User;
 import org.apache.archiva.redback.users.UserManager;
 import org.apache.archiva.redback.users.UserManagerException;
 import org.apache.archiva.redback.users.UserNotFoundException;
@@ -83,7 +83,7 @@
     @Context
     private UriInfo uriInfo;
 
-    private static final String[] DEFAULT_SEARCH_FIELDS = {"name", "description"};
+    private static final String[] DEFAULT_SEARCH_FIELDS = {"id", "name", "description"};
     private static final Map<String, BiPredicate<String, org.apache.archiva.redback.rbac.Role>> FILTER_MAP = new HashMap<>( );
     private static final Map<String, Comparator<org.apache.archiva.redback.rbac.Role>> ORDER_MAP = new HashMap<>( );
     private static final QueryHelper<org.apache.archiva.redback.rbac.Role> QUERY_HELPER;
@@ -92,6 +92,7 @@
     {
 
         QUERY_HELPER = new QueryHelper<>( FILTER_MAP, ORDER_MAP, DEFAULT_SEARCH_FIELDS );
+        QUERY_HELPER.addStringFilter( "id", org.apache.archiva.redback.rbac.Role::getId );
         QUERY_HELPER.addStringFilter( "name", org.apache.archiva.redback.rbac.Role::getName );
         QUERY_HELPER.addStringFilter( "description", org.apache.archiva.redback.rbac.Role::getDescription );
         QUERY_HELPER.addBooleanFilter( "assignable", org.apache.archiva.redback.rbac.Role::isAssignable );
@@ -103,6 +104,7 @@
         QUERY_HELPER.addNullsafeFieldComparator( "id", org.apache.archiva.redback.rbac.Role::getId );
         QUERY_HELPER.addNullsafeFieldComparator( "resource", org.apache.archiva.redback.rbac.Role::getResource );
         QUERY_HELPER.addNullsafeFieldComparator( "assignable", org.apache.archiva.redback.rbac.Role::isAssignable );
+        QUERY_HELPER.addNullsafeFieldComparator( "description", org.apache.archiva.redback.rbac.Role::getDescription );
         QUERY_HELPER.addNullsafeFieldComparator( "template_instance", org.apache.archiva.redback.rbac.Role::isTemplateInstance );
     }
 
@@ -121,7 +123,7 @@
     @Override
     public PagedResult<RoleInfo> getAllRoles( String searchTerm, Integer offset, Integer limit, List<String> orderBy, String order ) throws RedbackServiceException
     {
-        boolean ascending = !"desc".equals( order );
+        boolean ascending = isAscending( order );
         try
         {
             // UserQuery does not work here, because the configurable user manager does only return the query for
@@ -389,7 +391,7 @@
     }
 
     @Override
-    public RoleInfo unassignRole( String roleId, String userId )
+    public RoleInfo deleteRoleAssignment( String roleId, String userId )
         throws RedbackServiceException
     {
         try
@@ -415,8 +417,28 @@
         }
         catch ( RbacObjectNotFoundException e )
         {
+            throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_NOT_FOUND, e.getMessage( ) ), 404 );
+        }
+        catch ( RbacManagerException e )
+        {
             throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_RBACMANAGER_FAIL, e.getMessage( ) ) );
         }
+    }
+
+    @Override
+    public PagedResult<UserInfo> getRoleUsers( String roleId, String searchTerm, Integer offset, Integer limit, List<String> orderBy, String order )  throws RedbackServiceException
+    {
+        boolean ascending = isAscending( order );
+        try
+        {
+            org.apache.archiva.redback.rbac.Role rbacRole = rbacManager.getRoleById( roleId );
+            List<User> rawUsers = getAssignedRedbackUsersRecursive( rbacRole );
+            return getUserInfoPagedResult( rawUsers, searchTerm, offset, limit, orderBy, ascending );
+        }
+        catch ( RbacObjectNotFoundException e )
+        {
+            throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_ROLE_NOT_FOUND, e.getMessage( ) ), 404 );
+        }
         catch ( RbacManagerException e )
         {
             throw new RedbackServiceException( ErrorMessage.of( MessageKeys.ERR_RBACMANAGER_FAIL, e.getMessage( ) ) );
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java
index f32d595..b89a550 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultUserService.java
@@ -102,7 +102,6 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -118,33 +117,6 @@
     private static final String VALID_USERNAME_CHARS = "[a-zA-Z_0-9\\-.@]*";
     private static final String[] INVALID_CREATE_USER_NAMES = {"admin", "guest", "me"};
 
-    private static final String[] DEFAULT_SEARCH_FIELDS = {"user_id", "full_name", "email"};
-    private static final Map<String, BiPredicate<String, org.apache.archiva.redback.users.User>> FILTER_MAP = new HashMap<>( );
-    private static final Map<String, Comparator<org.apache.archiva.redback.users.User>> ORDER_MAP = new HashMap<>( );
-    private static final QueryHelper<org.apache.archiva.redback.users.User> QUERY_HELPER;
-
-    static
-    {
-        // The simple Comparator.comparing(attribute) is not null safe
-        // As there are attributes that may have a null value, we have to use a comparator with nullsLast(naturalOrder)
-        // and the wrapping Comparator.nullsLast(Comparator.comparing(attribute)) does not work, because the attribute is not checked by the nullsLast-Comparator
-        ORDER_MAP.put( "id", Comparator.comparing( org.apache.archiva.redback.users.User::getId, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "user_id", Comparator.comparing( org.apache.archiva.redback.users.User::getUsername, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "full_name", Comparator.comparing( org.apache.archiva.redback.users.User::getFullName, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "email", Comparator.comparing( org.apache.archiva.redback.users.User::getEmail, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "created", Comparator.comparing( org.apache.archiva.redback.users.User::getAccountCreationDate, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "last_login", Comparator.comparing( org.apache.archiva.redback.users.User::getLastLoginDate, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "validated", Comparator.comparing( org.apache.archiva.redback.users.User::isValidated, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "locked", Comparator.comparing( org.apache.archiva.redback.users.User::isLocked, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "password_change_required", Comparator.comparing( org.apache.archiva.redback.users.User::isPasswordChangeRequired, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-        ORDER_MAP.put( "last_password_change", Comparator.comparing( org.apache.archiva.redback.users.User::getLastPasswordChange, Comparator.nullsLast( Comparator.naturalOrder( ) ) ) );
-
-        FILTER_MAP.put( "user_id", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getUsername( ), q ) );
-        FILTER_MAP.put( "full_name", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getFullName( ), q ) );
-        FILTER_MAP.put( "email", ( String q, org.apache.archiva.redback.users.User u ) -> StringUtils.containsIgnoreCase( u.getEmail( ), q ) );
-
-        QUERY_HELPER = new QueryHelper<>( FILTER_MAP, ORDER_MAP, DEFAULT_SEARCH_FIELDS );
-    }
 
     private SecuritySystem securitySystem;
 
@@ -395,20 +367,13 @@
                                            Integer limit, List<String> orderBy, String order )
         throws RedbackServiceException
     {
-        boolean ascending = !"desc".equals( order );
+        boolean ascending = isAscending( order );
         try
         {
             // UserQuery does not work here, because the configurable user manager does only return the query for
             // the first user manager in the list. So we have to fetch the whole user list
             List<? extends org.apache.archiva.redback.users.User> rawUsers = userManager.getUsers( );
-            Predicate<org.apache.archiva.redback.users.User> filter = QUERY_HELPER.getQueryFilter( q );
-            long size = rawUsers.stream( ).filter( filter ).count( );
-            List<UserInfo> users = rawUsers.stream( )
-                .filter( filter )
-                .sorted( QUERY_HELPER.getComparator( orderBy, ascending ) ).skip( offset ).limit( limit )
-                .map( user -> getRestUser( user ) )
-                .collect( Collectors.toList( ) );
-            return new PagedResult<>( (int) size, offset, limit, users );
+            return getUserInfoPagedResult( rawUsers, q, offset, limit, orderBy, ascending );
         }
         catch ( UserManagerException e )
         {
@@ -416,6 +381,7 @@
         }
     }
 
+
     @Override
     public UserInfo updateMe( SelfUserData user )
         throws RedbackServiceException
@@ -573,15 +539,6 @@
         return new PingResult( true );
     }
 
-    private UserInfo getRestUser( org.apache.archiva.redback.users.User user )
-    {
-        if ( user == null )
-        {
-            return null;
-        }
-        return new UserInfo( user );
-    }
-
     @Override
     public UserInfo createAdminUser( User adminUser )
         throws RedbackServiceException
diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeRoleServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeRoleServiceTest.java
index 936a160..125b8f0 100644
--- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeRoleServiceTest.java
+++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeRoleServiceTest.java
@@ -19,8 +19,14 @@
  */
 
 import io.restassured.response.Response;
+import io.restassured.response.ResponseBodyExtractionOptions;
+import org.apache.archiva.redback.rest.api.model.User;
+import org.apache.archiva.redback.rest.api.model.v2.BaseUserInfo;
+import org.apache.archiva.redback.rest.api.model.v2.PagedResult;
+import org.apache.archiva.redback.rest.api.model.v2.Permission;
 import org.apache.archiva.redback.rest.api.model.v2.RoleInfo;
 import org.apache.archiva.redback.rest.api.model.v2.RoleTemplate;
+import org.apache.archiva.redback.rest.api.model.v2.UserInfo;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.DisplayName;
@@ -38,8 +44,8 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
-import static io.restassured.RestAssured.get;
 import static io.restassured.RestAssured.given;
 import static io.restassured.http.ContentType.JSON;
 import static org.apache.archiva.redback.rest.api.Constants.DEFAULT_PAGE_LIMIT;
@@ -142,26 +148,26 @@
     void deleteTemplatedRole( )
     {
         String token = getAdminToken( );
-            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-                .when( )
-                .put( "templates/archiva-repository-manager/repository05" )
-                .then( ).statusCode( 201 ).extract( ).response( );
-            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-                .when( )
-                .delete( "templates/archiva-repository-manager/repository01" )
-                .then( ).statusCode( 404 );
-            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-                .when( )
-                .delete( "templates/archiva-repository-manager/repository05" )
-                .then( ).statusCode( 200 );
-            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-                .when( )
-                .delete( "templates/archiva-repository-manager/repository05" )
-                .then( ).statusCode( 404 );
-            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-                .when( )
-                .delete( "templates/archiva-repository-observer/repository05" )
-                .then( ).statusCode( 200 );
+        given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+            .when( )
+            .put( "templates/archiva-repository-manager/repository05" )
+            .then( ).statusCode( 201 ).extract( ).response( );
+        given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+            .when( )
+            .delete( "templates/archiva-repository-manager/repository01" )
+            .then( ).statusCode( 404 );
+        given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+            .when( )
+            .delete( "templates/archiva-repository-manager/repository05" )
+            .then( ).statusCode( 200 );
+        given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+            .when( )
+            .delete( "templates/archiva-repository-manager/repository05" )
+            .then( ).statusCode( 404 );
+        given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+            .when( )
+            .delete( "templates/archiva-repository-observer/repository05" )
+            .then( ).statusCode( 200 );
 
     }
 
@@ -343,12 +349,20 @@
     {
         String token = getAdminToken( );
         Response response = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-            .when( ).get( "archiva-system-administrator" ).then( ).statusCode( 200 ).extract( ).response( );
+            .when( ).get( "archiva-system-administrator" ).prettyPeek( ).then( ).statusCode( 200 ).extract( ).response( );
         assertNotNull( response );
         RoleInfo roleInfo = response.getBody( ).jsonPath( ).getObject( "", RoleInfo.class );
         assertNotNull( roleInfo );
         assertEquals( "archiva-system-administrator", roleInfo.getId( ) );
         assertEquals( "Archiva System Administrator", roleInfo.getName( ) );
+        List<Permission> perms = roleInfo.getPermissions( );
+        assertNotNull( perms );
+        assertTrue( perms.size( ) > 0 );
+        assertTrue( perms.stream( ).filter( perm -> "archiva-manage-configuration".equals( perm.getName( ) ) ).findAny( ).isPresent( ) );
+        List<String> childs = roleInfo.getChildRoleIds( );
+        assertNotNull( childs );
+        assertTrue( childs.size( ) > 0 );
+        assertTrue( childs.stream( ).filter( id -> "archiva-global-repository-manager".equals( id ) ).findAny( ).isPresent( ) );
     }
 
     @Test
@@ -463,6 +477,47 @@
 
     }
 
+    @Test
+    void getAssignedUsers( )
+    {
+        String token = getAdminToken( );
+        Map<String, Object> jsonAsMap = new HashMap<>( );
+        jsonAsMap.put( "user_id", "aragorn" );
+        jsonAsMap.put( "email", "aragorn@lordoftherings.org" );
+        jsonAsMap.put( "full_name", "Aragorn King of Gondor " );
+        jsonAsMap.put( "password", "pAssw0rD" );
+
+        try
+        {
+            given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
+                .body( jsonAsMap )
+                .when( )
+                .post( )
+                .then( ).statusCode( 201 );
+            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .put( "system-administrator/user/aragorn" )
+                .then( ).statusCode( 200 );
+            Response result = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .get( "system-administrator/user" )
+                .prettyPeek()
+                .then( ).statusCode( 200 ).extract( ).response( );
+            assertNotNull(result);
+            PagedResult<UserInfo> userResult = result.getBody( ).jsonPath( ).getObject( "", PagedResult.class );
+            assertNotNull( userResult );
+            assertEquals( 2, userResult.getPagination( ).getTotalCount( ) );
+            List<UserInfo> users = result.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
+            assertArrayEquals( new String[] {"admin","aragorn"}, users.stream( ).map( BaseUserInfo::getUserId ).sorted().toArray(String[]::new) );
+        }
+        finally
+        {
+            given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
+                .when( )
+                .delete( "aragorn" ).then( ).statusCode( 200 );
+        }
+
+    }
 
     @Test
     void assignRole( )
@@ -503,7 +558,7 @@
         {
             given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
                 .when( )
-                .delete( "aragorn" ).then().statusCode( 200 );
+                .delete( "aragorn" ).then( ).statusCode( 200 );
         }
     }
 
@@ -546,7 +601,7 @@
         {
             given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
                 .when( )
-                .delete( "aragorn" ).then().statusCode( 200 );
+                .delete( "aragorn" ).then( ).statusCode( 200 );
         }
     }
 
@@ -605,13 +660,13 @@
         {
             given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
                 .when( )
-                .delete( "aragorn" ).then().statusCode( 200 );
+                .delete( "aragorn" ).then( ).statusCode( 200 );
             given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
-                .delete( "templates/archiva-repository-manager/repository11" ).then().statusCode( 200 );
+                .delete( "templates/archiva-repository-manager/repository11" ).then( ).statusCode( 200 );
             given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
-                .delete( "templates/archiva-repository-observer/repository11" ).then().statusCode( 200 );
+                .delete( "templates/archiva-repository-observer/repository11" ).then( ).statusCode( 200 );
 
         }
     }
@@ -659,7 +714,7 @@
         {
             given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
                 .when( )
-                .delete( "aragorn" ).then().statusCode( 200 );
+                .delete( "aragorn" ).then( ).statusCode( 200 );
         }
     }
 
@@ -709,13 +764,13 @@
         {
             given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
                 .when( )
-                .delete( "aragorn" ).then().statusCode( 200 );
+                .delete( "aragorn" ).then( ).statusCode( 200 );
             given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
-                .delete( "templates/archiva-repository-manager/repository12" ).then().statusCode( 200 );
+                .delete( "templates/archiva-repository-manager/repository12" ).then( ).statusCode( 200 );
             given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
-                .delete( "templates/archiva-repository-observer/repository12" ).then().statusCode( 200 );
+                .delete( "templates/archiva-repository-observer/repository12" ).then( ).statusCode( 200 );
 
         }
     }
@@ -748,7 +803,13 @@
             assertEquals( "This description was updated.", updatedRole.getDescription( ) );
             assertEquals( true, updatedRole.isAssignable( ) );
             assertEquals( false, updatedRole.isPermanent( ) );
-            assertArrayEquals( roleInfo.getAssignedUsers( ).toArray( ), updatedRole.getAssignedUsers( ).toArray( ) );
+            response  = given().spec(getRequestSpec(token)).contentType( JSON )
+                .when()
+                .get("archiva-repository-manager.repository13/user")
+                .then()
+                .extract( ).response( );
+            List<UserInfo> userList = response.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
+            assertEquals( 1, userList.size( ) );
         }
         finally
         {
@@ -812,8 +873,15 @@
             assertEquals( "New description", updatedRole.getDescription( ) );
             assertEquals( false, updatedRole.isAssignable( ) );
             assertEquals( true, updatedRole.isPermanent( ) );
-            assertEquals( 2, updatedRole.getAssignedUsers( ).size() );
-            assertTrue( updatedRole.getAssignedUsers( ).stream( ).filter( user -> "aragorn".equals( user.getUserId( ) ) ).findAny().isPresent() );
+
+            response  = given().spec(getRequestSpec(token)).contentType( JSON )
+                .when()
+                .get("archiva-repository-manager.repository14/user")
+                .then()
+                .extract( ).response( );
+            List<UserInfo> userList = response.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
+            assertEquals( 2, userList.size( ) );
+            assertTrue( userList.stream( ).filter( user -> "aragorn".equals( user.getUserId( ) ) ).findAny( ).isPresent( ) );
         }
         finally
         {
@@ -829,7 +897,7 @@
 
             given( ).spec( getRequestSpec( token, getUserServicePath( ) ) ).contentType( JSON )
                 .when( )
-                .delete( "aragorn" ).then().statusCode( 200 );
+                .delete( "aragorn" ).then( ).statusCode( 200 );
 
             given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
@@ -937,20 +1005,21 @@
     void updateRoleNotExist( )
     {
         String token = getAdminToken( );
-            Map<String, Object> jsonAsMap = new HashMap<>( );
-            jsonAsMap.put( "id", "abcdefg" );
-            jsonAsMap.put( "name", "abcdefg" );
-            jsonAsMap.put( "description", "This description was updated." );
-            given( ).spec( getRequestSpec( token ) ).contentType( JSON )
-                .when( )
-                .body( jsonAsMap )
-                .patch( "abcdefg" )
-                .then( ).statusCode( 404 );
+        Map<String, Object> jsonAsMap = new HashMap<>( );
+        jsonAsMap.put( "id", "abcdefg" );
+        jsonAsMap.put( "name", "abcdefg" );
+        jsonAsMap.put( "description", "This description was updated." );
+        given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+            .when( )
+            .body( jsonAsMap )
+            .patch( "abcdefg" )
+            .then( ).statusCode( 404 );
     }
 
 
     @Test
-    void getTemplates() {
+    void getTemplates( )
+    {
         String token = getAdminToken( );
         Response response = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
             .when( )
@@ -959,8 +1028,8 @@
         assertNotNull( response );
         List<RoleTemplate> templates = response.getBody( ).jsonPath( ).getList( "", RoleTemplate.class );
         assertEquals( 2, templates.size( ) );
-        assertTrue( templates.stream( ).filter( tmpl -> "archiva-repository-manager".equals( tmpl.getId( ) ) ).findAny().isPresent() );
-        assertTrue( templates.stream( ).filter( tmpl -> "archiva-repository-observer".equals( tmpl.getId( ) ) ).findAny().isPresent() );
+        assertTrue( templates.stream( ).filter( tmpl -> "archiva-repository-manager".equals( tmpl.getId( ) ) ).findAny( ).isPresent( ) );
+        assertTrue( templates.stream( ).filter( tmpl -> "archiva-repository-observer".equals( tmpl.getId( ) ) ).findAny( ).isPresent( ) );
     }
 
 }