diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index bbe78c8cf7..42c159a767 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -35,8 +35,11 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ +import com.github.sdorra.ssp.PermissionCheck; + import java.io.Closeable; import java.io.IOException; +import java.util.function.Function; /** * The base class of all handlers. @@ -75,4 +78,15 @@ public interface HandlerBase * @throws IOException */ 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 permissionChecker) throws NotFoundException{ + modify(object); + } } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 14c458d923..20b3a16a8e 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -99,14 +99,14 @@ public interface Manager * @param limit parameter * * @since 1.4 - * @return objects from the store which are starts at the given + * @return objects from the store which are starts at the given * start parameter */ Collection getAll(int start, int limit); /** * Returns objects from the store which are starts at the given start - * parameter sorted by the given {@link java.util.Comparator}. + * parameter sorted by the given {@link java.util.Comparator}. * The objects returned are limited by the limit parameter. * * @@ -115,7 +115,7 @@ public interface Manager * @param limit parameter * * @since 1.4 - * @return objects from the store which are starts at the given + * @return objects from the store which are starts at the given * start parameter */ Collection getAll(Comparator comparator, int start, int limit); diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 8a7ae642bd..37546be0b8 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -1,6 +1,6 @@ package sonia.scm; -public class NotFoundException extends Exception { +public class NotFoundException extends RuntimeException { public NotFoundException(String type, String id) { super(type + " with id '" + id + "' not found"); } diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index e9dc277e5f..d9d2336387 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -56,7 +56,7 @@ import java.security.Principal; * * @author Sebastian Sdorra */ -@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"}) +@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete", "changeOwnPassword"}) @XmlRootElement(name = "users") @XmlAccessorType(XmlAccessType.FIELD) public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java index 7829cd9974..f786b6ac41 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java @@ -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 getUserTypeChecker() { + default Consumer getChangePasswordChecker() { return user -> { + UserPermissions.changeOwnPassword().check(); if (!isTypeDefault(user)) { throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType())); } diff --git a/scm-core/src/main/java/sonia/scm/util/AuthenticationUtil.java b/scm-core/src/main/java/sonia/scm/util/AuthenticationUtil.java new file mode 100644 index 0000000000..a0c021fa3c --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/util/AuthenticationUtil.java @@ -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(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/UserITCase.java b/scm-it/src/test/java/sonia/scm/it/UserITCase.java index 33fbe0cc5d..7e7ceae4f7 100644 --- a/scm-it/src/test/java/sonia/scm/it/UserITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/UserITCase.java @@ -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 public void shouldHidePasswordLinkIfUserTypeIsNotXML() { diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java index 44d4d9194d..ff29a770cf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java @@ -63,6 +63,6 @@ public class IllegalArgumentExceptionMapper public Response toResponse(IllegalArgumentException 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(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java index f00072cdcb..794f11893b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java @@ -35,6 +35,7 @@ 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; @@ -63,6 +64,8 @@ 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 ------------------------------------------------------------ @@ -196,27 +199,24 @@ public abstract class AbstractManagerResource { return response; } - /** - * Method description - * - * - * - * - * @param name - * @param item - * - * - * @return - */ - public Response update(String name, T item) - { + public Response update(T item, Function permissionChecker ){ + Consumer updateAction = mng -> mng.modify(item, permissionChecker); + return applyUpdate(item, updateAction); + } + + public Response update(String name, T item) { + Consumer updateAction = mng -> mng.modify(item); + return applyUpdate(item, updateAction); + } + + public Response applyUpdate(T item, Consumer updateAction) { Response response = null; preUpdate(item); try { - manager.modify(item); + updateAction.accept(manager); response = Response.noContent().build(); } catch (AuthorizationException ex) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index 9b890eb174..d58a870d6a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.github.sdorra.ssp.PermissionCheck; import de.otto.edison.hal.HalRepresentation; import sonia.scm.AlreadyExistsException; import sonia.scm.ConcurrentModificationException; @@ -7,6 +8,10 @@ import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.NotFoundException; 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 java.util.Optional; @@ -38,13 +43,31 @@ class IdResourceManagerAdapter applyChanges, Consumer 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 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 applyChanges, Consumer checker, Function permissionCheck) throws NotFoundException, ConcurrentModificationException { + return singleAdapter.changePassword( loadBy(id), applyChanges, idStaysTheSame(id), - checker - ); + checker, + permissionCheck); } public Response update(String id, Function applyChanges) throws NotFoundException, ConcurrentModificationException { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index e684bc25db..4c99b62618 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -10,6 +10,7 @@ import sonia.scm.NotFoundException; import sonia.scm.user.InvalidPasswordException; import sonia.scm.user.User; import sonia.scm.user.UserManager; +import sonia.scm.user.UserPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -80,7 +81,7 @@ public class MeResource { @Consumes(VndMediaType.PASSWORD_CHANGE) public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException { 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()); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index f2bc93d47e..743d9f9710 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -1,5 +1,6 @@ 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; @@ -16,8 +17,6 @@ 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}). * @@ -54,10 +53,12 @@ class SingleResourceManagerAdapter> reader, Function applyChanges, Predicate hasSameKey, Consumer checker) throws NotFoundException, ConcurrentModificationException { + public Response changePassword(Supplier> reader, Function applyChanges, Predicate hasSameKey, Consumer checker, Function permissionCheck) 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(reader,applyChanges,hasSameKey); + return update(changedModelObject, permissionCheck); } /** @@ -67,13 +68,17 @@ class SingleResourceManagerAdapter> reader, Function applyChanges, Predicate 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 hasSameKey, MODEL_OBJECT existingModelObject, MODEL_OBJECT changedModelObject) throws ConcurrentModificationException { 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)) { throw new ConcurrentModificationException(); } - return update(getId(existingModelObject), changedModelObject); } private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index c0bb1e65ad..42f9274d74 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -111,7 +111,9 @@ public class UserResource { * 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. * - * Note: This method requires "user:modify" privilege. + * Note: This method requires "user:modify" privilege to modify the password of other users. + * Note: 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 */ @@ -128,7 +130,7 @@ public class UserResource { }) @TypeHint(TypeHint.NO_CONTENT.class) 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)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 36b7f4089d..265e163c23 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -260,6 +260,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector builder.add(canReadOwnUser(user)); builder.add(getUserAutocompletePermission()); builder.add(getGroupAutocompletePermission()); + builder.add(getChangeOwnPasswordPermission()); permissions = builder.build(); } @@ -272,6 +273,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector return GroupPermissions.autocomplete().asShiroString(); } + private String getChangeOwnPasswordPermission() { + return UserPermissions.changeOwnPassword().asShiroString(); + } + private String getUserAutocompletePermission() { return UserPermissions.autocomplete().asShiroString(); } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 19802902b8..c9821575d2 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -36,6 +36,7 @@ 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.slf4j.Logger; @@ -63,6 +64,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.function.Function; //~--- JDK imports ------------------------------------------------------------ @@ -194,11 +196,15 @@ public class DefaultUserManager extends AbstractUserManager */ @Override 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 permissionChecker) throws NotFoundException { + logger.info("modify user {} of type {}", user.getName(), user.getType()); managerDaoAdapter.modify( user, - UserPermissions::modify, + permissionChecker, notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified), notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified)); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index a1abdb6ff4..8a71abb670 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -5,6 +5,7 @@ import org.jboss.resteasy.mock.MockDispatcherFactory; import sonia.scm.api.rest.AlreadyExistsExceptionMapper; import sonia.scm.api.rest.AuthorizationExceptionMapper; import sonia.scm.api.rest.ConcurrentModificationExceptionMapper; +import sonia.scm.api.rest.IllegalArgumentExceptionMapper; public class DispatcherMock { public static Dispatcher createDispatcher(Object resource) { @@ -17,6 +18,7 @@ public class DispatcherMock { dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(IllegalArgumentExceptionMapper.class); return dispatcher; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index c7b040172e..8aec18af01 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -17,6 +17,7 @@ import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; +import javax.lang.model.util.Types; import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; @@ -69,7 +70,7 @@ public class MeResourceTest { doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); - when(userManager.getUserTypeChecker()).thenCallRealMethod(); + when(userManager.getChangePasswordChecker()).thenCallRealMethod(); when(userManager.getDefaultType()).thenReturn("xml"); MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService); when(uriInfo.getApiRestUri()).thenReturn(URI.create("/")); @@ -97,21 +98,23 @@ public class MeResourceTest { public void shouldEncryptPasswordBeforeChanging() throws Exception { String newPassword = "pwd123"; String encryptedNewPassword = "encrypted123"; - String oldPassword = "notEncriptedSecret"; + 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(); - when(passwordService.encryptPassword(eq(newPassword))).thenReturn(encryptedNewPassword); - when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret"); + when(passwordService.encryptPassword(newPassword)).thenReturn(encryptedNewPassword); + when(passwordService.encryptPassword(oldPassword)).thenReturn("secret"); + ArgumentCaptor modifyUserCaptor = ArgumentCaptor.forClass(User.class); + doNothing().when(userManager).modify(modifyUserCaptor.capture(), any()); dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); - verify(userManager).modify(any(User.class)); - User updatedUser = userCaptor.getValue(); + verify(userManager).modify(any(), any()); + User updatedUser = modifyUserCaptor.getValue(); assertEquals(encryptedNewPassword, updatedUser.getPassword()); } @@ -120,14 +123,14 @@ public class MeResourceTest { public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception { originalUser.setType("not an xml type"); String newPassword = "pwd123"; - String oldPassword = "notEncriptedSecret"; + 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(); - when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123"); + when(passwordService.encryptPassword(eq(newPassword))).thenReturn("encrypted123"); when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret"); dispatcher.invoke(request, response); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 3d33fa0eb6..69f5d65fd6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -69,7 +69,7 @@ public class UserRootResourceTest { originalUser = createDummyUser("Neo"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); - when(userManager.getUserTypeChecker()).thenCallRealMethod(); + when(userManager.getChangePasswordChecker()).thenCallRealMethod(); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); when(userManager.getDefaultType()).thenReturn("xml"); @@ -151,7 +151,7 @@ public class UserRootResourceTest { dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); - verify(userManager).modify(any(User.class)); + verify(userManager).modify(any(), any()); User updatedUser = userCaptor.getValue(); assertEquals("encrypted123", updatedUser.getPassword()); } diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 9155af3b56..a85d03991d 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -1,10 +1,10 @@ /** * Copyright (c) 2014, Sebastian Sdorra * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, @@ -13,7 +13,7 @@ * 3. Neither the name of SCM-Manager; nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE @@ -24,9 +24,9 @@ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + * * http://bitbucket.org/sdorra/scm-manager - * + * */ package sonia.scm.security; @@ -57,7 +57,6 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; @@ -70,7 +69,7 @@ import static org.mockito.Mockito.when; /** * Unit tests for {@link AuthorizationCollector}. - * + * * @author Sebastian Sdorra */ @SuppressWarnings("unchecked") @@ -79,28 +78,28 @@ public class DefaultAuthorizationCollectorTest { @Mock private Cache cache; - + @Mock private CacheManager cacheManager; - + @Mock private RepositoryDAO repositoryDAO; @Mock private SecuritySystem securitySystem; - + private DefaultAuthorizationCollector collector; - + @Rule public ShiroRule shiro = new ShiroRule(); - + /** * Set up object to test. */ @Before public void setUp(){ when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache); - + collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem); } @@ -116,7 +115,7 @@ public class DefaultAuthorizationCollectorTest { assertThat(authInfo.getStringPermissions(), nullValue()); assertThat(authInfo.getObjectPermissions(), nullValue()); } - + /** * Tests {@link AuthorizationCollector#collect()} from cache. */ @@ -124,16 +123,15 @@ public class DefaultAuthorizationCollectorTest { @SubjectAware( configuration = "classpath:sonia/scm/shiro-001.ini" ) - public void testCollectFromCache() - { + public void testCollectFromCache() { AuthorizationInfo info = new SimpleAuthorizationInfo(); when(cache.get(anyObject())).thenReturn(info); authenticate(UserTestData.createTrillian(), "main"); - + AuthorizationInfo authInfo = collector.collect(); assertSame(info, authInfo); } - + /** * Tests {@link AuthorizationCollector#collect()} with cache. */ @@ -141,13 +139,13 @@ public class DefaultAuthorizationCollectorTest { @SubjectAware( configuration = "classpath:sonia/scm/shiro-001.ini" ) - public void testCollectWithCache(){ + public void testCollectWithCache() { authenticate(UserTestData.createTrillian(), "main"); - + AuthorizationInfo authInfo = collector.collect(); verify(cache).put(any(), any()); } - + /** * Tests {@link AuthorizationCollector#collect()} without permissions. */ @@ -155,17 +153,16 @@ public class DefaultAuthorizationCollectorTest { @SubjectAware( configuration = "classpath:sonia/scm/shiro-001.ini" ) - public void testCollectWithoutPermissions() - { + public void testCollectWithoutPermissions() { authenticate(UserTestData.createTrillian(), "main"); - + AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); - assertThat(authInfo.getStringPermissions(), hasSize(3)); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:read:trillian")); + assertThat(authInfo.getStringPermissions(), hasSize(4)); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changeOwnPassword", "user:read:trillian")); assertThat(authInfo.getObjectPermissions(), nullValue()); } - + /** * Tests {@link AuthorizationCollector#collect()} as admin. */ @@ -173,18 +170,17 @@ public class DefaultAuthorizationCollectorTest { @SubjectAware( configuration = "classpath:sonia/scm/shiro-001.ini" ) - public void testCollectAsAdmin() - { + public void testCollectAsAdmin() { User trillian = UserTestData.createTrillian(); trillian.setAdmin(true); authenticate(trillian, "main"); - + AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER, Role.ADMIN)); assertThat(authInfo.getObjectPermissions(), nullValue()); assertThat(authInfo.getStringPermissions(), Matchers.contains("*")); } - + /** * Tests {@link AuthorizationCollector#collect()} with repository permissions. */ @@ -192,8 +188,7 @@ public class DefaultAuthorizationCollectorTest { @SubjectAware( configuration = "classpath:sonia/scm/shiro-001.ini" ) - public void testCollectWithRepositoryPermissions() - { + public void testCollectWithRepositoryPermissions() { String group = "heart-of-gold-crew"; authenticate(UserTestData.createTrillian(), group); Repository heartOfGold = RepositoryTestData.createHeartOfGold(); @@ -204,14 +199,14 @@ public class DefaultAuthorizationCollectorTest { sonia.scm.repository.Permission permission = new sonia.scm.repository.Permission(group, PermissionType.WRITE, true); puzzle42.setPermissions(Lists.newArrayList(permission)); when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42)); - + // execute and assert AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); 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")); } - + /** * Tests {@link AuthorizationCollector#collect()} with global permissions. */ @@ -219,20 +214,20 @@ public class DefaultAuthorizationCollectorTest { @SubjectAware( configuration = "classpath:sonia/scm/shiro-001.ini" ) - public void testCollectWithGlobalPermissions(){ + public void testCollectWithGlobalPermissions() { authenticate(UserTestData.createTrillian(), "main"); - + StoredAssignedPermission p1 = new StoredAssignedPermission("one", new AssignedPermission("one", "one:one")); StoredAssignedPermission p2 = new StoredAssignedPermission("two", new AssignedPermission("two", "two:two")); when(securitySystem.getPermissions(Mockito.any(Predicate.class))).thenReturn(Lists.newArrayList(p1, p2)); - + // execute and assert AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); 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) { SimplePrincipalCollection spc = new SimplePrincipalCollection(); spc.add(user.getName(), "unit"); @@ -249,9 +244,9 @@ public class DefaultAuthorizationCollectorTest { public void testInvalidateCache() { collector.invalidateCache(AuthorizationChangedEvent.createForEveryUser()); verify(cache).clear(); - + collector.invalidateCache(AuthorizationChangedEvent.createForUser("dent")); verify(cache).removeAll(Mockito.any(Predicate.class)); } - + }