Merged in feature/password_modification_v2 (pull request #75)

Feature/password modification v2
This commit is contained in:
René Pfeuffer
2018-09-20 11:17:32 +00:00
39 changed files with 1477 additions and 436 deletions

View File

@@ -0,0 +1,11 @@
package sonia.scm.user;
public class ChangePasswordNotAllowedException extends RuntimeException {
public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password";
public ChangePasswordNotAllowedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.user;
public class InvalidPasswordException extends RuntimeException {
public static final String INVALID_MATCHING = "The given Password does not match with the stored one.";
public InvalidPasswordException(String message) {
super(message);
}
}

View File

@@ -274,6 +274,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO
//J+
}
public User changePassword(String password){
setPassword(password);
return this;
}
//~--- get methods ----------------------------------------------------------
/**

View File

@@ -38,6 +38,11 @@ package sonia.scm.user;
import sonia.scm.Manager;
import sonia.scm.search.Searchable;
import java.text.MessageFormat;
import java.util.function.Consumer;
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
/**
* The central class for managing {@link User} objects.
* This class is a singleton and is available via injection.
@@ -68,4 +73,22 @@ public interface UserManager
* @since 1.14
*/
public String getDefaultType();
/**
* Only account of the default type "xml" can change their password
*/
default Consumer<User> getUserTypeChecker() {
return user -> {
if (!isTypeDefault(user)) {
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
}
};
}
default boolean isTypeDefault(User user) {
return getDefaultType().equals(user.getType());
}
}

View File

@@ -35,6 +35,7 @@ public class VndMediaType {
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;

View File

@@ -0,0 +1,79 @@
package sonia.scm.it;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import static org.assertj.core.api.Assertions.assertThat;
public class MeITCase {
@Before
public void init() {
TestData.cleanup();
}
@Test
public void adminShouldChangeOwnPassword() {
String newPassword = TestData.USER_SCM_ADMIN + "1";
// admin change the own password
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getMeResource()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo("xml"))
.requestChangePassword(TestData.USER_SCM_ADMIN, newPassword)
.assertStatusCode(204);
// assert password is changed -> login with the new Password than undo changes
ScmRequests.start()
.given()
.url(TestData.getUserUrl(TestData.USER_SCM_ADMIN))
.usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
.getMeResource()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
.assertStatusCode(204);
}
@Test
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
String newUser = "user";
String password = "pass";
String type = "not XML Type";
TestData.createUser(newUser, password, true, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(403);
}
}

View File

