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
merge-requests/31/head
Madawa Soysa 6 years ago
parent a01886cb37
commit 6aca4650e2

@ -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(

@ -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<BasicUserInfo> filteredUserList = new ArrayList<>();
List<String> 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<BasicUserInfo> 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 <code>null</code> users will be filtered by username.
* @param filter the search query.
* @return <code>List<String></code> of users which matches.
* @throws UserStoreException If unable to search users.
*/
private ArrayList<String> 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 <code>true</code> if further search is needed.
*/
private boolean skipSearch(List<String> commonUsers) {
return commonUsers != null && commonUsers.size() == 0;
}
}

@ -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);
}
}

@ -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 '<div class="thumbnail icon viewEnabledIcon" data-url="' + context + '/user/view?username=' + data.filter + '">' +
data: 'username',
render: function (username, type, row, meta) {
return '<div class="thumbnail icon viewEnabledIcon" data-url="' + context + '/user/view?username=' + username + '">' +
'<i class="square-element text fw fw-user" style="font-size: 74px;"></i>' +
'</div>';
}
},
{
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 "<h4>" + data.firstname + " " + data.lastname + "</h4>";
} else {
return "<h4>" + namePattern + "</h4>";
}
}
},
{
targets: 2,
class: "remove-padding-top",
data: 'filter',
render: function (filter, type, row, meta) {
return '<i class="fw-user"></i>' + filter;
data: 'username',
render: function (username, type, row, meta) {
return '<i class="fw-user"></i>' + username;
}
},
{
targets: 3,
class: "hidden",
data: 'firstName',
render: function (firstName, type, row, meta) {
if (!firstName) {
return "";
} else if (firstName) {
return "<h4>" + firstName + "</h4>";
}
}
},
{
targets: 4,
class: "hidden",
data: 'lastName',
render: function (lastName, type, row, meta) {
if (!lastName) {
return "";
} else if (lastName) {
return "<h4>" + lastName + "</h4>";
}
}
},
{
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 "<a href='mailto:" + data.emailAddress + "' ><i class='fw-mail'></i>" + data.emailAddress + "</a>";
return "<a href='mailto:" + emailAddress + "' ><i class='fw-mail'></i>" + emailAddress + "</a>";
}
}
},
{
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 = '<a data-toggle="tooltip" data-placement="top" title="Edit User"href="' + context +
'/user/edit?username=' + encodeURIComponent(data.filter) + '" data-username="' + data.filter + '" ' +
'/user/edit?username=' + encodeURIComponent(data.username) + '" data-username="' + data.username + '" ' +
'data-click-event="edit-form" ' +
'class="btn padding-reduce-on-grid-view edit-user-link" data-placement="top" data-toggle="tooltip" data-original-title="Edit"> ' +
'<span class="fw-stack"> ' +
@ -354,18 +384,18 @@ function loadUsers() {
'<i class="fw fw-edit fw-stack-1x"></i>' +
'</span><span class="hidden-xs hidden-on-grid-view">Edit</span></a>';
var resetPasswordbtn = '<a data-toggle="tooltip" data-placement="top" title="Reset Password" href="#" data-username="' + data.filter + '" data-userid="' + data.filter + '" ' +
var resetPasswordbtn = '<a data-toggle="tooltip" data-placement="top" title="Reset Password" href="#" data-username="' + data.username + '" data-userid="' + data.username + '" ' +
'data-click-event="edit-form" ' +
'onclick="javascript:resetPassword(\'' + data.filter + '\')" ' +
'onclick="javascript:resetPassword(\'' + data.username + '\')" ' +
'class="btn padding-reduce-on-grid-view remove-user-link" data-placement="top" data-toggle="tooltip" data-original-title="Reset Password">' +
'<span class="fw-stack">' +
'<i class="fw fw-circle-outline fw-stack-2x"></i>' +
'<i class="fw fw-key fw-stack-1x"></i>' +
'</span><span class="hidden-xs hidden-on-grid-view">Reset Password</span></a>';
var removebtn = '<a data-toggle="tooltip" data-placement="top" title="Remove User" href="#" data-username="' + data.filter + '" data-userid="' + data.filter + '" ' +
var removebtn = '<a data-toggle="tooltip" data-placement="top" title="Remove User" href="#" data-username="' + data.username + '" data-userid="' + data.username + '" ' +
'data-click-event="remove-form" ' +
'onclick="javascript:removeUser(\'' + data.filter + '\')" ' +
'onclick="javascript:removeUser(\'' + data.username + '\')" ' +
'class="btn padding-reduce-on-grid-view remove-user-link" data-placement="top" data-toggle="tooltip" data-original-title="Remove">' +
'<span class="fw-stack">' +
'<i class="fw fw-circle-outline fw-stack-2x"></i>' +
@ -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();
});

@ -75,10 +75,25 @@
id="user-grid">
<thead>
<tr class="sort-row">
<th class="no-sort"></th>
<th class="no-sort"></th>
<th>By Username</th>
<th>By First Name</th>
<th>By Last Name</th>
<th>By Email</th>
<th class="no-sort"></th>
</tr>
<tr class="filter-row filter-box">
<th class="no-sort"></th>
<th class="no-sort"></th>
<th data-for="By Username" class="text-filter"></th>
<th data-for="By First Name" class="text-filter"></th>
<th data-for="By Last Name" class="text-filter"></th>
<th data-for="By Email" class="text-filter"></th>
<th class="no-sort"></th>
</tr>
<tr class="bulk-action-row hidden">
<th colspan="3">
<th colspan="7">
<ul class="tiles">
<li class="square">
<a id="invite-user-link" href="#" data-click-event="remove-form" class="btn square-element"
@ -101,6 +116,9 @@
<div class="sort-title">Sort By</div>
<div class="sort-options">
<a href="#">By Username</a>
<a href="#">By First Name</a>
<a href="#">By Last Name</a>
<a href="#">By Email</a>
</div>
</div>

Loading…
Cancel
Save