Move password logic to manager

This commit is contained in:
René Pfeuffer
2018-10-17 11:58:37 +02:00
parent 24b1241ed7
commit 9bfb2cdadb
24 changed files with 285 additions and 218 deletions

View File

@@ -35,7 +35,6 @@ package sonia.scm.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.github.sdorra.ssp.PermissionCheck;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
@@ -64,8 +63,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.function.Consumer;
import java.util.function.Function;
//~--- JDK imports ------------------------------------------------------------
@@ -199,24 +196,27 @@ public abstract class AbstractManagerResource<T extends ModelObject> {
return response;
}
public Response update(T item, Function<T, PermissionCheck> permissionChecker ){
Consumer<Manager> updateAction = mng -> mng.modify(item, permissionChecker);
return applyUpdate(item, updateAction);
}
public Response update(String name, T item) {
Consumer<Manager> updateAction = mng -> mng.modify(item);
return applyUpdate(item, updateAction);
}
public Response applyUpdate(T item, Consumer<Manager> updateAction) {
/**
* Method description
*
*
*
*
* @param name
* @param item
*
*
* @return
*/
public Response update(String name, T item)
{
Response response = null;
preUpdate(item);
try
{
updateAction.accept(manager);
manager.modify(item);
response = Response.noContent().build();
}
catch (AuthorizationException ex)

View File

@@ -47,54 +47,6 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
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 {
return singleAdapter.update(
loadBy(id),

View File

@@ -5,14 +5,12 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
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.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
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.Response;
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)
@Consumes(VndMediaType.PASSWORD_CHANGE)
public Response changePassword(PasswordChangeDto passwordChangeDto) throws ConcurrentModificationException {
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
if (passwordChangeDto.getOldPassword() == null){
throw new ChangePasswordNotAllowedException(ChangePasswordNotAllowedException.OLD_PASSWORD_REQUIRED);
}
return adapter.changePassword(name, user -> user.clone().changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword()), passwordService.encryptPassword(passwordChangeDto.getOldPassword())));
public Response changePassword(@Valid PasswordChangeDto passwordChangeDto) {
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChangeDto.getOldPassword()), passwordService.encryptPassword(passwordChangeDto.getNewPassword()));
return Response.noContent().build();
}
}

View File

@@ -10,6 +10,7 @@ import org.hibernate.validator.constraints.NotEmpty;
@ToString
public class PasswordChangeDto {
@NotEmpty
private String oldPassword;
@NotEmpty

View File

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

View File

@@ -87,7 +87,7 @@ class ResourceLinks {
}
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();
}
}

View File

@@ -1,6 +1,5 @@
package sonia.scm.api.v2.resources;
import com.github.sdorra.ssp.PermissionCheck;
import de.otto.edison.hal.HalRepresentation;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.Manager;
@@ -17,6 +16,8 @@ import java.util.function.Function;
import java.util.function.Predicate;
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}).
*
@@ -53,32 +54,26 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
.map(Response.ResponseBuilder::build)
.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 changedModelObject = applyChanges.apply(existingModelObject);
checkForUpdate(hasSameKey, existingModelObject, changedModelObject);
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.
* 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 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)) {
throw new IllegalArgumentException("illegal change of id");
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
}
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
throw new ConcurrentModificationException();
}
return update(getId(existingModelObject), changedModelObject);
}
private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) {

View File

@@ -3,10 +3,8 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.user.ChangePasswordNotAllowedException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
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.
*
* @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
@Path("password")
@Consumes(VndMediaType.PASSWORD_CHANGE)
@Consumes(VndMediaType.PASSWORD_OVERWRITE)
@StatusCodes({
@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"),
@@ -130,12 +128,8 @@ public class UserResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws ConcurrentModificationException {
String currentUserName = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
if (currentUserName.equals(name) && passwordChangeDto.getOldPassword() == null){
throw new ChangePasswordNotAllowedException(ChangePasswordNotAllowedException.OLD_PASSWORD_REQUIRED);
}
return adapter.changePassword(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())));
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwriteDto) {
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwriteDto.getNewPassword()));
return Response.noContent().build();
}
}

View File

@@ -39,9 +39,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.user().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
}
}
target.add(linksBuilder.build());
}

View File

@@ -260,7 +260,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
builder.add(canReadOwnUser(user));
builder.add(getUserAutocompletePermission());
builder.add(getGroupAutocompletePermission());
builder.add(getChangeOwnPasswordPermission());
builder.add(getChangeOwnPasswordPermission(user));
permissions = builder.build();
}
@@ -273,8 +273,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return GroupPermissions.autocomplete().asShiroString();
}
private String getChangeOwnPasswordPermission() {
return UserPermissions.changeOwnPassword().asShiroString();
private String getChangeOwnPasswordPermission(User user) {
return UserPermissions.changePassword(user).asShiroString();
}
private String getUserAutocompletePermission() {

View File

@@ -33,12 +33,10 @@
package sonia.scm.user;
//~--- non-JDK imports --------------------------------------------------------
import com.github.sdorra.ssp.PermissionActionCheck;
import com.github.sdorra.ssp.PermissionCheck;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
@@ -64,9 +62,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -196,15 +191,10 @@ public class DefaultUserManager extends AbstractUserManager
*/
@Override
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());
managerDaoAdapter.modify(
user,
permissionChecker,
UserPermissions::modify,
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified),
notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified));
}
@@ -408,6 +398,36 @@ public class DefaultUserManager extends AbstractUserManager
//~--- 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
*