mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 07:55:47 +01:00
add permission to modify the own password over the me and the user endpoints
This commit is contained in:
@@ -35,8 +35,11 @@ package sonia.scm;
|
|||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
|
import com.github.sdorra.ssp.PermissionCheck;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base class of all handlers.
|
* The base class of all handlers.
|
||||||
@@ -75,4 +78,15 @@ public interface HandlerBase<T extends TypedObject>
|
|||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
void modify(T object) throws NotFoundException;
|
void modify(T object) throws NotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies a persistent object after checking with the given permission checker.
|
||||||
|
*
|
||||||
|
* @param object to modify
|
||||||
|
* @param permissionChecker check permission before to modify
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
default void modify(T object, Function<T, PermissionCheck> permissionChecker) throws NotFoundException{
|
||||||
|
modify(object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package sonia.scm;
|
package sonia.scm;
|
||||||
|
|
||||||
public class NotFoundException extends Exception {
|
public class NotFoundException extends RuntimeException {
|
||||||
public NotFoundException(String type, String id) {
|
public NotFoundException(String type, String id) {
|
||||||
super(type + " with id '" + id + "' not found");
|
super(type + " with id '" + id + "' not found");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import java.security.Principal;
|
|||||||
*
|
*
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"})
|
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete", "changeOwnPassword"})
|
||||||
@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
|
||||||
|
|||||||
@@ -77,10 +77,15 @@ public interface UserManager
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only account of the default type "xml" can change their password
|
* 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
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
default Consumer<User> getUserTypeChecker() {
|
default Consumer<User> getChangePasswordChecker() {
|
||||||
return user -> {
|
return user -> {
|
||||||
|
UserPermissions.changeOwnPassword().check();
|
||||||
if (!isTypeDefault(user)) {
|
if (!isTypeDefault(user)) {
|
||||||
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
|
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package sonia.scm.util;
|
||||||
|
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
|
||||||
|
public class AuthenticationUtil {
|
||||||
|
|
||||||
|
public static String getAuthenticatedUsername() {
|
||||||
|
Object subject = SecurityUtils.getSubject().getPrincipal();
|
||||||
|
AssertUtil.assertIsNotNull(subject);
|
||||||
|
return subject.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,33 @@ public class UserITCase {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nonAdminUserShouldChangeOwnPassword() {
|
||||||
|
String newUser = "user";
|
||||||
|
String password = "pass";
|
||||||
|
TestData.createUser(newUser, password, false, "xml");
|
||||||
|
String newPassword = "new_password";
|
||||||
|
ScmRequests.start()
|
||||||
|
.given()
|
||||||
|
.url(TestData.getUserUrl(newUser))
|
||||||
|
.usernameAndPassword(newUser, password)
|
||||||
|
.getUserResource()
|
||||||
|
.assertStatusCode(200)
|
||||||
|
.usingUserResponse()
|
||||||
|
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.FALSE))
|
||||||
|
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
|
||||||
|
.assertStatusCode(204);
|
||||||
|
// assert password is changed -> login with the new Password
|
||||||
|
ScmRequests.start()
|
||||||
|
.given()
|
||||||
|
.url(TestData.getUserUrl(newUser))
|
||||||
|
.usernameAndPassword(newUser, newPassword)
|
||||||
|
.getUserResource()
|
||||||
|
.assertStatusCode(200)
|
||||||
|
.usingUserResponse()
|
||||||
|
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.FALSE))
|
||||||
|
.assertPassword(Assert::assertNull);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||||
|
|||||||
@@ -63,6 +63,6 @@ public class IllegalArgumentExceptionMapper
|
|||||||
public Response toResponse(IllegalArgumentException exception)
|
public Response toResponse(IllegalArgumentException exception)
|
||||||
{
|
{
|
||||||
log.info("caught IllegalArgumentException -- mapping to bad request", exception);
|
log.info("caught IllegalArgumentException -- mapping to bad request", exception);
|
||||||
return Response.status(Status.BAD_REQUEST).build();
|
return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ 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;
|
||||||
@@ -63,6 +64,8 @@ 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 ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -196,27 +199,24 @@ public abstract class AbstractManagerResource<T extends ModelObject> {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Response update(T item, Function<T, PermissionCheck> permissionChecker ){
|
||||||
* Method description
|
Consumer<Manager> updateAction = mng -> mng.modify(item, permissionChecker);
|
||||||
*
|
return applyUpdate(item, updateAction);
|
||||||
*
|
}
|
||||||
*
|
|
||||||
*
|
public Response update(String name, T item) {
|
||||||
* @param name
|
Consumer<Manager> updateAction = mng -> mng.modify(item);
|
||||||
* @param item
|
return applyUpdate(item, updateAction);
|
||||||
*
|
}
|
||||||
*
|
|
||||||
* @return
|
public Response applyUpdate(T item, Consumer<Manager> updateAction) {
|
||||||
*/
|
|
||||||
public Response update(String name, T item)
|
|
||||||
{
|
|
||||||
Response response = null;
|
Response response = null;
|
||||||
|
|
||||||
preUpdate(item);
|
preUpdate(item);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
manager.modify(item);
|
updateAction.accept(manager);
|
||||||
response = Response.noContent().build();
|
response = Response.noContent().build();
|
||||||
}
|
}
|
||||||
catch (AuthorizationException ex)
|
catch (AuthorizationException ex)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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.AlreadyExistsException;
|
import sonia.scm.AlreadyExistsException;
|
||||||
import sonia.scm.ConcurrentModificationException;
|
import sonia.scm.ConcurrentModificationException;
|
||||||
@@ -7,6 +8,10 @@ import sonia.scm.Manager;
|
|||||||
import sonia.scm.ModelObject;
|
import sonia.scm.ModelObject;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.PageResult;
|
import sonia.scm.PageResult;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
import sonia.scm.user.UserPermissions;
|
||||||
|
import sonia.scm.util.AssertUtil;
|
||||||
|
import sonia.scm.util.AuthenticationUtil;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -38,13 +43,31 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
|||||||
return singleAdapter.get(loadBy(id), mapToDto);
|
return singleAdapter.get(loadBy(id), mapToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
|
|
||||||
return singleAdapter.update(
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
public Function<User, PermissionCheck> getChangePasswordPermission(String usernameToChangePassword) {
|
||||||
|
AssertUtil.assertIsNotEmpty(usernameToChangePassword);
|
||||||
|
return user -> {
|
||||||
|
if (usernameToChangePassword.equals(AuthenticationUtil.getAuthenticatedUsername())) {
|
||||||
|
return UserPermissions.changeOwnPassword();
|
||||||
|
}
|
||||||
|
return UserPermissions.modify(user);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response changePassword(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Consumer<MODEL_OBJECT> checker, Function<MODEL_OBJECT, PermissionCheck> permissionCheck) throws NotFoundException, ConcurrentModificationException {
|
||||||
|
return singleAdapter.changePassword(
|
||||||
loadBy(id),
|
loadBy(id),
|
||||||
applyChanges,
|
applyChanges,
|
||||||
idStaysTheSame(id),
|
idStaysTheSame(id),
|
||||||
checker
|
checker,
|
||||||
);
|
permissionCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
|
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import sonia.scm.NotFoundException;
|
|||||||
import sonia.scm.user.InvalidPasswordException;
|
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.user.UserPermissions;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -80,7 +81,7 @@ public class MeResource {
|
|||||||
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
||||||
public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
|
public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
|
||||||
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
||||||
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker().andThen(getOldOriginalPasswordChecker(passwordChangeDto.getOldPassword())));
|
return adapter.changePassword(name, user -> user.clone().changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getChangePasswordChecker().andThen(getOldOriginalPasswordChecker(passwordChangeDto.getOldPassword())), user -> UserPermissions.changeOwnPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -16,8 +17,6 @@ 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}).
|
||||||
*
|
*
|
||||||
@@ -54,10 +53,12 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
|||||||
.map(Response.ResponseBuilder::build)
|
.map(Response.ResponseBuilder::build)
|
||||||
.orElseThrow(NotFoundException::new);
|
.orElseThrow(NotFoundException::new);
|
||||||
}
|
}
|
||||||
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 {
|
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 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(reader,applyChanges,hasSameKey);
|
return update(changedModelObject, permissionCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,13 +68,17 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
|||||||
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey) throws NotFoundException, 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)) {
|
||||||
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
|
throw new IllegalArgumentException("illegal change of id");
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ public class UserResource {
|
|||||||
* The oldPassword property of the DTO is not needed here. it will be ignored.
|
* The oldPassword property of the DTO is not needed here. it will be ignored.
|
||||||
* The oldPassword property is needed in the MeResources when the actual user change the own password.
|
* The oldPassword property is needed in the MeResources when the actual user change the own password.
|
||||||
*
|
*
|
||||||
* <strong>Note:</strong> This method requires "user:modify" privilege.
|
* <strong>Note:</strong> This method requires "user:modify" privilege to modify the password of other users.
|
||||||
|
* <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 passwordChangeDto change password object to modify password. the old password is here not required
|
||||||
*/
|
*/
|
||||||
@@ -128,7 +130,7 @@ public class UserResource {
|
|||||||
})
|
})
|
||||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||||
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
|
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
|
||||||
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker());
|
return adapter.changePassword(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getChangePasswordChecker(), adapter.getChangePasswordPermission(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +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());
|
||||||
permissions = builder.build();
|
permissions = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +273,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
|||||||
return GroupPermissions.autocomplete().asShiroString();
|
return GroupPermissions.autocomplete().asShiroString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getChangeOwnPasswordPermission() {
|
||||||
|
return UserPermissions.changeOwnPassword().asShiroString();
|
||||||
|
}
|
||||||
|
|
||||||
private String getUserAutocompletePermission() {
|
private String getUserAutocompletePermission() {
|
||||||
return UserPermissions.autocomplete().asShiroString();
|
return UserPermissions.autocomplete().asShiroString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ package sonia.scm.user;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- 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.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -63,6 +64,7 @@ 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 ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -194,11 +196,15 @@ public class DefaultUserManager extends AbstractUserManager
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void modify(User user) throws NotFoundException {
|
public void modify(User user) throws NotFoundException {
|
||||||
logger.info("modify user {} of type {}", user.getName(), user.getType());
|
modify(user,UserPermissions::modify);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(User user, Function<User, PermissionCheck> permissionChecker) throws NotFoundException {
|
||||||
|
logger.info("modify user {} of type {}", user.getName(), user.getType());
|
||||||
managerDaoAdapter.modify(
|
managerDaoAdapter.modify(
|
||||||
user,
|
user,
|
||||||
UserPermissions::modify,
|
permissionChecker,
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.jboss.resteasy.mock.MockDispatcherFactory;
|
|||||||
import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
|
import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
|
||||||
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
||||||
import sonia.scm.api.rest.ConcurrentModificationExceptionMapper;
|
import sonia.scm.api.rest.ConcurrentModificationExceptionMapper;
|
||||||
|
import sonia.scm.api.rest.IllegalArgumentExceptionMapper;
|
||||||
|
|
||||||
public class DispatcherMock {
|
public class DispatcherMock {
|
||||||
public static Dispatcher createDispatcher(Object resource) {
|
public static Dispatcher createDispatcher(Object resource) {
|
||||||
@@ -17,6 +18,7 @@ public class DispatcherMock {
|
|||||||
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
|
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
|
||||||
dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class);
|
dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class);
|
||||||
dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class);
|
dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class);
|
||||||
|
dispatcher.getProviderFactory().registerProvider(IllegalArgumentExceptionMapper.class);
|
||||||
return dispatcher;
|
return dispatcher;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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.lang.model.util.Types;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
@@ -69,7 +70,7 @@ public class MeResourceTest {
|
|||||||
doNothing().when(userManager).modify(userCaptor.capture());
|
doNothing().when(userManager).modify(userCaptor.capture());
|
||||||
doNothing().when(userManager).delete(userCaptor.capture());
|
doNothing().when(userManager).delete(userCaptor.capture());
|
||||||
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
|
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
|
||||||
when(userManager.getUserTypeChecker()).thenCallRealMethod();
|
when(userManager.getChangePasswordChecker()).thenCallRealMethod();
|
||||||
when(userManager.getDefaultType()).thenReturn("xml");
|
when(userManager.getDefaultType()).thenReturn("xml");
|
||||||
MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService);
|
MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService);
|
||||||
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
|
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
|
||||||
@@ -97,21 +98,23 @@ 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 oldPassword = "notEncriptedSecret";
|
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
|
||||||
.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(encryptedNewPassword);
|
when(passwordService.encryptPassword(newPassword)).thenReturn(encryptedNewPassword);
|
||||||
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
|
when(passwordService.encryptPassword(oldPassword)).thenReturn("secret");
|
||||||
|
ArgumentCaptor<User> modifyUserCaptor = ArgumentCaptor.forClass(User.class);
|
||||||
|
doNothing().when(userManager).modify(modifyUserCaptor.capture(), any());
|
||||||
|
|
||||||
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(User.class));
|
verify(userManager).modify(any(), any());
|
||||||
User updatedUser = userCaptor.getValue();
|
User updatedUser = modifyUserCaptor.getValue();
|
||||||
assertEquals(encryptedNewPassword, updatedUser.getPassword());
|
assertEquals(encryptedNewPassword, updatedUser.getPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +123,14 @@ public class MeResourceTest {
|
|||||||
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
|
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
|
||||||
originalUser.setType("not an xml type");
|
originalUser.setType("not an xml type");
|
||||||
String newPassword = "pwd123";
|
String newPassword = "pwd123";
|
||||||
String oldPassword = "notEncriptedSecret";
|
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
|
||||||
.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(newPassword))).thenReturn("encrypted123");
|
||||||
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
|
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
dispatcher.invoke(request, response);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class UserRootResourceTest {
|
|||||||
originalUser = createDummyUser("Neo");
|
originalUser = createDummyUser("Neo");
|
||||||
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
|
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
|
||||||
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
|
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
|
||||||
when(userManager.getUserTypeChecker()).thenCallRealMethod();
|
when(userManager.getChangePasswordChecker()).thenCallRealMethod();
|
||||||
doNothing().when(userManager).modify(userCaptor.capture());
|
doNothing().when(userManager).modify(userCaptor.capture());
|
||||||
doNothing().when(userManager).delete(userCaptor.capture());
|
doNothing().when(userManager).delete(userCaptor.capture());
|
||||||
when(userManager.getDefaultType()).thenReturn("xml");
|
when(userManager.getDefaultType()).thenReturn("xml");
|
||||||
@@ -151,7 +151,7 @@ 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(User.class));
|
verify(userManager).modify(any(), any());
|
||||||
User updatedUser = userCaptor.getValue();
|
User updatedUser = userCaptor.getValue();
|
||||||
assertEquals("encrypted123", updatedUser.getPassword());
|
assertEquals("encrypted123", updatedUser.getPassword());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import sonia.scm.repository.RepositoryTestData;
|
|||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserTestData;
|
import sonia.scm.user.UserTestData;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.contains;
|
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
@@ -124,8 +123,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||||
)
|
)
|
||||||
public void testCollectFromCache()
|
public void testCollectFromCache() {
|
||||||
{
|
|
||||||
AuthorizationInfo info = new SimpleAuthorizationInfo();
|
AuthorizationInfo info = new SimpleAuthorizationInfo();
|
||||||
when(cache.get(anyObject())).thenReturn(info);
|
when(cache.get(anyObject())).thenReturn(info);
|
||||||
authenticate(UserTestData.createTrillian(), "main");
|
authenticate(UserTestData.createTrillian(), "main");
|
||||||
@@ -141,7 +139,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||||
)
|
)
|
||||||
public void testCollectWithCache(){
|
public void testCollectWithCache() {
|
||||||
authenticate(UserTestData.createTrillian(), "main");
|
authenticate(UserTestData.createTrillian(), "main");
|
||||||
|
|
||||||
AuthorizationInfo authInfo = collector.collect();
|
AuthorizationInfo authInfo = collector.collect();
|
||||||
@@ -155,14 +153,13 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||||
)
|
)
|
||||||
public void testCollectWithoutPermissions()
|
public void testCollectWithoutPermissions() {
|
||||||
{
|
|
||||||
authenticate(UserTestData.createTrillian(), "main");
|
authenticate(UserTestData.createTrillian(), "main");
|
||||||
|
|
||||||
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(3));
|
assertThat(authInfo.getStringPermissions(), hasSize(4));
|
||||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:read:trillian"));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changeOwnPassword", "user:read:trillian"));
|
||||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +170,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||||
)
|
)
|
||||||
public void testCollectAsAdmin()
|
public void testCollectAsAdmin() {
|
||||||
{
|
|
||||||
User trillian = UserTestData.createTrillian();
|
User trillian = UserTestData.createTrillian();
|
||||||
trillian.setAdmin(true);
|
trillian.setAdmin(true);
|
||||||
authenticate(trillian, "main");
|
authenticate(trillian, "main");
|
||||||
@@ -192,8 +188,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||||
)
|
)
|
||||||
public void testCollectWithRepositoryPermissions()
|
public void testCollectWithRepositoryPermissions() {
|
||||||
{
|
|
||||||
String group = "heart-of-gold-crew";
|
String group = "heart-of-gold-crew";
|
||||||
authenticate(UserTestData.createTrillian(), group);
|
authenticate(UserTestData.createTrillian(), group);
|
||||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||||
@@ -209,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", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changeOwnPassword", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,7 +214,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
@SubjectAware(
|
@SubjectAware(
|
||||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||||
)
|
)
|
||||||
public void testCollectWithGlobalPermissions(){
|
public void testCollectWithGlobalPermissions() {
|
||||||
authenticate(UserTestData.createTrillian(), "main");
|
authenticate(UserTestData.createTrillian(), "main");
|
||||||
|
|
||||||
StoredAssignedPermission p1 = new StoredAssignedPermission("one", new AssignedPermission("one", "one:one"));
|
StoredAssignedPermission p1 = new StoredAssignedPermission("one", new AssignedPermission("one", "one:one"));
|
||||||
@@ -230,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" ));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changeOwnPassword"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void authenticate(User user, String group, String... groups) {
|
private void authenticate(User user, String group, String... groups) {
|
||||||
|
|||||||
Reference in New Issue
Block a user