From 6ce89971c6080f270796eef0820ae607baa894c2 Mon Sep 17 00:00:00 2001
From: Rick Herrick <jrherrick@wustl.edu>
Date: Tue, 6 Sep 2016 16:17:20 -0500
Subject: [PATCH] XNAT-2840 XNAT-4394 XNAT-4500 Added REST API functions to
 list and invalidate user sessions. Fixed investigator initialization error.
 Fixed password handling for new user operations.

---
 .../java/org/nrg/xapi/model/users/User.java   |  51 ++--
 .../org/nrg/xapi/rest/data/Investigator.java  |  46 +--
 .../nrg/xapi/rest/data/InvestigatorsApi.java  |   1 -
 .../org/nrg/xapi/rest/users/UsersApi.java     | 271 ++++++++++++++----
 4 files changed, 264 insertions(+), 105 deletions(-)

diff --git a/src/main/java/org/nrg/xapi/model/users/User.java b/src/main/java/org/nrg/xapi/model/users/User.java
index 53d039c0..b8b7574e 100644
--- a/src/main/java/org/nrg/xapi/model/users/User.java
+++ b/src/main/java/org/nrg/xapi/model/users/User.java
@@ -1,20 +1,19 @@
 package org.nrg.xapi.model.users;
 
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
-import org.apache.commons.lang3.StringUtils;
+import org.nrg.xdat.entities.UserAuthI;
 import org.nrg.xdat.om.XdatUser;
-import org.nrg.xdat.om.base.auto.AutoXdatUser;
 import org.nrg.xdat.security.XDATUser;
 import org.nrg.xft.security.UserI;
 
 import java.util.Date;
 
 @ApiModel(description = "Contains the properties that define a user on the system.")
