This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository coselmar. See http://git.codelutin.com/coselmar.git commit b837a8107a542acdb709c6a7d3326777f8dd7ebd Author: Yannick Martel <martel@©odelutin.com> Date: Tue Nov 25 11:48:43 2014 +0100 #6014 add service for modify user & some authorization management --- .../java/fr/ifremer/coselmar/beans/UserBean.java | 9 ++ .../fr/ifremer/coselmar/beans/UserWebToken.java | 88 ++++++++++++ .../services/CoselmarWebServiceSupport.java | 41 ++++++ .../services/errors/UnauthorizedException.java | 11 ++ .../coselmar/services/v1/UsersWebService.java | 159 +++++++++++++++++++-- coselmar-rest/src/main/resources/mapping | 1 + coselmar-ui/src/main/webapp/index.html | 4 +- coselmar-ui/src/main/webapp/js/coselmar.js | 2 +- coselmar-ui/src/main/webapp/views/users/users.html | 2 +- 9 files changed, 302 insertions(+), 15 deletions(-) diff --git a/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserBean.java b/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserBean.java index 4bc9260..c0b976e 100644 --- a/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserBean.java +++ b/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserBean.java @@ -15,6 +15,7 @@ public class UserBean implements Serializable { protected String qualification; protected String organization; protected String password; + protected String newPassword; public UserBean(String id, String firstName, String name, String mail, String role, String qualification, String organization) { this.id = id; @@ -89,4 +90,12 @@ public class UserBean implements Serializable { public void setPassword(String password) { this.password = password; } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } } diff --git a/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserWebToken.java b/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserWebToken.java new file mode 100644 index 0000000..d2f273d --- /dev/null +++ b/coselmar-rest/src/main/java/fr/ifremer/coselmar/beans/UserWebToken.java @@ -0,0 +1,88 @@ +package fr.ifremer.coselmar.beans; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author ymartel <martel@codelutin.com> + */ +public class UserWebToken implements Serializable { + + public static final String CLAIMS_USER_ID = "userId"; + public static final String CLAIMS_FIRST_NAME = "firstName"; + public static final String CLAIMS_LAST_NAME = "lastName"; + public static final String CLAIMS_ROLE = "role"; + + protected String userId; + protected String firstName; + protected String lastName; + protected String role; + + public UserWebToken(String userId, String firstName, String lastName, String role) { + this.userId = userId; + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + } + + public UserWebToken(Map<String, Object> claims) { + this.userId = (String) claims.get(CLAIMS_USER_ID); + this.firstName = (String) claims.get(CLAIMS_FIRST_NAME); + this.lastName = (String) claims.get(CLAIMS_LAST_NAME); + this.role = ((String) claims.get(CLAIMS_ROLE)).toUpperCase(); + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Map<String, Object> toJwtClaims() { + + Map<String, Object> claims = new HashMap<>(); + claims.put(CLAIMS_USER_ID, this.userId); + claims.put(CLAIMS_FIRST_NAME, this.firstName); + claims.put(CLAIMS_LAST_NAME, this.lastName); + claims.put(CLAIMS_ROLE, this.role); + return claims; + } + + public static Map<String, Object> toJwtClaims(String userId, String firstName, String lastName, String role) { + + Map<String, Object> claims = new HashMap<>(); + claims.put(CLAIMS_USER_ID, userId); + claims.put(CLAIMS_FIRST_NAME, firstName); + claims.put(CLAIMS_LAST_NAME, lastName); + claims.put(CLAIMS_ROLE, role.toUpperCase()); + return claims; + } + +} diff --git a/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/CoselmarWebServiceSupport.java b/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/CoselmarWebServiceSupport.java index 8e24e97..5213d75 100644 --- a/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/CoselmarWebServiceSupport.java +++ b/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/CoselmarWebServiceSupport.java @@ -24,14 +24,26 @@ package fr.ifremer.coselmar.services; * #L% */ +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; import java.util.Date; import java.util.Locale; +import java.util.Map; +import com.auth0.jwt.JWTVerifier; +import fr.ifremer.coselmar.beans.UserWebToken; import fr.ifremer.coselmar.persistence.CoselmarPersistenceContext; import fr.ifremer.coselmar.persistence.entity.CoselmarUserTopiaDao; import fr.ifremer.coselmar.persistence.entity.DocumentTopiaDao; import fr.ifremer.coselmar.services.config.CoselmarServicesConfig; +import fr.ifremer.coselmar.services.errors.InvalidCredentialException; import fr.ifremer.coselmar.services.v1.DocumentsWebService; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.debux.webmotion.server.WebMotionController; import org.debux.webmotion.server.call.HttpContext; @@ -40,6 +52,8 @@ import org.debux.webmotion.server.call.HttpContext; */ public abstract class CoselmarWebServiceSupport extends WebMotionController implements CoselmarService { + private static final Log log = LogFactory.getLog(CoselmarWebServiceSupport.class); + private CoselmarServicesContext servicesContext; @Override @@ -106,4 +120,31 @@ public abstract class CoselmarWebServiceSupport extends WebMotionController impl getPersistenceContext().rollback(); } + protected UserWebToken checkAuthentication(String authorization) throws InvalidCredentialException { + try { + + String webSecurityKey = getServicesContext().getCoselmarServicesConfig().getWebSecurityKey(); + JWTVerifier jwtVerifier = new JWTVerifier(Base64.encodeBase64String(webSecurityKey.getBytes("utf8")), "audience"); + + String token = StringUtils.replace(authorization, "Bearer ", ""); + Map<String, Object> claims = jwtVerifier.verify(token); + UserWebToken userWebToken = new UserWebToken(claims); + return userWebToken; + + } catch (NoSuchAlgorithmException|InvalidKeyException|IOException e) { + // This should not happened or this is really exceptional ! + if (log.isErrorEnabled()) { + log.error("Error during JWT verification : wrong Algorithm !", e); + } + throw new CoselmarTechnicalException(e); + + } catch (SignatureException e) { + // Invalid Signature ! It's a Fake ! + if (log.isErrorEnabled()) { + log.error("Error during JWT verification : bad signature!", e); + } + throw new InvalidCredentialException("Error with signature"); + } + + } } diff --git a/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/errors/UnauthorizedException.java b/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/errors/UnauthorizedException.java new file mode 100644 index 0000000..f67b509 --- /dev/null +++ b/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/errors/UnauthorizedException.java @@ -0,0 +1,11 @@ +package fr.ifremer.coselmar.services.errors; + +/** + * @author ymartel (martel@codelutin.com) + */ +public class UnauthorizedException extends Exception { + + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/v1/UsersWebService.java b/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/v1/UsersWebService.java index 6e99a34..e837cbd 100644 --- a/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/v1/UsersWebService.java +++ b/coselmar-rest/src/main/java/fr/ifremer/coselmar/services/v1/UsersWebService.java @@ -1,8 +1,8 @@ package fr.ifremer.coselmar.services.v1; import java.io.StringWriter; +import java.security.InvalidParameterException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -16,6 +16,7 @@ import com.github.mustachejava.MustacheFactory; import com.google.common.base.Preconditions; import fr.ifremer.coselmar.beans.UserAccountCreatedMail; import fr.ifremer.coselmar.beans.UserBean; +import fr.ifremer.coselmar.beans.UserWebToken; import fr.ifremer.coselmar.converter.BeanEntityConverter; import fr.ifremer.coselmar.persistence.entity.CoselmarUser; import fr.ifremer.coselmar.persistence.entity.CoselmarUserRole; @@ -23,6 +24,7 @@ import fr.ifremer.coselmar.services.CoselmarTechnicalException; import fr.ifremer.coselmar.services.CoselmarWebServiceSupport; import fr.ifremer.coselmar.services.config.CoselmarServicesConfig; import fr.ifremer.coselmar.services.errors.InvalidCredentialException; +import fr.ifremer.coselmar.services.errors.UnauthorizedException; import org.apache.commons.io.Charsets; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; @@ -30,6 +32,7 @@ import org.apache.commons.mail.Email; import org.apache.commons.mail.EmailException; import org.apache.commons.mail.SimpleEmail; import org.debux.webmotion.server.render.Render; +import org.nuiton.topia.persistence.TopiaNoResultException; import static org.apache.commons.logging.LogFactory.getLog; @@ -40,7 +43,21 @@ public class UsersWebService extends CoselmarWebServiceSupport { private static final Log log = getLog(UsersWebService.class); - public UserBean getUser(String userId) { + public UserBean getUser(String userId) throws InvalidCredentialException, UnauthorizedException, TopiaNoResultException { + + // Check authentication + String authorization = getContext().getHeader("Authorization"); + UserWebToken userWebToken = checkAuthentication(authorization); + + // Who is allowed here ? Admin and user himself + if (!StringUtils.equals(userWebToken.getRole(), CoselmarUserRole.ADMIN.name()) + && !StringUtils.equals(userWebToken.getUserId(), userId)) { + if (log.isDebugEnabled()) { + String message = String.format("A non admin user try to see account details with shortId '%s'", userId); + log.debug(message); + } + throw new UnauthorizedException("Not allowed to see user details"); + } // reconstitute full id String fullId = CoselmarUser.class.getCanonicalName() + "_" + userId; @@ -69,7 +86,7 @@ public class UsersWebService extends CoselmarWebServiceSupport { return result; } - public void addUser(UserBean user) { + public void addUser(UserBean user) throws InvalidParameterException { Preconditions.checkNotNull(user); CoselmarUser userEntity = getCoselmarUserDao().create(); @@ -77,6 +94,7 @@ public class UsersWebService extends CoselmarWebServiceSupport { userEntity.setFirstname(user.getFirstName()); userEntity.setName(user.getName()); String mail = getCleanMail(user.getMail()); + checkMailUnicity(mail, null); userEntity.setMail(mail); userEntity.setRole(CoselmarUserRole.valueOf(user.getRole().toUpperCase())); userEntity.setQualification(user.getQualification()); @@ -103,8 +121,90 @@ public class UsersWebService extends CoselmarWebServiceSupport { sendMail(userAccountCreatedMail); } - public void changePassword() { - //TODO ymartel + public void modifyUser(UserBean user) throws InvalidCredentialException, UnauthorizedException, InvalidParameterException, TopiaNoResultException { + + // Check authentication + String authorization = getContext().getHeader("Authorization"); + UserWebToken userWebToken = checkAuthentication(authorization); + + boolean isAdmin = StringUtils.equals(userWebToken.getRole(), CoselmarUserRole.ADMIN.name()); + + String userId = user.getId(); + if (StringUtils.isBlank(userId)) { + throw new InvalidParameterException("User.id is mandaotry"); + } + + // Admin does not need to give password, he should not know it anyway ! + if (StringUtils.isBlank(user.getPassword()) && !isAdmin) { + throw new InvalidParameterException("User.password is mandaotry"); + } + + // Who is allowed here ? Admin and user himself only + if (!isAdmin && !StringUtils.equals(userWebToken.getUserId(), userId)) { + if (log.isDebugEnabled()) { + String message = String.format("A non admin user try to modify account details with shortId '%s'", userId); + log.debug(message); + } + throw new UnauthorizedException("Not allowed to modify user details"); + } + + // Ok, now, retrieve this user + String fullId = CoselmarUser.class.getCanonicalName() + + getPersistenceContext().getTopiaIdFactory().getSeparator() + userId; + CoselmarUser coselmarUser = getCoselmarUserDao().forTopiaIdEquals(fullId).findAny(); + + // Ok, let's start the user update ! + + // start with mail : should be unique + String mail = user.getMail(); + if (StringUtils.isNotBlank(mail)) { + checkMailUnicity(mail, fullId); + coselmarUser.setMail(mail); + } + + String firstName = user.getFirstName(); + if (StringUtils.isNotBlank(firstName)) { + coselmarUser.setFirstname(firstName); + } else { + user.setOrganization(coselmarUser.getFirstname()); + } + + String userName = user.getName(); + if (StringUtils.isNotBlank(userName)) { + coselmarUser.setName(userName); + } else { + // only in case of mail need + user.setName(coselmarUser.getName()); + } + + String userRole = user.getRole(); + if (StringUtils.isNotBlank(userRole)) { + coselmarUser.setRole(CoselmarUserRole.valueOf(userName.toUpperCase())); + } + + String organization = user.getOrganization(); + if (StringUtils.isNotBlank(organization)) { + coselmarUser.setOrganization(organization); + } + + String newPassword = user.getNewPassword(); + if (StringUtils.isNotBlank(newPassword)) { + String salt = getServicesContext().generateSalt(); + String encodedPassword = getServicesContext().encodePassword(salt, newPassword); + coselmarUser.setSalt(salt); + coselmarUser.setPassword(encodedPassword); + + //if it is a modification by Admin, send new mail to user + if (isAdmin) { + UserAccountCreatedMail userAccountCreatedMail = new UserAccountCreatedMail(getServicesContext().getLocale()); + userAccountCreatedMail.setUser(user); + userAccountCreatedMail.setPassword(newPassword); + userAccountCreatedMail.setTo(coselmarUser.getMail()); + sendMail(userAccountCreatedMail); + } + } + + commit(); } public Render login(String mail, String password) throws InvalidCredentialException { @@ -125,20 +225,31 @@ public class UsersWebService extends CoselmarWebServiceSupport { JWTSigner.Options signerOption = new JWTSigner.Options(); signerOption.setAlgorithm(Algorithm.HS512); - Map<String, Object> claims = new HashMap<>(); String userTopiaId = user.getTopiaId(); String shortId = getPersistenceContext().getTopiaIdFactory().getRandomPart(userTopiaId); - claims.put("userId", shortId); - claims.put("userFirstName", user.getFirstname()); - claims.put("userName", user.getName()); - claims.put("userRole", user.getRole()); + Map<String, Object> claims = UserWebToken.toJwtClaims(shortId, user.getFirstname(), user.getName(), user.getRole().name()); String webToken = jwtSigner.sign(claims, signerOption); return renderJSON("jwt", webToken); } - public void deleteUser(String userId) { + public void deleteUser(String userId) throws InvalidCredentialException, UnauthorizedException { + + // Check authentication + String authorization = getContext().getHeader("Authorization"); + UserWebToken userWebToken = checkAuthentication(authorization); + + boolean isAdmin = StringUtils.equals(userWebToken.getRole(), CoselmarUserRole.ADMIN.name()); + + // Only admin is authorized to do this + if (!isAdmin) { + if (log.isDebugEnabled()) { + String message = String.format("A non admin user try to delete account with shortId '%s'", userId); + log.debug(message); + } + throw new UnauthorizedException("Not allowed to delete user"); + } // reconstitute full id String fullId = CoselmarUser.class.getCanonicalName() + "_" + userId; @@ -150,6 +261,32 @@ public class UsersWebService extends CoselmarWebServiceSupport { } ///////////////////////////////////////////// + ///////////// Internal Part ///////// + ///////////////////////////////////////////// + + /** + * Check if the mail is already used by an other user + * @param mail : simply mail to check + * @param userId : the current user#id : this parameter is needed to exclude from the search this user, cause it could already have this mail + * @throws InvalidParameterException if the mail is already used. + */ + protected void checkMailUnicity(String mail, String userId) throws InvalidParameterException { + boolean mailAlreadyUsed; + + if (StringUtils.isNotBlank(userId)) { + mailAlreadyUsed = getCoselmarUserDao().forMailEquals(mail).addNotEquals(CoselmarUser.PROPERTY_TOPIA_ID, userId).exists(); + + } else { + mailAlreadyUsed = getCoselmarUserDao().forMailEquals(mail).exists(); + } + + if (mailAlreadyUsed) { + String msg = String.format("mail '%s' is already used", mail); + throw new InvalidParameterException(msg); + } + } + + ///////////////////////////////////////////// /////////////// MAIL PART /////////////// ///////////////////////////////////////////// diff --git a/coselmar-rest/src/main/resources/mapping b/coselmar-rest/src/main/resources/mapping index 9c9cfcf..89a999e 100644 --- a/coselmar-rest/src/main/resources/mapping +++ b/coselmar-rest/src/main/resources/mapping @@ -38,6 +38,7 @@ DELETE /v1/documents/{documentId} DocumentsWebService.deleteDocume GET /v1/users UsersWebService.getUsers GET /v1/users/{userId} UsersWebService.getUser +PUT /v1/users/{userId} UsersWebService.modifyUser POST /v1/users UsersWebService.addUser DELETE /v1/users/{userId} UsersWebService.deleteUser POST /v1/users/login UsersWebService.login diff --git a/coselmar-ui/src/main/webapp/index.html b/coselmar-ui/src/main/webapp/index.html index e6f8235..d9febf5 100644 --- a/coselmar-ui/src/main/webapp/index.html +++ b/coselmar-ui/src/main/webapp/index.html @@ -63,7 +63,7 @@ <nav class="hidden-xs"> <ul class="nav navbar-nav"> <a href="#" role="button" class="navbar-brand">Coselmar Traceability</a> - <li class="dropdown" ng-if="currentUser.userRole == 'ADMIN'"> + <li class="dropdown" ng-if="currentUser.role == 'ADMIN'"> <a href="#/users" class="dropdown-toggle">User</a> </li> <li class="dropdown" ng-if="currentUser"> @@ -95,7 +95,7 @@ </div> <form class="navbar-form navbar-right" role="form" ng-if="currentUser"> - <div class="form-group">{{currentUser.userFirstName}} {{currentUser.userName}}</div> + <div class="form-group">{{currentUser.firstName}} {{currentUser.lastName}}</div> <button type="submit" class="btn btn-danger" ng-click="logout()">Logout</button> </form> </nav> diff --git a/coselmar-ui/src/main/webapp/js/coselmar.js b/coselmar-ui/src/main/webapp/js/coselmar.js index 409647b..6a14cca 100644 --- a/coselmar-ui/src/main/webapp/js/coselmar.js +++ b/coselmar-ui/src/main/webapp/js/coselmar.js @@ -49,7 +49,7 @@ coselmarApp.config(['$routeProvider', function($routeProvider) { }).when('/users/:userId', { controller : 'UserViewCtrl', - templateUrl : 'views/users/users.html' + templateUrl : 'views/users/user.html' }).otherwise({ redirectTo: '/', diff --git a/coselmar-ui/src/main/webapp/views/users/users.html b/coselmar-ui/src/main/webapp/views/users/users.html index 8f2c904..0f15a7d 100644 --- a/coselmar-ui/src/main/webapp/views/users/users.html +++ b/coselmar-ui/src/main/webapp/views/users/users.html @@ -30,7 +30,7 @@ <th></th> </tr> <tr ng-repeat="user in users"> - <td><a href="#/user/{{user.id}}">{{user.firstname}} {{user.name}}</a></td> + <td><a href="#/users/{{user.id}}">{{user.firstname}} {{user.name}}</a></td> <td>{{user.mail}}</td> <td>{{user.qualification}}</td> <td>{{user.role}}</td> -- To stop receiving notification emails like this one, please contact codelutin.com SCM administrator <admin+scm@codelutin.com>.