Improving role user assignment search
diff --git a/idea.run.configuration/All Rest Services.run.xml b/idea.run.configuration/All Rest Services.run.xml
index 07152e8..010200a 100644
--- a/idea.run.configuration/All Rest Services.run.xml
+++ b/idea.run.configuration/All Rest Services.run.xml
@@ -4,7 +4,7 @@
     <useClassPathOnly />
     <extension name="coverage">
       <pattern>
-        <option name="PATTERN" value="org.apache.archiva.redback.rest.services.v2.*" />
+        <option name="PATTERN" value="org.apache.archiva.redback.rest.api.services.v2.*" />
         <option name="ENABLED" value="true" />
       </pattern>
     </extension>
diff --git a/idea.run.configuration/V2 AuthenticationServiceTest.run.xml b/idea.run.configuration/V2 AuthenticationServiceTest.run.xml
index 2e822b7..116e14c 100644
--- a/idea.run.configuration/V2 AuthenticationServiceTest.run.xml
+++ b/idea.run.configuration/V2 AuthenticationServiceTest.run.xml
@@ -4,7 +4,7 @@
     <useClassPathOnly />
     <extension name="coverage">
       <pattern>
-        <option name="PATTERN" value="org.apache.archiva.redback.rest.services.v2.*" />
+        <option name="PATTERN" value="org.apache.archiva.redback.rest.api.services.v2.*" />
         <option name="ENABLED" value="true" />
       </pattern>
     </extension>
diff --git a/idea.run.configuration/org.apache.archiva.redback.rest.services.v2 in redback-rest-services.run.xml b/idea.run.configuration/org.apache.archiva.redback.rest.services.v2 in redback-rest-services.run.xml
index f4c9c8a..f4b1844 100644
--- a/idea.run.configuration/org.apache.archiva.redback.rest.services.v2 in redback-rest-services.run.xml
+++ b/idea.run.configuration/org.apache.archiva.redback.rest.services.v2 in redback-rest-services.run.xml
@@ -4,7 +4,7 @@
     <useClassPathOnly />
     <extension name="coverage">
       <pattern>
-        <option name="PATTERN" value="org.apache.archiva.redback.rest.services.v2.*" />
+        <option name="PATTERN" value="org.apache.archiva.redback.rest.api.services.v2.*" />
         <option name="ENABLED" value="true" />
       </pattern>
     </extension>
diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/Util.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/Util.java
new file mode 100644
index 0000000..db4d4f9
--- /dev/null
+++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/Util.java
@@ -0,0 +1,59 @@
+package org.apache.archiva.redback.rest.api;/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.commons.lang3.StringUtils;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * Central utility class that may be used by service implementations.
+ *
+ * @author Martin Stockhammer <martin_s@apache.org>
+ */
+public class Util
+{
+    /**
+     * Returns <code>false</code>, if the given parameter is not present in the given uriInfo, or is present and set to 'false' or '0'.
+     * In all other cases it returns <code>true</code>.
+     *
+     * This means you can activate a flag by setting '?param', '?param=true', '?param=1', ...
+     * It is deactivated, if the parameter is absent, or '?param=false', or '?param=0'
+     *
+     * @param uriInfo the uriInfo context instance, that is used to check for the parameter
+     * @param queryParameterName the query parameter name
+     * @return
+     */
+    public static boolean isFlagSet( final UriInfo uriInfo, final String queryParameterName) {
+        MultivaluedMap<String, String> params = uriInfo.getQueryParameters( );
+        if (!params.containsKey( queryParameterName )) {
+            return false;
+        }
+        // parameter is available
+        String value = params.getFirst( queryParameterName );
+        // if its available but without a value it is flagged as present
+        if (StringUtils.isEmpty( value )) {
+            return true;
+        }
+        // if it has a value, we check for false values:
+        if ("false".equalsIgnoreCase( value ) || "0".equalsIgnoreCase( value )) {
+            return false;
+        }
+        return true;
+    }
+}
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 3aec8ba..ca7f688 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
@@ -362,6 +362,26 @@
     RoleInfo deleteRoleAssignment( @PathParam( "roleId" ) String roleId, @PathParam( "userId" ) String userId )
         throws RedbackServiceException;
 