-@JsonIgnoreProperties(value = {"FullName", "Password", "Salt", "XdatUser"}, ignoreUnknown = true)
 public class User {
     public User() {
+        // Nothing to see here...
     }
 
     public User(final String username) {
@@ -28,11 +27,11 @@ public class User {
         _lastName = user.getLastname();
         _email = user.getEmail();
         _isAdmin = (user instanceof XDATUser && ((XDATUser) user).isSiteAdmin());
-        _dbName = "";
-        _password = "";
-        _salt = "";
-        _lastModified = null;
-        _authorization = null;
+        _dbName = user.getDBName();
+        _password = user.getPassword();
+        _salt = user.getSalt();
+        _lastModified = user.getLastModified();
+        _authorization = user.getAuthorization();
         _isEnabled = user.isEnabled();
         _isVerified = user.isVerified();
     }
@@ -143,6 +142,7 @@ public class User {
      * The user's encrypted password.
      **/
     @ApiModelProperty(value = "The user's encrypted password.")
+    @JsonIgnore
     public String getPassword() {
         return _password;
     }
@@ -155,6 +155,7 @@ public class User {
      * The _salt used to encrypt the user's _password.
      **/
     @ApiModelProperty(value = "The salt used to encrypt the user's password.")
+    @JsonIgnore
     public String getSalt() {
         return _salt;
     }
@@ -180,23 +181,23 @@ public class User {
      * The user's authorization record used when logging in.
      **/
     @ApiModelProperty(value = "The user's authorization record used when logging in.")
-    public UserAuth getAuthorization() {
+    public UserAuthI getAuthorization() {
         return _authorization;
     }
 
     @SuppressWarnings("unused")
-    public void setAuthorization(UserAuth authorization) {
+    public void setAuthorization(UserAuthI authorization) {
         _authorization = authorization;
     }
 
     @ApiModelProperty(value = "The user's full name.")
+    @JsonIgnore
     public String getFullName() {
         return String.format("%s %s", getFirstName(), getLastName());
     }
 
     @Override
     public String toString() {
-
         return "class User {\n" +
                "  id: " + _id + "\n" +
                "  username: " + _username + "\n" +
@@ -211,17 +212,17 @@ public class User {
                "}\n";
     }
 
-    private Integer _id        = null;
-    private String  _username  = null;
-    private String  _firstName = null;
-    private String  _lastName  = null;
-    private String  _email     = null;
-    private boolean _isAdmin;
-    private String _dbName = null;
-    private String _password = null;
-    private String _salt = null;
-    private Date _lastModified = null;
-    private UserAuth _authorization = null;
-    private boolean _isEnabled;
-    private boolean _isVerified;
+    private Integer   _id;
+    private String    _username;
+    private String    _firstName;
+    private String    _lastName;
+    private String    _email;
+    private String    _dbName;
+    private String    _password;
+    private String    _salt;
+    private Date      _lastModified;
+    private UserAuthI _authorization;
+    private boolean   _isAdmin;
+    private boolean   _isEnabled;
+    private boolean   _isVerified;
 }
diff --git a/src/main/java/org/nrg/xapi/rest/data/Investigator.java b/src/main/java/org/nrg/xapi/rest/data/Investigator.java
index aa0d4792..a32b6843 100644
--- a/src/main/java/org/nrg/xapi/rest/data/Investigator.java
+++ b/src/main/java/org/nrg/xapi/rest/data/Investigator.java
@@ -1,7 +1,6 @@
 package org.nrg.xapi.rest.data;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import org.apache.commons.lang3.NotImplementedException;
 import org.nrg.xdat.bean.XnatInvestigatordataBean;
 import org.nrg.xdat.model.XnatInvestigatordataI;
 import org.nrg.xdat.om.XnatInvestigatordata;
@@ -19,6 +18,10 @@ import java.util.*;
  * associated.
  */
 public class Investigator implements XnatInvestigatordataI {
+    public Investigator() {
+        //
+    }
+
     public Investigator(final XnatInvestigatordataI investigator, final Collection<String> primaryProjects, final Collection<String> investigatorProjects) {
         _xnatInvestigatordataId = investigator.getXnatInvestigatordataId();
         _id = investigator.getId();
@@ -65,7 +68,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setTitle(final String title) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _title = title;
     }
 
     @Override
@@ -75,7 +78,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setFirstname(final String firstname) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _firstname = firstname;
     }
 
     @Override
@@ -85,7 +88,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setLastname(final String lastname) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _lastname = lastname;
     }
 
     @Override
@@ -95,7 +98,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setInstitution(final String institution) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _institution = institution;
     }
 
     @Override
@@ -105,7 +108,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setDepartment(final String department) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _department = department;
     }
 
     @Override
@@ -115,7 +118,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setEmail(final String email) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _email = email;
     }
 
     @Override
@@ -125,7 +128,7 @@ public class Investigator implements XnatInvestigatordataI {
 
     @Override
     public void setPhone(final String phone) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+        _phone = phone;
     }
 
     @Override
@@ -134,8 +137,8 @@ public class Investigator implements XnatInvestigatordataI {
     }
 
     @Override
-    public void setId(final String v) {
-        throw new NotImplementedException("Set methods on this class are not implemented: the class is meant to be read-only and only for reference purposes.");
+    public void setId(final String id) {
+        _id = id;
     }
 
     @Override
@@ -143,6 +146,11 @@ public class Investigator implements XnatInvestigatordataI {
         return _xnatInvestigatordataId;
     }
 
+    @SuppressWarnings("unused")
+    public void setXnatInvestigatordataId(final Integer xnatInvestigatordataId) {
+        _xnatInvestigatordataId = xnatInvestigatordataId;
+    }
+
     /**
      * Gets the list of IDs for projects on which the investigator is the primary investigator.
      *
@@ -199,15 +207,15 @@ public class Investigator implements XnatInvestigatordataI {
         return Arrays.asList(projectIds);
     }
 
-    private final Integer _xnatInvestigatordataId;
-    private final String _id;
-    private final String _title;
-    private final String _firstname;
-    private final String _lastname;
-    private final String _institution;
-    private final String _department;
-    private final String _email;
-    private final String _phone;
+    private Integer _xnatInvestigatordataId;
+    private String _id;
+    private String _title;
+    private String _firstname;
+    private String _lastname;
+    private String _institution;
+    private String _department;
+    private String _email;
+    private String _phone;
     private final Set<String> _primaryProjects      = new HashSet<>();
     private final Set<String> _investigatorProjects = new HashSet<>();
 }
diff --git a/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java b/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java
index be3b56eb..8316ff34 100644
--- a/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java
+++ b/src/main/java/org/nrg/xapi/rest/data/InvestigatorsApi.java
@@ -10,7 +10,6 @@ import org.nrg.xdat.om.XnatInvestigatordata;
 import org.nrg.xdat.rest.AbstractXapiRestController;
 import org.nrg.xdat.security.helpers.Roles;
 import org.nrg.xdat.security.services.RoleHolder;
-import org.nrg.xdat.security.services.RoleRepositoryServiceI;
 import org.nrg.xdat.security.services.UserManagementServiceI;
 import org.nrg.xft.XFTItem;
 import org.nrg.xft.event.EventUtils;
diff --git a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java
index e2d6628a..118075f8 100644
--- a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java
+++ b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java
@@ -1,7 +1,9 @@
 package org.nrg.xapi.rest.users;
 
 import io.swagger.annotations.*;
+import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.Nullable;
 import org.nrg.framework.annotations.XapiRestController;
 import org.nrg.framework.exceptions.NrgServiceError;
 import org.nrg.framework.exceptions.NrgServiceRuntimeException;
@@ -10,11 +12,13 @@ import org.nrg.xapi.rest.NotFoundException;
 import org.nrg.xdat.XDAT;
 import org.nrg.xdat.preferences.SiteConfigPreferences;
 import org.nrg.xdat.rest.AbstractXapiRestController;
+import org.nrg.xdat.security.PasswordValidatorChain;
 import org.nrg.xdat.security.UserGroupI;
 import org.nrg.xdat.security.helpers.Groups;
 import org.nrg.xdat.security.helpers.Users;
 import org.nrg.xdat.security.services.RoleHolder;
 import org.nrg.xdat.security.services.UserManagementServiceI;
+import org.nrg.xdat.security.user.exceptions.PasswordComplexityException;
 import org.nrg.xdat.security.user.exceptions.UserInitException;
 import org.nrg.xdat.security.user.exceptions.UserNotFoundException;
 import org.nrg.xdat.services.AliasTokenService;
@@ -27,6 +31,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.encoding.ShaPasswordEncoder;
+import org.springframework.security.core.session.SessionInformation;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.*;
@@ -36,9 +44,11 @@ import java.util.*;
 @RequestMapping(value = "/users")
 public class UsersApi extends AbstractXapiRestController {
     @Autowired
-    public UsersApi(final SiteConfigPreferences preferences, final UserManagementServiceI userManagementService, final RoleHolder roleHolder) {
+    public UsersApi(final SiteConfigPreferences preferences, final UserManagementServiceI userManagementService, final RoleHolder roleHolder, final SessionRegistry sessionRegistry, final PasswordValidatorChain passwordValidator) {
         super(userManagementService, roleHolder);
         _preferences = preferences;
+        _sessionRegistry = sessionRegistry;
+        _passwordValidator = passwordValidator;
     }
 
     @ApiOperation(value = "Get list of users.", notes = "The primary users function returns a list of all users of the XNAT system. This includes just the username and nothing else. You can retrieve a particular user by adding the username to the REST API URL or a list of users with abbreviated user profiles by calling /xapi/users/profiles.", response = String.class, responseContainer = "List")
@@ -46,7 +56,7 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
                    @ApiResponse(code = 403, message = "You do not have sufficient permissions to access the list of usernames."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.GET)
+    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
     @ResponseBody
     public ResponseEntity<List<String>> usersGet() {
         if (_preferences.getRestrictUserListAccessToAdmins()) {
@@ -63,7 +73,7 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
                    @ApiResponse(code = 403, message = "You do not have sufficient permissions to access the list of usernames."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"profiles"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.GET)
+    @RequestMapping(value = "profiles", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
     @ResponseBody
     public ResponseEntity<List<Map<String, String>>> usersProfilesGet() {
         if (_preferences.getRestrictUserListAccessToAdmins()) {
@@ -95,24 +105,89 @@ public class UsersApi extends AbstractXapiRestController {
         return new ResponseEntity<>(userMaps, HttpStatus.OK);
     }
 
+    @ApiOperation(value = "Get list of active users.", notes = "Returns a map of usernames for users that have at least one currently active session, i.e. logged in or associated with a valid application session. The number of active sessions and a list of the session IDs is associated with each user.", response = Map.class, responseContainer = "Map")
+    @ApiResponses({@ApiResponse(code = 200, message = "A list of active users."),
+                   @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
+                   @ApiResponse(code = 403, message = "You do not have sufficient permissions to access the list of usernames."),
+                   @ApiResponse(code = 500, message = "An unexpected error occurred.")})
+    @RequestMapping(value = "active", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    @ResponseBody
+    public ResponseEntity<Map<String, Map<String, Object>>> getActiveUsers() {
+        final HttpStatus status = isPermitted();
+        if (status != null) {
+            return new ResponseEntity<>(status);
+        }
+
+        final Map<String, Map<String, Object>> activeUsers = new HashMap<>();
+        for (final Object principal : _sessionRegistry.getAllPrincipals()) {
+            final String username;
+            if (principal instanceof String) {
+                username = (String) principal;
+            } else if (principal instanceof UserDetails) {
+                username = ((UserDetails) principal).getUsername();
+            } else {
+                username = principal.toString();
+            }
+            final Map<String, Object> sessionData = new HashMap<>();
+            final List<SessionInformation> sessions = _sessionRegistry.getAllSessions(principal, false);
+            final ArrayList<String> sessionIds = new ArrayList<>();
+            for (final SessionInformation session : sessions) {
+                sessionIds.add(session.getSessionId());
+            }
+            sessionData.put("sessions", sessionIds);
+            sessionData.put("count", sessions.size());
+            activeUsers.put(username, sessionData);
+        }
+        return new ResponseEntity<>(activeUsers, HttpStatus.OK);
+    }
+
+    @ApiOperation(value = "Get information about active sessions for the indicated user.", notes = "Returns a map containing a list of session IDs usernames for users that have at least one currently active session, i.e. logged in or associated with a valid application session. This also includes the number of active sessions for each user.", response = String.class, responseContainer = "List")
+    @ApiResponses({@ApiResponse(code = 200, message = "A list of active users."),
+                   @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
+                   @ApiResponse(code = 403, message = "You do not have sufficient permissions to access the list of usernames."),
+                   @ApiResponse(code = 404, message = "The indicated user has no active sessions or is not a valid user."),
+                   @ApiResponse(code = 500, message = "An unexpected error occurred.")})
+    @RequestMapping(value = "active/{username}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    @ResponseBody
+    public ResponseEntity<List<String>> getUserActiveSessions(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("username") final String username) {
+        final HttpStatus status = isPermitted();
+        if (status != null) {
+            return new ResponseEntity<>(status);
+        }
+
+        for (final Object principal : _sessionRegistry.getAllPrincipals()) {
+            final Object located = locatePrincipalByUsername(username);
+            if (located == null) {
+                continue;
+            }
+            final List<SessionInformation> sessions = _sessionRegistry.getAllSessions(principal, false);
+            final List<String> sessionIds = new ArrayList<>();
+            for (final SessionInformation session : sessions) {
+                sessionIds.add(session.getSessionId());
+            }
+            return new ResponseEntity<>(sessionIds, HttpStatus.OK);
+        }
+        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+    }
+
     @ApiOperation(value = "Gets the user with the specified user ID.", notes = "Returns the serialized user object with the specified user ID.", response = User.class)
     @ApiResponses({@ApiResponse(code = 200, message = "User successfully retrieved."),
                    @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
                    @ApiResponse(code = 403, message = "Not authorized to view this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.GET})
-    public ResponseEntity<User> usersIdGet(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("id") String id) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    public ResponseEntity<User> usersIdGet(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("username") final String username) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         final UserI user;
         try {
-            user = getUserManagementService().getUser(id);
+            user = getUserManagementService().getUser(username);
             return user == null ? new ResponseEntity<User>(HttpStatus.NOT_FOUND) : new ResponseEntity<>(new User(user), HttpStatus.OK);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -125,8 +200,8 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to create or update this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.PUT})
-    public ResponseEntity<User> usersIdPut(@ApiParam(value = "The username of the user to create or update.", required = true) @PathVariable("id") String username, @RequestBody User model) throws NotFoundException {
+    @RequestMapping(value = "{username}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
+    public ResponseEntity<User> usersIdPut(@ApiParam(value = "The username of the user to create or update.", required = true) @PathVariable("username") String username, @RequestBody User model) throws NotFoundException, PasswordComplexityException {
         HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
@@ -166,6 +241,20 @@ public class UsersApi extends AbstractXapiRestController {
             user.setVerified(model.isVerified());
         }
 
+        final String password;
+        if (StringUtils.isNotBlank(model.getPassword())) {
+            password = model.getPassword();
+            if (!_passwordValidator.isValid(password, user)) {
+                throw new PasswordComplexityException(_passwordValidator.getMessage());
+            }
+        } else {
+            password = RandomStringUtils.randomAscii(32);
+        }
+        final String salt = Users.createNewSalt();
+        user.setPassword(new ShaPasswordEncoder(256).encodePassword(password, salt));
+        user.setPrimaryPassword_encrypt(true);
+        user.setSalt(salt);
+
         try {
             getUserManagementService().save(user, getSessionUser(), false, new EventDetails(EventUtils.CATEGORY.DATA, EventUtils.TYPE.WEB_SERVICE, Event.Modified, "", ""));
             return new ResponseEntity<>(HttpStatus.OK);
@@ -175,26 +264,67 @@ public class UsersApi extends AbstractXapiRestController {
         return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
     }
 
+    @ApiOperation(value = "Invalidates all active sessions associated with the specified username.", notes = "Returns a list of session IDs that were invalidated.", response = String.class, responseContainer = "List")
+    @ApiResponses({@ApiResponse(code = 200, message = "User successfully invalidated."),
+                   @ApiResponse(code = 304, message = "Indicated user has no active sessions, so no action was taken."),
+                   @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
+                   @ApiResponse(code = 403, message = "Not authorized to create or update this user."),
+                   @ApiResponse(code = 404, message = "User not found."),
+                   @ApiResponse(code = 500, message = "An unexpected error occurred.")})
+    @RequestMapping(value = {"{username}", "active/{username}"}, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.DELETE)
+    public ResponseEntity<List<String>> invalidateUser(@ApiParam(value = "The username of the user to invalidate.", required = true) @PathVariable("username") String username) throws NotFoundException {
+        HttpStatus status = isPermitted(username);
+        if (status != null) {
+            return new ResponseEntity<>(status);
+        }
+        final UserI user;
+        try {
+            user = getUserManagementService().getUser(username);
+            if (user == null) {
+                return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+            }
+        } catch (UserInitException e) {
+            _log.error("An error occurred initializing the user " + username, e);
+            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+        } catch (UserNotFoundException e) {
+            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+        }
+        Object located = locatePrincipalByUsername(user.getUsername());
+        if (located == null) {
+            return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
+        }
+        final List<SessionInformation> sessions = _sessionRegistry.getAllSessions(located, false);
+        if (sessions.size() == 0) {
+            return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
+        }
+        final List<String> sessionIds = new ArrayList<>();
+        for (final SessionInformation session : sessions) {
+            sessionIds.add(session.getSessionId());
+            session.expireNow();
+        }
+        return new ResponseEntity<>(sessionIds, HttpStatus.OK);
+    }
+
     @ApiOperation(value = "Returns whether the user with the specified user ID is enabled.", notes = "Returns true or false based on whether the specified user is enabled or not.", response = Boolean.class)
     @ApiResponses({@ApiResponse(code = 200, message = "User enabled status successfully retrieved."),
                    @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
                    @ApiResponse(code = 403, message = "Not authorized to view this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/enabled"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.GET})
-    public ResponseEntity<Boolean> usersIdEnabledGet(@ApiParam(value = "The ID of the user to retrieve the enabled status for.", required = true) @PathVariable("id") String id) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/enabled", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    public ResponseEntity<Boolean> usersIdEnabledGet(@ApiParam(value = "The ID of the user to retrieve the enabled status for.", required = true) @PathVariable("username") final String username) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
             return new ResponseEntity<>(user.isEnabled(), HttpStatus.OK);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -207,14 +337,14 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to enable or disable this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/enabled/{flag}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.PUT})
-    public ResponseEntity<Boolean> usersIdEnabledFlagPut(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("id") String id, @ApiParam(value = "The value to set for the enabled status.", required = true) @PathVariable("flag") Boolean flag) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/enabled/{flag}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
+    public ResponseEntity<Boolean> usersIdEnabledFlagPut(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("username") final String username, @ApiParam(value = "The value to set for the enabled status.", required = true) @PathVariable("flag") Boolean flag) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
@@ -227,7 +357,7 @@ public class UsersApi extends AbstractXapiRestController {
             }
             return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -240,20 +370,20 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to view this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/verified"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.GET})
-    public ResponseEntity<Boolean> usersIdVerifiedGet(@ApiParam(value = "The ID of the user to retrieve the verified status for.", required = true) @PathVariable("id") String id) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/verified", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    public ResponseEntity<Boolean> usersIdVerifiedGet(@ApiParam(value = "The ID of the user to retrieve the verified status for.", required = true) @PathVariable("username") final String username) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
             return new ResponseEntity<>(user.isVerified(), HttpStatus.OK);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -266,14 +396,14 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to verify or un-verify this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/verified/{flag}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.PUT})
-    public ResponseEntity<Boolean> usersIdVerifiedFlagPut(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("id") String id, @ApiParam(value = "The value to set for the verified status.", required = true) @PathVariable("flag") Boolean flag) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/verified/{flag}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
+    public ResponseEntity<Boolean> usersIdVerifiedFlagPut(@ApiParam(value = "ID of the user to fetch", required = true) @PathVariable("username") final String username, @ApiParam(value = "The value to set for the verified status.", required = true) @PathVariable("flag") Boolean flag) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
@@ -286,7 +416,7 @@ public class UsersApi extends AbstractXapiRestController {
             }
             return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -299,13 +429,13 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to view this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/roles"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.GET})
-    public ResponseEntity<Collection<String>> usersIdRolesGet(@ApiParam(value = "The ID of the user to retrieve the roles for.", required = true) @PathVariable("id") final String id) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/roles", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    public ResponseEntity<Collection<String>> usersIdRolesGet(@ApiParam(value = "The ID of the user to retrieve the roles for.", required = true) @PathVariable("username") final String username) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
-        final Collection<String> roles = getUserRoles(id);
+        final Collection<String> roles = getUserRoles(username);
         return roles != null ? new ResponseEntity<>(roles, HttpStatus.OK) : new ResponseEntity<Collection<String>>(HttpStatus.FORBIDDEN);
     }
 
@@ -315,14 +445,15 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to enable or disable this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/roles/{role}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.PUT})
-    public ResponseEntity<Boolean> usersIdAddRole(@ApiParam(value = "ID of the user to add a role to", required = true) @PathVariable("id") String id, @ApiParam(value = "The user's new role.", required = true) @PathVariable("role") String role) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/roles/{role}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
+    public ResponseEntity<Boolean> usersIdAddRole(@ApiParam(value = "ID of the user to add a role to", required = true) @PathVariable("username") final String username,
+                                                  @ApiParam(value = "The user's new role.", required = true) @PathVariable("role") final String role) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
@@ -334,7 +465,7 @@ public class UsersApi extends AbstractXapiRestController {
             }
             return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -347,14 +478,14 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to enable or disable this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/roles/{role}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.DELETE})
-    public ResponseEntity<Boolean> usersIdRemoveRole(@ApiParam(value = "ID of the user to delete a role from", required = true) @PathVariable("id") String id, @ApiParam(value = "The user role to delete.", required = true) @PathVariable("role") String role) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/roles/{role}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.DELETE)
+    public ResponseEntity<Boolean> usersIdRemoveRole(@ApiParam(value = "ID of the user to delete a role from", required = true) @PathVariable("username") final String username, @ApiParam(value = "The user role to delete.", required = true) @PathVariable("role") String role) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
@@ -366,7 +497,7 @@ public class UsersApi extends AbstractXapiRestController {
             }
             return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -379,21 +510,21 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to view this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/groups"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.GET})
-    public ResponseEntity<Set<String>> usersIdGroupsGet(@ApiParam(value = "The ID of the user to retrieve the groups for.", required = true) @PathVariable("id") String id) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/groups", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
+    public ResponseEntity<Set<String>> usersIdGroupsGet(@ApiParam(value = "The ID of the user to retrieve the groups for.", required = true) @PathVariable("username") final String username) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
             Map<String, UserGroupI> groups = Groups.getGroupsForUser(user);
             return new ResponseEntity<>(groups.keySet(), HttpStatus.OK);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -406,14 +537,14 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to enable or disable this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/groups/{group}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.PUT})
-    public ResponseEntity<Boolean> usersIdAddGroup(@ApiParam(value = "ID of the user to add to a group", required = true) @PathVariable("id") String id, @ApiParam(value = "The user's new group.", required = true) @PathVariable("group") final String group) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/groups/{group}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
+    public ResponseEntity<Boolean> usersIdAddGroup(@ApiParam(value = "ID of the user to add to a group", required = true) @PathVariable("username") final String username, @ApiParam(value = "The user's new group.", required = true) @PathVariable("group") final String group) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
@@ -425,7 +556,7 @@ public class UsersApi extends AbstractXapiRestController {
             }
             return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@@ -438,14 +569,14 @@ public class UsersApi extends AbstractXapiRestController {
                    @ApiResponse(code = 403, message = "Not authorized to enable or disable this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
-    @RequestMapping(value = {"{id}/groups/{group}"}, produces = {MediaType.APPLICATION_JSON_VALUE}, method = {RequestMethod.DELETE})
-    public ResponseEntity<Boolean> usersIdRemoveGroup(@ApiParam(value = "ID of the user to remove from group", required = true) @PathVariable("id") final String id, @ApiParam(value = "The group to remove the user from.", required = true) @PathVariable("group") final String group) {
-        HttpStatus status = isPermitted(id);
+    @RequestMapping(value = "{username}/groups/{group}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.DELETE)
+    public ResponseEntity<Boolean> usersIdRemoveGroup(@ApiParam(value = "ID of the user to remove from group", required = true) @PathVariable("username") final String username, @ApiParam(value = "The group to remove the user from.", required = true) @PathVariable("group") final String group) {
+        HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
         try {
-            final UserI user = getUserManagementService().getUser(id);
+            final UserI user = getUserManagementService().getUser(username);
             if (user == null) {
                 return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             }
@@ -457,13 +588,31 @@ public class UsersApi extends AbstractXapiRestController {
             }
             return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserInitException e) {
-            _log.error("An error occurred initializing the user " + id, e);
+            _log.error("An error occurred initializing the user " + username, e);
             return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
         } catch (UserNotFoundException e) {
             return new ResponseEntity<>(HttpStatus.NOT_FOUND);
         }
     }
 
+    @Nullable
+    private Object locatePrincipalByUsername(final String username) {
+        Object located = null;
+        for (final Object principal : _sessionRegistry.getAllPrincipals()) {
+            if (principal instanceof String && username.equals(principal)) {
+                located = principal;
+                break;
+            } else if (principal instanceof UserDetails && username.equals(((UserDetails) principal).getUsername())) {
+                located = principal;
+                break;
+            } else if (username.equals(principal.toString())) {
+                located = principal;
+                break;
+            }
+        }
+        return located;
+    }
+
     @SuppressWarnings("unused")
     public static class Event {
         public static String Added                 = "Added User";
@@ -480,5 +629,7 @@ public class UsersApi extends AbstractXapiRestController {
 
     private static final Logger _log = LoggerFactory.getLogger(UsersApi.class);
 
-    private final SiteConfigPreferences _preferences;
+    private final SiteConfigPreferences  _preferences;
+    private final SessionRegistry        _sessionRegistry;
+    private final PasswordValidatorChain _passwordValidator;
 }
-- 
GitLab