From 6aca4650e201f8236ca7526d5fded4e2bbec2176 Mon Sep 17 00:00:00 2001 From: Madawa Soysa Date: Sat, 20 Oct 2018 19:11:52 +1100 Subject: [PATCH] Introduce new API for user search based on user attributes This commit introduces a new API to search users based on user claims such as First Name, Last Name, Email, etc. This new API can be used to filter users through a search query. Also this commit includes the UI modifications to modify the exiting user listing page to allow filter users by attributes. Fixes to entgra/product-iots#6 --- .../service/api/UserManagementService.java | 86 +++++++++++ .../impl/UserManagementServiceImpl.java | 141 ++++++++++++++++++ .../impl/util/RequestValidationUtil.java | 6 + .../cdmf.page.users/public/js/listing.js | 98 ++++++++---- .../app/pages/cdmf.page.users/users.hbs | 20 ++- 5 files changed, 322 insertions(+), 29 deletions(-) diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/UserManagementService.java b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/UserManagementService.java index d0924d96c2..12630fcd68 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/UserManagementService.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/api/UserManagementService.java @@ -529,6 +529,92 @@ public interface UserManagementService { defaultValue = "5") @QueryParam("limit") int limit); + @GET + @Path(("/search")) + @ApiOperation( + produces = MediaType.APPLICATION_JSON, + httpMethod = "GET", + value = "Filter details of users based on the given claims", + notes = "You are able to manage users in WSO2 IoTS by adding, updating and removing users. If you wish to" + + " filter and get a list of users registered with WSO2 IoTS, you can do so using this REST API", + tags = "User Management", + extensions = { + @Extension(properties = { + @ExtensionProperty(name = Constants.SCOPE, value = "perm:users:user-details") + }) + } + ) + @ApiResponses(value = { + @ApiResponse( + code = 200, + message = "OK. \n Successfully fetched the list of users registered with WSO2 IoTS.", + response = BasicUserInfoList.class, + responseHeaders = { + @ResponseHeader( + name = "Content-Type", + description = "The content type of the body"), + @ResponseHeader( + name = "ETag", + description = "Entity Tag of the response resource.\n" + + "Used by caches, or in conditional requests."), + @ResponseHeader( + name = "Last-Modified", + description = "Date and time the resource was last modified.\n" + + "Used by caches, or in conditional requests."), + }), + @ApiResponse( + code = 304, + message = "Not Modified. \n Empty body because the client already has the latest version of " + + "the requested resource.\n"), + @ApiResponse( + code = 406, + message = "Not Acceptable.\n The requested media type is not supported", + response = ErrorResponse.class), + @ApiResponse( + code = 500, + message = "Internal Server Error. \n Server error occurred while fetching users.", + response = ErrorResponse.class) + }) + Response getUsers( + @ApiParam( + name = "username", + value = "Username of the user", + required = false + ) + @QueryParam("username") String username, + @ApiParam( + name = "firstName", + value = "First Name of the user", + required = false + ) + @QueryParam("firstName") String firstName, + @ApiParam( + name = "lastName", + value = "Last Name of the user", + required = false + ) + @QueryParam("lastName")String lastName, + @ApiParam( + name = "emailAddress", + value = "Email Address of the user", + required = false + ) + @QueryParam("emailAddress")String emailAddress, + @HeaderParam("If-Modified-Since") String timestamp, + @ApiParam( + name = "offset", + value = "The starting pagination index for the complete list of qualified items.", + required = false, + defaultValue = "0") + @QueryParam("offset") int offset, + @ApiParam( + name = "limit", + value = "Provide how many user details you require from the starting pagination index/offset.", + required = false, + defaultValue = "5") + @QueryParam("limit") int limit + ); + @GET @Path("/count") @ApiOperation( diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/UserManagementServiceImpl.java b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/UserManagementServiceImpl.java index 053d09f77c..a23f66a148 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/UserManagementServiceImpl.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/UserManagementServiceImpl.java @@ -426,6 +426,104 @@ public class UserManagementServiceImpl implements UserManagementService { } } + @GET + @Path("/search") + @Override + public Response getUsers(@QueryParam("username") String username, @QueryParam("firstName") String firstName, + @QueryParam("lastName") String lastName, @QueryParam("emailAddress") String emailAddress, + @HeaderParam("If-Modified-Since") String timestamp, @QueryParam("offset") int offset, + @QueryParam("limit") int limit) { + + if (RequestValidationUtil.isNonFilterRequest(username,firstName, lastName, emailAddress)) { + return getUsers(null, timestamp, offset, limit); + } + + RequestValidationUtil.validatePaginationParameters(offset, limit); + + if(log.isDebugEnabled()) { + log.debug("Filtering users - filter: {username: " + username +", firstName: " + firstName + ", lastName: " + + lastName + ", emailAddress: " + emailAddress + "}"); + } + + if (limit == 0) { + limit = Constants.DEFAULT_PAGE_LIMIT; + } + + List filteredUserList = new ArrayList<>(); + List commonUsers = null, tempList; + + try { + if (StringUtils.isNotEmpty(username)) { + commonUsers = getUserList(null, username); + } + + if (!skipSearch(commonUsers) && StringUtils.isNotEmpty(firstName)) { + tempList = getUserList(Constants.USER_CLAIM_FIRST_NAME, firstName); + if (commonUsers == null) { + commonUsers = tempList; + } else { + commonUsers.retainAll(tempList); + } + } + + if (!skipSearch(commonUsers) && StringUtils.isNotEmpty(lastName)) { + tempList = getUserList(Constants.USER_CLAIM_LAST_NAME, lastName); + if (commonUsers == null || commonUsers.size() == 0) { + commonUsers = tempList; + } else if (tempList.size() > 0){ + commonUsers.retainAll(tempList); + } + } + + if (!skipSearch(commonUsers) && StringUtils.isNotEmpty(emailAddress)) { + tempList = getUserList(Constants.USER_CLAIM_EMAIL_ADDRESS, emailAddress); + if (commonUsers == null || commonUsers.size() == 0) { + commonUsers = tempList; + } else if (tempList.size() > 0) { + commonUsers.retainAll(tempList); + } + } + + BasicUserInfo basicUserInfo; + if (commonUsers != null) { + for (String user : commonUsers) { + basicUserInfo = new BasicUserInfo(); + basicUserInfo.setUsername(user); + basicUserInfo.setEmailAddress(getClaimValue(user, Constants.USER_CLAIM_EMAIL_ADDRESS)); + basicUserInfo.setFirstname(getClaimValue(user, Constants.USER_CLAIM_FIRST_NAME)); + basicUserInfo.setLastname(getClaimValue(user, Constants.USER_CLAIM_LAST_NAME)); + filteredUserList.add(basicUserInfo); + } + } + + int toIndex = offset + limit; + int listSize = filteredUserList.size(); + int lastIndex = listSize - 1; + + List offsetList; + if (offset <= lastIndex) { + if (toIndex <= listSize) { + offsetList = filteredUserList.subList(offset, toIndex); + } else { + offsetList = filteredUserList.subList(offset, listSize); + } + } else { + offsetList = new ArrayList<>(); + } + + BasicUserInfoList result = new BasicUserInfoList(); + result.setList(offsetList); + result.setCount(commonUsers != null ? commonUsers.size() : 0); + + return Response.status(Response.Status.OK).entity(result).build(); + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of users."; + log.error(msg, e); + return Response.serverError().entity( + new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build(); + } + } + @GET @Path("/count") @Override @@ -700,4 +798,47 @@ public class UserManagementServiceImpl implements UserManagementService { return DeviceManagementConstants.EmailAttributes.DEFAULT_ENROLLMENT_TEMPLATE; } + /** + * Searches users which matches a given filter based on a claim + * + * @param claim the claim value to apply the filter. If null users will be filtered by username. + * @param filter the search query. + * @return List of users which matches. + * @throws UserStoreException If unable to search users. + */ + private ArrayList getUserList(String claim, String filter) throws UserStoreException { + String defaultFilter = "*"; + + org.wso2.carbon.user.core.UserStoreManager userStoreManager = + (org.wso2.carbon.user.core.UserStoreManager) DeviceMgtAPIUtils.getUserStoreManager(); + + String appliedFilter = filter + defaultFilter; + + String[] users; + if (log.isDebugEnabled()) { + log.debug("Searching Users - claim: " + claim + " filter: " + appliedFilter); + } + if (StringUtils.isEmpty(claim)) { + users = userStoreManager.listUsers(appliedFilter, -1); + } else { + users = userStoreManager.getUserList(claim, appliedFilter, null); + } + + if (log.isDebugEnabled()) { + log.debug("Returned user count: " + users.length); + } + + return new ArrayList<>(Arrays.asList(users)); + } + + /** + * User search provides an AND search result and if either of the filter returns an empty set of users, there is no + * need to carry on the search further. This method decides whether to carry on the search or not. + * + * @param commonUsers current filtered user list. + * @return true if further search is needed. + */ + private boolean skipSearch(List commonUsers) { + return commonUsers != null && commonUsers.size() == 0; + } } diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/util/RequestValidationUtil.java b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/util/RequestValidationUtil.java index 7c1b5dc86a..3936341384 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/util/RequestValidationUtil.java +++ b/components/device-mgt/org.wso2.carbon.device.mgt.api/src/main/java/org/wso2/carbon/device/mgt/jaxrs/service/impl/util/RequestValidationUtil.java @@ -18,6 +18,7 @@ */ package org.wso2.carbon.device.mgt.jaxrs.service.impl.util; +import org.apache.commons.lang.StringUtils; import org.wso2.carbon.device.mgt.jaxrs.beans.Scope; import org.wso2.carbon.device.mgt.common.DeviceIdentifier; import org.wso2.carbon.device.mgt.common.configuration.mgt.PlatformConfiguration; @@ -353,4 +354,9 @@ public class RequestValidationUtil { } } + public static boolean isNonFilterRequest(String username, String firstName, String lastName, String emailAddress) { + return StringUtils.isEmpty(username) && StringUtils.isEmpty(firstName) && StringUtils.isEmpty(lastName) + && StringUtils.isEmpty(emailAddress); + } + } diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/public/js/listing.js b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/public/js/listing.js index ae15c3aea9..05f582844a 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/public/js/listing.js +++ b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/public/js/listing.js @@ -275,10 +275,11 @@ function loadUsers() { $(data.users).each(function (index) { objects.push({ - filter: htmlspecialchars(data.users[index].username), - firstname: htmlspecialchars(data.users[index].firstname) ? htmlspecialchars(data.users[index].firstname) : "", - lastname: htmlspecialchars(data.users[index].lastname) ? htmlspecialchars(data.users[index].lastname) : "", + username: htmlspecialchars(data.users[index].username), + firstName: htmlspecialchars(data.users[index].firstname) ? htmlspecialchars(data.users[index].firstname) : "", + lastName: htmlspecialchars(data.users[index].lastname) ? htmlspecialchars(data.users[index].lastname) : "", emailAddress: htmlspecialchars(data.users[index].emailAddress) ? htmlspecialchars(data.users[index].emailAddress) : "", + namePattern: htmlspecialchars(data.users[index].firstname) + ' ' + htmlspecialchars(data.users[index].lastname), DT_RowId: "user-" + htmlspecialchars(data.users[index].username) }) }); @@ -304,49 +305,78 @@ function loadUsers() { //noinspection JSUnusedLocalSymbols var columns = [ { + targets: 0, class: "remove-padding icon-only content-fill", - data: null, - render: function (data, type, row, meta) { - return '
' + + data: 'username', + render: function (username, type, row, meta) { + return '
' + '' + '
'; } }, { + targets: 1, class: "", - data: null, - render: function (data, type, row, meta) { - if (!data.firstname && !data.lastname) { + data: 'namePattern', + render: function (namePattern, type, row, meta) { + if (!namePattern) { return ""; - } else if (data.firstname && data.lastname) { - return "