+    /**
+     * This returns the list of assigned users to a given role. The flag "recurse" is a query parameter.
+     * If the query parameter exists and is not set to 'false', or '0', it will recurse all parent roles and return a list of users
+     * assigned to the current role and the parent roles up to the root.
+     * If the query parameter does not exist or is set to 'false' or '0', it will return only the users assigned directly
+     * to the given role.
+     *
+     * @param roleId the role identifier, for which the assigned users are returned
+     * @param recurse if the parameter does not exist or is set to 'false' or '0', only directly assigned users are returned.
+     *                If the parameter value is set to 'parentsOnly', the users assigned to all parent roles up to the root excluding the
+     *                given role are returned.
+     *                Otherwise all users assigned to the given role and all parent roles up to the root are returned.
+     * @param searchTerm the substring query term to search for in the user ids and names
+     * @param offset the offset index in the user list for paging
+     * @param limit the maximum number of users returned
+     * @param orderBy the order attributes for ordering
+     * @param order the order direction 'asc' (ascending), or 'desc' (descending)
+     * @return the list of user objects
+     * @throws RedbackServiceException
+     */
     @Path("{roleId}/user")
     @GET
     @Produces({APPLICATION_JSON})
@@ -372,7 +392,11 @@
             @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)")
+            @Parameter(name = "order", description = "The sort order. Either ascending (asc) or descending (desc)"),
+            @Parameter(name = "recurse", description = "If not present, or set to 'false' or '0', only users assigned directly to this role are returned."+
+            " If present and set to 'parentsOnly', the list of users assigned to all parents of the given role up to the root."+
+                " If present and set to any other value than 'parentsOnly', 'false' or '0', the users assigned to this role or any parent role in the hierarchy"+
+                " up to the root are returned.")
         },
         security = {
             @SecurityRequirement( name = RedbackRoleConstants.USER_MANAGEMENT_RBAC_ADMIN_OPERATION )
@@ -388,11 +412,13 @@
         }
     )
     PagedResult<UserInfo> getRoleUsers(@PathParam( "roleId" ) String roleId,
+                                       @QueryParam("recurse") String recurse,
                                        @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;
+                                       @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/v2/BaseRedbackService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/BaseRedbackService.java
index 48de1bb..a2b693d 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
@@ -37,6 +37,7 @@
 
 import javax.inject.Named;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
@@ -135,11 +136,24 @@
         }
     }
 
-    protected List<User> getAssignedRedbackUsersRecursive( org.apache.archiva.redback.rbac.Role rbacRole ) throws RbacManagerException
+    protected List<User> getAssignedRedbackUsers( Role rbacRole ) {
+        try
+        {
+            return rbacManager.getUserAssignmentsForRoles( Arrays.asList( rbacRole.getId( ) ) ).stream( ).map(
+                assignment -> getRedbackUser( assignment.getPrincipal( ) )
+            ).collect( Collectors.toList( ) );
+        }
+        catch ( RbacManagerException e )
+        {
+            throw new RuntimeException( e );
+        }
+    }
+
+    protected List<User> getAssignedRedbackUsersRecursive( final Role rbacRole, final boolean parentsOnly ) throws RbacManagerException
     {
         try
         {
-            return rbacManager.getUserAssignmentsForRoles( recurseRoles( rbacRole ).map( role -> role.getId( ) ).collect( Collectors.toList( ) ) )
+            return rbacManager.getUserAssignmentsForRoles( recurseRoles( rbacRole ).map( role -> role.getId( ) ).filter(roleId -> ((!parentsOnly) || ( !rbacRole.getId().equals(roleId)))).collect( Collectors.toList( ) ) )
                 .stream( ).map( assignment -> getRedbackUser( assignment.getPrincipal( ) ) ).collect( Collectors.toList( ) );
         }
         catch ( RuntimeException e )
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 ea0c34c..b8d78a5 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
@@ -22,6 +22,7 @@
 import org.apache.archiva.redback.rbac.RbacManagerException;
 import org.apache.archiva.redback.rbac.RbacObjectNotFoundException;
 import org.apache.archiva.redback.rest.api.MessageKeys;
+import org.apache.archiva.redback.rest.api.Util;
 import org.apache.archiva.redback.rest.api.model.ErrorMessage;
 import org.apache.archiva.redback.rest.api.model.v2.PagedResult;
 import org.apache.archiva.redback.rest.api.model.v2.Role;
@@ -426,13 +427,16 @@
     }
 
     @Override
