mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 06:55:47 +01:00
merge with 2.0.0-m3 branch
This commit is contained in:
64
pom.xml
64
pom.xml
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
@@ -118,10 +119,26 @@
|
|||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- JUnit 5 -->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<scope>test</scope>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.vintage</groupId>
|
||||||
|
<artifactId>junit-vintage-engine</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -139,15 +156,11 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<version>${mockito.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.assertj</groupId>
|
<groupId>org.assertj</groupId>
|
||||||
<artifactId>assertj-core</artifactId>
|
<artifactId>assertj-core</artifactId>
|
||||||
<version>3.10.0</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -282,9 +295,32 @@
|
|||||||
<version>${jackson.version}</version>
|
<version>${jackson.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JUnit 5 -->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.vintage</groupId>
|
||||||
|
<artifactId>junit-vintage-engine</artifactId>
|
||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
@@ -348,7 +384,11 @@
|
|||||||
</pluginManagement>
|
</pluginManagement>
|
||||||
|
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>2.22.0</version>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-enforcer-plugin</artifactId>
|
<artifactId>maven-enforcer-plugin</artifactId>
|
||||||
@@ -695,7 +735,7 @@
|
|||||||
<!-- test libraries -->
|
<!-- test libraries -->
|
||||||
<mockito.version>2.10.0</mockito.version>
|
<mockito.version>2.10.0</mockito.version>
|
||||||
<hamcrest.version>1.3</hamcrest.version>
|
<hamcrest.version>1.3</hamcrest.version>
|
||||||
<junit.version>4.12</junit.version>
|
<junit.version>5.2.0</junit.version>
|
||||||
|
|
||||||
<!-- logging libraries -->
|
<!-- logging libraries -->
|
||||||
<slf4j.version>1.7.22</slf4j.version>
|
<slf4j.version>1.7.22</slf4j.version>
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
/**
|
/*
|
||||||
* Copyright (c) 2010, Sebastian Sdorra
|
Copyright (c) 2010, Sebastian Sdorra
|
||||||
* All rights reserved.
|
All rights reserved.
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
* modification, are permitted provided that the following conditions are met:
|
modification, are permitted provided that the following conditions are met:
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
* this list of conditions and the following disclaimer.
|
this list of conditions and the following disclaimer.
|
||||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
* this list of conditions and the following disclaimer in the documentation
|
this list of conditions and the following disclaimer in the documentation
|
||||||
* and/or other materials provided with the distribution.
|
and/or other materials provided with the distribution.
|
||||||
* 3. Neither the name of SCM-Manager; nor the names of its
|
3. Neither the name of SCM-Manager; nor the names of its
|
||||||
* contributors may be used to endorse or promote products derived from this
|
contributors may be used to endorse or promote products derived from this
|
||||||
* software without specific prior written permission.
|
software without specific prior written permission.
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
|
||||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
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
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
http://bitbucket.org/sdorra/scm-manager
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
@@ -57,10 +57,11 @@ import javax.xml.bind.annotation.XmlRootElement;
|
|||||||
public class Permission implements PermissionObject, Serializable
|
public class Permission implements PermissionObject, Serializable
|
||||||
{
|
{
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final long serialVersionUID = -2915175031430884040L;
|
private static final long serialVersionUID = -2915175031430884040L;
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
private boolean groupPermission = false;
|
||||||
|
private String name;
|
||||||
|
private PermissionType type = PermissionType.READ;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new {@link Permission}.
|
* Constructs a new {@link Permission}.
|
||||||
@@ -153,12 +154,7 @@ public class Permission implements PermissionObject, Serializable
|
|||||||
return Objects.hashCode(name, type, groupPermission);
|
return Objects.hashCode(name, type, groupPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
@@ -242,15 +238,4 @@ public class Permission implements PermissionObject, Serializable
|
|||||||
{
|
{
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private boolean groupPermission = false;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private PermissionType type = PermissionType.READ;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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() ));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -42,10 +42,10 @@ public enum PermissionType
|
|||||||
{
|
{
|
||||||
|
|
||||||
/** read permision */
|
/** read permision */
|
||||||
READ(0, "repository:read:"),
|
READ(0, "repository:read,pull:"),
|
||||||
|
|
||||||
/** read and write permissionPrefix */
|
/** read and write permissionPrefix */
|
||||||
WRITE(10, "repository:read,write:"),
|
WRITE(10, "repository:read,pull,push:"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* read, write and
|
* read, write and
|
||||||
|
|||||||
@@ -43,12 +43,7 @@ import sonia.scm.util.HttpUtil;
|
|||||||
import sonia.scm.util.Util;
|
import sonia.scm.util.Util;
|
||||||
import sonia.scm.util.ValidationUtil;
|
import sonia.scm.util.ValidationUtil;
|
||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.*;
|
||||||
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 java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -60,7 +55,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@StaticPermissions(
|
@StaticPermissions(
|
||||||
value = "repository",
|
value = "repository",
|
||||||
permissions = {"read", "write", "modify", "delete", "healthCheck"}
|
permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}
|
||||||
)
|
)
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@XmlRootElement(name = "repositories")
|
@XmlRootElement(name = "repositories")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class VndMediaType {
|
|||||||
public static final String USER = PREFIX + "user" + SUFFIX;
|
public static final String USER = PREFIX + "user" + SUFFIX;
|
||||||
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
||||||
public static final String REPOSITORY = PREFIX + "repository" + 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 BRANCH = PREFIX + "branch" + SUFFIX;
|
||||||
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
|
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
|
||||||
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;
|
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;
|
||||||
|
|||||||
@@ -36,13 +36,11 @@ package sonia.scm.web.filter;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
|
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import sonia.scm.ArgumentIsInvalidException;
|
import sonia.scm.ArgumentIsInvalidException;
|
||||||
import sonia.scm.SCMContext;
|
import sonia.scm.SCMContext;
|
||||||
import sonia.scm.config.ScmConfiguration;
|
import sonia.scm.config.ScmConfiguration;
|
||||||
@@ -53,17 +51,14 @@ import sonia.scm.security.ScmSecurityException;
|
|||||||
import sonia.scm.util.HttpUtil;
|
import sonia.scm.util.HttpUtil;
|
||||||
import sonia.scm.util.Util;
|
import sonia.scm.util.Util;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
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.
|
* Abstract http filter to check repository permissions.
|
||||||
@@ -339,7 +334,7 @@ public abstract class PermissionFilter extends HttpFilter
|
|||||||
|
|
||||||
if (writeRequest)
|
if (writeRequest)
|
||||||
{
|
{
|
||||||
permitted = RepositoryPermissions.write(repository).isPermitted();
|
permitted = RepositoryPermissions.push(repository).isPermitted();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
225
scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
Normal file
225
scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java
Normal file
@@ -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<String> 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<Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ package sonia.scm.it;
|
|||||||
//~--- non-JDK imports --------------------------------------------------------
|
//~--- non-JDK imports --------------------------------------------------------
|
||||||
|
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -41,17 +42,12 @@ import org.junit.rules.TemporaryFolder;
|
|||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.Parameterized;
|
import org.junit.runners.Parameterized;
|
||||||
import org.junit.runners.Parameterized.Parameters;
|
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.RepositoryClient;
|
||||||
import sonia.scm.repository.client.api.RepositoryClientFactory;
|
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.UUID;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
@@ -66,8 +62,6 @@ import static sonia.scm.it.TestData.repositoryJson;
|
|||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class RepositoriesITCase {
|
public class RepositoriesITCase {
|
||||||
|
|
||||||
public static final Person AUTHOR = new Person("SCM Administrator", "scmadmin@scm-manager.org");
|
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
@@ -142,57 +136,16 @@ public class RepositoriesITCase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldCloneRepository() throws IOException {
|
public void shouldCloneRepository() throws IOException {
|
||||||
RepositoryClient client = createRepositoryClient();
|
RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.getRoot());
|
||||||
assertEquals("expected metadata dir", 1, client.getWorkingCopy().list().length);
|
assertEquals("expected metadata dir", 1, Objects.requireNonNull(client.getWorkingCopy().list()).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldCommitFiles() throws IOException {
|
public void shouldCommitFiles() throws IOException {
|
||||||
RepositoryClient client = createRepositoryClient();
|
RepositoryClient client = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin");
|
||||||
|
String name = RepositoryUtil.addAndCommitRandomFile(client, "scmadmin");
|
||||||
for (int i = 0; i < 5; i++) {
|
RepositoryClient checkClient = RepositoryUtil.createRepositoryClient(repositoryType, temporaryFolder.newFolder(), "scmadmin", "scmadmin");
|
||||||
createRandomFile(client);
|
Assertions.assertThat(checkClient.getWorkingCopy().list()).contains(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
commit(client);
|
|
||||||
|
|
||||||
RepositoryClient checkClient = createRepositoryClient();
|
|
||||||
assertEquals("expected 5 files and metadata dir", 6, checkClient.getWorkingCopy().list().length);
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package sonia.scm.it;
|
package sonia.scm.it;
|
||||||
|
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
|
import org.junit.Assume;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.Parameterized;
|
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.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assume.assumeFalse;
|
|
||||||
import static sonia.scm.it.RestUtil.given;
|
import static sonia.scm.it.RestUtil.given;
|
||||||
import static sonia.scm.it.ScmTypes.availableScmTypes;
|
import static sonia.scm.it.ScmTypes.availableScmTypes;
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ public class RepositoryAccessITCase {
|
|||||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||||
|
|
||||||
private final String repositoryType;
|
private final String repositoryType;
|
||||||
private RepositoryUtil repositoryUtil;
|
private File folder;
|
||||||
|
|
||||||
public RepositoryAccessITCase(String repositoryType) {
|
public RepositoryAccessITCase(String repositoryType) {
|
||||||
this.repositoryType = repositoryType;
|
this.repositoryType = repositoryType;
|
||||||
@@ -35,16 +38,18 @@ public class RepositoryAccessITCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void initClient() throws IOException {
|
public void initClient() {
|
||||||
TestData.createDefault();
|
TestData.createDefault();
|
||||||
repositoryUtil = new RepositoryUtil(repositoryType, tempFolder.getRoot());
|
folder = tempFolder.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldFindBranches() throws IOException {
|
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()
|
String branchesUrl = given()
|
||||||
.when()
|
.when()
|
||||||
|
|||||||
@@ -8,52 +8,42 @@ import sonia.scm.repository.Person;
|
|||||||
import sonia.scm.repository.client.api.ClientCommand;
|
import sonia.scm.repository.client.api.ClientCommand;
|
||||||
import sonia.scm.repository.client.api.RepositoryClient;
|
import sonia.scm.repository.client.api.RepositoryClient;
|
||||||
import sonia.scm.repository.client.api.RepositoryClientFactory;
|
import sonia.scm.repository.client.api.RepositoryClientFactory;
|
||||||
import sonia.scm.web.VndMediaType;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
|
|
||||||
import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
|
|
||||||
import static sonia.scm.it.RestUtil.given;
|
|
||||||
|
|
||||||
public class RepositoryUtil {
|
public class RepositoryUtil {
|
||||||
|
|
||||||
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
|
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
|
||||||
|
|
||||||
private final RepositoryClient repositoryClient;
|
static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
|
||||||
private final File folder;
|
return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin");
|
||||||
|
|
||||||
RepositoryUtil(String repositoryType, File folder) throws IOException {
|
|
||||||
this.repositoryClient = createRepositoryClient(repositoryType, folder);
|
|
||||||
this.folder = folder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
|
static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
|
||||||
String httpProtocolUrl = given(VndMediaType.REPOSITORY)
|
String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
|
||||||
|
|
||||||
.when()
|
|
||||||
.get(TestData.getDefaultRepositoryUrl(repositoryType))
|
|
||||||
|
|
||||||
.then()
|
|
||||||
.statusCode(HttpStatus.SC_OK)
|
|
||||||
.extract()
|
.extract()
|
||||||
.path("_links.httpProtocol.href");
|
.path("_links.httpProtocol.href");
|
||||||
|
|
||||||
|
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
|
||||||
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, ADMIN_USERNAME, ADMIN_PASSWORD, folder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void createAndCommitFile(String fileName, String content) throws IOException {
|
static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
|
||||||
Files.write(content, new File(folder, fileName), Charsets.UTF_8);
|
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);
|
repositoryClient.getAddCommand().add(fileName);
|
||||||
commit("added " + fileName);
|
commit(repositoryClient, username, "added " + fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Changeset commit(String message) throws IOException {
|
static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException {
|
||||||
Changeset changeset = repositoryClient.getCommitCommand().commit(
|
Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message);
|
||||||
new Person("scmadmin", "scmadmin@scm-manager.org"), message
|
|
||||||
);
|
|
||||||
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
|
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {
|
||||||
repositoryClient.getPushCommand().push();
|
repositoryClient.getPushCommand().push();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,19 @@ public class RestUtil {
|
|||||||
public static final String ADMIN_USERNAME = "scmadmin";
|
public static final String ADMIN_USERNAME = "scmadmin";
|
||||||
public static final String ADMIN_PASSWORD = "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() {
|
public static RequestSpecification given() {
|
||||||
return RestAssured.given()
|
return RestAssured.given()
|
||||||
.auth().preemptive().basic(ADMIN_USERNAME, ADMIN_PASSWORD);
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package sonia.scm.it;
|
package sonia.scm.it;
|
||||||
|
|
||||||
|
import io.restassured.response.ValidatableResponse;
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.PermissionType;
|
||||||
import sonia.scm.web.VndMediaType;
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
import javax.json.Json;
|
import javax.json.Json;
|
||||||
@@ -19,7 +21,9 @@ public class TestData {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(TestData.class);
|
private static final Logger LOG = LoggerFactory.getLogger(TestData.class);
|
||||||
|
|
||||||
private static final List<String> PROTECTED_USERS = asList("scmadmin", "anonymous");
|
public static final String USER_SCM_ADMIN = "scmadmin";
|
||||||
|
public static final String USER_ANONYMOUS = "anonymous";
|
||||||
|
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
|
||||||
|
|
||||||
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
|
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
|
||||||
|
|
||||||
@@ -38,6 +42,77 @@ public class TestData {
|
|||||||
return DEFAULT_REPOSITORIES.get(repositoryType);
|
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<Object> 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() {
|
private static void cleanupRepositories() {
|
||||||
List<String> repositories = given(VndMediaType.REPOSITORY_COLLECTION)
|
List<String> repositories = given(VndMediaType.REPOSITORY_COLLECTION)
|
||||||
.when()
|
.when()
|
||||||
|
|||||||
@@ -28,12 +28,6 @@
|
|||||||
<version>2.0.0-SNAPSHOT</version>
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>junit</groupId>
|
|
||||||
<artifactId>junit</artifactId>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.sdorra</groupId>
|
<groupId>com.github.sdorra</groupId>
|
||||||
<artifactId>shiro-unit</artifactId>
|
<artifactId>shiro-unit</artifactId>
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"repositories": "Repositories",
|
"repositories": "Repositories",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"logout": "Logout",
|
"logout": "Logout",
|
||||||
"groups": "Groups"
|
"groups": "Groups",
|
||||||
|
"config": "Configuration"
|
||||||
},
|
},
|
||||||
"paginator": {
|
"paginator": {
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
|||||||
68
scm-ui/public/locales/en/config.json
Normal file
68
scm-ui/public/locales/en/config.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
scm-ui/src/components/buttons/RemoveEntryOfTableButton.js
Normal file
33
scm-ui/src/components/buttons/RemoveEntryOfTableButton.js
Normal file
@@ -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<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { label, entryname, removeEntry, disabled } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classNames("is-pulled-right")}>
|
||||||
|
<DeleteButton
|
||||||
|
label={label}
|
||||||
|
action={(event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
removeEntry(entryname);
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveEntryOfTableButton;
|
||||||
@@ -4,3 +4,4 @@ export { default as CreateButton } from "./CreateButton";
|
|||||||
export { default as DeleteButton } from "./DeleteButton";
|
export { default as DeleteButton } from "./DeleteButton";
|
||||||
export { default as EditButton } from "./EditButton";
|
export { default as EditButton } from "./EditButton";
|
||||||
export { default as SubmitButton } from "./SubmitButton";
|
export { default as SubmitButton } from "./SubmitButton";
|
||||||
|
export {default as RemoveEntryOfTableButton} from "./RemoveEntryOfTableButton";
|
||||||
|
|||||||
68
scm-ui/src/components/forms/AddEntryToTableField.js
Normal file
68
scm-ui/src/components/forms/AddEntryToTableField.js
Normal file
@@ -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<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
entryToAdd: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<InputField
|
||||||
|
label={fieldLabel}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
onChange={this.handleAddEntryChange}
|
||||||
|
validationError={false}
|
||||||
|
value={this.state.entryToAdd}
|
||||||
|
onReturnPressed={this.appendEntry}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<AddButton
|
||||||
|
label={buttonLabel}
|
||||||
|
action={this.addButtonClicked}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -4,7 +4,8 @@ import React from "react";
|
|||||||
type Props = {
|
type Props = {
|
||||||
label?: string,
|
label?: string,
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
onChange?: boolean => void
|
onChange?: boolean => void,
|
||||||
|
disabled?: boolean
|
||||||
};
|
};
|
||||||
class Checkbox extends React.Component<Props> {
|
class Checkbox extends React.Component<Props> {
|
||||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
@@ -17,11 +18,12 @@ class Checkbox extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<label className="checkbox">
|
<label className="checkbox" disabled={this.props.disabled}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.props.checked}
|
checked={this.props.checked}
|
||||||
onChange={this.onCheckboxChange}
|
onChange={this.onCheckboxChange}
|
||||||
|
disabled={this.props.disabled}
|
||||||
/>
|
/>
|
||||||
{this.props.label}
|
{this.props.label}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ type Props = {
|
|||||||
onChange: string => void,
|
onChange: string => void,
|
||||||
onReturnPressed?: () => void,
|
onReturnPressed?: () => void,
|
||||||
validationError: boolean,
|
validationError: boolean,
|
||||||
errorMessage: string
|
errorMessage: string,
|
||||||
|
disabled?: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputField extends React.Component<Props> {
|
class InputField extends React.Component<Props> {
|
||||||
@@ -40,21 +41,33 @@ class InputField extends React.Component<Props> {
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||||
const onReturnPressed = this.props.onReturnPressed;
|
const onReturnPressed = this.props.onReturnPressed;
|
||||||
if (!onReturnPressed) {
|
if (!onReturnPressed) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onReturnPressed();
|
onReturnPressed();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { type, placeholder, value, validationError, errorMessage } = this.props;
|
const {
|
||||||
|
type,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
validationError,
|
||||||
|
errorMessage,
|
||||||
|
disabled
|
||||||
|
} = this.props;
|
||||||
const errorView = validationError ? "is-danger" : "";
|
const errorView = validationError ? "is-danger" : "";
|
||||||
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
const helper = validationError ? (
|
||||||
|
<p className="help is-danger">{errorMessage}</p>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
{this.renderLabel()}
|
{this.renderLabel()}
|
||||||
@@ -63,15 +76,13 @@ class InputField extends React.Component<Props> {
|
|||||||
ref={input => {
|
ref={input => {
|
||||||
this.field = input;
|
this.field = input;
|
||||||
}}
|
}}
|
||||||
className={ classNames(
|
className={classNames("input", errorView)}
|
||||||
"input",
|
|
||||||
errorView
|
|
||||||
)}
|
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.handleInput}
|
onChange={this.handleInput}
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{helper}
|
{helper}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Loading from "./../Loading";
|
import Loading from "./../Loading";
|
||||||
import ErrorNotification from "./../ErrorNotification";
|
import ErrorNotification from "./../ErrorNotification";
|
||||||
|
import Title from "./Title";
|
||||||
|
import Subtitle from "./Subtitle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string,
|
title?: string,
|
||||||
subtitle?: string,
|
subtitle?: string,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
error?: Error,
|
error?: Error,
|
||||||
@@ -14,12 +16,12 @@ type Props = {
|
|||||||
|
|
||||||
class Page extends React.Component<Props> {
|
class Page extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { title, error } = this.props;
|
const { title, error, subtitle } = this.props;
|
||||||
return (
|
return (
|
||||||
<section className="section">
|
<section className="section">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<h1 className="title">{title}</h1>
|
<Title title={title} />
|
||||||
{this.renderSubtitle()}
|
<Subtitle subtitle={subtitle} />
|
||||||
<ErrorNotification error={error} />
|
<ErrorNotification error={error} />
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
</div>
|
</div>
|
||||||
@@ -27,14 +29,6 @@ class Page extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSubtitle() {
|
|
||||||
const { subtitle } = this.props;
|
|
||||||
if (subtitle) {
|
|
||||||
return <h2 className="subtitle">{subtitle}</h2>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { loading, children, showContentOnError, error } = this.props;
|
const { loading, children, showContentOnError, error } = this.props;
|
||||||
if (error && !showContentOnError) {
|
if (error && !showContentOnError) {
|
||||||
|
|||||||
17
scm-ui/src/components/layout/Subtitle.js
Normal file
17
scm-ui/src/components/layout/Subtitle.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
subtitle?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
class Subtitle extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { subtitle } = this.props;
|
||||||
|
if (subtitle) {
|
||||||
|
return <h1 className="subtitle">{subtitle}</h1>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Subtitle;
|
||||||
17
scm-ui/src/components/layout/Title.js
Normal file
17
scm-ui/src/components/layout/Title.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
class Title extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { title } = this.props;
|
||||||
|
if (title) {
|
||||||
|
return <h1 className="title">{title}</h1>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Title;
|
||||||
@@ -28,6 +28,10 @@ class PrimaryNavigation extends React.Component<Props> {
|
|||||||
match="/(group|groups)"
|
match="/(group|groups)"
|
||||||
label={t("primary-navigation.groups")}
|
label={t("primary-navigation.groups")}
|
||||||
/>
|
/>
|
||||||
|
<PrimaryNavigationLink
|
||||||
|
to="/config"
|
||||||
|
label={t("primary-navigation.config")}
|
||||||
|
/>
|
||||||
<PrimaryNavigationLink
|
<PrimaryNavigationLink
|
||||||
to="/logout"
|
to="/logout"
|
||||||
label={t("primary-navigation.logout")}
|
label={t("primary-navigation.logout")}
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ const mailRegex = /^[A-z0-9][\w.-]*@[A-z0-9][\w\-.]*\.[A-z0-9][A-z0-9-]+$/;
|
|||||||
export const isMailValid = (mail: string) => {
|
export const isMailValid = (mail: string) => {
|
||||||
return mailRegex.test(mail);
|
return mailRegex.test(mail);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isNumberValid = (number: string) => {
|
||||||
|
return !isNaN(number);
|
||||||
|
};
|
||||||
|
|||||||
@@ -85,3 +85,18 @@ describe("test mail validation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("test number validation", () => {
|
||||||
|
it("should return false", () => {
|
||||||
|
const invalid = ["1a", "35gu", "dj6", "45,5", "test"];
|
||||||
|
for (let number of invalid) {
|
||||||
|
expect(validator.isNumberValid(number)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("should return true", () => {
|
||||||
|
const valid = ["1", "35", "2", "235", "34.4"];
|
||||||
|
for (let number of valid) {
|
||||||
|
expect(validator.isNumberValid(number)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
87
scm-ui/src/config/components/form/AdminSettings.js
Normal file
87
scm-ui/src/config/components/form/AdminSettings.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import Subtitle from "../../../components/layout/Subtitle";
|
||||||
|
import AdminGroupTable from "../table/AdminGroupTable";
|
||||||
|
import AdminUserTable from "../table/AdminUserTable";
|
||||||
|
import AddEntryToTableField from "../../../components/forms/AddEntryToTableField";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminGroups: string[],
|
||||||
|
adminUsers: string[],
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
hasUpdatePermission: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class AdminSettings extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { t, adminGroups, adminUsers, hasUpdatePermission } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Subtitle subtitle={t("admin-settings.name")} />
|
||||||
|
<AdminGroupTable
|
||||||
|
adminGroups={adminGroups}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.props.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<AddEntryToTableField
|
||||||
|
addEntry={this.addGroup}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
buttonLabel={t("admin-settings.add-group-button")}
|
||||||
|
fieldLabel={t("admin-settings.add-group-textfield")}
|
||||||
|
errorMessage={t("admin-settings.add-group-error")}
|
||||||
|
/>
|
||||||
|
<AdminUserTable
|
||||||
|
adminUsers={adminUsers}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.props.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<AddEntryToTableField
|
||||||
|
addEntry={this.addUser}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
buttonLabel={t("admin-settings.add-user-button")}
|
||||||
|
fieldLabel={t("admin-settings.add-user-textfield")}
|
||||||
|
errorMessage={t("admin-settings.add-user-error")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addGroup = (groupname: string) => {
|
||||||
|
if (this.isAdminGroupMember(groupname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onChange(
|
||||||
|
true,
|
||||||
|
[...this.props.adminGroups, groupname],
|
||||||
|
"adminGroups"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
isAdminGroupMember = (groupname: string) => {
|
||||||
|
return this.props.adminGroups.includes(groupname);
|
||||||
|
};
|
||||||
|
|
||||||
|
addUser = (username: string) => {
|
||||||
|
if (this.isAdminUserMember(username)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onChange(
|
||||||
|
true,
|
||||||
|
[...this.props.adminUsers, username],
|
||||||
|
"adminUsers"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
isAdminUserMember = (username: string) => {
|
||||||
|
return this.props.adminUsers.includes(username);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AdminSettings);
|
||||||
46
scm-ui/src/config/components/form/BaseUrlSettings.js
Normal file
46
scm-ui/src/config/components/form/BaseUrlSettings.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Checkbox, InputField } from "../../../components/forms/index";
|
||||||
|
import Subtitle from "../../../components/layout/Subtitle";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
baseUrl: string,
|
||||||
|
forceBaseUrl: boolean,
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
hasUpdatePermission: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class BaseUrlSettings extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { t, baseUrl, forceBaseUrl, hasUpdatePermission } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Subtitle subtitle={t("base-url-settings.name")} />
|
||||||
|
<Checkbox
|
||||||
|
checked={forceBaseUrl}
|
||||||
|
label={t("base-url-settings.force-base-url")}
|
||||||
|
onChange={this.handleForceBaseUrlChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("base-url-settings.base-url")}
|
||||||
|
onChange={this.handleBaseUrlChange}
|
||||||
|
value={baseUrl}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBaseUrlChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "baseUrl");
|
||||||
|
};
|
||||||
|
handleForceBaseUrlChange = (value: boolean) => {
|
||||||
|
this.props.onChange(true, value, "forceBaseUrl");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(BaseUrlSettings);
|
||||||
195
scm-ui/src/config/components/form/ConfigForm.js
Normal file
195
scm-ui/src/config/components/form/ConfigForm.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { SubmitButton } from "../../../components/buttons/index";
|
||||||
|
import type { Config } from "../../types/Config";
|
||||||
|
import ProxySettings from "./ProxySettings";
|
||||||
|
import GeneralSettings from "./GeneralSettings";
|
||||||
|
import BaseUrlSettings from "./BaseUrlSettings";
|
||||||
|
import AdminSettings from "./AdminSettings";
|
||||||
|
import Notification from "../../../components/Notification";
|
||||||
|
import LoginAttempt from "./LoginAttempt";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
submitForm: Config => void,
|
||||||
|
config?: Config,
|
||||||
|
loading?: boolean,
|
||||||
|
t: string => string,
|
||||||
|
configUpdatePermission: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
config: Config,
|
||||||
|
showNotification: boolean,
|
||||||
|
error: {
|
||||||
|
loginAttemptLimitTimeout: boolean,
|
||||||
|
loginAttemptLimit: boolean
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConfigForm extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
config: {
|
||||||
|
proxyPassword: null,
|
||||||
|
proxyPort: 0,
|
||||||
|
proxyServer: "",
|
||||||
|
proxyUser: null,
|
||||||
|
enableProxy: false,
|
||||||
|
realmDescription: "",
|
||||||
|
enableRepositoryArchive: false,
|
||||||
|
disableGroupingGrid: false,
|
||||||
|
dateFormat: "",
|
||||||
|
anonymousAccessEnabled: false,
|
||||||
|
adminGroups: [],
|
||||||
|
adminUsers: [],
|
||||||
|
baseUrl: "",
|
||||||
|
forceBaseUrl: false,
|
||||||
|
loginAttemptLimit: 0,
|
||||||
|
proxyExcludes: [],
|
||||||
|
skipFailedAuthenticators: false,
|
||||||
|
pluginUrl: "",
|
||||||
|
loginAttemptLimitTimeout: 0,
|
||||||
|
enabledXsrfProtection: true,
|
||||||
|
defaultNamespaceStrategy: "",
|
||||||
|
_links: {}
|
||||||
|
},
|
||||||
|
showNotification: false,
|
||||||
|
error: {
|
||||||
|
loginAttemptLimitTimeout: false,
|
||||||
|
loginAttemptLimit: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { config, configUpdatePermission } = this.props;
|
||||||
|
if (config) {
|
||||||
|
this.setState({ ...this.state, config: { ...config } });
|
||||||
|
}
|
||||||
|
if (!configUpdatePermission) {
|
||||||
|
this.setState({ ...this.state, showNotification: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.props.submitForm(this.state.config);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, t, configUpdatePermission } = this.props;
|
||||||
|
const config = this.state.config;
|
||||||
|
|
||||||
|
let noPermissionNotification = null;
|
||||||
|
|
||||||
|
if (this.state.showNotification) {
|
||||||
|
noPermissionNotification = (
|
||||||
|
<Notification
|
||||||
|
type={"info"}
|
||||||
|
children={t("config-form.no-permission-notification")}
|
||||||
|
onClose={() => this.onClose()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.submit}>
|
||||||
|
{noPermissionNotification}
|
||||||
|
<GeneralSettings
|
||||||
|
realmDescription={config.realmDescription}
|
||||||
|
enableRepositoryArchive={config.enableRepositoryArchive}
|
||||||
|
disableGroupingGrid={config.disableGroupingGrid}
|
||||||
|
dateFormat={config.dateFormat}
|
||||||
|
anonymousAccessEnabled={config.anonymousAccessEnabled}
|
||||||
|
skipFailedAuthenticators={config.skipFailedAuthenticators}
|
||||||
|
pluginUrl={config.pluginUrl}
|
||||||
|
enabledXsrfProtection={config.enabledXsrfProtection}
|
||||||
|
defaultNamespaceStrategy={config.defaultNamespaceStrategy}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
hasUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<LoginAttempt
|
||||||
|
loginAttemptLimit={config.loginAttemptLimit}
|
||||||
|
loginAttemptLimitTimeout={config.loginAttemptLimitTimeout}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
hasUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<BaseUrlSettings
|
||||||
|
baseUrl={config.baseUrl}
|
||||||
|
forceBaseUrl={config.forceBaseUrl}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
hasUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<AdminSettings
|
||||||
|
adminGroups={config.adminGroups}
|
||||||
|
adminUsers={config.adminUsers}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
hasUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<ProxySettings
|
||||||
|
proxyPassword={config.proxyPassword ? config.proxyPassword : ""}
|
||||||
|
proxyPort={config.proxyPort}
|
||||||
|
proxyServer={config.proxyServer ? config.proxyServer : ""}
|
||||||
|
proxyUser={config.proxyUser ? config.proxyUser : ""}
|
||||||
|
enableProxy={config.enableProxy}
|
||||||
|
proxyExcludes={config.proxyExcludes}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
hasUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<SubmitButton
|
||||||
|
loading={loading}
|
||||||
|
label={t("config-form.submit")}
|
||||||
|
disabled={!configUpdatePermission || this.hasError()}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (isValid: boolean, changedValue: any, name: string) => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
config: {
|
||||||
|
...this.state.config,
|
||||||
|
[name]: changedValue
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
...this.state.error,
|
||||||
|
[name]: !isValid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
hasError = () => {
|
||||||
|
return (
|
||||||
|
this.state.error.loginAttemptLimit ||
|
||||||
|
this.state.error.loginAttemptLimitTimeout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onClose = () => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
showNotification: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(ConfigForm);
|
||||||
128
scm-ui/src/config/components/form/GeneralSettings.js
Normal file
128
scm-ui/src/config/components/form/GeneralSettings.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Checkbox, InputField } from "../../../components/forms/index";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
realmDescription: string,
|
||||||
|
enableRepositoryArchive: boolean,
|
||||||
|
disableGroupingGrid: boolean,
|
||||||
|
dateFormat: string,
|
||||||
|
anonymousAccessEnabled: boolean,
|
||||||
|
skipFailedAuthenticators: boolean,
|
||||||
|
pluginUrl: string,
|
||||||
|
enabledXsrfProtection: boolean,
|
||||||
|
defaultNamespaceStrategy: string,
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
hasUpdatePermission: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class GeneralSettings extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
realmDescription,
|
||||||
|
enableRepositoryArchive,
|
||||||
|
disableGroupingGrid,
|
||||||
|
dateFormat,
|
||||||
|
anonymousAccessEnabled,
|
||||||
|
skipFailedAuthenticators,
|
||||||
|
pluginUrl,
|
||||||
|
enabledXsrfProtection,
|
||||||
|
defaultNamespaceStrategy,
|
||||||
|
hasUpdatePermission
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InputField
|
||||||
|
label={t("general-settings.realm-description")}
|
||||||
|
onChange={this.handleRealmDescriptionChange}
|
||||||
|
value={realmDescription}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("general-settings.date-format")}
|
||||||
|
onChange={this.handleDateFormatChange}
|
||||||
|
value={dateFormat}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("general-settings.plugin-url")}
|
||||||
|
onChange={this.handlePluginUrlChange}
|
||||||
|
value={pluginUrl}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("general-settings.default-namespace-strategy")}
|
||||||
|
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||||
|
value={defaultNamespaceStrategy}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={enabledXsrfProtection}
|
||||||
|
label={t("general-settings.enabled-xsrf-protection")}
|
||||||
|
onChange={this.handleEnabledXsrfProtectionChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={enableRepositoryArchive}
|
||||||
|
label={t("general-settings.enable-repository-archive")}
|
||||||
|
onChange={this.handleEnableRepositoryArchiveChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={disableGroupingGrid}
|
||||||
|
label={t("general-settings.disable-grouping-grid")}
|
||||||
|
onChange={this.handleDisableGroupingGridChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={anonymousAccessEnabled}
|
||||||
|
label={t("general-settings.anonymous-access-enabled")}
|
||||||
|
onChange={this.handleAnonymousAccessEnabledChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
checked={skipFailedAuthenticators}
|
||||||
|
label={t("general-settings.skip-failed-authenticators")}
|
||||||
|
onChange={this.handleSkipFailedAuthenticatorsChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRealmDescriptionChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "realmDescription");
|
||||||
|
};
|
||||||
|
handleEnableRepositoryArchiveChange = (value: boolean) => {
|
||||||
|
this.props.onChange(true, value, "enableRepositoryArchive");
|
||||||
|
};
|
||||||
|
handleDisableGroupingGridChange = (value: boolean) => {
|
||||||
|
this.props.onChange(true, value, "disableGroupingGrid");
|
||||||
|
};
|
||||||
|
handleDateFormatChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "dateFormat");
|
||||||
|
};
|
||||||
|
handleAnonymousAccessEnabledChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "anonymousAccessEnabled");
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSkipFailedAuthenticatorsChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "skipFailedAuthenticators");
|
||||||
|
};
|
||||||
|
handlePluginUrlChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "pluginUrl");
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEnabledXsrfProtectionChange = (value: boolean) => {
|
||||||
|
this.props.onChange(true, value, "enabledXsrfProtection");
|
||||||
|
};
|
||||||
|
handleDefaultNamespaceStrategyChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "defaultNamespaceStrategy");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(GeneralSettings);
|
||||||
87
scm-ui/src/config/components/form/LoginAttempt.js
Normal file
87
scm-ui/src/config/components/form/LoginAttempt.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { InputField } from "../../../components/forms/index";
|
||||||
|
import Subtitle from "../../../components/layout/Subtitle";
|
||||||
|
import * as validator from "../../../components/validation";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loginAttemptLimit: number,
|
||||||
|
loginAttemptLimitTimeout: number,
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
hasUpdatePermission: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
loginAttemptLimitError: boolean,
|
||||||
|
loginAttemptLimitTimeoutError: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class LoginAttempt extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loginAttemptLimitError: false,
|
||||||
|
loginAttemptLimitTimeoutError: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
loginAttemptLimit,
|
||||||
|
loginAttemptLimitTimeout,
|
||||||
|
hasUpdatePermission
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Subtitle subtitle={t("login-attempt.name")} />
|
||||||
|
<InputField
|
||||||
|
label={t("login-attempt.login-attempt-limit")}
|
||||||
|
onChange={this.handleLoginAttemptLimitChange}
|
||||||
|
value={loginAttemptLimit}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
validationError={this.state.loginAttemptLimitError}
|
||||||
|
errorMessage={t("validation.login-attempt-limit-invalid")}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("login-attempt.login-attempt-limit-timeout")}
|
||||||
|
onChange={this.handleLoginAttemptLimitTimeoutChange}
|
||||||
|
value={loginAttemptLimitTimeout}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
validationError={this.state.loginAttemptLimitTimeoutError}
|
||||||
|
errorMessage={t("validation.login-attempt-limit-timeout-invalid")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: set Error in ConfigForm to disable Submit Button!
|
||||||
|
handleLoginAttemptLimitChange = (value: string) => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
loginAttemptLimitError: !validator.isNumberValid(value)
|
||||||
|
});
|
||||||
|
this.props.onChange(
|
||||||
|
validator.isNumberValid(value),
|
||||||
|
value,
|
||||||
|
"loginAttemptLimit"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoginAttemptLimitTimeoutChange = (value: string) => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
loginAttemptLimitTimeoutError: !validator.isNumberValid(value)
|
||||||
|
});
|
||||||
|
this.props.onChange(
|
||||||
|
validator.isNumberValid(value),
|
||||||
|
value,
|
||||||
|
"loginAttemptLimitTimeout"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(LoginAttempt);
|
||||||
118
scm-ui/src/config/components/form/ProxySettings.js
Normal file
118
scm-ui/src/config/components/form/ProxySettings.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Checkbox, InputField } from "../../../components/forms/index";
|
||||||
|
import Subtitle from "../../../components/layout/Subtitle";
|
||||||
|
import ProxyExcludesTable from "../table/ProxyExcludesTable";
|
||||||
|
import AddEntryToTableField from "../../../components/forms/AddEntryToTableField";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
proxyPassword: string,
|
||||||
|
proxyPort: number,
|
||||||
|
proxyServer: string,
|
||||||
|
proxyUser: string,
|
||||||
|
enableProxy: boolean,
|
||||||
|
proxyExcludes: string[],
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
hasUpdatePermission: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProxySettings extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
proxyPassword,
|
||||||
|
proxyPort,
|
||||||
|
proxyServer,
|
||||||
|
proxyUser,
|
||||||
|
enableProxy,
|
||||||
|
proxyExcludes,
|
||||||
|
hasUpdatePermission
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Subtitle subtitle={t("proxy-settings.name")} />
|
||||||
|
<Checkbox
|
||||||
|
checked={enableProxy}
|
||||||
|
label={t("proxy-settings.enable-proxy")}
|
||||||
|
onChange={this.handleEnableProxyChange}
|
||||||
|
disabled={!hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("proxy-settings.proxy-password")}
|
||||||
|
onChange={this.handleProxyPasswordChange}
|
||||||
|
value={proxyPassword}
|
||||||
|
type="password"
|
||||||
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("proxy-settings.proxy-port")}
|
||||||
|
value={proxyPort}
|
||||||
|
onChange={this.handleProxyPortChange}
|
||||||
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("proxy-settings.proxy-server")}
|
||||||
|
value={proxyServer}
|
||||||
|
onChange={this.handleProxyServerChange}
|
||||||
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label={t("proxy-settings.proxy-user")}
|
||||||
|
value={proxyUser}
|
||||||
|
onChange={this.handleProxyUserChange}
|
||||||
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<ProxyExcludesTable
|
||||||
|
proxyExcludes={proxyExcludes}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.props.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
|
/>
|
||||||
|
<AddEntryToTableField
|
||||||
|
addEntry={this.addProxyExclude}
|
||||||
|
disabled={!enableProxy || !hasUpdatePermission}
|
||||||
|
buttonLabel={t("proxy-settings.add-proxy-exclude-button")}
|
||||||
|
fieldLabel={t("proxy-settings.add-proxy-exclude-textfield")}
|
||||||
|
errorMessage={t("proxy-settings.add-proxy-exclude-error")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProxyPasswordChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "proxyPassword");
|
||||||
|
};
|
||||||
|
handleProxyPortChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "proxyPort");
|
||||||
|
};
|
||||||
|
handleProxyServerChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "proxyServer");
|
||||||
|
};
|
||||||
|
handleProxyUserChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "proxyUser");
|
||||||
|
};
|
||||||
|
handleEnableProxyChange = (value: string) => {
|
||||||
|
this.props.onChange(true, value, "enableProxy");
|
||||||
|
};
|
||||||
|
|
||||||
|
addProxyExclude = (proxyExcludeName: string) => {
|
||||||
|
if (this.isProxyExcludeMember(proxyExcludeName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onChange(
|
||||||
|
true,
|
||||||
|
[...this.props.proxyExcludes, proxyExcludeName],
|
||||||
|
"proxyExcludes"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
isProxyExcludeMember = (proxyExcludeName: string) => {
|
||||||
|
return this.props.proxyExcludes.includes(proxyExcludeName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(ProxySettings);
|
||||||
36
scm-ui/src/config/components/table/AdminGroupTable.js
Normal file
36
scm-ui/src/config/components/table/AdminGroupTable.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import ArrayConfigTable from "./ArrayConfigTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminGroups: string[],
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
disabled: boolean,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
class AdminGroupTable extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { t, disabled, adminGroups } = this.props;
|
||||||
|
return (
|
||||||
|
<ArrayConfigTable
|
||||||
|
items={adminGroups}
|
||||||
|
label={t("admin-settings.admin-groups")}
|
||||||
|
removeLabel={t("admin-settings.remove-group-button")}
|
||||||
|
onRemove={this.removeEntry}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEntry = (newGroups: string[]) => {
|
||||||
|
this.props.onChange(true, newGroups, "adminGroups");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AdminGroupTable);
|
||||||
34
scm-ui/src/config/components/table/AdminUserTable.js
Normal file
34
scm-ui/src/config/components/table/AdminUserTable.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import ArrayConfigTable from "./ArrayConfigTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminUsers: string[],
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
disabled: boolean,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class AdminUserTable extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { adminUsers, t, disabled } = this.props;
|
||||||
|
return (
|
||||||
|
<ArrayConfigTable
|
||||||
|
items={adminUsers}
|
||||||
|
label={t("admin-settings.admin-users")}
|
||||||
|
removeLabel={t("admin-settings.remove-user-button")}
|
||||||
|
onRemove={this.removeEntry}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEntry = (newUsers: string[]) => {
|
||||||
|
this.props.onChange(true, newUsers, "adminUsers");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AdminUserTable);
|
||||||
48
scm-ui/src/config/components/table/ArrayConfigTable.js
Normal file
48
scm-ui/src/config/components/table/ArrayConfigTable.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { RemoveEntryOfTableButton } from "../../../components/buttons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: string[],
|
||||||
|
label: string,
|
||||||
|
removeLabel: string,
|
||||||
|
onRemove: (string[], string) => void,
|
||||||
|
disabled: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArrayConfigTable extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { label, disabled, removeLabel, items } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="label">{label}</label>
|
||||||
|
<table className="table is-hoverable is-fullwidth">
|
||||||
|
<tbody>
|
||||||
|
{items.map(item => {
|
||||||
|
return (
|
||||||
|
<tr key={item}>
|
||||||
|
<td>{item}</td>
|
||||||
|
<td>
|
||||||
|
<RemoveEntryOfTableButton
|
||||||
|
entryname={item}
|
||||||
|
removeEntry={this.removeEntry}
|
||||||
|
disabled={disabled}
|
||||||
|
label={removeLabel}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEntry = (item: string) => {
|
||||||
|
const newItems = this.props.items.filter(name => name !== item);
|
||||||
|
this.props.onRemove(newItems, item);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArrayConfigTable;
|
||||||
34
scm-ui/src/config/components/table/ProxyExcludesTable.js
Normal file
34
scm-ui/src/config/components/table/ProxyExcludesTable.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import ArrayConfigTable from "./ArrayConfigTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
proxyExcludes: string[],
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void,
|
||||||
|
disabled: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
class ProxyExcludesTable extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { proxyExcludes, disabled, t } = this.props;
|
||||||
|
return (
|
||||||
|
<ArrayConfigTable
|
||||||
|
items={proxyExcludes}
|
||||||
|
label={t("proxy-settings.proxy-excludes")}
|
||||||
|
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
|
||||||
|
onRemove={this.removeEntry}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEntry = (newExcludes: string[]) => {
|
||||||
|
this.props.onChange(true, newExcludes, "proxyExcludes");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(ProxyExcludesTable);
|
||||||
56
scm-ui/src/config/containers/Config.js
Normal file
56
scm-ui/src/config/containers/Config.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { Route } from "react-router";
|
||||||
|
|
||||||
|
import { Page } from "../../components/layout";
|
||||||
|
import { Navigation, NavLink, Section } from "../../components/navigation";
|
||||||
|
import GlobalConfig from "./GlobalConfig";
|
||||||
|
import type { History } from "history";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
// context objects
|
||||||
|
t: string => string,
|
||||||
|
match: any,
|
||||||
|
history: History
|
||||||
|
};
|
||||||
|
|
||||||
|
class Config extends React.Component<Props> {
|
||||||
|
stripEndingSlash = (url: string) => {
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
return url.substring(0, url.length - 2);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
matchedUrl = () => {
|
||||||
|
return this.stripEndingSlash(this.props.match.url);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
const url = this.matchedUrl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-three-quarters">
|
||||||
|
<Route path={url} exact component={GlobalConfig} />
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<Navigation>
|
||||||
|
<Section label={t("config.navigation-title")}>
|
||||||
|
<NavLink
|
||||||
|
to={`${url}`}
|
||||||
|
label={t("global-config.navigation-label")}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
</Navigation>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(Config);
|
||||||
109
scm-ui/src/config/containers/GlobalConfig.js
Normal file
109
scm-ui/src/config/containers/GlobalConfig.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import Title from "../../components/layout/Title";
|
||||||
|
import {
|
||||||
|
fetchConfig,
|
||||||
|
getFetchConfigFailure,
|
||||||
|
isFetchConfigPending,
|
||||||
|
getConfig,
|
||||||
|
modifyConfig,
|
||||||
|
isModifyConfigPending,
|
||||||
|
getConfigUpdatePermission,
|
||||||
|
getModifyConfigFailure,
|
||||||
|
modifyConfigReset
|
||||||
|
} from "../modules/config";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import ErrorPage from "../../components/ErrorPage";
|
||||||
|
import type { Config } from "../types/Config";
|
||||||
|
import ConfigForm from "../components/form/ConfigForm";
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
|
config: Config,
|
||||||
|
configUpdatePermission: boolean,
|
||||||
|
|
||||||
|
// dispatch functions
|
||||||
|
modifyConfig: (config: Config, callback?: () => void) => void,
|
||||||
|
fetchConfig: void => void,
|
||||||
|
configReset: void => void,
|
||||||
|
|
||||||
|
// context objects
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class GlobalConfig extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.configReset();
|
||||||
|
this.props.fetchConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyConfig = (config: Config) => {
|
||||||
|
this.props.modifyConfig(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t, error, loading, config, configUpdatePermission } = this.props;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
title={t("global-config.error-title")}
|
||||||
|
subtitle={t("global-config.error-subtitle")}
|
||||||
|
error={error}
|
||||||
|
configUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title title={t("global-config.title")} />
|
||||||
|
<ConfigForm
|
||||||
|
submitForm={config => this.modifyConfig(config)}
|
||||||
|
config={config}
|
||||||
|
loading={loading}
|
||||||
|
configUpdatePermission={configUpdatePermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
fetchConfig: () => {
|
||||||
|
dispatch(fetchConfig());
|
||||||
|
},
|
||||||
|
modifyConfig: (config: Config, callback?: () => void) => {
|
||||||
|
dispatch(modifyConfig(config, callback));
|
||||||
|
},
|
||||||
|
configReset: () => {
|
||||||
|
dispatch(modifyConfigReset());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const loading = isFetchConfigPending(state) || isModifyConfigPending(state);
|
||||||
|
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
||||||
|
const config = getConfig(state);
|
||||||
|
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
config,
|
||||||
|
configUpdatePermission
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(translate("config")(GlobalConfig));
|
||||||
178
scm-ui/src/config/modules/config.js
Normal file
178
scm-ui/src/config/modules/config.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// @flow
|
||||||
|
import { apiClient } from "../../apiclient";
|
||||||
|
import * as types from "../../modules/types";
|
||||||
|
import type { Action } from "../../types/Action";
|
||||||
|
import { isPending } from "../../modules/pending";
|
||||||
|
import { getFailure } from "../../modules/failure";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import type { Config } from "../types/Config";
|
||||||
|
|
||||||
|
export const FETCH_CONFIG = "scm/config/FETCH_CONFIG";
|
||||||
|
export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`;
|
||||||
|
export const FETCH_CONFIG_SUCCESS = `${FETCH_CONFIG}_${types.SUCCESS_SUFFIX}`;
|
||||||
|
export const FETCH_CONFIG_FAILURE = `${FETCH_CONFIG}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
|
export const MODIFY_CONFIG = "scm/config/MODIFY_CONFIG";
|
||||||
|
export const MODIFY_CONFIG_PENDING = `${MODIFY_CONFIG}_${types.PENDING_SUFFIX}`;
|
||||||
|
export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`;
|
||||||
|
export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`;
|
||||||
|
export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
|
||||||
|
|
||||||
|
const CONFIG_URL = "config";
|
||||||
|
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
|
||||||
|
|
||||||
|
//fetch config
|
||||||
|
export function fetchConfig() {
|
||||||
|
return function(dispatch: any) {
|
||||||
|
dispatch(fetchConfigPending());
|
||||||
|
return apiClient
|
||||||
|
.get(CONFIG_URL)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
dispatch(fetchConfigSuccess(data));
|
||||||
|
})
|
||||||
|
.catch(cause => {
|
||||||
|
const error = new Error(`could not fetch config: ${cause.message}`);
|
||||||
|
dispatch(fetchConfigFailure(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchConfigPending(): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_CONFIG_PENDING
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchConfigSuccess(config: Config): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_CONFIG_SUCCESS,
|
||||||
|
payload: config
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchConfigFailure(error: Error): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_CONFIG_FAILURE,
|
||||||
|
payload: {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify config
|
||||||
|
export function modifyConfig(config: Config, callback?: () => void) {
|
||||||
|
return function(dispatch: Dispatch) {
|
||||||
|
dispatch(modifyConfigPending(config));
|
||||||
|
return apiClient
|
||||||
|
.put(config._links.update.href, config, CONTENT_TYPE_CONFIG)
|
||||||
|
.then(() => {
|
||||||
|
dispatch(modifyConfigSuccess(config));
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(cause => {
|
||||||
|
dispatch(
|
||||||
|
modifyConfigFailure(
|
||||||
|
config,
|
||||||
|
new Error(`could not modify config: ${cause.message}`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyConfigPending(config: Config): Action {
|
||||||
|
return {
|
||||||
|
type: MODIFY_CONFIG_PENDING,
|
||||||
|
payload: config
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyConfigSuccess(config: Config): Action {
|
||||||
|
return {
|
||||||
|
type: MODIFY_CONFIG_SUCCESS,
|
||||||
|
payload: config
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyConfigFailure(config: Config, error: Error): Action {
|
||||||
|
return {
|
||||||
|
type: MODIFY_CONFIG_FAILURE,
|
||||||
|
payload: {
|
||||||
|
error,
|
||||||
|
config
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modifyConfigReset() {
|
||||||
|
return {
|
||||||
|
type: MODIFY_CONFIG_RESET
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//reducer
|
||||||
|
|
||||||
|
function removeNullValues(config: Config) {
|
||||||
|
if (!config.adminGroups) {
|
||||||
|
config.adminGroups = [];
|
||||||
|
}
|
||||||
|
if (!config.adminUsers) {
|
||||||
|
config.adminUsers = [];
|
||||||
|
}
|
||||||
|
if (!config.proxyExcludes) {
|
||||||
|
config.proxyExcludes = [];
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer(state: any = {}, action: any = {}) {
|
||||||
|
switch (action.type) {
|
||||||
|
case MODIFY_CONFIG_SUCCESS:
|
||||||
|
case FETCH_CONFIG_SUCCESS:
|
||||||
|
const config = removeNullValues(action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
entries: config,
|
||||||
|
configUpdatePermission: action.payload._links.update ? true : false
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reducer;
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
|
||||||
|
export function isFetchConfigPending(state: Object) {
|
||||||
|
return isPending(state, FETCH_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchConfigFailure(state: Object) {
|
||||||
|
return getFailure(state, FETCH_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isModifyConfigPending(state: Object) {
|
||||||
|
return isPending(state, MODIFY_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModifyConfigFailure(state: Object) {
|
||||||
|
return getFailure(state, MODIFY_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfig(state: Object) {
|
||||||
|
if (state.config && state.config.entries) {
|
||||||
|
return state.config.entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigUpdatePermission(state: Object) {
|
||||||
|
if (state.config && state.config.configUpdatePermission) {
|
||||||
|
return state.config.configUpdatePermission;
|
||||||
|
}
|
||||||
|
}
|
||||||
287
scm-ui/src/config/modules/config.test.js
Normal file
287
scm-ui/src/config/modules/config.test.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
//@flow
|
||||||
|
import configureMockStore from "redux-mock-store";
|
||||||
|
import thunk from "redux-thunk";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
|
||||||
|
import reducer, {
|
||||||
|
FETCH_CONFIG,
|
||||||
|
FETCH_CONFIG_PENDING,
|
||||||
|
FETCH_CONFIG_SUCCESS,
|
||||||
|
FETCH_CONFIG_FAILURE,
|
||||||
|
MODIFY_CONFIG,
|
||||||
|
MODIFY_CONFIG_PENDING,
|
||||||
|
MODIFY_CONFIG_SUCCESS,
|
||||||
|
MODIFY_CONFIG_FAILURE,
|
||||||
|
fetchConfig,
|
||||||
|
fetchConfigSuccess,
|
||||||
|
getFetchConfigFailure,
|
||||||
|
isFetchConfigPending,
|
||||||
|
modifyConfig,
|
||||||
|
isModifyConfigPending,
|
||||||
|
getModifyConfigFailure,
|
||||||
|
getConfig,
|
||||||
|
getConfigUpdatePermission
|
||||||
|
} from "./config";
|
||||||
|
|
||||||
|
const CONFIG_URL = "/scm/api/rest/v2/config";
|
||||||
|
|
||||||
|
const error = new Error("You have an error!");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
proxyPassword: null,
|
||||||
|
proxyPort: 8080,
|
||||||
|
proxyServer: "proxy.mydomain.com",
|
||||||
|
proxyUser: null,
|
||||||
|
enableProxy: false,
|
||||||
|
realmDescription: "SONIA :: SCM Manager",
|
||||||
|
enableRepositoryArchive: false,
|
||||||
|
disableGroupingGrid: false,
|
||||||
|
dateFormat: "YYYY-MM-DD HH:mm:ss",
|
||||||
|
anonymousAccessEnabled: false,
|
||||||
|
adminGroups: [],
|
||||||
|
adminUsers: [],
|
||||||
|
baseUrl: "http://localhost:8081/scm",
|
||||||
|
forceBaseUrl: false,
|
||||||
|
loginAttemptLimit: -1,
|
||||||
|
proxyExcludes: [],
|
||||||
|
skipFailedAuthenticators: false,
|
||||||
|
pluginUrl:
|
||||||
|
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
|
||||||
|
loginAttemptLimitTimeout: 300,
|
||||||
|
enabledXsrfProtection: true,
|
||||||
|
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||||
|
_links: {
|
||||||
|
self: { href: "http://localhost:8081/scm/api/rest/v2/config" },
|
||||||
|
update: { href: "http://localhost:8081/scm/api/rest/v2/config" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const configWithNullValues = {
|
||||||
|
proxyPassword: null,
|
||||||
|
proxyPort: 8080,
|
||||||
|
proxyServer: "proxy.mydomain.com",
|
||||||
|
proxyUser: null,
|
||||||
|
enableProxy: false,
|
||||||
|
realmDescription: "SONIA :: SCM Manager",
|
||||||
|
enableRepositoryArchive: false,
|
||||||
|
disableGroupingGrid: false,
|
||||||
|
dateFormat: "YYYY-MM-DD HH:mm:ss",
|
||||||
|
anonymousAccessEnabled: false,
|
||||||
|
adminGroups: null,
|
||||||
|
adminUsers: null,
|
||||||
|
baseUrl: "http://localhost:8081/scm",
|
||||||
|
forceBaseUrl: false,
|
||||||
|
loginAttemptLimit: -1,
|
||||||
|
proxyExcludes: null,
|
||||||
|
skipFailedAuthenticators: false,
|
||||||
|
pluginUrl:
|
||||||
|
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
|
||||||
|
loginAttemptLimitTimeout: 300,
|
||||||
|
enabledXsrfProtection: true,
|
||||||
|
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||||
|
_links: {
|
||||||
|
self: { href: "http://localhost:8081/scm/api/rest/v2/config" },
|
||||||
|
update: { href: "http://localhost:8081/scm/api/rest/v2/config" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseBody = {
|
||||||
|
entries: config,
|
||||||
|
configUpdatePermission: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
responseBody
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("config fetch()", () => {
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully fetch config", () => {
|
||||||
|
fetchMock.getOnce(CONFIG_URL, response);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_CONFIG_PENDING },
|
||||||
|
{
|
||||||
|
type: FETCH_CONFIG_SUCCESS,
|
||||||
|
payload: response
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store.dispatch(fetchConfig()).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail getting config on HTTP 500", () => {
|
||||||
|
fetchMock.getOnce(CONFIG_URL, {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchConfig()).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully modify config", () => {
|
||||||
|
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", {
|
||||||
|
status: 204
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store.dispatch(modifyConfig(config)).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(MODIFY_CONFIG_SUCCESS);
|
||||||
|
expect(actions[1].payload).toEqual(config);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the callback after modifying config", () => {
|
||||||
|
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", {
|
||||||
|
status: 204
|
||||||
|
});
|
||||||
|
|
||||||
|
let called = false;
|
||||||
|
const callback = () => {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store.dispatch(modifyConfig(config, callback)).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(MODIFY_CONFIG_SUCCESS);
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail modifying config on HTTP 500", () => {
|
||||||
|
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
|
||||||
|
return store.dispatch(modifyConfig(config)).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING);
|
||||||
|
expect(actions[1].type).toEqual(MODIFY_CONFIG_FAILURE);
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("config reducer", () => {
|
||||||
|
it("should update state correctly according to FETCH_CONFIG_SUCCESS action", () => {
|
||||||
|
const newState = reducer({}, fetchConfigSuccess(config));
|
||||||
|
|
||||||
|
expect(newState).toEqual({
|
||||||
|
entries: config,
|
||||||
|
configUpdatePermission: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set configUpdatePermission to true if update link is present", () => {
|
||||||
|
const newState = reducer({}, fetchConfigSuccess(config));
|
||||||
|
|
||||||
|
expect(newState.configUpdatePermission).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update state according to FETCH_CONFIG_SUCCESS action", () => {
|
||||||
|
const newState = reducer({}, fetchConfigSuccess(config));
|
||||||
|
expect(newState.entries).toBe(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty arrays for null values", () => {
|
||||||
|
const config = reducer({}, fetchConfigSuccess(configWithNullValues))
|
||||||
|
.entries;
|
||||||
|
expect(config.adminUsers).toEqual([]);
|
||||||
|
expect(config.adminGroups).toEqual([]);
|
||||||
|
expect(config.proxyExcludes).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("selector tests", () => {
|
||||||
|
it("should return true, when fetch config is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[FETCH_CONFIG]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isFetchConfigPending(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when fetch config is not pending", () => {
|
||||||
|
expect(isFetchConfigPending({})).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return error when fetch config did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[FETCH_CONFIG]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getFetchConfigFailure(state)).toEqual(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when fetch config did not fail", () => {
|
||||||
|
expect(getFetchConfigFailure({})).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true, when modify group is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[MODIFY_CONFIG]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isModifyConfigPending(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when modify config is not pending", () => {
|
||||||
|
expect(isModifyConfigPending({})).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return error when modify config did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[MODIFY_CONFIG]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getModifyConfigFailure(state)).toEqual(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when modify config did not fail", () => {
|
||||||
|
expect(getModifyConfigFailure({})).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return config", () => {
|
||||||
|
const state = {
|
||||||
|
config: {
|
||||||
|
entries: config
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getConfig(state)).toEqual(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return configUpdatePermission", () => {
|
||||||
|
const state = {
|
||||||
|
config: {
|
||||||
|
configUpdatePermission: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getConfigUpdatePermission(state)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
27
scm-ui/src/config/types/Config.js
Normal file
27
scm-ui/src/config/types/Config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//@flow
|
||||||
|
import type { Links } from "../../types/hal";
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
proxyPassword: string | null,
|
||||||
|
proxyPort: number,
|
||||||
|
proxyServer: string,
|
||||||
|
proxyUser: string | null,
|
||||||
|
enableProxy: boolean,
|
||||||
|
realmDescription: string,
|
||||||
|
enableRepositoryArchive: boolean,
|
||||||
|
disableGroupingGrid: boolean,
|
||||||
|
dateFormat: string,
|
||||||
|
anonymousAccessEnabled: boolean,
|
||||||
|
adminGroups: string[],
|
||||||
|
adminUsers: string[],
|
||||||
|
baseUrl: string,
|
||||||
|
forceBaseUrl: boolean,
|
||||||
|
loginAttemptLimit: number,
|
||||||
|
proxyExcludes: string[],
|
||||||
|
skipFailedAuthenticators: boolean,
|
||||||
|
pluginUrl: string,
|
||||||
|
loginAttemptLimitTimeout: number,
|
||||||
|
enabledXsrfProtection: boolean,
|
||||||
|
defaultNamespaceStrategy: string,
|
||||||
|
_links: Links
|
||||||
|
};
|
||||||
@@ -19,6 +19,8 @@ import Groups from "../groups/containers/Groups";
|
|||||||
import SingleGroup from "../groups/containers/SingleGroup";
|
import SingleGroup from "../groups/containers/SingleGroup";
|
||||||
import AddGroup from "../groups/containers/AddGroup";
|
import AddGroup from "../groups/containers/AddGroup";
|
||||||
|
|
||||||
|
import Config from "../config/containers/Config";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated?: boolean
|
authenticated?: boolean
|
||||||
};
|
};
|
||||||
@@ -99,6 +101,12 @@ class Main extends React.Component<Props> {
|
|||||||
component={Groups}
|
component={Groups}
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
/>
|
/>
|
||||||
|
<ProtectedRoute
|
||||||
|
exact
|
||||||
|
path="/config"
|
||||||
|
component={Config}
|
||||||
|
authenticated={authenticated}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import groups from "./groups/modules/groups";
|
|||||||
import auth from "./modules/auth";
|
import auth from "./modules/auth";
|
||||||
import pending from "./modules/pending";
|
import pending from "./modules/pending";
|
||||||
import failure from "./modules/failure";
|
import failure from "./modules/failure";
|
||||||
|
import config from "./config/modules/config";
|
||||||
|
|
||||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||||
|
|
||||||
@@ -26,7 +27,8 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
repos,
|
repos,
|
||||||
repositoryTypes,
|
repositoryTypes,
|
||||||
groups,
|
groups,
|
||||||
auth
|
auth,
|
||||||
|
config
|
||||||
});
|
});
|
||||||
|
|
||||||
return createStore(
|
return createStore(
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import { AddButton } from "../../components/buttons";
|
|
||||||
import InputField from "../../components/forms/InputField";
|
|
||||||
import { isMemberNameValid } from "./groupValidation";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
t: string => string,
|
|
||||||
addMember: string => void
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
memberToAdd: string,
|
|
||||||
validationError: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddMemberField extends React.Component<Props, State> {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
memberToAdd: "",
|
|
||||||
validationError: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { t } = this.props;
|
|
||||||
return (
|
|
||||||
<div className="field">
|
|
||||||
<InputField
|
|
||||||
label={t("add-member-textfield.label")}
|
|
||||||
errorMessage={t("add-member-textfield.error")}
|
|
||||||
onChange={this.handleAddMemberChange}
|
|
||||||
validationError={this.state.validationError}
|
|
||||||
value={this.state.memberToAdd}
|
|
||||||
onReturnPressed={this.appendMember}
|
|
||||||
/>
|
|
||||||
<AddButton
|
|
||||||
label={t("add-member-button.label")}
|
|
||||||
action={this.addButtonClicked}
|
|
||||||
disabled={!isMemberNameValid(this.state.memberToAdd)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addButtonClicked = (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
this.appendMember();
|
|
||||||
};
|
|
||||||
|
|
||||||
appendMember = () => {
|
|
||||||
const { memberToAdd } = this.state;
|
|
||||||
if (isMemberNameValid(memberToAdd)) {
|
|
||||||
this.props.addMember(memberToAdd);
|
|
||||||
this.setState({ ...this.state, memberToAdd: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAddMemberChange = (membername: string) => {
|
|
||||||
this.setState({
|
|
||||||
...this.state,
|
|
||||||
memberToAdd: membername,
|
|
||||||
validationError: membername.length > 0 && !isMemberNameValid(membername)
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default translate("groups")(AddMemberField);
|
|
||||||
@@ -6,9 +6,9 @@ import { SubmitButton } from "../../components/buttons";
|
|||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import type { Group } from "../types/Group";
|
import type { Group } from "../types/Group";
|
||||||
import * as validator from "./groupValidation";
|
import * as validator from "./groupValidation";
|
||||||
import AddMemberField from "./AddMemberField";
|
|
||||||
import MemberNameTable from "./MemberNameTable";
|
import MemberNameTable from "./MemberNameTable";
|
||||||
import Textarea from "../../components/forms/Textarea";
|
import Textarea from "../../components/forms/Textarea";
|
||||||
|
import AddEntryToTableField from "../../components/forms/AddEntryToTableField";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
t: string => string,
|
t: string => string,
|
||||||
@@ -96,7 +96,13 @@ class GroupForm extends React.Component<Props, State> {
|
|||||||
members={this.state.group.members}
|
members={this.state.group.members}
|
||||||
memberListChanged={this.memberListChanged}
|
memberListChanged={this.memberListChanged}
|
||||||
/>
|
/>
|
||||||
<AddMemberField addMember={this.addMember} />
|
<AddEntryToTableField
|
||||||
|
addEntry={this.addMember}
|
||||||
|
disabled={false}
|
||||||
|
buttonLabel={t("add-member-button.label")}
|
||||||
|
fieldLabel={t("add-member-textfield.label")}
|
||||||
|
errorMessage={t("add-member-textfield.error")}
|
||||||
|
/>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
disabled={!this.isValid()}
|
disabled={!this.isValid()}
|
||||||
label={t("group-form.submit")}
|
label={t("group-form.submit")}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//@flow
|
//@flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import RemoveMemberButton from "./buttons/RemoveMemberButton";
|
import { RemoveEntryOfTableButton } from "../../components/buttons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
members: string[],
|
members: string[],
|
||||||
@@ -24,9 +24,11 @@ class MemberNameTable extends React.Component<Props, State> {
|
|||||||
<tr key={member}>
|
<tr key={member}>
|
||||||
<td key={member}>{member}</td>
|
<td key={member}>{member}</td>
|
||||||
<td>
|
<td>
|
||||||
<RemoveMemberButton
|
<RemoveEntryOfTableButton
|
||||||
membername={member}
|
entryname={member}
|
||||||
removeMember={this.removeMember}
|
removeEntry={this.removeEntry}
|
||||||
|
disabled={false}
|
||||||
|
label={t("remove-member-button.label")}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -38,7 +40,7 @@ class MemberNameTable extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMember = (membername: string) => {
|
removeEntry = (membername: string) => {
|
||||||
const newMembers = this.props.members.filter(name => name !== membername);
|
const newMembers = this.props.members.filter(name => name !== membername);
|
||||||
this.props.memberListChanged(newMembers);
|
this.props.memberListChanged(newMembers);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
//@flow
|
|
||||||
import React from "react";
|
|
||||||
import { DeleteButton } from "../../../components/buttons";
|
|
||||||
import { translate } from "react-i18next";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
t: string => string,
|
|
||||||
membername: string,
|
|
||||||
removeMember: string => void
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveMemberButton extends React.Component<Props, State> {
|
|
||||||
render() {
|
|
||||||
const { t , membername, removeMember} = this.props;
|
|
||||||
return (
|
|
||||||
<div className={classNames("is-pulled-right")}>
|
|
||||||
<DeleteButton
|
|
||||||
label={t("remove-member-button.label")}
|
|
||||||
action={(event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
removeMember(membername);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default translate("groups")(RemoveMemberButton);
|
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
/**
|
/*
|
||||||
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
|
Copyright (c) 2010, Sebastian Sdorra All rights reserved.
|
||||||
*
|
|
||||||
* Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
* modification, are permitted provided that the following conditions are met:
|
modification, are permitted provided that the following conditions are met:
|
||||||
*
|
|
||||||
* 1. Redistributions of source code must retain the above copyright notice,
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
* this list of conditions and the following disclaimer. 2. Redistributions in
|
this list of conditions and the following disclaimer. 2. Redistributions in
|
||||||
* binary form must reproduce the above copyright notice, this list of
|
binary form must reproduce the above copyright notice, this list of
|
||||||
* conditions and the following disclaimer in the documentation and/or other
|
conditions and the following disclaimer in the documentation and/or other
|
||||||
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
|
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
|
nor the names of its contributors may be used to endorse or promote products
|
||||||
* derived from this software without specific prior written permission.
|
derived from this software without specific prior written permission.
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
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
|
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.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
|
||||||
* http://bitbucket.org/sdorra/scm-manager
|
http://bitbucket.org/sdorra/scm-manager
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
@@ -56,14 +56,14 @@ public class StatusExceptionMapper<E extends Throwable>
|
|||||||
private static final Logger logger =
|
private static final Logger logger =
|
||||||
LoggerFactory.getLogger(StatusExceptionMapper.class);
|
LoggerFactory.getLogger(StatusExceptionMapper.class);
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
private final Response.Status status;
|
||||||
|
private final Class<E> type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs ...
|
* Map an Exception to a HTTP Response
|
||||||
*
|
*
|
||||||
*
|
* @param type the exception class
|
||||||
* @param type
|
* @param status the http status to be mapped
|
||||||
* @param status
|
|
||||||
*/
|
*/
|
||||||
public StatusExceptionMapper(Class<E> type, Response.Status status)
|
public StatusExceptionMapper(Class<E> type, Response.Status status)
|
||||||
{
|
{
|
||||||
@@ -71,15 +71,12 @@ public class StatusExceptionMapper<E extends Throwable>
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- methods --------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* provide a http responses from an exception
|
||||||
*
|
*
|
||||||
|
* @param exception the thrown exception
|
||||||
*
|
*
|
||||||
* @param exception
|
* @return the http response with the exception presentation
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Response toResponse(E exception)
|
public Response toResponse(E exception)
|
||||||
@@ -95,12 +92,4 @@ public class StatusExceptionMapper<E extends Throwable>
|
|||||||
|
|
||||||
return Response.status(status).build();
|
return Response.status(status).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final Response.Status status;
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private final Class<E> type;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule {
|
|||||||
bind(RepositoryTypeCollectionToDtoMapper.class);
|
bind(RepositoryTypeCollectionToDtoMapper.class);
|
||||||
|
|
||||||
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
|
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());
|
||||||
|
|
||||||
// no mapstruct required
|
// no mapstruct required
|
||||||
bind(UIPluginDtoMapper.class);
|
bind(UIPluginDtoMapper.class);
|
||||||
|
|||||||
@@ -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<PermissionAlreadyExistsException> {
|
||||||
|
|
||||||
|
public PermissionAlreadyExistsExceptionMapper() {
|
||||||
|
super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<PermissionDto> 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<PermissionDto> permissionDtoList) {
|
||||||
|
return embeddedBuilder()
|
||||||
|
.with("permissions", permissionDtoList)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<PermissionNotFoundException> {
|
||||||
|
|
||||||
|
public PermissionNotFoundExceptionMapper() {
|
||||||
|
super(PermissionNotFoundException.class, Response.Status.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,220 @@
|
|||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||||
import javax.inject.Provider;
|
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||||
import javax.ws.rs.Path;
|
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.*;
|
||||||
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.ws.rs.*;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class PermissionRootResource {
|
public class PermissionRootResource {
|
||||||
|
|
||||||
private final Provider<PermissionCollectionResource> permissionCollectionResource;
|
private PermissionDtoToPermissionMapper dtoToModelMapper;
|
||||||
|
private PermissionToPermissionDtoMapper modelToDtoMapper;
|
||||||
|
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
|
||||||
|
private ResourceLinks resourceLinks;
|
||||||
|
private final RepositoryManager manager;
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PermissionRootResource(Provider<PermissionCollectionResource> permissionCollectionResource) {
|
public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) {
|
||||||
this.permissionCollectionResource = permissionCollectionResource;
|
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("")
|
@Path("")
|
||||||
public PermissionCollectionResource getPermissionCollectionResource() {
|
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException {
|
||||||
return permissionCollectionResource.get();
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import de.otto.edison.hal.Links;
|
||||||
|
import org.mapstruct.*;
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<RepositoryNotFoundException> {
|
||||||
|
|
||||||
|
public RepositoryNotFoundExceptionMapper() {
|
||||||
|
super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,11 +128,17 @@ public class RepositoryResource {
|
|||||||
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) {
|
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) {
|
||||||
return adapter.update(
|
return adapter.update(
|
||||||
loadBy(namespace, name),
|
loadBy(namespace, name),
|
||||||
existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()),
|
existing -> processUpdate(repositoryDto, existing),
|
||||||
nameAndNamespaceStaysTheSame(namespace, name)
|
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/")
|
@Path("tags/")
|
||||||
public TagRootResource tags() {
|
public TagRootResource tags() {
|
||||||
return tagRootResource.get();
|
return tagRootResource.get();
|
||||||
@@ -176,6 +182,6 @@ public class RepositoryResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<Repository> nameAndNamespaceStaysTheSame(String namespace, String name) {
|
private Predicate<Repository> 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
|
|||||||
}
|
}
|
||||||
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
if (RepositoryPermissions.modify(repository).isPermitted()) {
|
||||||
linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName())));
|
linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName())));
|
||||||
linksBuilder.single(link("permissions", resourceLinks.permissionCollection().self(target.getNamespace(), target.getName())));
|
linksBuilder.single(link("permissions", resourceLinks.permission().all(target.getNamespace(), target.getName())));
|
||||||
}
|
}
|
||||||
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
|
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
|
||||||
if (repositoryService.isSupported(Command.TAGS)) {
|
if (repositoryService.isSupported(Command.TAGS)) {
|
||||||
|
|||||||
@@ -298,20 +298,39 @@ class ResourceLinks {
|
|||||||
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href();
|
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public PermissionLinks permission() {
|
||||||
public PermissionCollectionLinks permissionCollection() {
|
return new PermissionLinks(uriInfoStore.get());
|
||||||
return new PermissionCollectionLinks(uriInfoStore.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class PermissionCollectionLinks {
|
static class PermissionLinks {
|
||||||
private final LinkBuilder permissionLinkBuilder;
|
private final LinkBuilder permissionLinkBuilder;
|
||||||
|
|
||||||
PermissionCollectionLinks(UriInfo uriInfo) {
|
PermissionLinks(UriInfo uriInfo) {
|
||||||
permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class, PermissionCollectionResource.class);
|
permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
String self(String namespace, String name) {
|
String all(String namespace, String name) {
|
||||||
return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getPermissionCollectionResource").parameters().method("getAll").parameters().href();
|
return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getAll").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
|
String create(String repositoryNamespace, String repositoryName) {
|
||||||
|
return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method("create").parameters().href();
|
||||||
|
}
|
||||||
|
|
||||||
|
String self(String repositoryNamespace, String repositoryName, String permissionName) {
|
||||||
|
return getLink(repositoryNamespace, repositoryName, permissionName, "get");
|
||||||
|
}
|
||||||
|
|
||||||
|
String update(String repositoryNamespace, String repositoryName, String permissionName) {
|
||||||
|
return getLink(repositoryNamespace, repositoryName, permissionName, "update");
|
||||||
|
}
|
||||||
|
|
||||||
|
String delete(String repositoryNamespace, String repositoryName, String permissionName) {
|
||||||
|
return getLink(repositoryNamespace, repositoryName, permissionName, "delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLink(String repositoryNamespace, String repositoryName, String permissionName, String methodName) {
|
||||||
|
return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method(methodName).parameters(permissionName).href();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
|||||||
boolean hasPermission = false;
|
boolean hasPermission = false;
|
||||||
for (sonia.scm.repository.Permission permission : repositoryPermissions)
|
for (sonia.scm.repository.Permission permission : repositoryPermissions)
|
||||||
{
|
{
|
||||||
if (isUserPermitted(user, groups, permission))
|
hasPermission = isUserPermitted(user, groups, permission);
|
||||||
|
if (hasPermission)
|
||||||
{
|
{
|
||||||
|
|
||||||
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
|
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
|
||||||
if (logger.isTraceEnabled())
|
if (logger.isTraceEnabled())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import de.otto.edison.hal.HalRepresentation;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.shiro.authz.AuthorizationException;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.apache.shiro.subject.support.SubjectThreadState;
|
||||||
|
import org.apache.shiro.util.ThreadContext;
|
||||||
|
import org.apache.shiro.util.ThreadState;
|
||||||
|
import org.assertj.core.util.Lists;
|
||||||
|
import org.jboss.resteasy.core.Dispatcher;
|
||||||
|
import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||||
|
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||||
|
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
|
import org.junit.jupiter.api.TestFactory;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
||||||
|
import sonia.scm.repository.*;
|
||||||
|
import sonia.scm.web.VndMediaType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static de.otto.edison.hal.Link.link;
|
||||||
|
import static de.otto.edison.hal.Links.linkingTo;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.mockito.MockitoAnnotations.initMocks;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@SubjectAware(
|
||||||
|
username = "trillian",
|
||||||
|
password = "secret",
|
||||||
|
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||||
|
)
|
||||||
|
public class PermissionRootResourceTest {
|
||||||
|
private static final String REPOSITORY_NAMESPACE = "repo_namespace";
|
||||||
|
private static final String REPOSITORY_NAME = "repo";
|
||||||
|
private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME;
|
||||||
|
private static final String PERMISSION_READ = "repository:permissionRead:" + REPOSITORY_NAME;
|
||||||
|
private static final String PERMISSION_OWNER = "repository:modify:" + REPOSITORY_NAME;
|
||||||
|
|
||||||
|
private static final String PERMISSION_NAME = "perm";
|
||||||
|
private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/";
|
||||||
|
private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME;
|
||||||
|
private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }";
|
||||||
|
private static final ArrayList<Permission> 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<DynamicTest> 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<DynamicTest> 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<DynamicTest> 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<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
|
||||||
|
permissions.add(newPermission);
|
||||||
|
ImmutableList<Permission> 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<Permission> 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<Permission> 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<Permission> 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<Permission> 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<HalRepresentation> actualPermissionDtos = halRepresentation.getEmbedded().getItemsBy("permissions", HalRepresentation.class);
|
||||||
|
List<PermissionDto> 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<Permission> 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<Permission> permissions, String userPermission) {
|
||||||
|
when(createUserWithRepository(userPermission).getPermissions()).thenReturn(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<DynamicTest> 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<MockHttpResponse> 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<MockHttpResponse> responseValidator) {
|
||||||
|
this.responseValidator = responseValidator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,10 +11,13 @@ import org.junit.Before;
|
|||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import sonia.scm.PageResult;
|
import sonia.scm.PageResult;
|
||||||
import sonia.scm.repository.NamespaceAndName;
|
import sonia.scm.repository.NamespaceAndName;
|
||||||
|
import sonia.scm.repository.Permission;
|
||||||
|
import sonia.scm.repository.PermissionType;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.RepositoryException;
|
import sonia.scm.repository.RepositoryException;
|
||||||
import sonia.scm.repository.RepositoryIsNotArchivedException;
|
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_OK;
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
|
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentCaptor.forClass;
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.anyObject;
|
import static org.mockito.Matchers.anyObject;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -240,6 +246,28 @@ public class RepositoryRootResourceTest {
|
|||||||
verify(repositoryManager).create(any(Repository.class));
|
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<Repository> 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<Repository> createSingletonPageResult(Repository repository) {
|
private PageResult<Repository> createSingletonPageResult(Repository repository) {
|
||||||
return new PageResult<>(singletonList(repository), 0);
|
return new PageResult<>(singletonList(repository), 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class ResourceLinksMock {
|
|||||||
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
|
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
|
||||||
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
|
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
|
||||||
when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(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.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo));
|
||||||
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo));
|
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo));
|
||||||
when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));
|
when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));
|
||||||
|
|||||||
@@ -120,15 +120,15 @@ public final class IntegrationTestUtil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Collection<String> createRepositoryTypeParameters() {
|
public static Collection<String[]> createRepositoryTypeParameters() {
|
||||||
Collection<String> params = new ArrayList<>();
|
Collection<String[]> params = new ArrayList<>();
|
||||||
|
|
||||||
params.add("git");
|
params.add(new String[]{"git"});
|
||||||
params.add("svn" );
|
params.add(new String[]{"svn"});
|
||||||
|
|
||||||
if (IOUtil.search("hg") != null)
|
if (IOUtil.search("hg") != null)
|
||||||
{
|
{
|
||||||
params.add("hg");
|
params.add(new String[]{"hg"});
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
|
|||||||
@@ -51,11 +51,7 @@ import java.util.Collection;
|
|||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static sonia.scm.it.IntegrationTestUtil.createAdminClient;
|
import static sonia.scm.it.IntegrationTestUtil.*;
|
||||||
import static sonia.scm.it.IntegrationTestUtil.createResource;
|
|
||||||
import static sonia.scm.it.IntegrationTestUtil.getLink;
|
|
||||||
import static sonia.scm.it.IntegrationTestUtil.readJson;
|
|
||||||
import static sonia.scm.it.IntegrationTestUtil.serialize;
|
|
||||||
import static sonia.scm.it.RepositoryITUtil.createRepository;
|
import static sonia.scm.it.RepositoryITUtil.createRepository;
|
||||||
import static sonia.scm.it.RepositoryITUtil.deleteRepository;
|
import static sonia.scm.it.RepositoryITUtil.deleteRepository;
|
||||||
|
|
||||||
@@ -81,7 +77,7 @@ public class RepositoryArchiveITCase
|
|||||||
//~--- methods --------------------------------------------------------------
|
//~--- methods --------------------------------------------------------------
|
||||||
|
|
||||||
@Parameterized.Parameters(name = "{0}")
|
@Parameterized.Parameters(name = "{0}")
|
||||||
public static Collection<String> createParameters() {
|
public static Collection<String[]> createParameters() {
|
||||||
return IntegrationTestUtil.createRepositoryTypeParameters();
|
return IntegrationTestUtil.createRepositoryTypeParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ package sonia.scm.it;
|
|||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.io.Files;
|
import com.google.common.io.Files;
|
||||||
import com.sun.jersey.api.client.WebResource;
|
import com.sun.jersey.api.client.WebResource;
|
||||||
import org.junit.After;
|
import org.junit.*;
|
||||||
import org.junit.Assume;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.Parameterized;
|
import org.junit.runners.Parameterized;
|
||||||
@@ -208,7 +204,7 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase
|
|||||||
* @return repository types test parameter
|
* @return repository types test parameter
|
||||||
*/
|
*/
|
||||||
@Parameters(name = "{0}")
|
@Parameters(name = "{0}")
|
||||||
public static Collection<String> createParameters()
|
public static Collection<String[]> createParameters()
|
||||||
{
|
{
|
||||||
return IntegrationTestUtil.createRepositoryTypeParameters();
|
return IntegrationTestUtil.createRepositoryTypeParameters();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,15 +40,12 @@ import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
|||||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
import org.apache.shiro.subject.Subject;
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
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 org.mockito.junit.MockitoJUnitRunner;
|
||||||
import sonia.scm.cache.Cache;
|
import sonia.scm.cache.Cache;
|
||||||
import sonia.scm.cache.CacheManager;
|
import sonia.scm.cache.CacheManager;
|
||||||
@@ -60,6 +57,12 @@ import sonia.scm.repository.RepositoryTestData;
|
|||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
import sonia.scm.user.UserTestData;
|
import sonia.scm.user.UserTestData;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.mockito.Mockito.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link AuthorizationCollector}.
|
* Unit tests for {@link AuthorizationCollector}.
|
||||||
*
|
*
|
||||||
@@ -200,7 +203,7 @@ public class DefaultAuthorizationCollectorTest {
|
|||||||
AuthorizationInfo authInfo = collector.collect();
|
AuthorizationInfo authInfo = collector.collect();
|
||||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read:one", "repository:read,write:two"));
|
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user