mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-15 09:46:16 +01:00
Move password logic to manager
This commit is contained in:
@@ -2,12 +2,10 @@ package sonia.scm.user;
|
|||||||
|
|
||||||
public class ChangePasswordNotAllowedException extends RuntimeException {
|
public class ChangePasswordNotAllowedException extends RuntimeException {
|
||||||
|
|
||||||
public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password";
|
public static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password";
|
||||||
@SuppressWarnings("squid:S2068")
|
|
||||||
public static final String OLD_PASSWORD_REQUIRED = "the old password is required.";
|
|
||||||
|
|
||||||
public ChangePasswordNotAllowedException(String message) {
|
public ChangePasswordNotAllowedException(String type) {
|
||||||
super(message);
|
super(String.format(WRONG_USER_TYPE, type));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package sonia.scm.user;
|
|||||||
|
|
||||||
public class InvalidPasswordException extends RuntimeException {
|
public class InvalidPasswordException extends RuntimeException {
|
||||||
|
|
||||||
public static final String INVALID_MATCHING = "The given Password does not match with the stored one.";
|
public InvalidPasswordException() {
|
||||||
|
super("The given Password does not match with the stored one.");
|
||||||
public InvalidPasswordException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,15 +50,16 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
|||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
|
||||||
import static sonia.scm.user.InvalidPasswordException.INVALID_MATCHING;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete", "changeOwnPassword"})
|
@StaticPermissions(
|
||||||
|
value = "user",
|
||||||
|
globalPermissions = {"create", "list", "autocomplete"},
|
||||||
|
permissions = {"read", "modify", "delete", "changePassword"})
|
||||||
@XmlRootElement(name = "users")
|
@XmlRootElement(name = "users")
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
|
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
|
||||||
@@ -276,25 +277,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
|
|||||||
//J+
|
//J+
|
||||||
}
|
}
|
||||||
|
|
||||||
public User changePassword(String password){
|
|
||||||
setPassword(password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Match given old password from the dto with the stored password before updating
|
|
||||||
*
|
|
||||||
* @param newPassword the new password
|
|
||||||
* @param oldPassword the old password
|
|
||||||
* @return this
|
|
||||||
*/
|
|
||||||
public User changePassword(String newPassword, String oldPassword){
|
|
||||||
if (!getPassword().equals(oldPassword)) {
|
|
||||||
throw new InvalidPasswordException(INVALID_MATCHING);
|
|
||||||
}
|
|
||||||
setPassword(newPassword);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ package sonia.scm.user;
|
|||||||
import sonia.scm.Manager;
|
import sonia.scm.Manager;
|
||||||
import sonia.scm.search.Searchable;
|
import sonia.scm.search.Searchable;
|
||||||
|
|
||||||
import java.text.MessageFormat;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The central class for managing {@link User} objects.
|
* The central class for managing {@link User} objects.
|
||||||
@@ -87,5 +83,17 @@ public interface UserManager
|
|||||||
*/
|
*/
|
||||||
Collection<User> autocomplete(String filter);
|
Collection<User> autocomplete(String filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the password of the logged in user.
|
||||||
|
* @param oldPassword The current encrypted password of the user.
|
||||||
|
* @param newPassword The new encrypted password of the user.
|
||||||
|
*/
|
||||||
|
void changePasswordForLoggedInUser(String oldPassword, String newPassword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrites the password for the given user id. This needs user write privileges.
|
||||||
|
* @param userId The id of the user to change the password for.
|
||||||
|
* @param newPassword The new encrypted password.
|
||||||
|
*/
|
||||||
|
void overwritePassword(String userId, String newPassword);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,16 @@ public class UserManagerDecorator extends ManagerDecorator<User>
|
|||||||
return decorated.autocomplete(filter);
|
return decorated.autocomplete(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
@Override
|
||||||
|
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
|
||||||
|
decorated.changePasswordForLoggedInUser(oldPassword, newPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void overwritePassword(String userId, String newPassword) {
|
||||||
|
decorated.overwritePassword(userId, newPassword);
|
||||||
|
}
|
||||||
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
private final UserManager decorated;
|
private final UserManager decorated;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public class VndMediaType {
|
|||||||
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
||||||
@SuppressWarnings("squid:S2068")
|
@SuppressWarnings("squid:S2068")
|
||||||
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
||||||
|
@SuppressWarnings("squid:S2068")
|
||||||
|
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
|
||||||
|
|
||||||
public static final String ME = PREFIX + "me" + SUFFIX;
|
public static final String ME = PREFIX + "me" + SUFFIX;
|
||||||
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
||||||
|
|||||||
@@ -85,30 +85,6 @@ public class UserITCase {
|
|||||||
.assertStatusCode(403);
|
.assertStatusCode(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void nonAdminUserShouldChangeOwnPassword() {
|
|
||||||
String newUser = "user1";
|
|
||||||
String password = "pass";
|
|
||||||
TestData.createUser(newUser, password, false, "xml", "em@l.de");
|
|
||||||
String newPassword = "new_password";
|
|
||||||
ScmRequests.start()
|
|
||||||
.requestIndexResource(newUser, password)
|
|
||||||
.assertUsersLinkDoesNotExists();
|
|
||||||
// use the users/password endpoint bypassed the index resource
|
|
||||||
ScmRequests.start()
|
|
||||||
.requestUser(newUser, password, newUser)
|
|
||||||
.assertStatusCode(200)
|
|
||||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.FALSE))
|
|
||||||
.requestChangePassword(password, newPassword) // the oldPassword is needed when the own password should be changed
|
|
||||||
.assertStatusCode(204);
|
|
||||||
// // assert password is changed -> login with the new Password
|
|
||||||
ScmRequests.start()
|
|
||||||
.requestUser(newUser, newPassword, newUser)
|
|
||||||
.assertStatusCode(200)
|
|
||||||
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.FALSE))
|
|
||||||
.assertPassword(Assert::assertNull);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||||
String newUser = "user";
|
String newUser = "user";
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package sonia.scm.it.utils;
|
|||||||
|
|
||||||
import io.restassured.RestAssured;
|
import io.restassured.RestAssured;
|
||||||
import io.restassured.response.Response;
|
import io.restassured.response.Response;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,7 +26,8 @@ import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
|
|||||||
*/
|
*/
|
||||||
public class ScmRequests {
|
public class ScmRequests {
|
||||||
|
|
||||||
private String url;
|
private static final Logger LOG = LoggerFactory.getLogger(ScmRequests.class);
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ public class ScmRequests {
|
|||||||
public ChangePasswordResponse<ChangePasswordResponse> requestUserChangePassword(String username, String password, String userPathParam, String newPassword) {
|
public ChangePasswordResponse<ChangePasswordResponse> requestUserChangePassword(String username, String password, String userPathParam, String newPassword) {
|
||||||
setUsername(username);
|
setUsername(username);
|
||||||
setPassword(password);
|
setPassword(password);
|
||||||
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_CHANGE, TestData.createPasswordChangeJson(password,newPassword)), null);
|
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password,newPassword)), null);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +88,7 @@ public class ScmRequests {
|
|||||||
* @return the response of the GET request using the given <code>url</code>
|
* @return the response of the GET request using the given <code>url</code>
|
||||||
*/
|
*/
|
||||||
private Response applyGETRequestWithQueryParams(String url, String params) {
|
private Response applyGETRequestWithQueryParams(String url, String params) {
|
||||||
|
LOG.info("GET {}", url);
|
||||||
return RestAssured.given()
|
return RestAssured.given()
|
||||||
.auth().preemptive().basic(username, password)
|
.auth().preemptive().basic(username, password)
|
||||||
.when()
|
.when()
|
||||||
@@ -127,6 +131,7 @@ public class ScmRequests {
|
|||||||
* @return the response of the PUT request using the given <code>url</code>
|
* @return the response of the PUT request using the given <code>url</code>
|
||||||
*/
|
*/
|
||||||
private Response applyPUTRequest(String url, String mediaType, String body) {
|
private Response applyPUTRequest(String url, String mediaType, String body) {
|
||||||
|
LOG.info("PUT {}", url);
|
||||||
return RestAssured.given()
|
return RestAssured.given()
|
||||||
.auth().preemptive().basic(username, password)
|
.auth().preemptive().basic(username, password)
|
||||||
.when()
|
.when()
|
||||||
@@ -144,8 +149,6 @@ public class ScmRequests {
|
|||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public class IndexResponse extends ModelResponse<IndexResponse, IndexResponse> {
|
public class IndexResponse extends ModelResponse<IndexResponse, IndexResponse> {
|
||||||
public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href";
|
public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href";
|
||||||
public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href";
|
public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href";
|
||||||
@@ -292,7 +295,9 @@ public class ScmRequests {
|
|||||||
super(response, previousResponse);
|
super(response, previousResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
|
||||||
|
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserResponse<PREV extends ModelResponse> extends ModelResponse<UserResponse<PREV>, PREV> {
|
public class UserResponse<PREV extends ModelResponse> extends ModelResponse<UserResponse<PREV>, PREV> {
|
||||||
@@ -328,7 +333,7 @@ public class ScmRequests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
|
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
|
||||||
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
|
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_OVERWRITE, createPasswordChangeJson(oldPassword, newPassword)), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ package sonia.scm.api.rest.resources;
|
|||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.github.sdorra.ssp.PermissionCheck;
|
|
||||||
import org.apache.commons.beanutils.BeanComparator;
|
import org.apache.commons.beanutils.BeanComparator;
|
||||||
import org.apache.shiro.authz.AuthorizationException;
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -64,8 +63,6 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -199,24 +196,27 @@ public abstract class AbstractManagerResource<T extends ModelObject> {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response update(T item, Function<T, PermissionCheck> permissionChecker ){
|
/**
|
||||||
Consumer<Manager> updateAction = mng -> mng.modify(item, permissionChecker);
|
* Method description
|
||||||
return applyUpdate(item, updateAction);
|
*
|
||||||
}
|
*
|
||||||
|
*
|
||||||
public Response update(String name, T item) {
|
*
|
||||||
Consumer<Manager> updateAction = mng -> mng.modify(item);
|
* @param name
|
||||||
return applyUpdate(item, updateAction);
|
* @param item
|
||||||
}
|
*
|
||||||
|
*
|
||||||
public Response applyUpdate(T item, Consumer<Manager> updateAction) {
|
* @return
|
||||||
|
*/
|
||||||
|
public Response update(String name, T item)
|
||||||
|
{
|
||||||
Response response = null;
|
Response response = null;
|
||||||
|
|
||||||
preUpdate(item);
|
preUpdate(item);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
updateAction.accept(manager);
|
manager.modify(item);
|
||||||
response = Response.noContent().build();
|
response = Response.noContent().build();
|
||||||
}
|
}
|
||||||
catch (AuthorizationException ex)
|
catch (AuthorizationException ex)
|
||||||
|
|||||||
@@ -47,54 +47,6 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
|||||||
return singleAdapter.get(loadBy(id), mapToDto);
|
return singleAdapter.get(loadBy(id), mapToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the authenticated user is the same user that want to change password than return the changeOwnPassword verification function
|
|
||||||
* if the authenticated user is different he should have the modify permission to be able to modify passwords of other users
|
|
||||||
*
|
|
||||||
* @param usernameToChangePassword the user name of the user we want to change password
|
|
||||||
* @return function to verify permission
|
|
||||||
*/
|
|
||||||
private Function<MODEL_OBJECT, PermissionCheck> getChangePasswordPermission(String usernameToChangePassword) {
|
|
||||||
AssertUtil.assertIsNotEmpty(usernameToChangePassword);
|
|
||||||
return model -> {
|
|
||||||
User user = (User) model;
|
|
||||||
if (usernameToChangePassword.equals(AuthenticationUtil.getAuthenticatedUsername())) {
|
|
||||||
return UserPermissions.changeOwnPassword();
|
|
||||||
}
|
|
||||||
return UserPermissions.modify(user);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a user can modify the password
|
|
||||||
*
|
|
||||||
* 1 - the permission changeOwnPassword should be checked
|
|
||||||
* 2 - Only account of the default type "xml" can change their password
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private Consumer<MODEL_OBJECT> getChangePasswordChecker() {
|
|
||||||
return model -> {
|
|
||||||
User user = (User) model;
|
|
||||||
UserPermissions.changeOwnPassword().check();
|
|
||||||
UserManager userManager = (UserManager) manager;
|
|
||||||
if (!userManager.isTypeDefault(user)) {
|
|
||||||
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Response changePassword(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges ) throws ConcurrentModificationException {
|
|
||||||
return singleAdapter.changePassword(
|
|
||||||
loadBy(id),
|
|
||||||
applyChanges,
|
|
||||||
idStaysTheSame(id),
|
|
||||||
getChangePasswordChecker(),
|
|
||||||
getChangePasswordPermission(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws ConcurrentModificationException {
|
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws ConcurrentModificationException {
|
||||||
return singleAdapter.update(
|
return singleAdapter.update(
|
||||||
loadBy(id),
|
loadBy(id),
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
|||||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.apache.shiro.authc.credential.PasswordService;
|
import org.apache.shiro.authc.credential.PasswordService;
|
||||||
import sonia.scm.ConcurrentModificationException;
|
|
||||||
import sonia.scm.user.ChangePasswordNotAllowedException;
|
|
||||||
import sonia.scm.user.InvalidPasswordException;
|
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.validation.Valid;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
@@ -22,9 +20,6 @@ import javax.ws.rs.core.Context;
|
|||||||
import javax.ws.rs.core.Request;
|
import javax.ws.rs.core.Request;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import static sonia.scm.user.InvalidPasswordException.INVALID_MATCHING;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,12 +73,8 @@ public class MeResource {
|
|||||||
})
|
})
|
||||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||||
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
||||||
public Response changePassword(PasswordChangeDto passwordChangeDto) throws ConcurrentModificationException {
|
public Response changePassword(@Valid PasswordChangeDto passwordChangeDto) {
|
||||||
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChangeDto.getOldPassword()), passwordService.encryptPassword(passwordChangeDto.getNewPassword()));
|
||||||
if (passwordChangeDto.getOldPassword() == null){
|
return Response.noContent().build();
|
||||||
throw new ChangePasswordNotAllowedException(ChangePasswordNotAllowedException.OLD_PASSWORD_REQUIRED);
|
|
||||||
}
|
|
||||||
return adapter.changePassword(name, user -> user.clone().changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword()), passwordService.encryptPassword(passwordChangeDto.getOldPassword())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.hibernate.validator.constraints.NotEmpty;
|
|||||||
@ToString
|
@ToString
|
||||||
public class PasswordChangeDto {
|
public class PasswordChangeDto {
|
||||||
|
|
||||||
|
@NotEmpty
|
||||||
private String oldPassword;
|
private String oldPassword;
|
||||||
|
|
||||||
@NotEmpty
|
@NotEmpty
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.hibernate.validator.constraints.NotEmpty;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@ToString
|
||||||
|
public class PasswordOverwriteDto {
|
||||||
|
@NotEmpty
|
||||||
|
private String newPassword;
|
||||||
|
}
|
||||||
@@ -87,7 +87,7 @@ class ResourceLinks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String passwordChange(String name) {
|
public String passwordChange(String name) {
|
||||||
return userLinkBuilder.method("getUserResource").parameters(name).method("changePassword").parameters().href();
|
return userLinkBuilder.method("getUserResource").parameters(name).method("overwritePassword").parameters().href();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import com.github.sdorra.ssp.PermissionCheck;
|
|
||||||
import de.otto.edison.hal.HalRepresentation;
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
import sonia.scm.ConcurrentModificationException;
|
import sonia.scm.ConcurrentModificationException;
|
||||||
import sonia.scm.Manager;
|
import sonia.scm.Manager;
|
||||||
@@ -17,6 +16,8 @@ import java.util.function.Function;
|
|||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}).
|
* Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}).
|
||||||
*
|
*
|
||||||
@@ -53,32 +54,26 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
|||||||
.map(Response.ResponseBuilder::build)
|
.map(Response.ResponseBuilder::build)
|
||||||
.orElseThrow(NotFoundException::new);
|
.orElseThrow(NotFoundException::new);
|
||||||
}
|
}
|
||||||
public Response changePassword(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey, Consumer<MODEL_OBJECT> checker, Function<MODEL_OBJECT, PermissionCheck> permissionCheck) throws ConcurrentModificationException {
|
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
|
||||||
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
|
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
|
||||||
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
|
|
||||||
checkForUpdate(hasSameKey, existingModelObject, changedModelObject);
|
|
||||||
checker.accept(existingModelObject);
|
checker.accept(existingModelObject);
|
||||||
return update(changedModelObject, permissionCheck);
|
return update(reader,applyChanges,hasSameKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the model object for the given id according to the given function and returns a corresponding http response.
|
* Update the model object for the given id according to the given function and returns a corresponding http response.
|
||||||
* This handles all corner cases, eg. no matching object for the id or missing privileges.
|
* This handles all corner cases, eg. no matching object for the id or missing privileges.
|
||||||
*/
|
*/
|
||||||
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) throws ConcurrentModificationException {
|
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) throws NotFoundException, ConcurrentModificationException {
|
||||||
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
|
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
|
||||||
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
|
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
|
||||||
checkForUpdate(hasSameKey, existingModelObject, changedModelObject);
|
|
||||||
return update(getId(existingModelObject), changedModelObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkForUpdate(Predicate<MODEL_OBJECT> hasSameKey, MODEL_OBJECT existingModelObject, MODEL_OBJECT changedModelObject) throws ConcurrentModificationException {
|
|
||||||
if (!hasSameKey.test(changedModelObject)) {
|
if (!hasSameKey.test(changedModelObject)) {
|
||||||
throw new IllegalArgumentException("illegal change of id");
|
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
|
||||||
}
|
}
|
||||||
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
|
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
|
||||||
throw new ConcurrentModificationException();
|
throw new ConcurrentModificationException();
|
||||||
}
|
}
|
||||||
|
return update(getId(existingModelObject), changedModelObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) {
|
private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) {
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package sonia.scm.api.v2.resources;
|
|||||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||||
import org.apache.shiro.SecurityUtils;
|
|
||||||
import org.apache.shiro.authc.credential.PasswordService;
|
import org.apache.shiro.authc.credential.PasswordService;
|
||||||
import sonia.scm.ConcurrentModificationException;
|
import sonia.scm.ConcurrentModificationException;
|
||||||
import sonia.scm.user.ChangePasswordNotAllowedException;
|
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
@@ -116,11 +114,11 @@ public class UserResource {
|
|||||||
* <strong>Note:</strong> This method requires "user:changeOwnPassword" privilege to modify the own password.
|
* <strong>Note:</strong> This method requires "user:changeOwnPassword" privilege to modify the own password.
|
||||||
*
|
*
|
||||||
* @param name name of the user to be modified
|
* @param name name of the user to be modified
|
||||||
* @param passwordChangeDto change password object to modify password. the old password is here not required
|
* @param passwordOverwriteDto change password object to modify password. the old password is here not required
|
||||||
*/
|
*/
|
||||||
@PUT
|
@PUT
|
||||||
@Path("password")
|
@Path("password")
|
||||||
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
@Consumes(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
@StatusCodes({
|
@StatusCodes({
|
||||||
@ResponseCode(code = 204, condition = "update success"),
|
@ResponseCode(code = 204, condition = "update success"),
|
||||||
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
|
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
|
||||||
@@ -130,12 +128,8 @@ public class UserResource {
|
|||||||
@ResponseCode(code = 500, condition = "internal server error")
|
@ResponseCode(code = 500, condition = "internal server error")
|
||||||
})
|
})
|
||||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||||
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws ConcurrentModificationException {
|
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwriteDto) {
|
||||||
String currentUserName = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwriteDto.getNewPassword()));
|
||||||
if (currentUserName.equals(name) && passwordChangeDto.getOldPassword() == null){
|
return Response.noContent().build();
|
||||||
throw new ChangePasswordNotAllowedException(ChangePasswordNotAllowedException.OLD_PASSWORD_REQUIRED);
|
|
||||||
}
|
|
||||||
return adapter.changePassword(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
|
|||||||
}
|
}
|
||||||
if (UserPermissions.modify(user).isPermitted()) {
|
if (UserPermissions.modify(user).isPermitted()) {
|
||||||
linksBuilder.single(link("update", resourceLinks.user().update(target.getName())));
|
linksBuilder.single(link("update", resourceLinks.user().update(target.getName())));
|
||||||
}
|
if (userManager.isTypeDefault(user)) {
|
||||||
if (userManager.isTypeDefault(user)) {
|
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
|
||||||
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
|
}
|
||||||
}
|
}
|
||||||
target.add(linksBuilder.build());
|
target.add(linksBuilder.build());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
|||||||
builder.add(canReadOwnUser(user));
|
builder.add(canReadOwnUser(user));
|
||||||
builder.add(getUserAutocompletePermission());
|
builder.add(getUserAutocompletePermission());
|
||||||
builder.add(getGroupAutocompletePermission());
|
builder.add(getGroupAutocompletePermission());
|
||||||
builder.add(getChangeOwnPasswordPermission());
|
builder.add(getChangeOwnPasswordPermission(user));
|
||||||
permissions = builder.build();
|
permissions = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,8 +273,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
|||||||
return GroupPermissions.autocomplete().asShiroString();
|
return GroupPermissions.autocomplete().asShiroString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getChangeOwnPasswordPermission() {
|
private String getChangeOwnPasswordPermission(User user) {
|
||||||
return UserPermissions.changeOwnPassword().asShiroString();
|
return UserPermissions.changePassword(user).asShiroString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getUserAutocompletePermission() {
|
private String getUserAutocompletePermission() {
|
||||||
|
|||||||
@@ -33,12 +33,10 @@
|
|||||||
|
|
||||||
package sonia.scm.user;
|
package sonia.scm.user;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import com.github.sdorra.ssp.PermissionActionCheck;
|
import com.github.sdorra.ssp.PermissionActionCheck;
|
||||||
import com.github.sdorra.ssp.PermissionCheck;
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.AlreadyExistsException;
|
import sonia.scm.AlreadyExistsException;
|
||||||
@@ -64,9 +62,6 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -196,15 +191,10 @@ public class DefaultUserManager extends AbstractUserManager
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void modify(User user) {
|
public void modify(User user) {
|
||||||
modify(user,UserPermissions::modify);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void modify(User user, Function<User, PermissionCheck> permissionChecker) {
|
|
||||||
logger.info("modify user {} of type {}", user.getName(), user.getType());
|
logger.info("modify user {} of type {}", user.getName(), user.getType());
|
||||||
managerDaoAdapter.modify(
|
managerDaoAdapter.modify(
|
||||||
user,
|
user,
|
||||||
permissionChecker,
|
UserPermissions::modify,
|
||||||
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified),
|
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified),
|
||||||
notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified));
|
notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified));
|
||||||
}
|
}
|
||||||
@@ -408,6 +398,36 @@ public class DefaultUserManager extends AbstractUserManager
|
|||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
|
||||||
|
User user = get((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal());
|
||||||
|
|
||||||
|
if (!user.getPassword().equals(oldPassword)) {
|
||||||
|
throw new InvalidPasswordException();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setPassword(newPassword);
|
||||||
|
|
||||||
|
managerDaoAdapter.modify(
|
||||||
|
user,
|
||||||
|
UserPermissions::changePassword,
|
||||||
|
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified),
|
||||||
|
notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void overwritePassword(String userId, String newPassword) {
|
||||||
|
User user = get(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
if (!isTypeDefault(user)) {
|
||||||
|
throw new ChangePasswordNotAllowedException(user.getType());
|
||||||
|
}
|
||||||
|
user.setPassword(newPassword);
|
||||||
|
this.modify(user);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.junit.Test;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import sonia.scm.user.InvalidPasswordException;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
@@ -27,6 +28,7 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.mockito.MockitoAnnotations.initMocks;
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
@@ -97,6 +99,7 @@ public class MeResourceTest {
|
|||||||
public void shouldEncryptPasswordBeforeChanging() throws Exception {
|
public void shouldEncryptPasswordBeforeChanging() throws Exception {
|
||||||
String newPassword = "pwd123";
|
String newPassword = "pwd123";
|
||||||
String encryptedNewPassword = "encrypted123";
|
String encryptedNewPassword = "encrypted123";
|
||||||
|
String encryptedOldPassword = "encryptedOld";
|
||||||
String oldPassword = "secret";
|
String oldPassword = "secret";
|
||||||
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
@@ -104,33 +107,32 @@ public class MeResourceTest {
|
|||||||
.contentType(VndMediaType.PASSWORD_CHANGE)
|
.contentType(VndMediaType.PASSWORD_CHANGE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
when(passwordService.encryptPassword(newPassword)).thenReturn(encryptedNewPassword);
|
when(passwordService.encryptPassword(newPassword)).thenReturn(encryptedNewPassword);
|
||||||
when(passwordService.encryptPassword(oldPassword)).thenReturn("secret");
|
when(passwordService.encryptPassword(oldPassword)).thenReturn(encryptedOldPassword);
|
||||||
ArgumentCaptor<User> modifyUserCaptor = ArgumentCaptor.forClass(User.class);
|
|
||||||
doNothing().when(userManager).modify(modifyUserCaptor.capture(), any());
|
ArgumentCaptor<String> encryptedOldPasswordCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
ArgumentCaptor<String> encryptedNewPasswordCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
doNothing().when(userManager).changePasswordForLoggedInUser(encryptedOldPasswordCaptor.capture(), encryptedNewPasswordCaptor.capture());
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||||
verify(userManager).modify(any(), any());
|
assertEquals(encryptedNewPassword, encryptedNewPasswordCaptor.getValue());
|
||||||
User updatedUser = modifyUserCaptor.getValue();
|
assertEquals(encryptedOldPassword, encryptedOldPasswordCaptor.getValue());
|
||||||
assertEquals(encryptedNewPassword, updatedUser.getPassword());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "trillian", password = "secret")
|
@SubjectAware(username = "trillian", password = "secret")
|
||||||
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
|
public void shouldGet400OnMissingOldPassword() throws Exception {
|
||||||
originalUser.setType("not an xml type");
|
originalUser.setType("not an xml type");
|
||||||
String newPassword = "pwd123";
|
String newPassword = "pwd123";
|
||||||
String oldPassword = "secret";
|
String content = String.format("{ \"newPassword\": \"%s\" }", newPassword);
|
||||||
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.put("/" + MeResource.ME_PATH_V2 + "password")
|
.put("/" + MeResource.ME_PATH_V2 + "password")
|
||||||
.contentType(VndMediaType.PASSWORD_CHANGE)
|
.contentType(VndMediaType.PASSWORD_CHANGE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
when(passwordService.encryptPassword(eq(newPassword))).thenReturn("encrypted123");
|
|
||||||
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
@@ -139,17 +141,34 @@ public class MeResourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "trillian", password = "secret")
|
@SubjectAware(username = "trillian", password = "secret")
|
||||||
public void shouldGet400OnChangePasswordIfOldPasswordDoesNotMatchOriginalPassword() throws Exception {
|
public void shouldGet400OnMissingEmptyPassword() throws Exception {
|
||||||
String newPassword = "pwd123";
|
String newPassword = "pwd123";
|
||||||
String oldPassword = "notEncriptedSecret";
|
String oldPassword = "";
|
||||||
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.put("/" + MeResource.ME_PATH_V2 + "password")
|
.put("/" + MeResource.ME_PATH_V2 + "password")
|
||||||
.contentType(VndMediaType.PASSWORD_CHANGE)
|
.contentType(VndMediaType.PASSWORD_CHANGE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
|
||||||
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("differentThanSecret");
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "trillian", password = "secret")
|
||||||
|
public void shouldMapExceptionFromManager() throws Exception {
|
||||||
|
String newPassword = "pwd123";
|
||||||
|
String oldPassword = "secret";
|
||||||
|
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.put("/" + MeResource.ME_PATH_V2 + "password")
|
||||||
|
.contentType(VndMediaType.PASSWORD_CHANGE)
|
||||||
|
.content(content.getBytes());
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
doThrow(InvalidPasswordException.class).when(userManager).changePasswordForLoggedInUser(any(), any());
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import org.junit.Test;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.PageResult;
|
import sonia.scm.PageResult;
|
||||||
|
import sonia.scm.user.ChangePasswordNotAllowedException;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserManager;
|
import sonia.scm.user.UserManager;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
@@ -31,6 +33,7 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -142,7 +145,7 @@ public class UserRootResourceTest {
|
|||||||
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
.contentType(VndMediaType.PASSWORD_CHANGE)
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
||||||
@@ -150,26 +153,61 @@ public class UserRootResourceTest {
|
|||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||||
verify(userManager).modify(any(), any());
|
verify(userManager).overwritePassword("Neo", "encrypted123");
|
||||||
User updatedUser = userCaptor.getValue();
|
|
||||||
assertEquals("encrypted123", updatedUser.getPassword());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
|
public void shouldGet400OnOverwritePasswordWhenManagerThrowsNotAllowed() throws Exception {
|
||||||
originalUser.setType("not an xml type");
|
originalUser.setType("not an xml type");
|
||||||
String newPassword = "pwd123";
|
String newPassword = "pwd123";
|
||||||
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
||||||
MockHttpRequest request = MockHttpRequest
|
MockHttpRequest request = MockHttpRequest
|
||||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
.contentType(VndMediaType.PASSWORD_CHANGE)
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
|
.content(content.getBytes());
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
doThrow(ChangePasswordNotAllowedException.class).when(userManager).overwritePassword(any(), any());
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldGet404OnOverwritePasswordWhenNotFound() throws Exception {
|
||||||
|
originalUser.setType("not an xml type");
|
||||||
|
String newPassword = "pwd123";
|
||||||
|
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
|
.content(content.getBytes());
|
||||||
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
|
|
||||||
|
doThrow(NotFoundException.class).when(userManager).overwritePassword(any(), any());
|
||||||
|
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldEncryptPasswordOnOverwritePassword() throws Exception {
|
||||||
|
originalUser.setType("not an xml type");
|
||||||
|
String newPassword = "pwd123";
|
||||||
|
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
||||||
|
MockHttpRequest request = MockHttpRequest
|
||||||
|
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||||
|
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||||
.content(content.getBytes());
|
.content(content.getBytes());
|
||||||
MockHttpResponse response = new MockHttpResponse();
|
MockHttpResponse response = new MockHttpResponse();
|
||||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
|
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||||
|
verify(userManager).overwritePassword("Neo", "encrypted123");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class UserToUserDtoMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
|
public void shouldGetPasswordLinkForAdmin() {
|
||||||
User user = createDefaultUser();
|
User user = createDefaultUser();
|
||||||
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
|
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
|
||||||
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
|
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
|
||||||
@@ -73,14 +73,15 @@ public class UserToUserDtoMapperTest {
|
|||||||
UserDto userDto = mapper.map(user);
|
UserDto userDto = mapper.map(user);
|
||||||
|
|
||||||
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
|
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
|
||||||
|
}
|
||||||
|
|
||||||
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
|
@Test
|
||||||
userDto = mapper.map(user);
|
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
|
||||||
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
|
User user = createDefaultUser();
|
||||||
|
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
|
||||||
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
|
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
|
||||||
|
|
||||||
userDto = mapper.map(user);
|
UserDto userDto = mapper.map(user);
|
||||||
|
|
||||||
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
|
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
AuthorizationInfo authInfo = collector.collect();
|
AuthorizationInfo authInfo = collector.collect();
|
||||||
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
|
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
|
||||||
assertThat(authInfo.getStringPermissions(), hasSize(4));
|
assertThat(authInfo.getStringPermissions(), hasSize(4));
|
||||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changeOwnPassword", "user:read:trillian"));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:read:trillian"));
|
||||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
AuthorizationInfo authInfo = collector.collect();
|
AuthorizationInfo authInfo = collector.collect();
|
||||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changeOwnPassword", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,7 +225,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
AuthorizationInfo authInfo = collector.collect();
|
AuthorizationInfo authInfo = collector.collect();
|
||||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changeOwnPassword"));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changePassword:trillian"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void authenticate(User user, String group, String... groups) {
|
private void authenticate(User user, String group, String... groups) {
|
||||||
|
|||||||
@@ -39,9 +39,13 @@ import com.github.sdorra.shiro.ShiroRule;
|
|||||||
import com.github.sdorra.shiro.SubjectAware;
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||||
import sonia.scm.user.xml.XmlUserDAO;
|
import sonia.scm.user.xml.XmlUserDAO;
|
||||||
import sonia.scm.util.MockUtil;
|
import sonia.scm.util.MockUtil;
|
||||||
@@ -70,6 +74,10 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
|
||||||
|
private UserDAO userDAO = mock(UserDAO.class);
|
||||||
|
private User trillian;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
@@ -82,6 +90,16 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
|||||||
return new DefaultUserManager(createXmlUserDAO());
|
return new DefaultUserManager(createXmlUserDAO());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void initDao() {
|
||||||
|
trillian = UserTestData.createTrillian();
|
||||||
|
trillian.setPassword("oldEncrypted");
|
||||||
|
|
||||||
|
userDAO = mock(UserDAO.class);
|
||||||
|
when(userDAO.getType()).thenReturn("xml");
|
||||||
|
when(userDAO.get("trillian")).thenReturn(trillian);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
*
|
*
|
||||||
@@ -89,7 +107,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
|||||||
@Test
|
@Test
|
||||||
public void testDefaultAccountAfterFristStart()
|
public void testDefaultAccountAfterFristStart()
|
||||||
{
|
{
|
||||||
UserDAO userDAO = mock(UserDAO.class);
|
|
||||||
List<User> users = Lists.newArrayList(new User("tuser"));
|
List<User> users = Lists.newArrayList(new User("tuser"));
|
||||||
|
|
||||||
when(userDAO.getAll()).thenReturn(users);
|
when(userDAO.getAll()).thenReturn(users);
|
||||||
@@ -108,8 +125,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void testDefaultAccountCreation()
|
public void testDefaultAccountCreation()
|
||||||
{
|
{
|
||||||
UserDAO userDAO = mock(UserDAO.class);
|
|
||||||
|
|
||||||
when(userDAO.getAll()).thenReturn(Collections.EMPTY_LIST);
|
when(userDAO.getAll()).thenReturn(Collections.EMPTY_LIST);
|
||||||
|
|
||||||
UserManager userManager = new DefaultUserManager(userDAO);
|
UserManager userManager = new DefaultUserManager(userDAO);
|
||||||
@@ -118,6 +133,55 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
|||||||
verify(userDAO, times(2)).add(any(User.class));
|
verify(userDAO, times(2)).add(any(User.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = InvalidPasswordException.class)
|
||||||
|
public void shouldFailChangePasswordForWrongOldPassword() {
|
||||||
|
UserManager userManager = new DefaultUserManager(userDAO);
|
||||||
|
|
||||||
|
userManager.changePasswordForLoggedInUser("wrongPassword", "---");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSucceedChangePassword() {
|
||||||
|
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||||
|
|
||||||
|
doNothing().when(userDAO).modify(userCaptor.capture());
|
||||||
|
|
||||||
|
UserManager userManager = new DefaultUserManager(userDAO);
|
||||||
|
|
||||||
|
userManager.changePasswordForLoggedInUser("oldEncrypted", "newEncrypted");
|
||||||
|
|
||||||
|
Assertions.assertThat(userCaptor.getValue().getPassword()).isEqualTo("newEncrypted");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = ChangePasswordNotAllowedException.class)
|
||||||
|
public void shouldFailOverwritePasswordForWrongType() {
|
||||||
|
trillian.setType("wrongType");
|
||||||
|
|
||||||
|
UserManager userManager = new DefaultUserManager(userDAO);
|
||||||
|
|
||||||
|
userManager.overwritePassword("trillian", "---");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = NotFoundException.class)
|
||||||
|
public void shouldFailOverwritePasswordForMissingUser() {
|
||||||
|
UserManager userManager = new DefaultUserManager(userDAO);
|
||||||
|
|
||||||
|
userManager.overwritePassword("notExisting", "---");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSucceedOverwritePassword() {
|
||||||
|
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||||
|
|
||||||
|
doNothing().when(userDAO).modify(userCaptor.capture());
|
||||||
|
|
||||||
|
UserManager userManager = new DefaultUserManager(userDAO);
|
||||||
|
|
||||||
|
userManager.overwritePassword("trillian", "newEncrypted");
|
||||||
|
|
||||||
|
Assertions.assertThat(userCaptor.getValue().getPassword()).isEqualTo("newEncrypted");
|
||||||
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
private XmlUserDAO createXmlUserDAO() {
|
private XmlUserDAO createXmlUserDAO() {
|
||||||
|
|||||||
Reference in New Issue
Block a user