-    public PagedResult<UserInfo> getRoleUsers( String roleId, String searchTerm, Integer offset, Integer limit, List<String> orderBy, String order )  throws RedbackServiceException
+    public PagedResult<UserInfo> getRoleUsers( String roleId, String recurse,
+                                               String searchTerm, Integer offset, Integer limit, List<String> orderBy, String order )  throws RedbackServiceException
     {
         boolean ascending = isAscending( order );
+        boolean recursePresent = Util.isFlagSet( uriInfo, "recurse" );
+        boolean parentsOnly = "parentsOnly".equals( recurse );
         try
         {
             org.apache.archiva.redback.rbac.Role rbacRole = rbacManager.getRoleById( roleId );
-            List<User> rawUsers = getAssignedRedbackUsersRecursive( rbacRole );
+            List<User> rawUsers = recursePresent ? getAssignedRedbackUsersRecursive( rbacRole, parentsOnly ) : getAssignedRedbackUsers( rbacRole );
             return getUserInfoPagedResult( rawUsers, searchTerm, offset, limit, orderBy, ascending );
         }
         catch ( RbacObjectNotFoundException e )
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 125b8f0..48028c8 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
@@ -478,7 +478,7 @@
     }
 
     @Test
-    void getAssignedUsers( )
+    void getAssignedUsersNonRecursive( )
     {
         String token = getAdminToken( );
         Map<String, Object> jsonAsMap = new HashMap<>( );
@@ -496,11 +496,54 @@
                 .then( ).statusCode( 201 );
             given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
-                .put( "system-administrator/user/aragorn" )
+                .put( "archiva-global-repository-observer/user/aragorn" )
                 .then( ).statusCode( 200 );
             Response result = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
                 .when( )
-                .get( "system-administrator/user" )
+                .get( "archiva-global-repository-observer/user" )
+                .prettyPeek()
+                .then( ).statusCode( 200 ).extract( ).response( );
+            assertNotNull(result);
+            PagedResult<UserInfo> userResult = result.getBody( ).jsonPath( ).getObject( "", PagedResult.class );
+            assertNotNull( userResult );
+            assertEquals( 1, userResult.getPagination( ).getTotalCount( ) );
+            List<UserInfo> users = result.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
+            assertArrayEquals( new String[] {"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 getAssignedUsersRecursive( )
+    {
+        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( "archiva-global-repository-observer/user/aragorn" )
+                .then( ).statusCode( 200 );
+            Response result = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .param( "recurse")
+                .get( "archiva-global-repository-observer/user" )
                 .prettyPeek()
                 .then( ).statusCode( 200 ).extract( ).response( );
             assertNotNull(result);
@@ -520,6 +563,49 @@
     }
 
     @Test
+    void getAssignedUsersRecursiveParentsOnly( )
+    {
+        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( "archiva-global-repository-observer/user/aragorn" )
+                .then( ).statusCode( 200 );
+            Response result = given( ).spec( getRequestSpec( token ) ).contentType( JSON )
+                .when( )
+                .param( "recurse","parentsOnly")
+                .get( "archiva-global-repository-observer/user" )
+                .prettyPeek()
+                .then( ).statusCode( 200 ).extract( ).response( );
+            assertNotNull(result);
+            PagedResult<UserInfo> userResult = result.getBody( ).jsonPath( ).getObject( "", PagedResult.class );
+            assertNotNull( userResult );
+            assertEquals( 1, userResult.getPagination( ).getTotalCount( ) );
+            List<UserInfo> users = result.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
+            assertArrayEquals( new String[] {"admin"}, 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( )
     {
         String token = getAdminToken( );
@@ -809,7 +895,7 @@
                 .then()
                 .extract( ).response( );
             List<UserInfo> userList = response.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
-            assertEquals( 1, userList.size( ) );
+            assertEquals( 0, userList.size( ) );
         }
         finally
         {
@@ -880,7 +966,7 @@
                 .then()
                 .extract( ).response( );
             List<UserInfo> userList = response.getBody( ).jsonPath( ).getList( "data", UserInfo.class );
-            assertEquals( 2, userList.size( ) );
+            assertEquals( 1, userList.size( ) );
             assertTrue( userList.stream( ).filter( user -> "aragorn".equals( user.getUserId( ) ) ).findAny( ).isPresent( ) );
         }
         finally