@@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException;
@@ -51,11 +53,11 @@ import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.TestData.callRepository;
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class)
public class PermissionsITCase {

View File

@@ -42,6 +42,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.web.VndMediaType;
@@ -53,11 +55,11 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static sonia.scm.it.RegExMatcher.matchesPattern;
import static sonia.scm.it.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.repositoryJson;
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.TestData.repositoryJson;
@RunWith(Parameterized.class)
public class RepositoriesITCase {

View File

@@ -4,7 +4,6 @@ import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.apache.http.HttpStatus;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Maps;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
@@ -12,6 +11,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient;
@@ -29,10 +31,10 @@ import static java.lang.Thread.sleep;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class)
public class RepositoryAccessITCase {
@@ -42,7 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType;
private File folder;
private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest;
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType;
@@ -57,12 +59,17 @@ public class RepositoryAccessITCase {
public void init() {
TestData.createDefault();
folder = tempFolder.getRoot();
repositoryGetRequest = RepositoryRequests.start()
repositoryGetRequest = ScmRequests.start()
.given()
.url(TestData.getDefaultRepositoryUrl(repositoryType))
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.get()
.getRepositoryResource()
.assertStatusCode(HttpStatus.SC_OK);
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.getMeResource();
}
@Test

View File

@@ -1,293 +0,0 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* Encapsulate rest requests of a repository in builder pattern
* <p>
* A Get Request can be applied with the methods request*()
* These methods return a AppliedGet*Request object
* This object can be used to apply general assertions over the rest Assured response
* In the AppliedGet*Request classes there is a using*Response() method
* that return the *Response class containing specific operations related to the specific response
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
*/
public class RepositoryRequests {
private String url;
private String username;
private String password;
static RepositoryRequests start() {
return new RepositoryRequests();
}
Given given() {
return new Given();
}
/**
* Apply a GET Request to the extracted url from the given link
*
* @param linkPropertyName the property name of link
* @param response the response containing the link
* @return the response of the GET request using the given link
*/
private Response getResponseFromLink(Response response, String linkPropertyName) {
return getResponse(response
.then()
.extract()
.path(linkPropertyName));
}
/**
* Apply a GET Request to the given <code>url</code> and return the response.
*
* @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code>
*/
private Response getResponse(String url) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.get(url);
}
private void setUrl(String url) {
this.url = url;
}
private void setUsername(String username) {
this.username = username;
}
private void setPassword(String password) {
this.password = password;
}
private String getUrl() {
return url;
}
private String getUsername() {
return username;
}
private String getPassword() {
return password;
}
class Given {
GivenUrl url(String url) {
setUrl(url);
return new GivenUrl();
}
}
class GivenWithUrlAndAuth {
AppliedRepositoryGetRequest get() {
return new AppliedRepositoryGetRequest(
getResponse(url)
);
}
}
class AppliedGetRequest<SELF extends AppliedGetRequest> {
private Response response;
public AppliedGetRequest(Response response) {
this.response = response;
}
/**
* apply custom assertions to the actual response
*
* @param consumer consume the response in order to assert the content. the header, the payload etc..
* @return the self object
*/
SELF assertResponse(Consumer<Response> consumer) {
consumer.accept(response);
return (SELF) this;
}
/**
* special assertion of the status code
*
* @param expectedStatusCode the expected status code
* @return the self object
*/
SELF assertStatusCode(int expectedStatusCode) {
this.response.then().assertThat().statusCode(expectedStatusCode);
return (SELF) this;
}
}
class AppliedRepositoryGetRequest extends AppliedGetRequest<AppliedRepositoryGetRequest> {
AppliedRepositoryGetRequest(Response response) {
super(response);
}
RepositoryResponse usingRepositoryResponse() {
return new RepositoryResponse(super.response);
}
}
class RepositoryResponse {
private Response repositoryResponse;
public RepositoryResponse(Response repositoryResponse) {
this.repositoryResponse = repositoryResponse;
}
AppliedGetSourcesRequest requestSources() {
return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href"));
}
AppliedGetChangesetsRequest requestChangesets() {
return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href"));
}
}
class AppliedGetChangesetsRequest extends AppliedGetRequest<AppliedGetChangesetsRequest> {
AppliedGetChangesetsRequest(Response response) {
super(response);
}
ChangesetsResponse usingChangesetsResponse() {
return new ChangesetsResponse(super.response);
}
}
class ChangesetsResponse {
private Response changesetsResponse;
public ChangesetsResponse(Response changesetsResponse) {
this.changesetsResponse = changesetsResponse;
}
ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
changesetsConsumer.accept(changesets);
return this;
}
AppliedGetDiffRequest requestDiff(String revision) {
return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
}
public AppliedGetModificationsRequest requestModifications(String revision) {
return new AppliedGetModificationsRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
}
}
class AppliedGetSourcesRequest extends AppliedGetRequest<AppliedGetSourcesRequest> {
public AppliedGetSourcesRequest(Response sourcesResponse) {
super(sourcesResponse);
}
SourcesResponse usingSourcesResponse() {
return new SourcesResponse(super.response);
}
}
class SourcesResponse {
private Response sourcesResponse;
SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
}
SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files");
assertFiles.accept(files);
return this;
}
AppliedGetChangesetsRequest requestFileHistory(String fileName) {
return new AppliedGetChangesetsRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
}
AppliedGetSourcesRequest requestSelf(String fileName) {
return new AppliedGetSourcesRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
}
}
class AppliedGetDiffRequest extends AppliedGetRequest<AppliedGetDiffRequest> {
AppliedGetDiffRequest(Response response) {
super(response);
}
}
class GivenUrl {
GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
setUsername(username);
setPassword(password);
return new GivenWithUrlAndAuth();
}
}
class AppliedGetModificationsRequest extends AppliedGetRequest<AppliedGetModificationsRequest> {
public AppliedGetModificationsRequest(Response response) { super(response); }
ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String > added = resource.then().extract().path("added");
assertAdded.accept(added);
return this;
}
ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String > removed = resource.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String > modified = resource.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
}

View File

@@ -0,0 +1,113 @@
package sonia.scm.it;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import static org.assertj.core.api.Assertions.assertThat;
public class UserITCase {
@Before
public void init(){
TestData.cleanup();
}
@Test
public void adminShouldChangeOwnPassword() {
String newUser = "user";
String password = "pass";
TestData.createUser(newUser, password, true, "xml");
String newPassword = "new_password";
// admin change the own password
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.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.TRUE))
.assertPassword(Assert::assertNull);
}
@Test
public void adminShouldChangePasswordOfOtherUser() {
String newUser = "user";
String password = "pass";
TestData.createUser(newUser, password, true, "xml");
String newPassword = "new_password";
// admin change the password of the user
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))// the admin get the user object
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204);
// assert password is changed
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, newPassword)
.getUserResource()
.assertStatusCode(200);
}
@Test
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
String newUser = "user";
String password = "pass";
String type = "not XML Type";
TestData.createUser(newUser, password, true, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(403);
}
}

View File

