From 87f20105767a99dc28d4afdbb9b5a6fa41605edf Mon Sep 17 00:00:00 2001 From: Rick Herrick <jrherrick@wustl.edu> Date: Wed, 7 Sep 2016 18:16:49 -0500 Subject: [PATCH] XNAT-2840 One more stab at securing user wrappers while keeping them functional. --- .../java/org/nrg/xapi/model/users/User.java | 27 ++++-- .../org/nrg/xapi/rest/users/UsersApi.java | 23 ++--- .../model/users/TestUserSerialization.java | 96 +++++++++++++------ 3 files changed, 93 insertions(+), 53 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 9e0f751e..8b774c1d 100644 --- a/src/main/java/org/nrg/xapi/model/users/User.java +++ b/src/main/java/org/nrg/xapi/model/users/User.java @@ -1,7 +1,6 @@ package org.nrg.xapi.model.users; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import org.nrg.xdat.entities.UserAuthI; @@ -11,6 +10,13 @@ import org.nrg.xft.security.UserI; import java.util.Date; +/** + * A transport container for user details. The {@link #isSecured() secured property} controls whether security-related + * properties like password and salt are available. When a new user object is created through one of the wrapper + * constructors, such as {@link #User(String)} or {@link #User(UserI)}, secure is set to true. This means that + * serializing beans with existing user accounts won't expose the password data. Newly created beans have secure set to + * false by default to allow for serializing the bean for REST calls with all data intact. + */ @ApiModel(description = "Contains the properties that define a user on the system.") public class User { public User() { @@ -35,6 +41,7 @@ public class User { _authorization = user.getAuthorization(); _isEnabled = user.isEnabled(); _isVerified = user.isVerified(); + _secured = true; } public User(final XdatUser user) { @@ -143,9 +150,8 @@ public class User { * The user's encrypted password. **/ @ApiModelProperty(value = "The user's encrypted password.") - @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String getPassword() { - return _password; + return _secured ? null : _password; } public void setPassword(String password) { @@ -156,9 +162,8 @@ public class User { * The _salt used to encrypt the user's _password. **/ @ApiModelProperty(value = "The salt used to encrypt the user's password.") - @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String getSalt() { - return _salt; + return _secured ? null : _salt; } @SuppressWarnings("unused") @@ -183,7 +188,7 @@ public class User { **/ @ApiModelProperty(value = "The user's authorization record used when logging in.") public UserAuthI getAuthorization() { - return _authorization; + return _secured ? null : _authorization; } @SuppressWarnings("unused") @@ -197,6 +202,15 @@ public class User { return String.format("%s %s", getFirstName(), getLastName()); } + @ApiModelProperty(value = "Indicates whether the user object is secured, which causes secure fields like password and salt to return null.") + public boolean isSecured() { + return _secured; + } + + public void setSecured(final boolean secured) { + _secured = secured; + } + @Override public String toString() { return "class User {\n" + @@ -221,6 +235,7 @@ public class User { private String _dbName; private String _password; private String _salt; + private boolean _secured; private Date _lastModified; private UserAuthI _authorization; private Boolean _isAdmin; 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 3e687d89..e3f1c8b0 100644 --- a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java +++ b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java @@ -78,34 +78,25 @@ public class UsersApi extends AbstractXapiRestController { @ApiResponse(code = 500, message = "An unexpected error occurred.")}) @RequestMapping(value = "profiles", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) @ResponseBody - public ResponseEntity<List<Map<String, String>>> usersProfilesGet() { + public ResponseEntity<List<User>> usersProfilesGet() { if (_preferences.getRestrictUserListAccessToAdmins()) { final HttpStatus status = isPermitted(); if (status != null) { return new ResponseEntity<>(status); } } - List<UserI> users = getUserManagementService().getUsers(); - List<Map<String, String>> userMaps = null; + final List<UserI> users = getUserManagementService().getUsers(); + final List<User> beans = new ArrayList<>(); if (users != null && users.size() > 0) { - userMaps = new ArrayList<>(); for (UserI user : users) { try { - Map<String, String> userMap = new HashMap<>(); - userMap.put("firstname", user.getFirstname()); - userMap.put("lastname", user.getLastname()); - userMap.put("username", user.getUsername()); - userMap.put("email", user.getEmail()); - userMap.put("id", String.valueOf(user.getID())); - userMap.put("enabled", String.valueOf(user.isEnabled())); - userMap.put("verified", String.valueOf(user.isVerified())); - userMaps.add(userMap); + beans.add(new User(user)); } catch (Exception e) { _log.error("", e); } } } - return new ResponseEntity<>(userMaps, HttpStatus.OK); + return new ResponseEntity<>(beans, 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") @@ -180,7 +171,7 @@ public class UsersApi extends AbstractXapiRestController { @ApiResponse(code = 404, message = "User not found."), @ApiResponse(code = 500, message = "An unexpected error occurred.")}) @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) { + public ResponseEntity<User> getUser(@ApiParam(value = "Username of the user to fetch.", required = true) @PathVariable("username") final String username) { HttpStatus status = isPermitted(username); if (status != null) { return new ResponseEntity<>(status); @@ -225,7 +216,7 @@ public class UsersApi extends AbstractXapiRestController { if (model.isEnabled() != null) { user.setEnabled(model.isEnabled()); } - if (model.isVerified()) { + if (model.isVerified() != null) { user.setVerified(model.isVerified()); } user.setPassword(model.getPassword()); diff --git a/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java b/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java index 8ac87ebd..86bc2e28 100644 --- a/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java +++ b/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java @@ -22,39 +22,73 @@ public class TestUserSerialization { } @Test - public void testHiddenProperties() throws IOException { - final User input = new User(); - input.setUsername("name"); - input.setEmail("foo@bar.com"); - input.setPassword("password"); - input.setSalt("salt"); - input.setAdmin(false); - input.setEnabled(true); - - final String json = _serializer.toJson(input); - assertNotNull(json); - assertTrue(StringUtils.isNotBlank(json)); + public void testSecuredProperties() throws IOException { + final User secured = new User(); + secured.setUsername("name"); + secured.setEmail("foo@bar.com"); + secured.setPassword("password"); + secured.setSalt("salt"); + secured.setAdmin(false); + secured.setEnabled(true); + secured.setSecured(true); + + // Users are unsecured by default when created by default constructor. + final User unsecured = new User(); + unsecured.setUsername("name"); + unsecured.setEmail("foo@bar.com"); + unsecured.setPassword("password"); + unsecured.setSalt("salt"); + unsecured.setAdmin(false); + unsecured.setEnabled(true); + + final String secureJson = _serializer.toJson(secured); + assertNotNull(secureJson); + assertTrue(StringUtils.isNotBlank(secureJson)); + final String unsecureJson = _serializer.toJson(unsecured); + assertNotNull(unsecureJson); + assertTrue(StringUtils.isNotBlank(unsecureJson)); // Here's where we make sure the password and salt aren't serialized. - final JsonNode map = _serializer.deserializeJson(json); - assertNotNull(map); - assertTrue(map.has("username")); - assertTrue(map.has("email")); - assertFalse(map.has("password")); - assertFalse(map.has("salt")); - assertTrue(map.has("admin")); - assertTrue(map.has("enabled")); - assertFalse(map.has("verified")); - - final User output = _serializer.deserializeJson(json, User.class); - assertNotNull(output); - assertTrue(StringUtils.isNotBlank(output.getUsername())); - assertTrue(StringUtils.isNotBlank(output.getEmail())); - assertTrue(StringUtils.isBlank(output.getPassword())); - assertTrue(StringUtils.isBlank(output.getSalt())); - assertFalse(output.isAdmin()); - assertTrue(output.isEnabled()); - assertNull(output.isVerified()); + final JsonNode securedMap = _serializer.deserializeJson(secureJson); + assertNotNull(securedMap); + assertTrue(securedMap.has("username")); + assertTrue(securedMap.has("email")); + assertFalse(securedMap.has("password")); + assertFalse(securedMap.has("salt")); + assertTrue(securedMap.has("admin")); + assertTrue(securedMap.has("enabled")); + assertFalse(securedMap.has("verified")); + + // Here's where we make sure the password and salt ARE serialized. + final JsonNode unsecuredMap = _serializer.deserializeJson(unsecureJson); + assertNotNull(unsecuredMap); + assertTrue(unsecuredMap.has("username")); + assertTrue(unsecuredMap.has("email")); + assertTrue(unsecuredMap.has("password")); + assertTrue(unsecuredMap.has("salt")); + assertTrue(unsecuredMap.has("admin")); + assertTrue(unsecuredMap.has("enabled")); + assertFalse(unsecuredMap.has("verified")); + + final User securedOutput = _serializer.deserializeJson(secureJson, User.class); + assertNotNull(securedOutput); + assertTrue(StringUtils.isNotBlank(securedOutput.getUsername())); + assertTrue(StringUtils.isNotBlank(securedOutput.getEmail())); + assertTrue(StringUtils.isBlank(securedOutput.getPassword())); + assertTrue(StringUtils.isBlank(securedOutput.getSalt())); + assertFalse(securedOutput.isAdmin()); + assertTrue(securedOutput.isEnabled()); + assertNull(securedOutput.isVerified()); + + final User unsecuredOutput = _serializer.deserializeJson(unsecureJson, User.class); + assertNotNull(unsecuredOutput); + assertTrue(StringUtils.isNotBlank(unsecuredOutput.getUsername())); + assertTrue(StringUtils.isNotBlank(unsecuredOutput.getEmail())); + assertTrue(StringUtils.isNotBlank(unsecuredOutput.getPassword())); + assertTrue(StringUtils.isNotBlank(unsecuredOutput.getSalt())); + assertFalse(unsecuredOutput.isAdmin()); + assertTrue(unsecuredOutput.isEnabled()); + assertNull(unsecuredOutput.isVerified()); } private SerializerService _serializer; -- GitLab