" + data.firstname + " " + data.lastname + "

"; + } else { + return "

" + namePattern + "

"; } } }, { + targets: 2, class: "remove-padding-top", - data: 'filter', - render: function (filter, type, row, meta) { - return '' + filter; + data: 'username', + render: function (username, type, row, meta) { + return '' + username; + } + }, + { + targets: 3, + class: "hidden", + data: 'firstName', + render: function (firstName, type, row, meta) { + if (!firstName) { + return ""; + } else if (firstName) { + return "

" + firstName + "

"; + } + } + }, + { + targets: 4, + class: "hidden", + data: 'lastName', + render: function (lastName, type, row, meta) { + if (!lastName) { + return ""; + } else if (lastName) { + return "

" + lastName + "

"; + } } }, { + targets: 5, class: "remove-padding-top", - data: null, - render: function (data, type, row, meta) { - if (!data.emailAddress) { + data: 'emailAddress', + render: function (emailAddress, type, row, meta) { + if (!emailAddress) { return ""; } else { - return "" + data.emailAddress + ""; + return "" + emailAddress + ""; } } }, { + targets: 6, class: "text-right content-fill text-left-on-grid-view no-wrap tooltip-overflow-fix", data: null, render: function (data, type, row, meta) { var editbtn = ' ' + ' ' + @@ -354,18 +384,18 @@ function loadUsers() { '' + ''; - var resetPasswordbtn = '' + '' + '' + '' + ''; - var removebtn = '' + '' + '' + @@ -375,20 +405,19 @@ function loadUsers() { var returnbtnSet = ''; var adminUser = $("#user-table").data("user"); var currentUser = $("#user-table").data("logged-user"); - if ($("#can-edit").length > 0 && adminUser !== data.filter) { + if ($("#can-edit").length > 0 && adminUser !== data.username) { returnbtnSet = returnbtnSet + editbtn; } - if ($("#can-reset-password").length > 0 && adminUser !== data.filter) { + if ($("#can-reset-password").length > 0 && adminUser !== data.username) { returnbtnSet = returnbtnSet + resetPasswordbtn; } - if ($("#can-remove").length > 0 && adminUser !== data.filter && currentUser !== data.filter) { + if ($("#can-remove").length > 0 && adminUser !== data.username && currentUser !== data.username) { returnbtnSet = returnbtnSet + removebtn; } return returnbtnSet; } } - ]; var options = { @@ -400,18 +429,31 @@ function loadUsers() { "sorting": false }; - $('#user-grid').datatables_extended_serverside_paging(settings, '/api/device-mgt/v1.0/users', dataFilter, columns, fnCreatedRow, null, options); + $('#user-grid').datatables_extended_serverside_paging( + settings, + '/api/device-mgt/v1.0/users/search', + dataFilter, + columns, + fnCreatedRow, + null, + options + ); + $(loadingContentView).hide(); } $(document).ready(function () { loadUsers(); + $(function () { $('[data-toggle="tooltip"]').tooltip() }); + if (!$("#can-invite").val()) { $("#invite-user-button").remove(); } + $("#user-grid_filter").hide(); + }); diff --git a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/users.hbs b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/users.hbs index 26f3f97444..34071d41b9 100644 --- a/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/users.hbs +++ b/components/device-mgt/org.wso2.carbon.device.mgt.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.users/users.hbs @@ -75,10 +75,25 @@ id="user-grid"> + + By Username + By First Name + By Last Name + By Email + + + + + + + + + + - +