@@ -0,0 +1,95 @@
package sonia.scm.it.utils;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonValue;
import java.math.BigDecimal;
import java.math.BigInteger;
public class NullAwareJsonObjectBuilder implements JsonObjectBuilder {
public static JsonObjectBuilder wrap(JsonObjectBuilder builder) {
if (builder == null) {
throw new IllegalArgumentException("Can't wrap nothing.");
}
return new NullAwareJsonObjectBuilder(builder);
}
private final JsonObjectBuilder builder;
private NullAwareJsonObjectBuilder(JsonObjectBuilder builder) {
this.builder = builder;
}
public JsonObjectBuilder add(String name, JsonValue value) {
return builder.add(name, (value == null) ? JsonValue.NULL : value);
}
@Override
public JsonObjectBuilder add(String name, String value) {
if (value != null){
return builder.add(name, value );
}else{
return builder.addNull(name);
}
}
@Override
public JsonObjectBuilder add(String name, BigInteger value) {
if (value != null){
return builder.add(name, value );
}else{
return builder.addNull(name);
}
}
@Override
public JsonObjectBuilder add(String name, BigDecimal value) {
if (value != null){
return builder.add(name, value );
}else{
return builder.addNull(name);
}
}
@Override
public JsonObjectBuilder add(String s, int i) {
return builder.add(s, i);
}
@Override
public JsonObjectBuilder add(String s, long l) {
return builder.add(s, l);
}
@Override
public JsonObjectBuilder add(String s, double v) {
return builder.add(s, v);
}
@Override
public JsonObjectBuilder add(String s, boolean b) {
return builder.add(s, b);
}
@Override
public JsonObjectBuilder addNull(String s) {
return builder.addNull(s);
}
@Override
public JsonObjectBuilder add(String s, JsonObjectBuilder jsonObjectBuilder) {
return builder.add(s, jsonObjectBuilder);
}
@Override
public JsonObjectBuilder add(String s, JsonArrayBuilder jsonArrayBuilder) {
return builder.add(s, jsonArrayBuilder);
}
@Override
public JsonObject build() {
return builder.build();
}
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
@@ -6,7 +6,7 @@ import org.hamcrest.Matcher;
import java.util.regex.Pattern;
class RegExMatcher extends BaseMatcher<String> {
public class RegExMatcher extends BaseMatcher<String> {
public static Matcher<String> matchesPattern(String pattern) {
return new RegExMatcher(pattern);
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
@@ -24,11 +24,11 @@ public class RepositoryUtil {
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
public static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin");
}
static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
.extract()
.path("_links.protocol.find{it.name=='http'}.href");
@@ -36,14 +36,14 @@ public class RepositoryUtil {
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
}
static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
String uuid = UUID.randomUUID().toString();
String name = "file-" + uuid + ".uuid";
createAndCommitFile(client, username, name, uuid);
return name;
}
static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
public static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
writeAndAddFile(repositoryClient, fileName, content);
return commit(repositoryClient, username, "added " + fileName);
}
@@ -59,7 +59,7 @@ public class RepositoryUtil {
* @return the changeset with all modifications
* @throws IOException
*/
static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
public static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
for (String fileName : addedFiles.keySet()) {
writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName));
}
@@ -80,7 +80,7 @@ public class RepositoryUtil {
return file;
}
static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
return commit(repositoryClient, username, "removed " + fileName);
}
@@ -115,7 +115,7 @@ public class RepositoryUtil {
return changeset;
}
static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
public static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;

View File

