diff --git a/pom.xml b/pom.xml index 3295e732db..1c08c334f5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -83,7 +84,7 @@ scm-manager release repository http://maven.scm-manager.org/nexus/content/groups/public - + ossrh https://oss.sonatype.org/content/repositories/snapshots @@ -118,10 +119,26 @@ + + - junit - junit - test + org.junit.jupiter + junit-jupiter-api + + + + org.junit.jupiter + junit-jupiter-params + + + + org.junit.jupiter + junit-jupiter-engine + + + + org.junit.vintage + junit-vintage-engine @@ -139,15 +156,11 @@ org.mockito mockito-core - ${mockito.version} - test org.assertj assertj-core - 3.10.0 - test @@ -161,7 +174,7 @@ provided - + @@ -282,9 +295,32 @@ ${jackson.version} + + - junit - junit + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + org.junit.vintage + junit-vintage-engine ${junit.version} test @@ -348,7 +384,11 @@ - + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + org.apache.maven.plugins maven-enforcer-plugin @@ -536,9 +576,9 @@ maven-eclipse-plugin 2.6 - + - + org.jacoco jacoco-maven-plugin @@ -558,7 +598,7 @@ - + @@ -695,13 +735,13 @@ 2.10.0 1.3 - 4.12 + 5.2.0 1.7.22 1.1.10 3.0.1 - + 2.0.1 3.1.3.Final 1.19.4 @@ -711,7 +751,7 @@ 1.3.0 - + 9.2.10.v20150310 9.2.10.v20150310 diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/Permission.java index b5de810f75..0632795648 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ b/scm-core/src/main/java/sonia/scm/repository/Permission.java @@ -1,32 +1,32 @@ -/** - * Copyright (c) 2010, 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, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 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 - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * 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 - * +/* + Copyright (c) 2010, 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, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 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 + DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + 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 + */ @@ -36,16 +36,14 @@ package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Objects; - import sonia.scm.security.PermissionObject; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.Serializable; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +//~--- JDK imports ------------------------------------------------------------ /** * Permissions controls the access to {@link Repository}. @@ -57,10 +55,11 @@ import javax.xml.bind.annotation.XmlRootElement; public class Permission implements PermissionObject, Serializable { - /** Field description */ private static final long serialVersionUID = -2915175031430884040L; - //~--- constructors --------------------------------------------------------- + private boolean groupPermission = false; + private String name; + private PermissionType type = PermissionType.READ; /** * Constructs a new {@link Permission}. @@ -153,12 +152,7 @@ public class Permission implements PermissionObject, Serializable return Objects.hashCode(name, type, groupPermission); } - /** - * Method description - * - * - * @return - */ + @Override public String toString() { @@ -242,15 +236,4 @@ public class Permission implements PermissionObject, Serializable { this.type = type; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private boolean groupPermission = false; - - /** Field description */ - private String name; - - /** Field description */ - private PermissionType type = PermissionType.READ; } diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java new file mode 100644 index 0000000000..43ad0a5e1d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java @@ -0,0 +1,11 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionAlreadyExistsException extends RepositoryException { + + public PermissionAlreadyExistsException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} already exists", permissionName, repository.getNamespace(), repository.getName())); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java new file mode 100644 index 0000000000..9e1b644faa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java @@ -0,0 +1,12 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionNotFoundException extends RepositoryException{ + + + public PermissionNotFoundException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} does not exists", permissionName,repository.getNamespace(), repository.getName() )); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java b/scm-core/src/main/java/sonia/scm/repository/PermissionType.java index bd4d773877..bba0d44f3d 100644 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionType.java @@ -42,10 +42,10 @@ public enum PermissionType { /** read permision */ - READ(0, "repository:read:"), + READ(0, "repository:read,pull:"), /** read and write permissionPrefix */ - WRITE(10, "repository:read,write:"), + WRITE(10, "repository:read,pull,push:"), /** * read, write and diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 44841893e7..190f252a9f 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -60,7 +60,7 @@ import java.util.List; */ @StaticPermissions( value = "repository", - permissions = {"read", "write", "modify", "delete", "healthCheck"} + permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"} ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 9dd6b68d36..4cbf815e45 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -16,6 +16,7 @@ public class VndMediaType { public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; + public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index 7517f4848c..dd3c82e800 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -36,13 +36,11 @@ package sonia.scm.web.filter; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Splitter; - import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ArgumentIsInvalidException; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; @@ -53,17 +51,14 @@ import sonia.scm.security.ScmSecurityException; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Iterator; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.shiro.authz.AuthorizationException; +import java.io.IOException; +import java.util.Iterator; + +//~--- JDK imports ------------------------------------------------------------ /** * Abstract http filter to check repository permissions. @@ -339,7 +334,7 @@ public abstract class PermissionFilter extends HttpFilter if (writeRequest) { - permitted = RepositoryPermissions.write(repository).isPermitted(); + permitted = RepositoryPermissions.push(repository).isPermitted(); } else { diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java new file mode 100644 index 0000000000..63d8b46d47 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2010, 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, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 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 + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * 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.it; + +import org.apache.http.HttpStatus; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientException; +import sonia.scm.web.VndMediaType; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +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; + +@RunWith(Parameterized.class) +public class PermissionsITCase { + + public static final String USER_READ = "user_read"; + public static final String USER_PASS = "pass"; + private static final String USER_WRITE = "user_write"; + private static final String USER_OWNER = "user_owner"; + private static final String USER_OTHER = "user_other"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private final String repositoryType; + private int createdPermissions; + + + public PermissionsITCase(String repositoryType) { + this.repositoryType = repositoryType; + } + + @Parameters(name = "{0}") + public static Collection createParameters() { + return availableScmTypes(); + } + + @Before + public void prepareEnvironment() { + TestData.createDefault(); + TestData.createUser(USER_READ, USER_PASS); + TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType); + TestData.createUser(USER_WRITE, USER_PASS); + TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType); + TestData.createUser(USER_OWNER, USER_PASS); + TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); + TestData.createUser(USER_OTHER, USER_PASS); + createdPermissions = 3; + } + + @Test + public void readUserShouldNotSeePermissions() { + assertNull(callRepository(USER_READ, USER_PASS, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } + + @Test + public void readUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.PERMISSION, USER_READ, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void writeUserShouldNotSeePermissions() { + assertNull(callRepository(USER_WRITE, USER_PASS, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href")); + } + + @Test + public void writeUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void ownerShouldSeePermissions() { + List userPermissions = TestData.getUserPermissions(USER_OWNER, USER_PASS, repositoryType); + assertEquals(userPermissions.size(), createdPermissions); + } + + @Test + public void otherUserShouldNotSeeRepository() { + callRepository(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + + @Test + public void otherUserShouldNotSeeBruteForcePermissions() { + given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS) + .when() + .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_FORBIDDEN); + } + + @Test + public void readUserShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_READ, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void writeUserShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_WRITE, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void ownerShouldCloneRepository() throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), USER_OWNER, USER_PASS); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + + @Test + public void otherUserShouldNotCloneRepository() { + TestData.callRepository(USER_OTHER, USER_PASS, repositoryType, HttpStatus.SC_FORBIDDEN); + } + + @Test(expected = RepositoryClientException.class) + public void userWithReadPermissionShouldBeNotAuthorizedToCommit() throws IOException { + createAndCommit(USER_READ); + } + + @Test + public void userWithOwnerPermissionShouldBeAuthorizedToCommit() throws IOException { + createAndCommit(USER_OWNER); + } + + @Test + public void userWithWritePermissionShouldBeAuthorizedToCommit() throws IOException { + createAndCommit(USER_WRITE); + } + + private void createAndCommit(String username) throws IOException { + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), username, PermissionsITCase.USER_PASS); + addAndCommitRandomFile(client, username); + } + + @Test + public void userWithOwnerPermissionShouldBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_NO_CONTENT, HttpStatus.SC_NOT_FOUND, USER_OWNER, repositoryType); + } + + @Test + public void userWithReadPermissionShouldNotBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_READ, repositoryType); + } + + @Test + public void userWithWritePermissionShouldNotBeAuthorizedToDeleteRepository(){ + assertDeleteRepositoryOperation(HttpStatus.SC_FORBIDDEN, HttpStatus.SC_OK, USER_WRITE, repositoryType); + } + + private void assertDeleteRepositoryOperation(int expectedDeleteStatus, int expectedGetStatus, String user, String repositoryType) { + given(VndMediaType.REPOSITORY, user, PermissionsITCase.USER_PASS) + + .when() + .delete(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedDeleteStatus); + + given(VndMediaType.REPOSITORY, user, PermissionsITCase.USER_PASS) + + .when() + .get(TestData.getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedGetStatus); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index cd791cb013..21d4d97b1b 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -34,6 +34,7 @@ package sonia.scm.it; //~--- non-JDK imports -------------------------------------------------------- import org.apache.http.HttpStatus; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -41,17 +42,12 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import sonia.scm.repository.Person; -import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; -import sonia.scm.repository.client.api.RepositoryClientFactory; import sonia.scm.web.VndMediaType; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; -import java.util.UUID; +import java.util.Objects; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -66,8 +62,6 @@ import static sonia.scm.it.TestData.repositoryJson; @RunWith(Parameterized.class) public class RepositoriesITCase { - public static final Person AUTHOR = new Person("SCM Administrator", "scmadmin@scm-manager.org"); - @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -142,57 +136,16 @@ public class RepositoriesITCase { @Test public void shouldCloneRepository() throws IOException { - RepositoryClient client = createRepositoryClient(); - assertEquals("expected metadata dir", 1, client.getWorkingCopy().list().length); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.getRoot()); + assertEquals("expected metadata dir", 1, Objects.requireNonNull(client.getWorkingCopy().list()).length); } @Test public void shouldCommitFiles() throws IOException { - RepositoryClient client = createRepositoryClient(); - - for (int i = 0; i < 5; i++) { - createRandomFile(client); - } - - commit(client); - - RepositoryClient checkClient = createRepositoryClient(); - assertEquals("expected 5 files and metadata dir", 6, checkClient.getWorkingCopy().list().length); + RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin"); + String name = RepositoryUtil.addAndCommitRandomFile(client, "scmadmin"); + RepositoryClient checkClient = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin"); + Assertions.assertThat(checkClient.getWorkingCopy().list()).contains(name); } - private static void createRandomFile(RepositoryClient client) throws IOException { - String uuid = UUID.randomUUID().toString(); - String name = "file-" + uuid + ".uuid"; - - File file = new File(client.getWorkingCopy(), name); - try (FileOutputStream out = new FileOutputStream(file)) { - out.write(uuid.getBytes()); - } - - client.getAddCommand().add(name); - } - - private static void commit(RepositoryClient repositoryClient) throws IOException { - repositoryClient.getCommitCommand().commit(AUTHOR, "commit"); - if ( repositoryClient.isCommandSupported(ClientCommand.PUSH) ) { - repositoryClient.getPushCommand().push(); - } - } - - private RepositoryClient createRepositoryClient() throws IOException { - RepositoryClientFactory clientFactory = new RepositoryClientFactory(); - String cloneUrl = readCloneUrl(); - return clientFactory.create(repositoryType, cloneUrl, "scmadmin", "scmadmin", temporaryFolder.newFolder()); - } - - private String readCloneUrl() { - return given(VndMediaType.REPOSITORY) - - .when() - .get(repositoryUrl) - - .then() - .extract() - .path("_links.httpProtocol.href"); - } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index c5376a3ff2..e3dd5c0b88 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -1,20 +1,23 @@ package sonia.scm.it; import org.apache.http.HttpStatus; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import sonia.scm.repository.client.api.ClientCommand; +import sonia.scm.repository.client.api.RepositoryClient; +import java.io.File; import java.io.IOException; import java.util.Collection; import static java.lang.Thread.sleep; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeFalse; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; @@ -25,7 +28,7 @@ public class RepositoryAccessITCase { public TemporaryFolder tempFolder = new TemporaryFolder(); private final String repositoryType; - private RepositoryUtil repositoryUtil; + private File folder; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -37,16 +40,18 @@ public class RepositoryAccessITCase { } @Before - public void initClient() throws IOException { + public void initClient() { TestData.createDefault(); - repositoryUtil = new RepositoryUtil(repositoryType, tempFolder.getRoot()); + folder = tempFolder.getRoot(); } @Test public void shouldFindBranches() throws IOException { - assumeFalse("There are no branches for SVN", repositoryType.equals("svn")); + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); - repositoryUtil.createAndCommitFile("a.txt", "a"); + Assume.assumeTrue("There are no branches for " + repositoryType, repositoryClient.isCommandSupported(ClientCommand.BRANCH)); + + RepositoryUtil.createAndCommitFile(repositoryClient, "scmadmin", "a.txt", "a"); String branchesUrl = given() .when() diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index cb6d34559b..ef2332074b 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -8,56 +8,48 @@ import sonia.scm.repository.Person; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClientFactory; -import sonia.scm.web.VndMediaType; import java.io.File; import java.io.IOException; - -import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; -import static sonia.scm.it.RestUtil.ADMIN_USERNAME; -import static sonia.scm.it.RestUtil.given; +import java.util.UUID; public class RepositoryUtil { private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); - private final RepositoryClient repositoryClient; - private final File folder; - - RepositoryUtil(String repositoryType, File folder) throws IOException { - this.repositoryClient = createRepositoryClient(repositoryType, folder); - this.folder = folder; + static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { + return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin"); } - static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { - String httpProtocolUrl = given(VndMediaType.REPOSITORY) - - .when() - .get(TestData.getDefaultRepositoryUrl(repositoryType)) - - .then() - .statusCode(HttpStatus.SC_OK) + 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.httpProtocol.href"); - - return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, ADMIN_USERNAME, ADMIN_PASSWORD, folder); + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); } - void createAndCommitFile(String fileName, String content) throws IOException { - File file = new File(folder, fileName); + 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 void createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + File file = new File(repositoryClient.getWorkingCopy(), fileName); Files.write(content, file, Charsets.UTF_8); - addWithParentDirectories(file); - commit("added " + fileName); + addWithParentDirectories(repositoryClient, file); + commit(repositoryClient, username, "added " + fileName); } - private String addWithParentDirectories(File file) throws IOException { + private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException { File parent = file.getParentFile(); String thisName = file.getName(); String path; - if (!folder.equals(parent)) { - addWithParentDirectories(parent); - path = addWithParentDirectories(parent) + File.separator + thisName; + if (!repositoryClient.getWorkingCopy().equals(parent)) { + addWithParentDirectories(repositoryClient, parent); + path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; } else { path = thisName; } @@ -65,10 +57,8 @@ public class RepositoryUtil { return path; } - Changeset commit(String message) throws IOException { - Changeset changeset = repositoryClient.getCommitCommand().commit( - new Person("scmadmin", "scmadmin@scm-manager.org"), message - ); + static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException { + Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message); if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { repositoryClient.getPushCommand().push(); } diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/RestUtil.java index 76d2461637..a7409e1995 100644 --- a/scm-it/src/test/java/sonia/scm/it/RestUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RestUtil.java @@ -19,15 +19,19 @@ public class RestUtil { public static final String ADMIN_USERNAME = "scmadmin"; public static final String ADMIN_PASSWORD = "scmadmin"; - public static RequestSpecification given(String mediaType) { - return RestAssured.given() - .contentType(mediaType) - .accept(mediaType) - .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); - } - public static RequestSpecification given() { return RestAssured.given() .auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD); } + + public static RequestSpecification given(String mediaType) { + return given(mediaType, ADMIN_USERNAME, ADMIN_PASSWORD); + } + + public static RequestSpecification given(String mediaType, String username, String password) { + return RestAssured.given() + .contentType(mediaType) + .accept(mediaType) + .auth().preemptive().basic(username, password); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java index b2785b2051..ada67d588c 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -1,8 +1,10 @@ package sonia.scm.it; +import io.restassured.response.ValidatableResponse; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.repository.PermissionType; import sonia.scm.web.VndMediaType; import javax.json.Json; @@ -19,7 +21,9 @@ public class TestData { private static final Logger LOG = LoggerFactory.getLogger(TestData.class); - private static final List PROTECTED_USERS = asList("scmadmin", "anonymous"); + public static final String USER_SCM_ADMIN = "scmadmin"; + public static final String USER_ANONYMOUS = "anonymous"; + private static final List PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static Map DEFAULT_REPOSITORIES = new HashMap<>(); @@ -38,6 +42,77 @@ public class TestData { return DEFAULT_REPOSITORIES.get(repositoryType); } + public static void createUser(String username, String password) { + 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")) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + + public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { + given(VndMediaType.PERMISSION) + .when() + .content("{\n" + + "\t\"type\": \"" + permissionType.name() + "\",\n" + + "\t\"name\": \"" + name + "\",\n" + + "\t\"groupPermission\": false\n" + + "\t\n" + + "}") + .post(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .then() + .statusCode(HttpStatus.SC_CREATED) + ; + } + + public static List getUserPermissions(String username, String password, String repositoryType) { + return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK) + .extract() + .body().jsonPath().getList("_embedded.permissions"); + } + + public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { + return given(VndMediaType.PERMISSION, username, password) + .when() + .get(TestData.getDefaultPermissionUrl(username, password, repositoryType)) + .then() + .statusCode(expectedStatusCode); + } + + public static ValidatableResponse callRepository(String username, String password, String repositoryType, int expectedStatusCode) { + return given(VndMediaType.REPOSITORY, username, password) + + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedStatusCode); + } + + public static String getDefaultPermissionUrl(String username, String password, String repositoryType) { + return given(VndMediaType.REPOSITORY, username, password) + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .body().jsonPath().getString("_links.permissions.href"); + } + + private static void cleanupRepositories() { List repositories = given(VndMediaType.REPOSITORY_COLLECTION) .when() diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini similarity index 100% rename from scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini rename to scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 0da030652b..98441ec898 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -28,12 +28,6 @@ 2.0.0-SNAPSHOT - - junit - junit - compile - - com.github.sdorra shiro-unit diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 965bddf2b0..b34da3454e 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -287,7 +287,7 @@ ${jersey-client.version} test - + diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java index 08d6b984d2..fef7fc96c4 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java @@ -1,30 +1,30 @@ -/** - * Copyright (c) 2010, 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, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. 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 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON 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 - * +/* + Copyright (c) 2010, 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, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 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 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON 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 + */ @@ -36,11 +36,11 @@ package sonia.scm.api.rest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -//~--- JDK imports ------------------------------------------------------------ - import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -56,14 +56,14 @@ public class StatusExceptionMapper private static final Logger logger = LoggerFactory.getLogger(StatusExceptionMapper.class); - //~--- constructors --------------------------------------------------------- + private final Response.Status status; + private final Class type; /** - * Constructs ... + * Map an Exception to a HTTP Response * - * - * @param type - * @param status + * @param type the exception class + * @param status the http status to be mapped */ public StatusExceptionMapper(Class type, Response.Status status) { @@ -71,15 +71,12 @@ public class StatusExceptionMapper this.status = status; } - //~--- methods -------------------------------------------------------------- - /** - * Method description + * provide a http responses from an exception * + * @param exception the thrown exception * - * @param exception - * - * @return + * @return the http response with the exception presentation */ @Override public Response toResponse(E exception) @@ -95,12 +92,4 @@ public class StatusExceptionMapper return Response.status(status).build(); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Response.Status status; - - /** Field description */ - private final Class type; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 4cbe6406f8..9ceec9e230 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule { bind(RepositoryTypeCollectionToDtoMapper.class); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); + bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); + bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java new file mode 100644 index 0000000000..d654f8bca7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java @@ -0,0 +1,49 @@ +/* + 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, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 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 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON 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.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionAlreadyExistsException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @since 2.0.0 + */ +@Provider +public class PermissionAlreadyExistsExceptionMapper extends StatusExceptionMapper { + + public PermissionAlreadyExistsExceptionMapper() { + super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java deleted file mode 100644 index 6c4b52c16d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java +++ /dev/null @@ -1,18 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; - -public class PermissionCollectionResource { - @GET - @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java new file mode 100644 index 0000000000..4789915f3d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import java.util.List; + +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; +import static java.util.stream.Collectors.toList; + +public class PermissionCollectionToDtoMapper { + + private final ResourceLinks resourceLinks; + private final PermissionToPermissionDtoMapper permissionToPermissionDtoMapper; + + @Inject + public PermissionCollectionToDtoMapper(PermissionToPermissionDtoMapper permissionToPermissionDtoMapper, ResourceLinks resourceLinks) { + this.resourceLinks = resourceLinks; + this.permissionToPermissionDtoMapper = permissionToPermissionDtoMapper; + } + + public HalRepresentation map(Repository repository) { + List permissionDtoList = repository.getPermissions() + .stream() + .map(permission -> permissionToPermissionDtoMapper.map(permission, repository)) + .collect(toList()); + return new HalRepresentation(createLinks(repository), embedDtos(permissionDtoList)); + } + + private Links createLinks(Repository repository) { + RepositoryPermissions.permissionRead(repository).check(); + Links.Builder linksBuilder = linkingTo() + .with(Links.linkingTo().self(resourceLinks.permission().all(repository.getNamespace(), repository.getName())).build()); + if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { + linksBuilder.single(link("create", resourceLinks.permission().create(repository.getNamespace(), repository.getName()))); + } + return linksBuilder.build(); + } + + private Embedded embedDtos(List permissionDtoList) { + return embeddedBuilder() + .with("permissions", permissionDtoList) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java new file mode 100644 index 0000000000..b184bc3934 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -0,0 +1,35 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter @Setter @ToString +public class PermissionDto extends HalRepresentation { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String name; + + /** + * the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable + * the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version + * + * see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19 + * + **/ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String type ; + + + private boolean groupPermission = false; + + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java new file mode 100644 index 0000000000..1e90c23aa7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Permission; + +@Mapper +public abstract class PermissionDtoToPermissionMapper { + + public abstract Permission map(PermissionDto permissionDto); + + /** + * this method is needed to modify an existing permission object + * + * @param target the target permission + * @param permissionDto the source dto + * @return the mapped target permission object + */ + public abstract void modify(@MappingTarget Permission target, PermissionDto permissionDto); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java new file mode 100644 index 0000000000..61d62ecac5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java @@ -0,0 +1,49 @@ +/* + 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, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 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 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON 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.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @since 2.0.0 + */ +@Provider +public class PermissionNotFoundExceptionMapper extends StatusExceptionMapper { + + public PermissionNotFoundExceptionMapper() { + super(PermissionNotFoundException.class, Response.Status.NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index cd1e970e43..60153c5fbf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -1,20 +1,235 @@ package sonia.scm.api.v2.resources; -import javax.inject.Inject; -import javax.inject.Provider; -import javax.ws.rs.Path; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionAlreadyExistsException; +import sonia.scm.repository.PermissionNotFoundException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.web.VndMediaType; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Optional; + +@Slf4j public class PermissionRootResource { - private final Provider permissionCollectionResource; + private PermissionDtoToPermissionMapper dtoToModelMapper; + private PermissionToPermissionDtoMapper modelToDtoMapper; + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + private ResourceLinks resourceLinks; + private final RepositoryManager manager; + @Inject - public PermissionRootResource(Provider permissionCollectionResource) { - this.permissionCollectionResource = permissionCollectionResource; + public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + this.dtoToModelMapper = dtoToModelMapper; + this.modelToDtoMapper = modelToDtoMapper; + this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper; + this.resourceLinks = resourceLinks; + this.manager = manager; } + + /** + * Adds a new permission to the user or group managed by the repository + * + * @param permission permission to add + * @return a web response with the status code 201 and the url to GET the added permission + */ + @POST + @StatusCodes({ + @ResponseCode(code = 201, condition = "creates", additionalHeaders = { + @ResponseHeader(name = "Location", description = "uri of the created permission") + }), + @ResponseCode(code = 500, condition = "internal server error"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 409, condition = "conflict") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) @Path("") - public PermissionCollectionResource getPermissionCollectionResource() { - return permissionCollectionResource.get(); + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { + log.info("try to add new permission: {}", permission); + Repository repository = load(namespace, name); + RepositoryPermissions.permissionWrite(repository).check(); + checkPermissionAlreadyExists(permission, repository); + repository.getPermissions().add(dtoToModelMapper.map(permission)); + manager.modify(repository); + return Response.created(URI.create(resourceLinks.permission().self(namespace, name, permission.getName()))).build(); + } + + + /** + * Get the searched permission with permission name related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) + @Path("{permission-name}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { + Repository repository = load(namespace, name); + RepositoryPermissions.permissionRead(repository).check(); + return Response.ok( + repository.getPermissions() + .stream() + .filter(permission -> permissionName.equals(permission.getName())) + .map(permission -> modelToDtoMapper.map(permission, repository)) + .findFirst() + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ).build(); + } + + + /** + * Get all permissions related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) + @Path("") + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + Repository repository = load(namespace, name); + RepositoryPermissions.permissionRead(repository).check(); + return Response.ok(permissionCollectionToDtoMapper.map(repository)).build(); + } + + + /** + * Update a permission to the user or group managed by the repository + * + * @param permission permission to modify + * @param permissionName permission to modify + * @return a web response with the status code 204 + */ + @PUT + @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.PERMISSION) + @Path("{permission-name}") + public Response update(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { + log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); + Repository repository = load(namespace, name); + RepositoryPermissions.permissionWrite(repository).check(); + Permission existingPermission = repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)); + dtoToModelMapper.modify(existingPermission, permission); + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + /** + * Update a permission to the user or group managed by the repository + * + * @param permissionName permission to delete + * @return a web response with the status code 204 + */ + @DELETE + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Path("{permission-name}") + public Response delete(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { + log.info("try to delete the permission with name: {}.", permissionName); + Repository repository = load(namespace, name); + RepositoryPermissions.modify(repository).check(); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .ifPresent(p -> repository.getPermissions().remove(p)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + + /** + * check if the actual user is permitted to manage the repository permissions + * return the repository if the user is permitted + * + * @param namespace the repository namespace + * @param name the repository name + * @return the repository if the user is permitted + * @throws RepositoryNotFoundException if the repository does not exists + */ + private Repository load(String namespace, String name) throws RepositoryNotFoundException { + return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) + .orElseThrow(() -> new RepositoryNotFoundException(name)); + } + + /** + * check if the permission already exists in the repository + * + * @param permission the searched permission + * @param repository the repository to be inspected + * @throws PermissionAlreadyExistsException if the permission already exists in the repository + */ + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws PermissionAlreadyExistsException { + boolean isPermissionAlreadyExist = repository.getPermissions() + .stream() + .anyMatch(p -> p.getName().equals(permission.getName())); + if (isPermissionAlreadyExist) { + throw new PermissionAlreadyExistsException(repository, permission.getName()); + } } } + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java new file mode 100644 index 0000000000..5a6fffc33a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.BeforeMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Permission; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class PermissionToPermissionDtoMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract PermissionDto map(Permission permission, @Context Repository repository); + + + @BeforeMapping + void validatePermissions(@Context Repository repository) { + RepositoryPermissions.permissionRead(repository).check(); + } + + /** + * Add the self, update and delete links. + * + * @param target the mapped dto + * @param repository the repository + */ + @AfterMapping + void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) { + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), target.getName())); + if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { + linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), target.getName()))); + linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), target.getName()))); + } + target.add(linksBuilder.build()); + } +} + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java new file mode 100644 index 0000000000..dcab8e4fc0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java @@ -0,0 +1,49 @@ +/* + 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, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 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 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON 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.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.RepositoryNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @since 2.0.0 + */ +@Provider +public class RepositoryNotFoundExceptionMapper extends StatusExceptionMapper { + + public RepositoryNotFoundExceptionMapper() { + super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 817eb29f11..3ee27f5f84 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -128,11 +128,17 @@ public class RepositoryResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update( loadBy(namespace, name), - existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), + existing -> processUpdate(repositoryDto, existing), nameAndNamespaceStaysTheSame(namespace, name) ); } + private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) { + Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId()); + changedRepository.setPermissions(existing.getPermissions()); + return changedRepository; + } + @Path("tags/") public TagRootResource tags() { return tagRootResource.get(); @@ -176,6 +182,6 @@ public class RepositoryResource { } private Predicate nameAndNamespaceStaysTheSame(String namespace, String name) { - return changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace); + return changed -> name.equals(changed.getName()) && namespace.equals(changed.getNamespace()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 8d9bb8add2..e134ac8f7a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper TEST_PERMISSIONS = Lists + .newArrayList( + new Permission("user_write", PermissionType.WRITE, false), + new Permission("user_read", PermissionType.READ, false), + new Permission("user_owner", PermissionType.OWNER, false), + new Permission("group_read", PermissionType.READ, true), + new Permission("group_write", PermissionType.WRITE, true), + new Permission("group_owner", PermissionType.OWNER, true) + ); + private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() + .description("GET all permissions") + .method("GET") + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestPOSTPermission = new ExpectedRequest() + .description("create new permission") + .method("POST") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestGETPermission = new ExpectedRequest() + .description("GET permission") + .method("GET") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestDELETEPermission = new ExpectedRequest() + .description("delete permission") + .method("DELETE") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestPUTPermission = new ExpectedRequest() + .description("update permission") + .method("PUT") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ONE_PERMISSION); + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private RepositoryManager repositoryManager; + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper; + + @InjectMocks + private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; + + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + + private PermissionRootResource permissionRootResource; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + @BeforeEach + @Before + public void prepareEnvironment() { + initMocks(this); + permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); + permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider + .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + subjectThreadState.bind(); + ThreadContext.bind(subject); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionAlreadyExistsExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + } + + @TestFactory + @DisplayName("test endpoints on missing repository") + Stream missedRepositoryTestFactory() { + return createDynamicTestsToAssertResponses( + requestGETAllPermissions.expectedResponseStatus(404), + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(404), + requestDELETEPermission.expectedResponseStatus(404), + requestPUTPermission.expectedResponseStatus(404)); + } + + @TestFactory + @DisplayName("test endpoints on missing permissions and user is Admin") + Stream missedPermissionTestFactory() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(201), + requestGETAllPermissions.expectedResponseStatus(200), + requestDELETEPermission.expectedResponseStatus(204), + requestPUTPermission.expectedResponseStatus(404)); + } + + @TestFactory + @DisplayName("test endpoints on missing permissions and user is not Admin") + Stream missedPermissionUserForbiddenTestFactory() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(403), + requestPOSTPermission.expectedResponseStatus(403), + requestGETAllPermissions.expectedResponseStatus(403), + requestDELETEPermission.expectedResponseStatus(403), + requestPUTPermission.expectedResponseStatus(403)); + } + + @Test + public void userWithPermissionWritePermissionShouldGetAllPermissionsWithCreateAndUpdateLinks() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS), PERMISSION_WRITE); + } + + @Test + public void userWithPermissionReadPermissionShouldGetAllPermissionsWithoutCreateAndUpdateLinks() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS), PERMISSION_READ); + } + + @Test + public void shouldGetAllPermissions() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); + assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS), PERMISSION_READ); + } + + @Test + public void shouldGetPermissionByName() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); + Permission expectedPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestGETPermission + .expectedResponseStatus(200) + .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class); + assertThat(actualPermissionDto) + .as("response payload match permission object model") + .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission, PERMISSION_READ)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + @Test + public void shouldGetCreatedPermissions() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); + Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); + ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); + permissions.add(newPermission); + ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(201) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("POST response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_WRITE); + } + + @Test + public void shouldNotAddExistingPermission() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); + Permission newPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(409) + ); + } + + @Test + public void shouldGetUpdatedPermissions() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); + Permission modifiedPermission = TEST_PERMISSIONS.get(0); + // modify the type to owner + modifiedPermission.setType(PermissionType.OWNER); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); + assertExpectedRequest(requestPUTPermission + .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") + .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("PUT response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_WRITE); + } + + + @Test + public void shouldDeletePermissions() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER); + Permission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); + } + + @Test + public void deletingNotExistingPermissionShouldProcess() throws URISyntaxException { + createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER); + Permission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); + } + + private void assertGettingExpectedPermissions(ImmutableList expectedPermissions, String userPermission) throws URISyntaxException { + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + HalRepresentation halRepresentation = mapper.readValue(body, HalRepresentation.class); + List actualPermissionDtos = halRepresentation.getEmbedded().getItemsBy("permissions", HalRepresentation.class); + List permissionDtoStream = actualPermissionDtos.stream() + .map(hal -> { + PermissionDto result = new PermissionDto(); + result.setName(hal.getAttribute("name").asText()); + result.setType(hal.getAttribute("type").asText()); + result.setGroupPermission(hal.getAttribute("groupPermission").asBoolean()); + result.add(hal.getLinks()); + return result; + }).collect(Collectors.toList()); + assertThat(permissionDtoStream) + .as("response payload match permission object models") + .hasSize(expectedPermissions.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions), userPermission)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + private PermissionDto[] getExpectedPermissionDtos(ArrayList permissions, String userPermission) { + return permissions + .stream() + .map(p -> getExpectedPermissionDto(p, userPermission)) + .toArray(PermissionDto[]::new); + } + + private PermissionDto getExpectedPermissionDto(Permission permission, String userPermission) { + PermissionDto result = new PermissionDto(); + result.setName(permission.getName()); + result.setGroupPermission(permission.isGroupPermission()); + result.setType(permission.getType().name()); + String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName(); + if (PERMISSION_READ.equals(userPermission)) { + result.add(linkingTo() + .self(permissionHref) + .build()); + } else { + result.add(linkingTo() + .self(permissionHref) + .single(link("update", permissionHref)) + .single(link("delete", permissionHref)) + .build()); + } + return result; + } + + private Repository createUserWithRepository(String userPermission) { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + when(subject.isPermitted(userPermission != null ? eq(userPermission) : any(String.class))).thenReturn(true); + return mockRepository; + } + + private void createUserWithRepositoryAndPermissions(ArrayList permissions, String userPermission) { + when(createUserWithRepository(userPermission).getPermissions()).thenReturn(permissions); + } + + private Stream createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { + return Stream.of(expectedRequests) + .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); + } + + private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) throws URISyntaxException { + MockHttpResponse response = new MockHttpResponse(); + HttpRequest request = null; + request = MockHttpRequest + .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) + .content(entry.content) + .contentType(VndMediaType.PERMISSION); + dispatcher.invoke(request, response); + log.info("Test the Request :{}", entry); + assertThat(response.getStatus()) + .as("assert status code") + .isEqualTo(entry.expectedResponseStatus); + if (entry.responseValidator != null) { + entry.responseValidator.accept(response); + } + return response; + } + + @ToString + public class ExpectedRequest { + private String description; + private String method; + private String path; + private int expectedResponseStatus; + private byte[] content = new byte[]{}; + private Consumer responseValidator; + + public ExpectedRequest description(String description) { + this.description = description; + return this; + } + + public ExpectedRequest method(String method) { + this.method = method; + return this; + } + + public ExpectedRequest path(String path) { + this.path = path; + return this; + } + + public ExpectedRequest content(String content) { + if (content != null) { + this.content = content.getBytes(); + } + return this; + } + + public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) { + this.expectedResponseStatus = expectedResponseStatus; + return this; + } + + public ExpectedRequest responseValidator(Consumer responseValidator) { + this.responseValidator = responseValidator; + return this; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index d47b26e5eb..31cacf2d72 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -11,10 +11,13 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryIsNotArchivedException; @@ -35,10 +38,13 @@ import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -240,6 +246,28 @@ public class RepositoryRootResourceTest { verify(repositoryManager).create(any(Repository.class)); } + @Test + public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception { + Repository existingRepository = mockRepository("space", "repo"); + existingRepository.setPermissions(singletonList(new Permission("user", PermissionType.READ))); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + ArgumentCaptor modifiedRepositoryCaptor = forClass(Repository.class); + doNothing().when(repositoryManager).modify(modifiedRepositoryCaptor.capture()); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertFalse(modifiedRepositoryCaptor.getValue().getPermissions().isEmpty()); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 882d754329..6df5ac1a7a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -23,7 +23,7 @@ public class ResourceLinksMock { when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); - when(resourceLinks.permissionCollection()).thenReturn(new ResourceLinks.PermissionCollectionLinks(uriInfo)); + when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java index 741702ad81..75599770b3 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java +++ b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java @@ -120,15 +120,15 @@ public final class IntegrationTestUtil } } - public static Collection createRepositoryTypeParameters() { - Collection params = new ArrayList<>(); + public static Collection createRepositoryTypeParameters() { + Collection params = new ArrayList<>(); - params.add("git"); - params.add("svn" ); + params.add(new String[]{"git"}); + params.add(new String[]{"svn"}); if (IOUtil.search("hg") != null) { - params.add("hg"); + params.add(new String[]{"hg"}); } return params; diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java index 4cf7cd6571..bc79b9fa20 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryArchiveITCase.java @@ -81,7 +81,7 @@ public class RepositoryArchiveITCase //~--- methods -------------------------------------------------------------- @Parameterized.Parameters(name = "{0}") - public static Collection createParameters() { + public static Collection createParameters() { return IntegrationTestUtil.createRepositoryTypeParameters(); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java index 2be3bfd81b..dae80cd00d 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java @@ -208,7 +208,7 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase * @return repository types test parameter */ @Parameters(name = "{0}") - public static Collection createParameters() + public static Collection createParameters() { return IntegrationTestUtil.createRepositoryTypeParameters(); } 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 2b157c4dab..ed5689c9ce 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -40,15 +40,12 @@ import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.hamcrest.Matchers; -import org.junit.Test; import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; -import static org.mockito.Mockito.*; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import org.junit.Rule; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; @@ -60,6 +57,16 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyObject; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + /** * Unit tests for {@link AuthorizationCollector}. * @@ -200,7 +207,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read:one", "repository:read,write:two")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two")); } /**