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..2a1f34aa85 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 + */ @@ -57,10 +57,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 +154,7 @@ public class Permission implements PermissionObject, Serializable return Objects.hashCode(name, type, groupPermission); } - /** - * Method description - * - * - * @return - */ + @Override public String toString() { @@ -242,15 +238,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..aabd022ecb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -43,12 +43,7 @@ import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.*; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -60,7 +55,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 b2218f2a84..9396b83fe9 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 a461e40dea..861c9e7aee 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -1,18 +1,21 @@ 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 org.junit.Assert.assertNotNull; -import static org.junit.Assume.assumeFalse; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; @@ -23,7 +26,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; @@ -35,16 +38,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 98d4c8cdab..e755f3c3c1 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -8,52 +8,42 @@ 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 { - Files.write(content, new File(folder, fileName), Charsets.UTF_8); + 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 { + Files.write(content, new File(repositoryClient.getWorkingCopy(), fileName), Charsets.UTF_8); repositoryClient.getAddCommand().add(fileName); - commit("added " + fileName); + commit(repositoryClient, username, "added " + fileName); } - 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-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-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index 1feaa80a9b..b1bad56993 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -32,7 +32,8 @@ "repositories": "Repositories", "users": "Users", "logout": "Logout", - "groups": "Groups" + "groups": "Groups", + "config": "Configuration" }, "paginator": { "next": "Next", diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json new file mode 100644 index 0000000000..143755a318 --- /dev/null +++ b/scm-ui/public/locales/en/config.json @@ -0,0 +1,68 @@ +{ + "config": { + "navigation-title": "Navigation" + }, + "global-config": { + "title": "Configuration", + "navigation-label": "Global Configuration", + "error-title": "Error", + "error-subtitle": "Unknown Config Error" + }, + "config-form": { + "submit": "Submit", + "no-permission-notification": "Please note: You do not have the permission to edit the config!" + }, + "proxy-settings": { + "name": "Proxy Settings", + "proxy-password": "Proxy Password", + "proxy-port": "Proxy Port", + "proxy-server": "Proxy Server", + "proxy-user": "Proxy User", + "enable-proxy": "Enable Proxy", + "proxy-excludes": "Proxy Excludes", + "remove-proxy-exclude-button": "Remove Proxy Exclude", + "add-proxy-exclude-error": "The proxy exclude you want to add is not valid", + "add-proxy-exclude-textfield": "Add proxy exclude you want to add to proxy excludes here", + "add-proxy-exclude-button": "Add Proxy Exclude" + }, + "base-url-settings": { + "name": "Base URL Settings", + "base-url": "Base URL", + "force-base-url": "Force Base URL" + }, + "admin-settings": { + "name": "Administration Settings", + "admin-groups": "Admin Groups", + "admin-users": "Admin Users", + "remove-group-button": "Remove Admin Group", + "remove-user-button": "Remove Admin User", + "add-group-error": "The group name you want to add is not valid", + "add-group-textfield": "Add group you want to add to admin groups here", + "add-group-button": "Add Admin Group", + "add-user-error": "The user name you want to add is not valid", + "add-user-textfield": "Add user you want to add to admin users here", + "add-user-button": "Add Admin User" + }, + "login-attempt": { + "name": "Login Attempt", + "login-attempt-limit": "Login Attempt Limit", + "login-attempt-limit-timeout": "Login Attempt Limit Timeout" + }, + "general-settings": { + "realm-description": "Realm Description", + "enable-repository-archive": "Enable Repository Archive", + "disable-grouping-grid": "Disable Grouping Grid", + "date-format": "Date Format", + "anonymous-access-enabled": "Anonymous Access Enabled", + "skip-failed-authenticators": "Skip Failed Authenticators", + "plugin-url": "Plugin URL", + "enabled-xsrf-protection": "Enabled XSRF Protection", + "default-namespace-strategy": "Default Namespace Strategy" + }, + "validation": { + "date-format-invalid": "The date format is not valid", + "login-attempt-limit-timeout-invalid": "This is not a number", + "login-attempt-limit-invalid": "This is not a number", + "plugin-url-invalid": "This is not a valid url" + } +} diff --git a/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js b/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js new file mode 100644 index 0000000000..280226b938 --- /dev/null +++ b/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js @@ -0,0 +1,33 @@ +//@flow +import React from "react"; +import { DeleteButton } from "."; +import classNames from "classnames"; + +type Props = { + entryname: string, + removeEntry: string => void, + disabled: boolean, + label: string +}; + +type State = {}; + +class RemoveEntryOfTableButton extends React.Component { + render() { + const { label, entryname, removeEntry, disabled } = this.props; + return ( +
+ { + event.preventDefault(); + removeEntry(entryname); + }} + disabled={disabled} + /> +
+ ); + } +} + +export default RemoveEntryOfTableButton; diff --git a/scm-ui/src/components/buttons/index.js b/scm-ui/src/components/buttons/index.js index 8dfc6e00ef..e0d94d29b9 100644 --- a/scm-ui/src/components/buttons/index.js +++ b/scm-ui/src/components/buttons/index.js @@ -4,3 +4,4 @@ export { default as CreateButton } from "./CreateButton"; export { default as DeleteButton } from "./DeleteButton"; export { default as EditButton } from "./EditButton"; export { default as SubmitButton } from "./SubmitButton"; +export {default as RemoveEntryOfTableButton} from "./RemoveEntryOfTableButton"; diff --git a/scm-ui/src/components/forms/AddEntryToTableField.js b/scm-ui/src/components/forms/AddEntryToTableField.js new file mode 100644 index 0000000000..1770e07807 --- /dev/null +++ b/scm-ui/src/components/forms/AddEntryToTableField.js @@ -0,0 +1,68 @@ +//@flow +import React from "react"; + +import { AddButton } from "../buttons"; +import InputField from "./InputField"; + +type Props = { + addEntry: string => void, + disabled: boolean, + buttonLabel: string, + fieldLabel: string, + errorMessage: string +}; + +type State = { + entryToAdd: string +}; + +class AddEntryToTableField extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + entryToAdd: "" + }; + } + + render() { + const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props; + return ( +
+ + +
+ ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendEntry(); + }; + + appendEntry = () => { + const { entryToAdd } = this.state; + this.props.addEntry(entryToAdd); + this.setState({ ...this.state, entryToAdd: "" }); + }; + + handleAddEntryChange = (entryname: string) => { + this.setState({ + ...this.state, + entryToAdd: entryname + }); + }; +} + +export default AddEntryToTableField; diff --git a/scm-ui/src/components/forms/Checkbox.js b/scm-ui/src/components/forms/Checkbox.js index b37f951e2b..72dfbf4af8 100644 --- a/scm-ui/src/components/forms/Checkbox.js +++ b/scm-ui/src/components/forms/Checkbox.js @@ -4,7 +4,8 @@ import React from "react"; type Props = { label?: string, checked: boolean, - onChange?: boolean => void + onChange?: boolean => void, + disabled?: boolean }; class Checkbox extends React.Component { onCheckboxChange = (event: SyntheticInputEvent) => { @@ -17,11 +18,12 @@ class Checkbox extends React.Component { return (
-