@@ -0,0 +1,465 @@
package sonia.scm.it.utils;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import sonia.scm.web.VndMediaType;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.hamcrest.Matchers.is;
import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
/**
* Encapsulate rest requests of a repository in builder pattern
* <p>
* A Get Request can be applied with the methods request*()
* These methods return a AppliedGet*Request object
* This object can be used to apply general assertions over the rest Assured response
* In the AppliedGet*Request classes there is a using*Response() method
* that return the *Response class containing specific operations related to the specific response
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
*/
public class ScmRequests {
private String url;
private String username;
private String password;
public static ScmRequests start() {
return new ScmRequests();
}
public Given given() {
return new Given();
}
/**
* Apply a GET Request to the extracted url from the given link
*
* @param linkPropertyName the property name of link
* @param response the response containing the link
* @return the response of the GET request using the given link
*/
private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
return applyGETRequest(response
.then()
.extract()
.path(linkPropertyName));
}
/**
* Apply a GET Request to the given <code>url</code> and return the response.
*
* @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code>
*/
private Response applyGETRequest(String url) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.get(url);
}
/**
* Apply a PUT Request to the extracted url from the given link
*
* @param response the response containing the link
* @param linkPropertyName the property name of link
* @param body
* @return the response of the PUT request using the given link
*/
private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) {
return applyPUTRequest(response
.then()
.extract()
.path(linkPropertyName), content, body);
}
/**
* Apply a PUT Request to the given <code>url</code> and return the response.
*
* @param url the url of the PUT request
* @param mediaType
* @param body
* @return the response of the PUT request using the given <code>url</code>
*/
private Response applyPUTRequest(String url, String mediaType, String body) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.contentType(mediaType)
.accept(mediaType)
.body(body)
.put(url);
}
private void setUrl(String url) {
this.url = url;
}
private void setUsername(String username) {
this.username = username;
}
private void setPassword(String password) {
this.password = password;
}
private String getUrl() {
return url;
}
private String getUsername() {
return username;
}
private String getPassword() {
return password;
}
public class Given {
public GivenUrl url(String url) {
setUrl(url);
return new GivenUrl();
}
public GivenUrl url(URI url) {
setUrl(url.toString());
return new GivenUrl();
}
}
public class GivenWithUrlAndAuth {
public AppliedMeRequest getMeResource() {
return new AppliedMeRequest(applyGETRequest(url));
}
public AppliedUserRequest getUserResource() {
return new AppliedUserRequest(applyGETRequest(url));
}
public AppliedRepositoryRequest getRepositoryResource() {
return new AppliedRepositoryRequest(
applyGETRequest(url)
);
}
}
public class AppliedRequest<SELF extends AppliedRequest> {
private Response response;
public AppliedRequest(Response response) {
this.response = response;
}
/**
* apply custom assertions to the actual response
*
* @param consumer consume the response in order to assert the content. the header, the payload etc..
* @return the self object
*/
public SELF assertResponse(Consumer<Response> consumer) {
consumer.accept(response);
return (SELF) this;
}
/**
* special assertion of the status code
*
* @param expectedStatusCode the expected status code
* @return the self object
*/
public SELF assertStatusCode(int expectedStatusCode) {
this.response.then().assertThat().statusCode(expectedStatusCode);
return (SELF) this;
}
}
public class AppliedRepositoryRequest extends AppliedRequest<AppliedRepositoryRequest> {
public AppliedRepositoryRequest(Response response) {
super(response);
}
public RepositoryResponse usingRepositoryResponse() {
return new RepositoryResponse(super.response);
}
}
public class RepositoryResponse {
private Response repositoryResponse;
public RepositoryResponse(Response repositoryResponse) {
this.repositoryResponse = repositoryResponse;
}
public AppliedSourcesRequest requestSources() {
return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href"));
}
public AppliedChangesetsRequest requestChangesets() {
return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href"));
}
}
public class AppliedChangesetsRequest extends AppliedRequest<AppliedChangesetsRequest> {
public AppliedChangesetsRequest(Response response) {
super(response);
}
public ChangesetsResponse usingChangesetsResponse() {
return new ChangesetsResponse(super.response);
}
}
public class ChangesetsResponse {
private Response changesetsResponse;
public ChangesetsResponse(Response changesetsResponse) {
this.changesetsResponse = changesetsResponse;
}
public ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
changesetsConsumer.accept(changesets);
return this;
}
public AppliedDiffRequest requestDiff(String revision) {
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
}
public AppliedModificationsRequest requestModifications(String revision) {
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
}
}
public class AppliedSourcesRequest extends AppliedRequest<AppliedSourcesRequest> {
public AppliedSourcesRequest(Response sourcesResponse) {
super(sourcesResponse);
}
public SourcesResponse usingSourcesResponse() {
return new SourcesResponse(super.response);
}
}
public class SourcesResponse {
private Response sourcesResponse;
public SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
}
public SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files");
assertFiles.accept(files);
return this;
}
public AppliedChangesetsRequest requestFileHistory(String fileName) {
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
}
public AppliedSourcesRequest requestSelf(String fileName) {
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
}
}
public class AppliedDiffRequest extends AppliedRequest<AppliedDiffRequest> {
public AppliedDiffRequest(Response response) {
super(response);
}
}
public class GivenUrl {
public GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
setUsername(username);
setPassword(password);
return new GivenWithUrlAndAuth();
}
}
public class AppliedModificationsRequest extends AppliedRequest<AppliedModificationsRequest> {
public AppliedModificationsRequest(Response response) {
super(response);
}
public ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
public class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
public ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String> added = resource.then().extract().path("added");
assertAdded.accept(added);
return this;
}
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String> removed = resource.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String> modified = resource.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
public class AppliedMeRequest extends AppliedRequest<AppliedMeRequest> {
public AppliedMeRequest(Response response) {
super(response);
}
public MeResponse usingMeResponse() {
return new MeResponse(super.response);
}
}
public class MeResponse extends UserResponse<MeResponse> {
public MeResponse(Response response) {
super(response);
}
public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)));
}
}
public class UserResponse<SELF extends UserResponse> extends ModelResponse<SELF> {
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
public UserResponse(Response response) {
super(response);
}
public SELF assertPassword(Consumer<String> assertPassword) {
return super.assertSingleProperty(assertPassword, "password");
}
public SELF assertType(Consumer<String> assertType) {
return assertSingleProperty(assertType, "type");
}
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
return assertSingleProperty(assertAdmin, "admin");
}
public SELF assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public SELF assertPasswordLinkExists() {
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
}
public AppliedChangePasswordRequest requestChangePassword(String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword)));
}
}
/**
* encapsulate standard assertions over model properties
*/
public class ModelResponse<SELF extends ModelResponse> {
protected Response response;
public ModelResponse(Response response) {
this.response = response;
}
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
T propertyValue = response.then().extract().path(propertyJsonPath);
assertSingleProperty.accept(propertyValue);
return (SELF) this;
}
public SELF assertPropertyPathExists(String propertyJsonPath) {
response.then().assertThat().body("any { it.containsKey('" + propertyJsonPath + "')}", is(true));
return (SELF) this;
}
public SELF assertPropertyPathDoesNotExists(String propertyJsonPath) {
response.then().assertThat().body("this.any { it.containsKey('" + propertyJsonPath + "')}", is(false));
return (SELF) this;
}
public SELF assertArrayProperty(Consumer<List> assertProperties, String propertyJsonPath) {
List properties = response.then().extract().path(propertyJsonPath);
assertProperties.accept(properties);
return (SELF) this;
}
}
public class AppliedChangePasswordRequest extends AppliedRequest<AppliedChangePasswordRequest> {
public AppliedChangePasswordRequest(Response response) {
super(response);
}
}
public class AppliedUserRequest extends AppliedRequest<AppliedUserRequest> {
public AppliedUserRequest(Response response) {
super(response);
}
public UserResponse usingUserResponse() {
return new UserResponse(super.response);
}
}
}

View File

@@ -1,12 +1,12 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import sonia.scm.util.IOUtil;
import java.util.ArrayList;
import java.util.Collection;
class ScmTypes {
static Collection<String> availableScmTypes() {
public class ScmTypes {
public static Collection<String> availableScmTypes() {
Collection<String> params = new ArrayList<>();
params.add("git");

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import io.restassured.response.ValidatableResponse;
import org.apache.http.HttpStatus;
@@ -8,14 +8,16 @@ import sonia.scm.repository.PermissionType;
import sonia.scm.web.VndMediaType;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.asList;
import static sonia.scm.it.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
public class TestData {
@@ -26,6 +28,7 @@ public class TestData {
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder());
public static void createDefault() {
cleanup();
@@ -44,27 +47,31 @@ public class TestData {
}
public static void createUser(String username, String password) {
createUser(username, password, false, "xml");
}
public static void createUser(String username, String password, boolean isAdmin, String type) {
LOG.info("create user with username: {}", username);
String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER)
.when()
.content(" {\n" +
" \"active\": true,\n" +
" \"admin\": false,\n" +
" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" +
" \"displayName\": \"" + username + "\",\n" +
" \"mail\": \"user1@scm-manager.org\",\n" +
" \"name\": \"" + username + "\",\n" +
" \"password\": \"" + password + "\",\n" +
" \"type\": \"xml\"\n" +
" \n" +
" }")
.post(createResourceUrl("users"))
.content(new StringBuilder()
.append(" {\n")
.append(" \"active\": true,\n")
.append(" \"admin\": ").append(admin).append(",\n")
.append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
.append(" \"displayName\": \"").append(username).append("\",\n")
.append(" \"mail\": \"user1@scm-manager.org\",\n")
.append(" \"name\": \"").append(username).append("\",\n")
.append(" \"password\": \"").append(password).append("\",\n")
.append(" \"type\": \"").append(type).append("\"\n")
.append(" }").toString())
.post(getUsersUrl())
.then()
.statusCode(HttpStatus.SC_CREATED)
;
}
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
@@ -183,7 +190,7 @@ public class TestData {
}
public static String repositoryJson(String repositoryType) {
return Json.createObjectBuilder()
return JSON_BUILDER
.add("contact", "zaphod.beeblebrox@hitchhiker.com")
.add("description", "Heart of Gold")
.add("name", "HeartOfGold-" + repositoryType)
@@ -192,6 +199,29 @@ public class TestData {
.build().toString();
}
public static URI getMeUrl() {
return RestUtil.createResourceUrl("me/");
}
public static URI getUsersUrl() {
return RestUtil.createResourceUrl("users/");
}
public static URI getUserUrl(String username) {
return getUsersUrl().resolve(username);
}
public static String createPasswordChangeJson(String oldPassword, String newPassword) {
return JSON_BUILDER
.add("oldPassword", oldPassword)
.add("newPassword", newPassword)
.build().toString();
}
public static void main(String[] args) {
cleanup();
}

View File

@@ -56,7 +56,7 @@ const userZaphod = {
displayName: "Z. Beeblebrox",
mail: "president@heartofgold.universe",
name: "zaphod",
password: "__dummypassword__",
password: "",
type: "xml",
properties: {},
_links: {
@@ -79,7 +79,7 @@ const userFord = {
displayName: "F. Prefect",
mail: "ford@prefect.universe",
name: "ford",
password: "__dummypassword__",
password: "",
type: "xml",
properties: {},
_links: {

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.user.ChangePasswordNotAllowedException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ChangePasswordNotAllowedExceptionMapper implements ExceptionMapper<ChangePasswordNotAllowedException> {
@Override
public Response toResponse(ChangePasswordNotAllowedException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -10,6 +10,7 @@ import sonia.scm.PageResult;
import javax.ws.rs.core.Response;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -37,6 +38,15 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
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(
loadBy(id),
applyChanges,
idStaysTheSame(id),
checker
);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
return singleAdapter.update(
loadBy(id),

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.user.InvalidPasswordException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidPasswordExceptionMapper implements ExceptionMapper<InvalidPasswordException> {
@Override
public Response toResponse(InvalidPasswordException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -8,6 +8,7 @@ public class MapperModule extends AbstractModule {
@Override
protected void configure() {
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass());
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
bind(UserCollectionToDtoMapper.class);

View File

@@ -4,19 +4,27 @@ 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.NotFoundException;
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.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
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;
/**
@@ -24,15 +32,20 @@ import javax.ws.rs.core.UriInfo;
*/
@Path(MeResource.ME_PATH_V2)
public class MeResource {
static final String ME_PATH_V2 = "v2/me/";
public static final String ME_PATH_V2 = "v2/me/";
private final UserToUserDtoMapper userToDtoMapper;
private final MeToUserDtoMapper meToUserDtoMapper;
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
private final UserManager userManager;
@Inject
public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) {
this.userToDtoMapper = userToDtoMapper;
public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) {
this.meToUserDtoMapper = meToUserDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.passwordService = passwordService;
this.userManager = manager;
}
/**
@@ -50,6 +63,34 @@ public class MeResource {
public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException {
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, userToDtoMapper::map);
return adapter.get(id, meToUserDtoMapper::map);
}
/**
* Change password of the current user
*/
@PUT
@Path("password")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@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())));
}
/**
* Match given old password from the dto with the stored password before updating
*/
private Consumer<User> getOldOriginalPasswordChecker(String oldPassword) {
return user -> {
if (!user.getPassword().equals(passwordService.encryptPassword(oldPassword))) {
throw new InvalidPasswordException(INVALID_MATCHING);
}
};
}
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class MeToUserDtoMapper extends UserToUserDtoMapper{
@Inject
private UserManager userManager;
@Inject
private ResourceLinks resourceLinks;
@Override
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
target.add(linksBuilder.build());
}
}

View File

@@ -0,0 +1,17 @@
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 PasswordChangeDto {
private String oldPassword;
@NotEmpty
private String newPassword;
}

View File

@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
class ResourceLinks {
@@ -85,8 +86,43 @@ class ResourceLinks {
String update(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("update").parameters().href();
}
public String passwordChange(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("changePassword").parameters().href();
}
}
MeLinks me() {
return new MeLinks(scmPathInfoStore.get(), this.user());
}
static class MeLinks {
private final LinkBuilder meLinkBuilder;
private UserLinks userLinks;
MeLinks(ScmPathInfo pathInfo, UserLinks user) {
meLinkBuilder = new LinkBuilder(pathInfo, MeResource.class);
userLinks = user;
}
String self() {
return meLinkBuilder.method("get").parameters().href();
}
String delete(String name) {
return userLinks.delete(name);
}
String update(String name) {
return userLinks.update(name);
}
public String passwordChange() {
return meLinkBuilder.method("changePassword").parameters().href();
}
}
UserCollectionLinks userCollection() {
return new UserCollectionLinks(scmPathInfoStore.get());
}

View File

@@ -11,6 +11,7 @@ import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -53,6 +54,11 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
.map(Response.ResponseBuilder::build)
.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 {
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
checker.accept(existingModelObject);
return update(reader,applyChanges,hasSameKey);
}
/**
* Update the model object for the given id according to the given function and returns a corresponding http response.

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.AlreadyExistsException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
@@ -29,14 +30,16 @@ public class UserCollectionResource {
private final ResourceLinks resourceLinks;
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
@Inject
public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper,
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) {
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks, PasswordService passwordService) {
this.dtoToUserMapper = dtoToUserMapper;
this.userCollectionToDtoMapper = userCollectionToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.resourceLinks = resourceLinks;
this.passwordService = passwordService;
}
/**
@@ -89,8 +92,6 @@ public class UserCollectionResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
public Response create(@Valid UserDto userDto) throws AlreadyExistsException {
return adapter.create(userDto,
() -> dtoToUserMapper.map(userDto, ""),
user -> resourceLinks.user().self(user.getName()));
return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName()));
}
}

View File

@@ -1,37 +1,35 @@
package sonia.scm.api.v2.resources;
import org.apache.shiro.authc.credential.PasswordService;
import org.mapstruct.AfterMapping;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import javax.inject.Inject;
import java.time.Instant;
import static sonia.scm.api.rest.resources.UserResource.DUMMY_PASSWORT;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
@Mapper
public abstract class UserDtoToUserMapper extends BaseDtoMapper {
@Inject
private PasswordService passwordService;
@Mapping(source = "password", target = "password", qualifiedByName = "encrypt")
@Mapping(target = "creationDate", ignore = true)
public abstract User map(UserDto userDto, @Context String originalPassword);
public abstract User map(UserDto userDto, @Context String usedPassword);
@Named("encrypt")
String encrypt(String password, @Context String originalPassword) {
if (DUMMY_PASSWORT.equals(password)) {
return originalPassword;
} else {
return passwordService.encryptPassword(password);
}
/**
* depends on the use case the right password will be mapped.
* The given Password in the context parameter will be set.
* The mapper consumer have the control of what password should be set.
* </p>
* eg. for update user action the password will be set to the original password
* for create user and change password actions the password is the user input
*
* @param usedPassword the password to be set
* @param user the target
*/
@AfterMapping
void overridePassword(@MappingTarget User user, @Context String usedPassword) {
user.setPassword(usedPassword);
}
}

View File

@@ -3,6 +3,7 @@ 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.authc.credential.PasswordService;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.user.User;
@@ -26,12 +27,16 @@ public class UserResource {
private final UserToUserDtoMapper userToDtoMapper;
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final UserManager userManager;
private final PasswordService passwordService;
@Inject
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) {
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) {
this.dtoToUserMapper = dtoToUserMapper;
this.userToDtoMapper = userToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.userManager = manager;
this.passwordService = passwordService;
}
/**
@@ -40,7 +45,6 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param id the id/name of the user
*
*/
@GET
@Path("")
@@ -63,7 +67,6 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param name the name of the user to delete.
*
*/
@DELETE
@Path("")
@@ -80,10 +83,11 @@ public class UserResource {
/**
* Modifies the given user.
* The given Password in the payload will be ignored. To Change Password use the changePassword endpoint
*
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param name name of the user to be modified
* @param name name of the user to be modified
* @param userDto user object to modify
*/
@PUT
@@ -101,4 +105,30 @@ public class UserResource {
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
}
/**
* This Endpoint is for Admin user to modify a user password.
* 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.
*
* <strong>Note:</strong> This method requires "user:modify" privilege.
* @param name name of the user to be modified
* @param passwordChangeDto change password object to modify password. the old password is here not required
*/
@PUT
@Path("password")
@Consumes(VndMediaType.PASSWORD_CHANGE)
@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"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@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());
}
}

View File

@@ -4,9 +4,10 @@ import com.google.common.annotations.VisibleForTesting;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.api.rest.resources.UserResource;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
@@ -19,19 +20,17 @@ import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
@Inject
private UserManager userManager;
@Override
@Mapping(target = "attributes", ignore = true)
@Mapping(target = "password", ignore = true)
public abstract UserDto map(User modelObject);
@Inject
private ResourceLinks resourceLinks;
@VisibleForTesting
void setResourceLinks(ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
}
@AfterMapping
void removePassword(@MappingTarget UserDto target) {
target.setPassword(UserResource.DUMMY_PASSWORT);
}
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName()));
@@ -41,6 +40,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())));
}
target.add(linksBuilder.build());
}

View File

@@ -15,6 +15,8 @@ public class DispatcherMock {
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class);
return dispatcher;
}
}

View File

@@ -2,8 +2,8 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.authc.credential.PasswordService;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
@@ -22,11 +22,17 @@ import java.net.URISyntaxException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@SubjectAware(
username = "trillian",
password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
public class MeResourceTest {
@@ -34,8 +40,7 @@ public class MeResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private Dispatcher dispatcher;
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@Mock
@@ -47,22 +52,28 @@ public class MeResourceTest {
private UserManager userManager;
@InjectMocks
private UserToUserDtoMapperImpl userToDtoMapper;
private MeToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@Mock
private PasswordService passwordService;
private User originalUser;
@Before
public void prepareEnvironment() throws Exception {
initMocks(this);
createDummyUser("trillian");
originalUser = createDummyUser("trillian");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture());
userToDtoMapper.setResourceLinks(resourceLinks);
MeResource meResource = new MeResource(userToDtoMapper, userManager);
dispatcher.getRegistry().addSingletonResource(meResource);
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getUserTypeChecker()).thenCallRealMethod();
when(userManager.getDefaultType()).thenReturn("xml");
MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
when(scmPathInfoStore.get()).thenReturn(uriInfo);
dispatcher = createDispatcher(meResource);
}
@Test
@@ -76,14 +87,78 @@ public class MeResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"trillian\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/trillian\"}"));
assertTrue(response.getContentAsString().contains("\"password\":null"));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/me/\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}"));
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String encryptedNewPassword = "encrypted123";
String oldPassword = "notEncriptedSecret";
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");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals(encryptedNewPassword, updatedUser.getPassword());
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
originalUser.setType("not an xml type");
String newPassword = "pwd123";
String oldPassword = "notEncriptedSecret";
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(oldPassword))).thenReturn("secret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnChangePasswordIfOldPasswordDoesNotMatchOriginalPassword() throws Exception {
String newPassword = "pwd123";
String oldPassword = "notEncriptedSecret";
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(oldPassword))).thenReturn("differentThanSecret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
private User createDummyUser(String name) {
User user = new User();
user.setName(name);
user.setType("xml");
user.setPassword("secret");
user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user);

View File

@@ -0,0 +1,135 @@
package sonia.scm.api.v2.resources;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class MeToUserDtoMapperTest {
private final URI baseUri = URI.create("http://example.com/base/");
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks
private MeToUserDtoMapperImpl mapper;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
private URI expectedBaseUri;
private URI expectedUserBaseUri;
@Before
public void init() {
initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(MeResource.ME_PATH_V2 + "/");
expectedUserBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind();
ThreadContext.bind(subject);
}
@After
public void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test
public void shouldMapTheUpdateLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no update link", userDto.getLinks().getLinkBy("update").isPresent());
}
@Test
public void shouldMapTheSelfLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.toString(), userDto.getLinks().getLinkBy("self").get().getHref());
}
@Test
public void shouldMapTheDeleteLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:delete:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref());
when(subject.isPermitted("user:delete:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no delete link", userDto.getLinks().getLinkBy("delete").isPresent());
}
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).as("hide password for the me resource").isBlank();
}
private User createDefaultUser() {
User user = new User();
user.setName("abc");
user.setCreationDate(1L);
return user;
}
}

View File

@@ -12,7 +12,9 @@ public class ResourceLinksMock {
ScmPathInfo uriInfo = mock(ScmPathInfo.class);
when(uriInfo.getApiRestUri()).thenReturn(baseUri);
when(resourceLinks.user()).thenReturn(new ResourceLinks.UserLinks(uriInfo));
ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo);
when(resourceLinks.user()).thenReturn(userLinks);
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks));
when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo));
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));

View File

@@ -23,18 +23,9 @@ public class UserDtoToUserMapperTest {
@Test
public void shouldMapFields() {
UserDto dto = createDefaultDto();
User user = mapper.map(dto, "original password");
User user = mapper.map(dto, "used password");
assertEquals("abc" , user.getName());
}
@Test
public void shouldEncodePassword() {
when(passwordService.encryptPassword("unencrypted")).thenReturn("encrypted");
UserDto dto = createDefaultDto();
dto.setPassword("unencrypted");
User user = mapper.map(dto, "original password");
assertEquals("encrypted" , user.getPassword());
assertEquals("used password" , user.getPassword());
}
@Before

View File

@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
@@ -61,21 +62,25 @@ public class UserRootResourceTest {
private UserToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
private User originalUser;
@Before
public void prepareEnvironment() throws Exception {
initMocks(this);
User dummyUser = createDummyUser("Neo");
originalUser = createDummyUser("Neo");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getUserTypeChecker()).thenCallRealMethod();
doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture());
when(userManager.getDefaultType()).thenReturn("xml");
UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks);
UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper,
userCollectionToDtoMapper, resourceLinks);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager);
userCollectionToDtoMapper, resourceLinks, passwordService);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService);
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
Providers.of(userResource));
Providers.of(userResource));
dispatcher = createDispatcher(userRootResource);
}
@@ -89,7 +94,7 @@ public class UserRootResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"password\":null"));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
}
@@ -104,13 +109,49 @@ public class UserRootResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"password\":null"));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}"));
assertFalse(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
}
@Test
public void shouldCreateNewUserWithEncryptedPassword() throws Exception {
public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals("encrypted123", updatedUser.getPassword());
}
@Test
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() 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_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
public void shouldEncryptPasswordBeforeCreatingUser() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/user-test-create.json");
byte[] userJson = Resources.toByteArray(url);
@@ -130,7 +171,7 @@ public class UserRootResourceTest {
}
@Test
public void shouldUpdateChangedUserWithEncryptedPassword() throws Exception {
public void shouldIgnoreGivenPasswordOnUpdatingUser() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/user-test-update.json");
byte[] userJson = Resources.toByteArray(url);
@@ -139,14 +180,13 @@ public class UserRootResourceTest {
.contentType(VndMediaType.USER)
.content(userJson);
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals("encrypted123", updatedUser.getPassword());
assertEquals(originalUser.getPassword(), updatedUser.getPassword());
}
@Test
@@ -154,7 +194,7 @@ public class UserRootResourceTest {
MockHttpRequest request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2)
.contentType(VndMediaType.USER)
.content(new byte[] {});
.content(new byte[]{});
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
@@ -265,6 +305,7 @@ public class UserRootResourceTest {
private User createDummyUser(String name) {
User user = new User();
user.setName(name);
user.setType("xml");
user.setPassword("redpill");
user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user);

View File

@@ -8,14 +8,17 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import sonia.scm.api.rest.resources.UserResource;
import org.mockito.Mock;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import java.net.URI;
import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -26,6 +29,9 @@ public class UserToUserDtoMapperTest {
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks
private UserToUserDtoMapperImpl mapper;
@@ -37,6 +43,7 @@ public class UserToUserDtoMapperTest {
@Before
public void init() {
initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind();
ThreadContext.bind(subject);
@@ -53,11 +60,42 @@ public class UserToUserDtoMapperTest {
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref());
assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref());
assertEquals("expected update link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
}
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
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);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).isBlank();
}
@Test
public void shouldMapLinks_forDelete() {
User user = createDefaultUser();
@@ -65,7 +103,7 @@ public class UserToUserDtoMapperTest {
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref());
assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref());
assertEquals("expected delete link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref());
}
@@ -97,16 +135,6 @@ public class UserToUserDtoMapperTest {
assertEquals("abc", userDto.getName());
}
@Test
public void shouldRemovePassword() {
User user = createDefaultUser();
user.setPassword("password");
UserDto userDto = mapper.map(user);
assertEquals(UserResource.DUMMY_PASSWORT, userDto.getPassword());
}
@Test
public void shouldMapTimes() {
User user = createDefaultUser();