mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 15:05:44 +01:00
merge repository heads
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* 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.repository;
|
||||
|
||||
/**
|
||||
* Type of permissionPrefix for a {@link Repository}.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public enum PermissionType
|
||||
{
|
||||
|
||||
/** read permision */
|
||||
READ(0, "repository:read,pull:"),
|
||||
|
||||
/** read and write permissionPrefix */
|
||||
WRITE(10, "repository:read,pull,push:"),
|
||||
|
||||
/**
|
||||
* read, write and
|
||||
* also the ability to manage the properties and permissions
|
||||
*/
|
||||
OWNER(100, "repository:*:");
|
||||
|
||||
/**
|
||||
* Constructs a new permissionPrefix type
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
private PermissionType(int value, String permissionPrefix)
|
||||
{
|
||||
this.value = value;
|
||||
this.permissionPrefix = permissionPrefix;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public String getPermissionPrefix()
|
||||
{
|
||||
return permissionPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer representation of the {@link PermissionType}
|
||||
*
|
||||
*
|
||||
* @return integer representation
|
||||
*/
|
||||
public int getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final String permissionPrefix;
|
||||
|
||||
/** Field description */
|
||||
private final int value;
|
||||
}
|
||||
@@ -68,7 +68,6 @@ import java.util.Set;
|
||||
@XmlRootElement(name = "repositories")
|
||||
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
|
||||
|
||||
|
||||
private static final long serialVersionUID = 3486560714961909711L;
|
||||
|
||||
private String contact;
|
||||
@@ -81,6 +80,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
||||
private Long lastModified;
|
||||
private String namespace;
|
||||
private String name;
|
||||
@XmlElement(name = "permission")
|
||||
private final Set<RepositoryPermission> permissions = new HashSet<>();
|
||||
@XmlElement(name = "public")
|
||||
private boolean publicReadable = false;
|
||||
|
||||
@@ -37,12 +37,19 @@ package sonia.scm.repository;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import sonia.scm.security.PermissionObject;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableCollection;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -60,54 +67,19 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
|
||||
private boolean groupPermission = false;
|
||||
private String name;
|
||||
private PermissionType type = PermissionType.READ;
|
||||
@XmlElement(name = "verb")
|
||||
private Collection<String> verbs;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryPermission}.
|
||||
* This constructor is used by JAXB.
|
||||
*
|
||||
* This constructor is used by JAXB and mapstruct.
|
||||
*/
|
||||
public RepositoryPermission() {}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ}
|
||||
* for the specified user.
|
||||
*
|
||||
*
|
||||
* @param name name of the user
|
||||
*/
|
||||
public RepositoryPermission(String name)
|
||||
public RepositoryPermission(String name, Collection<String> verbs, boolean groupPermission)
|
||||
{
|
||||
this();
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryPermission} with the specified type for
|
||||
* the given user.
|
||||
*
|
||||
*
|
||||
* @param name name of the user
|
||||
* @param type type of the permission
|
||||
*/
|
||||
public RepositoryPermission(String name, PermissionType type)
|
||||
{
|
||||
this(name);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryPermission} with the specified type for
|
||||
* the given user or group.
|
||||
*
|
||||
*
|
||||
* @param name name of the user or group
|
||||
* @param type type of the permission
|
||||
* @param groupPermission true if the permission is a permission for a group
|
||||
*/
|
||||
public RepositoryPermission(String name, PermissionType type, boolean groupPermission)
|
||||
{
|
||||
this(name, type);
|
||||
this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs));
|
||||
this.groupPermission = groupPermission;
|
||||
}
|
||||
|
||||
@@ -137,7 +109,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
final RepositoryPermission other = (RepositoryPermission) obj;
|
||||
|
||||
return Objects.equal(name, other.name)
|
||||
&& Objects.equal(type, other.type)
|
||||
&& CollectionUtils.isEqualCollection(verbs, other.verbs)
|
||||
&& Objects.equal(groupPermission, other.groupPermission);
|
||||
}
|
||||
|
||||
@@ -150,7 +122,9 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(name, type, groupPermission);
|
||||
// Normally we do not have a log of repository permissions having the same size of verbs, but different content.
|
||||
// Therefore we do not use the verbs themselves for the hash code but only the number of verbs.
|
||||
return Objects.hashCode(name, verbs.size(), groupPermission);
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +134,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
.add("type", type)
|
||||
.add("verbs", verbs)
|
||||
.add("groupPermission", groupPermission)
|
||||
.toString();
|
||||
//J+
|
||||
@@ -181,14 +155,14 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PermissionType} of the permission.
|
||||
* Returns the verb of the permission.
|
||||
*
|
||||
*
|
||||
* @return {@link PermissionType} of the permission
|
||||
* @return verb of the permission
|
||||
*/
|
||||
public PermissionType getType()
|
||||
public Collection<String> getVerbs()
|
||||
{
|
||||
return type;
|
||||
return verbs == null? emptyList(): verbs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,13 +202,13 @@ public class RepositoryPermission implements PermissionObject, Serializable
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of the permission.
|
||||
* Sets the verb of the permission.
|
||||
*
|
||||
*
|
||||
* @param type type of the permission
|
||||
* @param verbs verbs of the permission
|
||||
*/
|
||||
public void setType(PermissionType type)
|
||||
public void setVerbs(Collection<String> verbs)
|
||||
{
|
||||
this.type = type;
|
||||
this.verbs = verbs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,11 @@ import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.IncomingCommand;
|
||||
import sonia.scm.repository.spi.IncomingCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -94,8 +93,7 @@ public final class IncomingCommandBuilder
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.checkPermission(new RepositoryPermission(remoteRepository,
|
||||
PermissionType.READ));
|
||||
subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
|
||||
|
||||
request.setRemoteRepository(remoteRepository);
|
||||
|
||||
|
||||
@@ -34,12 +34,11 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.OutgoingCommand;
|
||||
import sonia.scm.repository.spi.OutgoingCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -84,8 +83,7 @@ public final class OutgoingCommandBuilder
|
||||
{
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
subject.checkPermission(new RepositoryPermission(remoteRepository,
|
||||
PermissionType.READ));
|
||||
subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
|
||||
|
||||
request.setRemoteRepository(remoteRepository);
|
||||
|
||||
|
||||
@@ -38,11 +38,10 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.PullCommand;
|
||||
import sonia.scm.repository.spi.PullCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -96,9 +95,7 @@ public final class PullCommandBuilder
|
||||
public PullResponse pull(String url) throws IOException {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
//J-
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(localRepository, PermissionType.WRITE)
|
||||
);
|
||||
subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
URL remoteUrl = new URL(url);
|
||||
@@ -124,12 +121,8 @@ public final class PullCommandBuilder
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
//J-
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(localRepository, PermissionType.WRITE)
|
||||
);
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(remoteRepository, PermissionType.READ)
|
||||
);
|
||||
subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
|
||||
subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
request.reset();
|
||||
|
||||
@@ -39,11 +39,10 @@ import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.PushCommand;
|
||||
import sonia.scm.repository.spi.PushCommandRequest;
|
||||
import sonia.scm.security.RepositoryPermission;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -92,9 +91,7 @@ public final class PushCommandBuilder
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
|
||||
//J-
|
||||
subject.checkPermission(
|
||||
new RepositoryPermission(remoteRepository, PermissionType.WRITE)
|
||||
);
|
||||
subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
|
||||
//J+
|
||||
|
||||
logger.info("push changes to repository {}", remoteRepository.getId());
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
/**
|
||||
* 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.security;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import org.apache.shiro.authz.Permission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This class represents the permission to a repository of a user.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.21
|
||||
*/
|
||||
public final class RepositoryPermission
|
||||
implements StringablePermission, Serializable
|
||||
{
|
||||
|
||||
/**
|
||||
* Type string of the permission
|
||||
* @since 1.31
|
||||
*/
|
||||
public static final String TYPE = "repository";
|
||||
|
||||
/** Field description */
|
||||
public static final String WILDCARD = "*";
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = 3832804235417228043L;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param repository
|
||||
* @param permissionType
|
||||
*/
|
||||
public RepositoryPermission(Repository repository,
|
||||
PermissionType permissionType)
|
||||
{
|
||||
this(repository.getId(), permissionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param repositoryId
|
||||
* @param permissionType
|
||||
*/
|
||||
public RepositoryPermission(String repositoryId,
|
||||
PermissionType permissionType)
|
||||
{
|
||||
this.repositoryId = repositoryId;
|
||||
this.permissionType = permissionType;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param obj
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
final RepositoryPermission other = (RepositoryPermission) obj;
|
||||
|
||||
return Objects.equal(repositoryId, other.repositoryId)
|
||||
&& Objects.equal(permissionType, other.permissionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(repositoryId, permissionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param p
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean implies(Permission p)
|
||||
{
|
||||
boolean result = false;
|
||||
|
||||
if (p instanceof RepositoryPermission)
|
||||
{
|
||||
RepositoryPermission rp = (RepositoryPermission) p;
|
||||
|
||||
//J-
|
||||
result = (repositoryId.equals(WILDCARD) || repositoryId.equals(rp.repositoryId))
|
||||
&& (permissionType.getValue() >= rp.permissionType.getValue());
|
||||
//J+
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("repositoryId", repositoryId)
|
||||
.add("permissionType", permissionType)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String getAsString()
|
||||
{
|
||||
StringBuilder buffer = new StringBuilder(TYPE);
|
||||
|
||||
buffer.append(":").append(repositoryId).append(":").append(permissionType);
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public PermissionType getPermissionType()
|
||||
{
|
||||
return permissionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getRepositoryId()
|
||||
{
|
||||
return repositoryId;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private PermissionType permissionType;
|
||||
|
||||
/** Field description */
|
||||
private String repositoryId;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public class VndMediaType {
|
||||
public static final String GROUP = PREFIX + "group" + SUFFIX;
|
||||
public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX;
|
||||
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
|
||||
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
|
||||
public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX;
|
||||
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
|
||||
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
|
||||
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
|
||||
@@ -33,6 +33,7 @@ public class VndMediaType {
|
||||
public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX;
|
||||
public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX;
|
||||
public static final String CONFIG = PREFIX + "config" + SUFFIX;
|
||||
public static final String REPOSITORY_PERMISSION_COLLECTION = PREFIX + "repositoryPermissionCollection" + SUFFIX;
|
||||
public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
|
||||
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
|
||||
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
|
||||
|
||||
@@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
|
||||
}
|
||||
else
|
||||
{
|
||||
permitted = RepositoryPermissions.read(repository).isPermitted();
|
||||
permitted = RepositoryPermissions.pull(repository).isPermitted();
|
||||
}
|
||||
|
||||
return permitted;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class RepositoryPermissionTest {
|
||||
|
||||
@Test
|
||||
void shouldBeEqualWithSameVerbs() {
|
||||
RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false);
|
||||
RepositoryPermission permission2 = new RepositoryPermission("name", asList("two", "one"), false);
|
||||
|
||||
assertThat(permission1).isEqualTo(permission2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHaveSameHashCodeWithSameVerbs() {
|
||||
long hash1 = new RepositoryPermission("name", asList("one", "two"), false).hashCode();
|
||||
long hash2 = new RepositoryPermission("name", asList("two", "one"), false).hashCode();
|
||||
|
||||
assertThat(hash1).isEqualTo(hash2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeEqualWithSameVerbs() {
|
||||
RepositoryPermission permission1 = new RepositoryPermission("name", asList("one", "two"), false);
|
||||
RepositoryPermission permission2 = new RepositoryPermission("name", asList("three", "one"), false);
|
||||
|
||||
assertThat(permission1).isNotEqualTo(permission2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeEqualWithDifferentType() {
|
||||
RepositoryPermission permission1 = new RepositoryPermission("name", asList("one"), false);
|
||||
RepositoryPermission permission2 = new RepositoryPermission("name", asList("one"), true);
|
||||
|
||||
assertThat(permission1).isNotEqualTo(permission2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeEqualWithDifferentName() {
|
||||
RepositoryPermission permission1 = new RepositoryPermission("name1", asList("one"), false);
|
||||
RepositoryPermission permission2 = new RepositoryPermission("name2", asList("one"), false);
|
||||
|
||||
assertThat(permission1).isNotEqualTo(permission2);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* 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.security;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import sonia.scm.repository.PermissionType;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class RepositoryPermissionTest
|
||||
{
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testImplies()
|
||||
{
|
||||
RepositoryPermission p = new RepositoryPermission("asd",
|
||||
PermissionType.READ);
|
||||
|
||||
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
|
||||
assertFalse(p.implies(new RepositoryPermission("asd",
|
||||
PermissionType.OWNER)));
|
||||
assertFalse(p.implies(new RepositoryPermission("asd",
|
||||
PermissionType.WRITE)));
|
||||
p = new RepositoryPermission("asd", PermissionType.OWNER);
|
||||
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
|
||||
assertFalse(p.implies(new RepositoryPermission("bdb",
|
||||
PermissionType.READ)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testImpliesWithWildcard()
|
||||
{
|
||||
RepositoryPermission p = new RepositoryPermission("*",
|
||||
PermissionType.OWNER);
|
||||
|
||||
assertTrue(p.implies(new RepositoryPermission("asd", PermissionType.READ)));
|
||||
assertTrue(p.implies(new RepositoryPermission("bdb",
|
||||
PermissionType.OWNER)));
|
||||
assertTrue(p.implies(new RepositoryPermission("cgd",
|
||||
PermissionType.WRITE)));
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,5 @@ unpriv = secret
|
||||
[roles]
|
||||
admin = *
|
||||
user = something:*
|
||||
repo_read = "repository:read:1"
|
||||
repo_write = "repository:push:1"
|
||||
repo_read = "repository:read,pull:1"
|
||||
repo_write = "repository:read,write,pull,push:1"
|
||||
|
||||
@@ -16,6 +16,7 @@ import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -24,8 +25,10 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Clock;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -70,9 +73,7 @@ class XmlRepositoryDAOTest {
|
||||
Clock clock = mock(Clock.class);
|
||||
when(clock.millis()).then(ic -> atomicClock.incrementAndGet());
|
||||
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
|
||||
|
||||
return dao;
|
||||
return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -329,6 +330,21 @@ class XmlRepositoryDAOTest {
|
||||
assertThat(content).contains("Awesome Spaceship");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistPermissions() throws IOException {
|
||||
Repository heartOfGold = createHeartOfGold();
|
||||
heartOfGold.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", Collections.singletonList("delete"), true)));
|
||||
dao.add(heartOfGold);
|
||||
|
||||
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
|
||||
Path metadataPath = dao.resolveMetadataPath(repositoryDirectory);
|
||||
|
||||
String content = content(metadataPath);
|
||||
System.out.println(content);
|
||||
assertThat(content).containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>");
|
||||
assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadPathDatabaseAndMetadataOfRepositories() {
|
||||
Repository heartOfGold = createHeartOfGold();
|
||||
|
||||
@@ -42,7 +42,6 @@ import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import sonia.scm.it.utils.RepositoryUtil;
|
||||
import sonia.scm.it.utils.TestData;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.client.api.RepositoryClient;
|
||||
import sonia.scm.repository.client.api.RepositoryClientException;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -59,7 +58,10 @@ import static org.junit.Assert.assertNull;
|
||||
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
|
||||
import static sonia.scm.it.utils.RestUtil.given;
|
||||
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
|
||||
import static sonia.scm.it.utils.TestData.OWNER;
|
||||
import static sonia.scm.it.utils.TestData.READ;
|
||||
import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
|
||||
import static sonia.scm.it.utils.TestData.WRITE;
|
||||
import static sonia.scm.it.utils.TestData.callRepository;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
@@ -91,11 +93,11 @@ public class PermissionsITCase {
|
||||
public void prepareEnvironment() {
|
||||
TestData.createDefault();
|
||||
TestData.createNotAdminUser(USER_READ, USER_PASS);
|
||||
TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType);
|
||||
TestData.createUserPermission(USER_READ, READ, repositoryType);
|
||||
TestData.createNotAdminUser(USER_WRITE, USER_PASS);
|
||||
TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType);
|
||||
TestData.createUserPermission(USER_WRITE, WRITE, repositoryType);
|
||||
TestData.createNotAdminUser(USER_OWNER, USER_PASS);
|
||||
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
|
||||
TestData.createUserPermission(USER_OWNER, OWNER, repositoryType);
|
||||
TestData.createNotAdminUser(USER_OTHER, USER_PASS);
|
||||
createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
|
||||
}
|
||||
@@ -109,7 +111,7 @@ public class PermissionsITCase {
|
||||
|
||||
@Test
|
||||
public void readUserShouldNotSeeBruteForcePermissions() {
|
||||
given(VndMediaType.PERMISSION, USER_READ, USER_PASS)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION, USER_READ, USER_PASS)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
|
||||
.then()
|
||||
@@ -125,7 +127,7 @@ public class PermissionsITCase {
|
||||
|
||||
@Test
|
||||
public void writeUserShouldNotSeeBruteForcePermissions() {
|
||||
given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION, USER_WRITE, USER_PASS)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
|
||||
.then()
|
||||
@@ -145,7 +147,7 @@ public class PermissionsITCase {
|
||||
|
||||
@Test
|
||||
public void otherUserShouldNotSeeBruteForcePermissions() {
|
||||
given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION, USER_OTHER, USER_PASS)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
|
||||
.then()
|
||||
|
||||
@@ -4,15 +4,16 @@ import io.restassured.response.ValidatableResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.json.Json;
|
||||
import javax.json.JsonObjectBuilder;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
|
||||
@@ -25,6 +26,11 @@ public class TestData {
|
||||
|
||||
public static final String USER_SCM_ADMIN = "scmadmin";
|
||||
public static final String USER_ANONYMOUS = "anonymous";
|
||||
|
||||
public static final Collection<String> READ = asList("read", "pull");
|
||||
public static final Collection<String> WRITE = asList("read", "write", "pull", "push");
|
||||
public static final Collection<String> OWNER = asList("*");
|
||||
|
||||
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
|
||||
|
||||
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
|
||||
@@ -82,13 +88,13 @@ public class TestData {
|
||||
;
|
||||
}
|
||||
|
||||
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
|
||||
public static void createUserPermission(String name, Collection<String> permissionType, String repositoryType) {
|
||||
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
|
||||
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
|
||||
given(VndMediaType.PERMISSION)
|
||||
given(VndMediaType.REPOSITORY_PERMISSION)
|
||||
.when()
|
||||
.content("{\n" +
|
||||
"\t\"type\": \"" + permissionType.name() + "\",\n" +
|
||||
"\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" +
|
||||
"\t\"name\": \"" + name + "\",\n" +
|
||||
"\t\"groupPermission\": false\n" +
|
||||
"\t\n" +
|
||||
@@ -106,7 +112,7 @@ public class TestData {
|
||||
}
|
||||
|
||||
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) {
|
||||
return given(VndMediaType.PERMISSION, username, password)
|
||||
return given(VndMediaType.REPOSITORY_PERMISSION, username, password)
|
||||
.when()
|
||||
.get(TestData.getDefaultPermissionUrl(username, password, repositoryType))
|
||||
.then()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import SubmitButton, { type ButtonProps } from "./SubmitButton";
|
||||
import { type ButtonProps } from "./Button";
|
||||
import SubmitButton from "./SubmitButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
|
||||
@@ -54,7 +54,7 @@ class Select extends React.Component<Props> {
|
||||
>
|
||||
{options.map(opt => {
|
||||
return (
|
||||
<option value={opt.value} key={opt.value}>
|
||||
<option value={opt.value} key={"KEY_" + opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
export type RepositoryRole = {
|
||||
name: string,
|
||||
verbs: string[]
|
||||
};
|
||||
|
||||
export type AvailableRepositoryPermissions = {
|
||||
availableVerbs: string[],
|
||||
availableRoles: RepositoryRole[]
|
||||
};
|
||||
@@ -7,7 +7,7 @@ export type Permission = PermissionCreateEntry & {
|
||||
|
||||
export type PermissionCreateEntry = {
|
||||
name: string,
|
||||
type: string,
|
||||
verbs: string[],
|
||||
groupPermission: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -24,3 +24,5 @@ export type { Permission, PermissionCreateEntry, PermissionCollection } from "./
|
||||
export type { SubRepository, File } from "./Sources";
|
||||
|
||||
export type { SelectValue, AutocompleteObject } from "./Autocomplete";
|
||||
|
||||
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";
|
||||
|
||||
@@ -91,14 +91,18 @@
|
||||
"group": "Group",
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown permissions error",
|
||||
"name": "User or Group",
|
||||
"type": "Type",
|
||||
"name": "User or group",
|
||||
"role": "Role",
|
||||
"permissions": "Permissions",
|
||||
"group-permission": "Group Permission",
|
||||
"user-permission": "User Permission",
|
||||
"edit-permission": {
|
||||
"delete-button": "Delete",
|
||||
"save-button": "Save Changes"
|
||||
},
|
||||
"advanced-button": {
|
||||
"label": "Advanced"
|
||||
},
|
||||
"delete-permission-button": {
|
||||
"label": "Delete",
|
||||
"confirm-alert": {
|
||||
@@ -114,9 +118,10 @@
|
||||
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
|
||||
},
|
||||
"help": {
|
||||
"groupPermissionHelpText": "States if a permission is a group permission.",
|
||||
"groupPermissionHelpText": "States if a permission is a group permission. If this is not checked, it is a user permission.",
|
||||
"nameHelpText": "Manage permissions for a specific user or group",
|
||||
"typeHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions"
|
||||
"roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.",
|
||||
"permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles"
|
||||
},
|
||||
"autocomplete": {
|
||||
"no-group-options": "No group suggestion available",
|
||||
@@ -124,6 +129,13 @@
|
||||
"no-user-options": "No user suggestion available",
|
||||
"user-placeholder": "Enter user",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"advanced": {
|
||||
"dialog": {
|
||||
"title": "Advanced permissions",
|
||||
"submit": "Submit",
|
||||
"abort": "Abort"
|
||||
}
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Checkbox } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
disabled: boolean,
|
||||
name: string,
|
||||
checked: boolean,
|
||||
onChange?: (value: boolean, name?: string) => void
|
||||
};
|
||||
|
||||
class PermissionCheckbox extends React.Component<Props> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Checkbox
|
||||
key={this.props.name}
|
||||
name={this.props.name}
|
||||
helpText={t("verbs.repository." + this.props.name + ".description")}
|
||||
label={t("verbs.repository." + this.props.name + ".displayName")}
|
||||
checked={this.props.checked}
|
||||
onChange={this.props.onChange}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("plugins")(PermissionCheckbox);
|
||||
55
scm-ui/src/repos/permissions/components/RoleSelector.js
Normal file
55
scm-ui/src/repos/permissions/components/RoleSelector.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
availableRoles: string[],
|
||||
handleRoleChange: string => void,
|
||||
role: string,
|
||||
label?: string,
|
||||
helpText?: string,
|
||||
loading?: boolean
|
||||
};
|
||||
|
||||
class RoleSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
availableRoles,
|
||||
role,
|
||||
handleRoleChange,
|
||||
loading,
|
||||
label,
|
||||
helpText
|
||||
} = this.props;
|
||||
|
||||
if (!availableRoles) return null;
|
||||
|
||||
const options = role
|
||||
? this.createSelectOptions(availableRoles)
|
||||
: ["", ...this.createSelectOptions(availableRoles)];
|
||||
|
||||
return (
|
||||
<Select
|
||||
onChange={handleRoleChange}
|
||||
value={role ? role : ""}
|
||||
options={options}
|
||||
loading={loading}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createSelectOptions(roles: string[]) {
|
||||
return roles.map(role => {
|
||||
return {
|
||||
label: role,
|
||||
value: role
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(RoleSelector);
|
||||
@@ -1,42 +0,0 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
handleTypeChange: string => void,
|
||||
type: string,
|
||||
label?: string,
|
||||
helpText?: string,
|
||||
loading?: boolean
|
||||
};
|
||||
|
||||
class TypeSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const { type, handleTypeChange, loading, label, helpText } = this.props;
|
||||
const types = ["READ", "OWNER", "WRITE"];
|
||||
|
||||
return (
|
||||
<Select
|
||||
onChange={handleTypeChange}
|
||||
value={type ? type : "READ"}
|
||||
options={this.createSelectOptions(types)}
|
||||
loading={loading}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
createSelectOptions(types: string[]) {
|
||||
return types.map(type => {
|
||||
return {
|
||||
label: type,
|
||||
value: type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("repos")(TypeSelector);
|
||||
@@ -18,7 +18,8 @@ describe("permission validation", () => {
|
||||
name: "PermissionName",
|
||||
groupPermission: true,
|
||||
type: "READ",
|
||||
_links: {}
|
||||
_links: {},
|
||||
verbs: []
|
||||
}
|
||||
];
|
||||
const name = "PermissionName";
|
||||
@@ -35,7 +36,8 @@ describe("permission validation", () => {
|
||||
name: "PermissionName",
|
||||
groupPermission: false,
|
||||
type: "READ",
|
||||
_links: {}
|
||||
_links: {},
|
||||
verbs: []
|
||||
}
|
||||
];
|
||||
const name = "PermissionName";
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Button, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import PermissionCheckbox from "../components/PermissionCheckbox";
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean,
|
||||
availableVerbs: string[],
|
||||
selectedVerbs: string[],
|
||||
onSubmit: (string[]) => void,
|
||||
onClose: () => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
verbs: any
|
||||
};
|
||||
|
||||
class AdvancedPermissionsDialog extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const verbs = {};
|
||||
props.availableVerbs.forEach(
|
||||
verb => (verbs[verb] = props.selectedVerbs.includes(verb))
|
||||
);
|
||||
|
||||
this.state = { verbs };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, onClose, readOnly } = this.props;
|
||||
const { verbs } = this.state;
|
||||
|
||||
const verbSelectBoxes = Object.entries(verbs).map(e => (
|
||||
<PermissionCheckbox
|
||||
disabled={readOnly}
|
||||
name={e[0]}
|
||||
checked={e[1]}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
));
|
||||
|
||||
const submitButton = !readOnly ? (
|
||||
<SubmitButton label={t("permission.advanced.dialog.submit")} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={"modal is-active"}>
|
||||
<div className="modal-background" />
|
||||
<div className="modal-card">
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">
|
||||
{t("permission.advanced.dialog.title")}
|
||||
</p>
|
||||
<button
|
||||
className="delete"
|
||||
aria-label="close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<div className="content">{verbSelectBoxes}</div>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
{submitButton}
|
||||
<Button
|
||||
label={t("permission.advanced.dialog.abort")}
|
||||
action={onClose}
|
||||
/>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleChange = (value: boolean, name: string) => {
|
||||
const { verbs } = this.state;
|
||||
const newVerbs = { ...verbs, [name]: value };
|
||||
this.setState({ verbs: newVerbs });
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.onSubmit(
|
||||
Object.entries(this.state.verbs)
|
||||
.filter(e => e[1])
|
||||
.map(e => e[0])
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("repos")(AdvancedPermissionsDialog);
|
||||
@@ -1,17 +1,26 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Autocomplete, Radio, SubmitButton } from "@scm-manager/ui-components";
|
||||
import TypeSelector from "./TypeSelector";
|
||||
import {
|
||||
Autocomplete,
|
||||
SubmitButton,
|
||||
Button,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
import RoleSelector from "../components/RoleSelector";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry,
|
||||
SelectValue
|
||||
} from "@scm-manager/ui-types";
|
||||
import * as validator from "./permissionValidation";
|
||||
import * as validator from "../components/permissionValidation";
|
||||
import { findMatchingRoleName } from "../modules/permissions";
|
||||
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
createPermission: (permission: PermissionCreateEntry) => void,
|
||||
loading: boolean,
|
||||
currentPermissions: PermissionCollection,
|
||||
@@ -21,10 +30,11 @@ type Props = {
|
||||
|
||||
type State = {
|
||||
name: string,
|
||||
type: string,
|
||||
verbs: string[],
|
||||
groupPermission: boolean,
|
||||
valid: boolean,
|
||||
value?: SelectValue
|
||||
value?: SelectValue,
|
||||
showAdvancedDialog: boolean
|
||||
};
|
||||
|
||||
class CreatePermissionForm extends React.Component<Props, State> {
|
||||
@@ -33,10 +43,11 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
name: "",
|
||||
type: "READ",
|
||||
verbs: props.availablePermissions.availableRoles[0].verbs,
|
||||
groupPermission: false,
|
||||
valid: true,
|
||||
value: undefined
|
||||
value: undefined,
|
||||
showAdvancedDialog: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,9 +132,23 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading } = this.props;
|
||||
const { t, availablePermissions, loading } = this.props;
|
||||
|
||||
const { type } = this.state;
|
||||
const { verbs, showAdvancedDialog } = this.state;
|
||||
|
||||
const availableRoleNames = availablePermissions.availableRoles.map(
|
||||
r => r.name
|
||||
);
|
||||
const matchingRole = findMatchingRoleName(availablePermissions, verbs);
|
||||
|
||||
const advancedDialog = showAdvancedDialog ? (
|
||||
<AdvancedPermissionsDialog
|
||||
availableVerbs={availablePermissions.availableVerbs}
|
||||
selectedVerbs={verbs}
|
||||
onClose={this.closeAdvancedPermissionsDialog}
|
||||
onSubmit={this.submitAdvancedPermissionsDialog}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -131,33 +156,58 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
<h2 className="subtitle">
|
||||
{t("permission.add-permission.add-permission-heading")}
|
||||
</h2>
|
||||
{advancedDialog}
|
||||
<form onSubmit={this.submit}>
|
||||
<Radio
|
||||
<div className="control">
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="permission_scope"
|
||||
value="USER_PERMISSION"
|
||||
checked={!this.state.groupPermission}
|
||||
label={t("permission.user-permission")}
|
||||
value="USER_PERMISSION"
|
||||
onChange={this.permissionScopeChanged}
|
||||
/>
|
||||
<Radio
|
||||
{t("permission.user-permission")}
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="permission_scope"
|
||||
value="GROUP_PERMISSION"
|
||||
checked={this.state.groupPermission}
|
||||
label={t("permission.group-permission")}
|
||||
onChange={this.permissionScopeChanged}
|
||||
/>
|
||||
{t("permission.group-permission")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<div className="column is-two-thirds">
|
||||
{this.renderAutocompletionField()}
|
||||
</div>
|
||||
<div className="column is-one-quarter">
|
||||
<TypeSelector
|
||||
label={t("permission.type")}
|
||||
helpText={t("permission.help.typeHelpText")}
|
||||
handleTypeChange={this.handleTypeChange}
|
||||
type={type ? type : "READ"}
|
||||
<div className="column is-one-third">
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<RoleSelector
|
||||
availableRoles={availableRoleNames}
|
||||
label={t("permission.role")}
|
||||
helpText={t("permission.help.roleHelpText")}
|
||||
handleRoleChange={this.handleRoleChange}
|
||||
role={matchingRole}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.permissions")}
|
||||
helpText={t("permission.help.permissionsHelpText")}
|
||||
/>
|
||||
<Button
|
||||
label={t("permission.advanced-button.label")}
|
||||
action={this.handleDetailedPermissionsPressed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
@@ -173,10 +223,25 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
handleDetailedPermissionsPressed = () => {
|
||||
this.setState({ showAdvancedDialog: true });
|
||||
};
|
||||
|
||||
closeAdvancedPermissionsDialog = () => {
|
||||
this.setState({ showAdvancedDialog: false });
|
||||
};
|
||||
|
||||
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
|
||||
this.setState({
|
||||
showAdvancedDialog: false,
|
||||
verbs: newVerbs
|
||||
});
|
||||
};
|
||||
|
||||
submit = e => {
|
||||
this.props.createPermission({
|
||||
name: this.state.name,
|
||||
type: this.state.type,
|
||||
verbs: this.state.verbs,
|
||||
groupPermission: this.state.groupPermission
|
||||
});
|
||||
this.removeState();
|
||||
@@ -186,17 +251,24 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
removeState = () => {
|
||||
this.setState({
|
||||
name: "",
|
||||
type: "READ",
|
||||
verbs: this.props.availablePermissions.availableRoles[0].verbs,
|
||||
groupPermission: false,
|
||||
valid: true
|
||||
});
|
||||
};
|
||||
|
||||
handleTypeChange = (type: string) => {
|
||||
handleRoleChange = (role: string) => {
|
||||
const selectedRole = this.findAvailableRole(role);
|
||||
this.setState({
|
||||
type: type
|
||||
verbs: selectedRole.verbs
|
||||
});
|
||||
};
|
||||
|
||||
findAvailableRole = (roleName: string) => {
|
||||
return this.props.availablePermissions.availableRoles.find(
|
||||
role => role.name === roleName
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("repos")(CreatePermissionForm);
|
||||
@@ -3,8 +3,12 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
fetchAvailablePermissionsIfNeeded,
|
||||
fetchPermissions,
|
||||
getFetchAvailablePermissionsFailure,
|
||||
getAvailablePermissions,
|
||||
getFetchPermissionsFailure,
|
||||
isFetchAvailablePermissionsPending,
|
||||
isFetchPermissionsPending,
|
||||
getPermissionsOfRepo,
|
||||
hasCreatePermission,
|
||||
@@ -17,14 +21,19 @@ import {
|
||||
modifyPermissionReset,
|
||||
deletePermissionReset
|
||||
} from "../modules/permissions";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import {
|
||||
Loading,
|
||||
ErrorPage,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
Permission,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry
|
||||
} from "@scm-manager/ui-types";
|
||||
import SinglePermission from "./SinglePermission";
|
||||
import CreatePermissionForm from "../components/CreatePermissionForm";
|
||||
import CreatePermissionForm from "./CreatePermissionForm";
|
||||
import type { History } from "history";
|
||||
import { getPermissionsLink } from "../../modules/repos";
|
||||
import {
|
||||
@@ -33,6 +42,7 @@ import {
|
||||
} from "../../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
loading: boolean,
|
||||
@@ -45,6 +55,7 @@ type Props = {
|
||||
userAutoCompleteLink: string,
|
||||
|
||||
//dispatch functions
|
||||
fetchAvailablePermissionsIfNeeded: () => void,
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
|
||||
createPermission: (
|
||||
link: string,
|
||||
@@ -65,6 +76,7 @@ type Props = {
|
||||
class Permissions extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const {
|
||||
fetchAvailablePermissionsIfNeeded,
|
||||
fetchPermissions,
|
||||
namespace,
|
||||
repoName,
|
||||
@@ -77,6 +89,7 @@ class Permissions extends React.Component<Props> {
|
||||
createPermissionReset(namespace, repoName);
|
||||
modifyPermissionReset(namespace, repoName);
|
||||
deletePermissionReset(namespace, repoName);
|
||||
fetchAvailablePermissionsIfNeeded();
|
||||
fetchPermissions(permissionsLink, namespace, repoName);
|
||||
}
|
||||
|
||||
@@ -91,6 +104,7 @@ class Permissions extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
availablePermissions,
|
||||
loading,
|
||||
error,
|
||||
permissions,
|
||||
@@ -112,12 +126,13 @@ class Permissions extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !permissions) {
|
||||
if (loading || !permissions || !availablePermissions) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const createPermissionForm = hasPermissionToCreate ? (
|
||||
<CreatePermissionForm
|
||||
availablePermissions={availablePermissions}
|
||||
createPermission={permission => this.createPermission(permission)}
|
||||
loading={loadingCreatePermission}
|
||||
currentPermissions={permissions}
|
||||
@@ -131,11 +146,30 @@ class Permissions extends React.Component<Props> {
|
||||
<table className="has-background-light table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("permission.name")}</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("permission.group-permission")}
|
||||
<th>
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.name")}
|
||||
helpText={t("permission.help.nameHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.group-permission")}
|
||||
helpText={t("permission.help.groupPermissionHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.role")}
|
||||
helpText={t("permission.help.roleHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<LabelWithHelpIcon
|
||||
label={t("permission.permissions")}
|
||||
helpText={t("permission.help.permissionsHelpText")}
|
||||
/>
|
||||
</th>
|
||||
<th>{t("permission.type")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -143,6 +177,7 @@ class Permissions extends React.Component<Props> {
|
||||
{permissions.map(permission => {
|
||||
return (
|
||||
<SinglePermission
|
||||
availablePermissions={availablePermissions}
|
||||
key={permission.name + permission.groupPermission.toString()}
|
||||
namespace={namespace}
|
||||
repoName={repoName}
|
||||
@@ -165,8 +200,11 @@ const mapStateToProps = (state, ownProps) => {
|
||||
getFetchPermissionsFailure(state, namespace, repoName) ||
|
||||
getCreatePermissionFailure(state, namespace, repoName) ||
|
||||
getDeletePermissionsFailure(state, namespace, repoName) ||
|
||||
getModifyPermissionsFailure(state, namespace, repoName);
|
||||
const loading = isFetchPermissionsPending(state, namespace, repoName);
|
||||
getModifyPermissionsFailure(state, namespace, repoName) ||
|
||||
getFetchAvailablePermissionsFailure(state);
|
||||
const loading =
|
||||
isFetchPermissionsPending(state, namespace, repoName) ||
|
||||
isFetchAvailablePermissionsPending(state);
|
||||
const permissions = getPermissionsOfRepo(state, namespace, repoName);
|
||||
const loadingCreatePermission = isCreatePermissionPending(
|
||||
state,
|
||||
@@ -177,7 +215,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const permissionsLink = getPermissionsLink(state, namespace, repoName);
|
||||
const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
|
||||
const userAutoCompleteLink = getUserAutoCompleteLink(state);
|
||||
const availablePermissions = getAvailablePermissions(state);
|
||||
return {
|
||||
availablePermissions,
|
||||
namespace,
|
||||
repoName,
|
||||
error,
|
||||
@@ -196,6 +236,9 @@ const mapDispatchToProps = dispatch => {
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => {
|
||||
dispatch(fetchPermissions(link, namespace, repoName));
|
||||
},
|
||||
fetchAvailablePermissionsIfNeeded: () => {
|
||||
dispatch(fetchAvailablePermissionsIfNeeded());
|
||||
},
|
||||
createPermission: (
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { Permission } from "@scm-manager/ui-types";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
Permission
|
||||
} from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
modifyPermission,
|
||||
isModifyPermissionPending,
|
||||
deletePermission,
|
||||
isDeletePermissionPending
|
||||
isDeletePermissionPending,
|
||||
findMatchingRoleName
|
||||
} from "../modules/permissions";
|
||||
import { connect } from "react-redux";
|
||||
import type { History } from "history";
|
||||
import { Checkbox } from "@scm-manager/ui-components";
|
||||
import { Button, Checkbox } from "@scm-manager/ui-components";
|
||||
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
|
||||
import TypeSelector from "../components/TypeSelector";
|
||||
import RoleSelector from "../components/RoleSelector";
|
||||
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
|
||||
|
||||
type Props = {
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
submitForm: Permission => void,
|
||||
modifyPermission: (Permission, string, string) => void,
|
||||
modifyPermission: (permission: Permission, namespace: string, name: string) => void,
|
||||
permission: Permission,
|
||||
t: string => string,
|
||||
namespace: string,
|
||||
@@ -24,38 +30,53 @@ type Props = {
|
||||
match: any,
|
||||
history: History,
|
||||
loading: boolean,
|
||||
deletePermission: (Permission, string, string) => void,
|
||||
deletePermission: (permission: Permission, namespace: string, name: string) => void,
|
||||
deleteLoading: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
permission: Permission
|
||||
role: string,
|
||||
permission: Permission,
|
||||
showAdvancedDialog: boolean
|
||||
};
|
||||
|
||||
class SinglePermission extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const defaultPermission = props.availablePermissions.availableRoles
|
||||
? props.availablePermissions.availableRoles[0]
|
||||
: {};
|
||||
|
||||
this.state = {
|
||||
permission: {
|
||||
name: "",
|
||||
type: "READ",
|
||||
verbs: defaultPermission.verbs,
|
||||
groupPermission: false,
|
||||
_links: {}
|
||||
}
|
||||
},
|
||||
role: defaultPermission.name,
|
||||
showAdvancedDialog: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { permission } = this.props;
|
||||
const { availablePermissions, permission } = this.props;
|
||||
|
||||
const matchingRole = findMatchingRoleName(
|
||||
availablePermissions,
|
||||
permission.verbs
|
||||
);
|
||||
|
||||
if (permission) {
|
||||
this.setState({
|
||||
permission: {
|
||||
name: permission.name,
|
||||
type: permission.type,
|
||||
verbs: permission.verbs,
|
||||
groupPermission: permission.groupPermission,
|
||||
_links: permission._links
|
||||
}
|
||||
},
|
||||
role: matchingRole
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -69,28 +90,57 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { permission } = this.state;
|
||||
const { loading, namespace, repoName } = this.props;
|
||||
const typeSelector =
|
||||
this.props.permission._links && this.props.permission._links.update ? (
|
||||
const { role, permission, showAdvancedDialog } = this.state;
|
||||
const {
|
||||
t,
|
||||
availablePermissions,
|
||||
loading,
|
||||
namespace,
|
||||
repoName
|
||||
} = this.props;
|
||||
const availableRoleNames = availablePermissions.availableRoles.map(
|
||||
r => r.name
|
||||
);
|
||||
const readOnly = !this.mayChangePermissions();
|
||||
const roleSelector = readOnly ? (
|
||||
<td>{role}</td>
|
||||
) : (
|
||||
<td>
|
||||
<TypeSelector
|
||||
handleTypeChange={this.handleTypeChange}
|
||||
type={permission.type ? permission.type : "READ"}
|
||||
<RoleSelector
|
||||
handleRoleChange={this.handleRoleChange}
|
||||
availableRoles={availableRoleNames}
|
||||
role={role}
|
||||
loading={loading}
|
||||
/>
|
||||
</td>
|
||||
) : (
|
||||
<td>{permission.type}</td>
|
||||
);
|
||||
|
||||
const advancedDialg = showAdvancedDialog ? (
|
||||
<AdvancedPermissionsDialog
|
||||
readOnly={readOnly}
|
||||
availableVerbs={availablePermissions.availableVerbs}
|
||||
selectedVerbs={permission.verbs}
|
||||
onClose={this.closeAdvancedPermissionsDialog}
|
||||
onSubmit={this.submitAdvancedPermissionsDialog}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{permission.name}</td>
|
||||
<td>
|
||||
<Checkbox checked={permission ? permission.groupPermission : false} />
|
||||
<Checkbox
|
||||
checked={permission ? permission.groupPermission : false}
|
||||
disabled={true}
|
||||
/>
|
||||
</td>
|
||||
{roleSelector}
|
||||
<td>
|
||||
<Button
|
||||
label={t("permission.advanced-button.label")}
|
||||
action={this.handleDetailedPermissionsPressed}
|
||||
/>
|
||||
</td>
|
||||
{typeSelector}
|
||||
<td>
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
@@ -99,39 +149,69 @@ class SinglePermission extends React.Component<Props, State> {
|
||||
deletePermission={this.deletePermission}
|
||||
loading={this.props.deleteLoading}
|
||||
/>
|
||||
{advancedDialg}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
handleTypeChange = (type: string) => {
|
||||
this.setState({
|
||||
permission: {
|
||||
...this.state.permission,
|
||||
type: type
|
||||
}
|
||||
});
|
||||
this.modifyPermission(type);
|
||||
mayChangePermissions = () => {
|
||||
return this.props.permission._links && this.props.permission._links.update;
|
||||
};
|
||||
|
||||
modifyPermission = (type: string) => {
|
||||
handleDetailedPermissionsPressed = () => {
|
||||
this.setState({ showAdvancedDialog: true });
|
||||
};
|
||||
|
||||
closeAdvancedPermissionsDialog = () => {
|
||||
this.setState({ showAdvancedDialog: false });
|
||||
};
|
||||
|
||||
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
|
||||
const { permission } = this.state;
|
||||
const newRole = findMatchingRoleName(
|
||||
this.props.availablePermissions,
|
||||
newVerbs
|
||||
);
|
||||
this.setState(
|
||||
{
|
||||
showAdvancedDialog: false,
|
||||
permission: { ...permission, verbs: newVerbs },
|
||||
role: newRole
|
||||
},
|
||||
() => this.modifyPermission(newVerbs)
|
||||
);
|
||||
};
|
||||
|
||||
handleRoleChange = (role: string) => {
|
||||
const selectedRole = this.findAvailableRole(role);
|
||||
this.setState(
|
||||
{
|
||||
permission: {
|
||||
...this.state.permission,
|
||||
verbs: selectedRole.verbs
|
||||
},
|
||||
role: role
|
||||
},
|
||||
() => this.modifyPermission(selectedRole.verbs)
|
||||
);
|
||||
};
|
||||
|
||||
findAvailableRole = (roleName: string) => {
|
||||
return this.props.availablePermissions.availableRoles.find(
|
||||
role => role.name === roleName
|
||||
);
|
||||
};
|
||||
|
||||
modifyPermission = (verbs: string[]) => {
|
||||
let permission = this.state.permission;
|
||||
permission.type = type;
|
||||
permission.verbs = verbs;
|
||||
this.props.modifyPermission(
|
||||
permission,
|
||||
this.props.namespace,
|
||||
this.props.repoName
|
||||
);
|
||||
};
|
||||
|
||||
createSelectOptions(types: string[]) {
|
||||
return types.map(type => {
|
||||
return {
|
||||
label: type,
|
||||
value: type
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Action } from "@scm-manager/ui-components";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import * as types from "../../../modules/types";
|
||||
import type {
|
||||
AvailableRepositoryPermissions,
|
||||
Permission,
|
||||
PermissionCollection,
|
||||
PermissionCreateEntry
|
||||
@@ -11,7 +12,18 @@ import type {
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
import { Dispatch } from "redux";
|
||||
import { getLinks } from "../../../modules/indexResource";
|
||||
|
||||
export const FETCH_AVAILABLE = "scm/permissions/FETCH_AVAILABLE";
|
||||
export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${
|
||||
types.PENDING_SUFFIX
|
||||
}`;
|
||||
export const FETCH_AVAILABLE_SUCCESS = `${FETCH_AVAILABLE}_${
|
||||
types.SUCCESS_SUFFIX
|
||||
}`;
|
||||
export const FETCH_AVAILABLE_FAILURE = `${FETCH_AVAILABLE}_${
|
||||
types.FAILURE_SUFFIX
|
||||
}`;
|
||||
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
|
||||
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
|
||||
types.PENDING_SUFFIX
|
||||
@@ -62,7 +74,71 @@ export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
|
||||
types.RESET_SUFFIX
|
||||
}`;
|
||||
|
||||
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
|
||||
const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json";
|
||||
|
||||
// fetch available permissions
|
||||
|
||||
export function fetchAvailablePermissionsIfNeeded() {
|
||||
return function(dispatch: any, getState: () => Object) {
|
||||
if (shouldFetchAvailablePermissions(getState())) {
|
||||
return fetchAvailablePermissions(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailablePermissions(
|
||||
dispatch: any,
|
||||
getState: () => Object
|
||||
) {
|
||||
dispatch(fetchAvailablePending());
|
||||
return apiClient
|
||||
.get(getLinks(getState()).availableRepositoryPermissions.href)
|
||||
.then(response => response.json())
|
||||
.then(available => {
|
||||
dispatch(fetchAvailableSuccess(available));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchAvailableFailure(err));
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldFetchAvailablePermissions(state: Object) {
|
||||
if (
|
||||
isFetchAvailablePermissionsPending(state) ||
|
||||
getFetchAvailablePermissionsFailure(state)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !state.available;
|
||||
}
|
||||
|
||||
export function fetchAvailablePending(): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_PENDING,
|
||||
payload: {},
|
||||
itemId: "available"
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailableSuccess(
|
||||
available: AvailableRepositoryPermissions
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_SUCCESS,
|
||||
payload: available,
|
||||
itemId: "available"
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAvailableFailure(error: Error): Action {
|
||||
return {
|
||||
type: FETCH_AVAILABLE_FAILURE,
|
||||
payload: {
|
||||
error
|
||||
},
|
||||
itemId: "available"
|
||||
};
|
||||
}
|
||||
|
||||
// fetch permissions
|
||||
|
||||
@@ -368,6 +444,7 @@ export function deletePermissionReset(namespace: string, repoName: string) {
|
||||
itemId: namespace + "/" + repoName
|
||||
};
|
||||
}
|
||||
|
||||
function deletePermissionFromState(
|
||||
oldPermissions: PermissionCollection,
|
||||
permission: Permission
|
||||
@@ -399,12 +476,17 @@ export default function reducer(
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case FETCH_AVAILABLE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
available: action.payload
|
||||
};
|
||||
case FETCH_PERMISSIONS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
[action.itemId]: {
|
||||
entries: action.payload._embedded.permissions,
|
||||
createPermission: action.payload._links.create ? true : false
|
||||
createPermission: !!action.payload._links.create
|
||||
}
|
||||
};
|
||||
case MODIFY_PERMISSION_SUCCESS:
|
||||
@@ -452,6 +534,12 @@ export default function reducer(
|
||||
|
||||
// selectors
|
||||
|
||||
export function getAvailablePermissions(state: Object) {
|
||||
if (state.permissions) {
|
||||
return state.permissions.available;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPermissionsOfRepo(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -463,6 +551,10 @@ export function getPermissionsOfRepo(
|
||||
}
|
||||
}
|
||||
|
||||
export function isFetchAvailablePermissionsPending(state: Object) {
|
||||
return isPending(state, FETCH_AVAILABLE, "available");
|
||||
}
|
||||
|
||||
export function isFetchPermissionsPending(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -471,6 +563,10 @@ export function isFetchPermissionsPending(
|
||||
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
|
||||
}
|
||||
|
||||
export function getFetchAvailablePermissionsFailure(state: Object) {
|
||||
return getFailure(state, FETCH_AVAILABLE, "available");
|
||||
}
|
||||
|
||||
export function getFetchPermissionsFailure(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -522,6 +618,7 @@ export function isCreatePermissionPending(
|
||||
) {
|
||||
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
|
||||
}
|
||||
|
||||
export function getCreatePermissionFailure(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
@@ -603,3 +700,33 @@ export function getModifyPermissionsFailure(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findMatchingRoleName(
|
||||
availablePermissions: AvailableRepositoryPermissions,
|
||||
verbs: string[]
|
||||
) {
|
||||
if (!verbs) {
|
||||
return "";
|
||||
}
|
||||
const matchingRole = availablePermissions.availableRoles.find(role => {
|
||||
return equalVerbs(role.verbs, verbs);
|
||||
});
|
||||
|
||||
if (matchingRole) {
|
||||
return matchingRole.name;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function equalVerbs(verbs1: string[], verbs2: string[]) {
|
||||
if (!verbs1 || !verbs2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (verbs1.length !== verbs2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return verbs1.every(verb => verbs2.includes(verb));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ const hitchhiker_puzzle42Permission_user_eins: Permission = {
|
||||
href:
|
||||
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
|
||||
}
|
||||
}
|
||||
},
|
||||
verbs: []
|
||||
};
|
||||
|
||||
const hitchhiker_puzzle42Permission_user_zwei: Permission = {
|
||||
@@ -79,7 +80,8 @@ const hitchhiker_puzzle42Permission_user_zwei: Permission = {
|
||||
href:
|
||||
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
|
||||
}
|
||||
}
|
||||
},
|
||||
verbs: []
|
||||
};
|
||||
|
||||
const hitchhiker_puzzle42Permissions: PermissionCollection = [
|
||||
@@ -175,8 +177,7 @@ describe("permission fetch", () => {
|
||||
}
|
||||
);
|
||||
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
editedPermission.type = "OWNER";
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -197,8 +198,7 @@ describe("permission fetch", () => {
|
||||
}
|
||||
);
|
||||
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
editedPermission.type = "OWNER";
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -227,8 +227,7 @@ describe("permission fetch", () => {
|
||||
}
|
||||
);
|
||||
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
editedPermission.type = "OWNER";
|
||||
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -451,8 +450,7 @@ describe("permissions reducer", () => {
|
||||
entries: [hitchhiker_puzzle42Permission_user_eins]
|
||||
}
|
||||
};
|
||||
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins };
|
||||
permissionEdited.type = "OWNER";
|
||||
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
|
||||
let expectedState = {
|
||||
"hitchhiker/puzzle42": {
|
||||
entries: [permissionEdited]
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
/**
|
||||
* 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.api.rest;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.Serializable;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@XmlRootElement
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class Permission implements Serializable
|
||||
{
|
||||
|
||||
/** Field description */
|
||||
private static final long serialVersionUID = 4320217034601679261L;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
public Permission() {}
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param id
|
||||
* @param value
|
||||
*/
|
||||
public Permission(String id, String value)
|
||||
{
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param obj
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getClass() != obj.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
final Permission other = (Permission) obj;
|
||||
|
||||
return Objects.equal(id, other.id) && Objects.equal(value, other.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(id, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
//J-
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("id", id)
|
||||
.add("value", value)
|
||||
.toString();
|
||||
//J+
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private String id;
|
||||
|
||||
/** Field description */
|
||||
private String value;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.security.RepositoryRole;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class AvailableRepositoryPermissionsDto extends HalRepresentation {
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<RepositoryRole> availableRoles;
|
||||
|
||||
public AvailableRepositoryPermissionsDto(Collection<String> availableVerbs, Collection<RepositoryRole> availableRoles) {
|
||||
this.availableVerbs = availableVerbs;
|
||||
this.availableRoles = availableRoles;
|
||||
}
|
||||
|
||||
public Collection<String> getAvailableVerbs() {
|
||||
return availableVerbs;
|
||||
}
|
||||
|
||||
public Collection<RepositoryRole> getAvailableRoles() {
|
||||
return availableRoles;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
return super.add(links);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public class IndexDtoGenerator extends LinkAppenderMapper {
|
||||
if (PermissionPermissions.list().isPermitted()) {
|
||||
builder.single(link("permissions", resourceLinks.permissions().self()));
|
||||
}
|
||||
builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self()));
|
||||
} else {
|
||||
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class MapperModule extends AbstractModule {
|
||||
bind(RepositoryTypeCollectionToDtoMapper.class);
|
||||
|
||||
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
|
||||
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass());
|
||||
bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass());
|
||||
bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass());
|
||||
|
||||
bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass());
|
||||
|
||||
@@ -6,10 +6,9 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
@@ -24,6 +23,7 @@ import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
public class RepositoryCollectionResource {
|
||||
@@ -100,7 +100,7 @@ public class RepositoryCollectionResource {
|
||||
|
||||
private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) {
|
||||
Repository repository = dtoToRepositoryMapper.map(repositoryDto, null);
|
||||
repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), PermissionType.OWNER)));
|
||||
repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), singletonList("*"), false)));
|
||||
return repository;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ public abstract class RepositoryDtoToRepositoryMapper extends BaseDtoMapper {
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "publicReadable", ignore = true)
|
||||
@Mapping(target = "healthCheckFailures", ignore = true)
|
||||
@Mapping(target = "permissions", ignore = true)
|
||||
public abstract Repository map(RepositoryDto repositoryDto, @Context String id);
|
||||
|
||||
@AfterMapping
|
||||
|
||||
@@ -7,9 +7,13 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
|
||||
|
||||
@Getter @Setter @ToString @NoArgsConstructor
|
||||
@@ -20,16 +24,8 @@ public class RepositoryPermissionDto extends HalRepresentation {
|
||||
@Pattern(regexp = USER_GROUP_PATTERN)
|
||||
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;
|
||||
|
||||
@NotEmpty
|
||||
private Collection<String> verbs;
|
||||
|
||||
private boolean groupPermission = false;
|
||||
|
||||
@@ -38,7 +34,6 @@ public class RepositoryPermissionDto extends HalRepresentation {
|
||||
this.groupPermission = groupPermission;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
|
||||
protected HalRepresentation add(Links links) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.mapstruct.CollectionMappingStrategy;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
|
||||
@Mapper
|
||||
public abstract class PermissionDtoToPermissionMapper {
|
||||
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE)
|
||||
public abstract class RepositoryPermissionDtoToRepositoryPermissionMapper {
|
||||
|
||||
public abstract RepositoryPermission map(RepositoryPermissionDto permissionDto);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.security.RepositoryPermissionProvider;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
|
||||
/**
|
||||
* RESTful Web Service Resource to get available repository types.
|
||||
*/
|
||||
@Path(RepositoryPermissionResource.PATH)
|
||||
public class RepositoryPermissionResource {
|
||||
|
||||
static final String PATH = "v2/repositoryPermissions/";
|
||||
|
||||
private final RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public RepositoryPermissionResource(RepositoryPermissionProvider repositoryPermissionProvider, ResourceLinks resourceLinks) {
|
||||
this.repositoryPermissionProvider = repositoryPermissionProvider;
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION_COLLECTION)
|
||||
public AvailableRepositoryPermissionsDto get() {
|
||||
AvailableRepositoryPermissionsDto dto = new AvailableRepositoryPermissionsDto(repositoryPermissionProvider.availableVerbs(), repositoryPermissionProvider.availableRoles());
|
||||
dto.add(Links.linkingTo().self(resourceLinks.availableRepositoryPermissions().self()).build());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
@@ -35,18 +35,21 @@ import static sonia.scm.NotFoundException.notFound;
|
||||
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
|
||||
|
||||
@Slf4j
|
||||
public class PermissionRootResource {
|
||||
public class RepositoryPermissionRootResource {
|
||||
|
||||
|
||||
private PermissionDtoToPermissionMapper dtoToModelMapper;
|
||||
private RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper;
|
||||
private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
|
||||
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
|
||||
private ResourceLinks resourceLinks;
|
||||
private final RepositoryManager manager;
|
||||
|
||||
|
||||
@Inject
|
||||
public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper, RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) {
|
||||
public RepositoryPermissionRootResource(
|
||||
RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper,
|
||||
RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper,
|
||||
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper,
|
||||
ResourceLinks resourceLinks,
|
||||
RepositoryManager manager) {
|
||||
this.dtoToModelMapper = dtoToModelMapper;
|
||||
this.modelToDtoMapper = modelToDtoMapper;
|
||||
this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper;
|
||||
@@ -54,7 +57,6 @@ public class PermissionRootResource {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a new permission to the user or group managed by the repository
|
||||
*
|
||||
@@ -71,9 +73,9 @@ public class PermissionRootResource {
|
||||
@ResponseCode(code = 409, condition = "conflict")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(VndMediaType.PERMISSION)
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Path("")
|
||||
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid RepositoryPermissionDto permission) {
|
||||
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryPermissionDto permission) {
|
||||
log.info("try to add new permission: {}", permission);
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionWrite(repository).check();
|
||||
@@ -84,7 +86,6 @@ public class PermissionRootResource {
|
||||
return Response.created(URI.create(resourceLinks.repositoryPermission().self(namespace, name, urlPermissionName))).build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the searched permission with permission name related to a repository
|
||||
*
|
||||
@@ -99,7 +100,7 @@ public class PermissionRootResource {
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.PERMISSION)
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@TypeHint(RepositoryPermissionDto.class)
|
||||
@Path("{permission-name}")
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) {
|
||||
@@ -115,7 +116,6 @@ public class PermissionRootResource {
|
||||
).build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all permissions related to a repository
|
||||
*
|
||||
@@ -130,7 +130,7 @@ public class PermissionRootResource {
|
||||
@ResponseCode(code = 404, condition = "not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces(VndMediaType.PERMISSION)
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@TypeHint(RepositoryPermissionDto.class)
|
||||
@Path("")
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) {
|
||||
@@ -139,7 +139,6 @@ public class PermissionRootResource {
|
||||
return Response.ok(repositoryPermissionCollectionToDtoMapper.map(repository)).build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update a permission to the user or group managed by the repository
|
||||
* ignore the user input for groupPermission and take it from the path parameter (if the group prefix (@) exists it is a group permission)
|
||||
@@ -155,7 +154,7 @@ public class PermissionRootResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@Consumes(VndMediaType.PERMISSION)
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Path("{permission-name}")
|
||||
public Response update(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@@ -172,6 +171,7 @@ public class PermissionRootResource {
|
||||
if (!extractedPermissionName.equals(permission.getName())) {
|
||||
checkPermissionAlreadyExists(permission, repository);
|
||||
}
|
||||
|
||||
RepositoryPermission existingPermission = repository.getPermissions()
|
||||
.stream()
|
||||
.filter(filterPermission(permissionName))
|
||||
@@ -208,17 +208,16 @@ public class PermissionRootResource {
|
||||
.stream()
|
||||
.filter(filterPermission(permissionName))
|
||||
.findFirst()
|
||||
.ifPresent(repository::removePermission)
|
||||
;
|
||||
.ifPresent(repository::removePermission);
|
||||
manager.modify(repository);
|
||||
log.info("the permission with name: {} is updated.", permissionName);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
Predicate<RepositoryPermission> filterPermission(String permissionName) {
|
||||
return permission -> getPermissionName(permissionName).equals(permission.getName())
|
||||
private Predicate<RepositoryPermission> filterPermission(String name) {
|
||||
return permission -> getPermissionName(name).equals(permission.getName())
|
||||
&&
|
||||
permission.isGroupPermission() == isGroupPermission(permissionName);
|
||||
permission.isGroupPermission() == isGroupPermission(name);
|
||||
}
|
||||
|
||||
private String getPermissionName(String permissionName) {
|
||||
@@ -231,7 +230,6 @@ public class PermissionRootResource {
|
||||
return permissionName.startsWith(GROUP_PREFIX);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* check if the actual user is permitted to manage the repository permissions
|
||||
* return the repository if the user is permitted
|
||||
@@ -39,7 +39,7 @@ public class RepositoryResource {
|
||||
private final Provider<ChangesetRootResource> changesetRootResource;
|
||||
private final Provider<SourceRootResource> sourceRootResource;
|
||||
private final Provider<ContentResource> contentResource;
|
||||
private final Provider<PermissionRootResource> permissionRootResource;
|
||||
private final Provider<RepositoryPermissionRootResource> permissionRootResource;
|
||||
private final Provider<DiffRootResource> diffRootResource;
|
||||
private final Provider<ModificationsRootResource> modificationsRootResource;
|
||||
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||
@@ -54,7 +54,7 @@ public class RepositoryResource {
|
||||
Provider<BranchRootResource> branchRootResource,
|
||||
Provider<ChangesetRootResource> changesetRootResource,
|
||||
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
|
||||
Provider<PermissionRootResource> permissionRootResource,
|
||||
Provider<RepositoryPermissionRootResource> permissionRootResource,
|
||||
Provider<DiffRootResource> diffRootResource,
|
||||
Provider<ModificationsRootResource> modificationsRootResource,
|
||||
Provider<FileHistoryRootResource> fileHistoryRootResource,
|
||||
@@ -154,7 +154,6 @@ public class RepositoryResource {
|
||||
|
||||
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {
|
||||
Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId());
|
||||
changedRepository.setPermissions(existing.getPermissions());
|
||||
return changedRepository;
|
||||
}
|
||||
|
||||
@@ -194,7 +193,7 @@ public class RepositoryResource {
|
||||
}
|
||||
|
||||
@Path("permissions/")
|
||||
public PermissionRootResource permissions() {
|
||||
public RepositoryPermissionRootResource permissions() {
|
||||
return permissionRootResource.get();
|
||||
}
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ class ResourceLinks {
|
||||
private final LinkBuilder permissionLinkBuilder;
|
||||
|
||||
RepositoryPermissionLinks(ScmPathInfo pathInfo) {
|
||||
permissionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
|
||||
permissionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, RepositoryPermissionRootResource.class);
|
||||
}
|
||||
|
||||
String all(String namespace, String name) {
|
||||
@@ -639,14 +639,30 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
static class PermissionsLinks {
|
||||
private final LinkBuilder permissionsLlinkBuilder;
|
||||
private final LinkBuilder permissionsLinkBuilder;
|
||||
|
||||
PermissionsLinks(ScmPathInfo scmPathInfo) {
|
||||
this.permissionsLlinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class);
|
||||
this.permissionsLinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
return permissionsLlinkBuilder.method("getAll").parameters().href();
|
||||
return permissionsLinkBuilder.method("getAll").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public AvailableRepositoryPermissionLinks availableRepositoryPermissions() {
|
||||
return new AvailableRepositoryPermissionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class AvailableRepositoryPermissionLinks {
|
||||
private final LinkBuilder linkBuilder;
|
||||
|
||||
AvailableRepositoryPermissionLinks(ScmPathInfo scmPathInfo) {
|
||||
this.linkBuilder = new LinkBuilder(scmPathInfo, RepositoryPermissionResource.class);
|
||||
}
|
||||
|
||||
String self() {
|
||||
return linkBuilder.method("get").parameters().href();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +198,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
private void collectRepositoryPermissions(Builder<String> builder,
|
||||
Repository repository, User user, GroupNames groups)
|
||||
{
|
||||
Collection<RepositoryPermission> repositoryPermissions
|
||||
= repository.getPermissions();
|
||||
Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions();
|
||||
|
||||
if (Util.isNotEmpty(repositoryPermissions))
|
||||
{
|
||||
@@ -207,9 +206,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
for (RepositoryPermission permission : repositoryPermissions)
|
||||
{
|
||||
hasPermission = isUserPermitted(user, groups, permission);
|
||||
if (hasPermission)
|
||||
if (hasPermission && !permission.getVerbs().isEmpty())
|
||||
{
|
||||
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
|
||||
String perm = "repository:" + String.join(",", permission.getVerbs()) + ":" + repository.getId();
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("add repository permission {} for user {} at repository {}",
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.unmodifiableCollection;
|
||||
|
||||
public class RepositoryPermissionProvider {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepositoryPermissionProvider.class);
|
||||
private static final String REPOSITORY_PERMISSION_DESCRIPTOR = "META-INF/scm/repository-permissions.xml";
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<RepositoryRole> availableRoles;
|
||||
|
||||
@Inject
|
||||
public RepositoryPermissionProvider(PluginLoader pluginLoader) {
|
||||
AvailableRepositoryPermissions availablePermissions = readAvailablePermissions(pluginLoader);
|
||||
this.availableVerbs = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableVerbs));
|
||||
this.availableRoles = unmodifiableCollection(new LinkedHashSet<>(availablePermissions.availableRoles.stream().map(r -> new RepositoryRole(r.name, r.verbs.verbs)).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
public Collection<String> availableVerbs() {
|
||||
return availableVerbs;
|
||||
}
|
||||
|
||||
public Collection<RepositoryRole> availableRoles() {
|
||||
return availableRoles;
|
||||
}
|
||||
|
||||
private static AvailableRepositoryPermissions readAvailablePermissions(PluginLoader pluginLoader) {
|
||||
Collection<String> availableVerbs = new ArrayList<>();
|
||||
Collection<RoleDescriptor> availableRoles = new ArrayList<>();
|
||||
|
||||
try {
|
||||
JAXBContext context =
|
||||
JAXBContext.newInstance(RepositoryPermissionsRoot.class);
|
||||
|
||||
// Querying permissions from uberClassLoader returns also the permissions from plugin
|
||||
Enumeration<URL> descriptorEnum =
|
||||
pluginLoader.getUberClassLoader().getResources(REPOSITORY_PERMISSION_DESCRIPTOR);
|
||||
|
||||
while (descriptorEnum.hasMoreElements()) {
|
||||
URL descriptorUrl = descriptorEnum.nextElement();
|
||||
|
||||
logger.debug("read repository permission descriptor from {}", descriptorUrl);
|
||||
|
||||
RepositoryPermissionsRoot repositoryPermissionsRoot = parsePermissionDescriptor(context, descriptorUrl);
|
||||
availableVerbs.addAll(repositoryPermissionsRoot.verbs.verbs);
|
||||
availableRoles.addAll(repositoryPermissionsRoot.roles.roles);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.error("could not read permission descriptors", ex);
|
||||
} catch (JAXBException ex) {
|
||||
logger.error(
|
||||
"could not create jaxb context to read permission descriptors", ex);
|
||||
}
|
||||
|
||||
return new AvailableRepositoryPermissions(availableVerbs, availableRoles);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static RepositoryPermissionsRoot parsePermissionDescriptor(JAXBContext context, URL descriptorUrl) {
|
||||
try {
|
||||
RepositoryPermissionsRoot descriptorWrapper =
|
||||
(RepositoryPermissionsRoot) context.createUnmarshaller().unmarshal(
|
||||
descriptorUrl);
|
||||
logger.trace("repository permissions from {}: {}", descriptorUrl, descriptorWrapper.verbs.verbs);
|
||||
logger.trace("repository roles from {}: {}", descriptorUrl, descriptorWrapper.roles.roles);
|
||||
return descriptorWrapper;
|
||||
} catch (JAXBException ex) {
|
||||
logger.error("could not parse permission descriptor", ex);
|
||||
return new RepositoryPermissionsRoot();
|
||||
}
|
||||
}
|
||||
|
||||
private static class AvailableRepositoryPermissions {
|
||||
private final Collection<String> availableVerbs;
|
||||
private final Collection<RoleDescriptor> availableRoles;
|
||||
|
||||
private AvailableRepositoryPermissions(Collection<String> availableVerbs, Collection<RoleDescriptor> availableRoles) {
|
||||
this.availableVerbs = unmodifiableCollection(availableVerbs);
|
||||
this.availableRoles = unmodifiableCollection(availableRoles);
|
||||
}
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "repository-permissions")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class RepositoryPermissionsRoot {
|
||||
private VerbListDescriptor verbs = new VerbListDescriptor();
|
||||
private RoleListDescriptor roles = new RoleListDescriptor();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "verbs")
|
||||
private static class VerbListDescriptor {
|
||||
@XmlElement(name = "verb")
|
||||
private List<String> verbs = new ArrayList<>();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "roles")
|
||||
private static class RoleListDescriptor {
|
||||
@XmlElement(name = "role")
|
||||
private List<RoleDescriptor> roles = new ArrayList<>();
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "role")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class RoleDescriptor {
|
||||
@XmlElement(name = "name")
|
||||
private String name;
|
||||
@XmlElement(name = "verbs")
|
||||
private VerbListDescriptor verbs = new VerbListDescriptor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
public class RepositoryRole {
|
||||
|
||||
private final String name;
|
||||
private final Collection<String> verbs;
|
||||
|
||||
public RepositoryRole(String name, Collection<String> verbs) {
|
||||
this.name = name;
|
||||
this.verbs = verbs;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Collection<String> getVerbs() {
|
||||
return Collections.unmodifiableCollection(verbs);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Role " + name + " (" + String.join(", ", verbs) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof RepositoryRole)) return false;
|
||||
RepositoryRole that = (RepositoryRole) o;
|
||||
return name.equals(that.name) &&
|
||||
CollectionUtils.isEqualCollection(this.verbs, that.verbs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, verbs.size());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PushStateDispatcher;
|
||||
import sonia.scm.filter.WebElement;
|
||||
@@ -74,6 +75,9 @@ public class HttpProtocolServlet extends HttpServlet {
|
||||
} catch (NotFoundException e) {
|
||||
log.debug(e.getMessage());
|
||||
resp.setStatus(HttpStatus.SC_NOT_FOUND);
|
||||
} catch (AuthorizationException e) {
|
||||
log.debug(e.getMessage());
|
||||
resp.setStatus(HttpStatus.SC_FORBIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<repository-permissions>
|
||||
<verbs>
|
||||
<verb>read</verb>
|
||||
<verb>modify</verb>
|
||||
<verb>delete</verb>
|
||||
<verb>pull</verb>
|
||||
<verb>push</verb>
|
||||
<verb>permissionRead</verb>
|
||||
<verb>permissionWrite</verb>
|
||||
<verb>healthCheck</verb>
|
||||
<verb>*</verb>
|
||||
</verbs>
|
||||
<roles>
|
||||
<role>
|
||||
<name>READ</name>
|
||||
<verbs>
|
||||
<verb>read</verb>
|
||||
<verb>pull</verb>
|
||||
</verbs>
|
||||
</role>
|
||||
<role>
|
||||
<name>WRITE</name>
|
||||
<verbs>
|
||||
<verb>read</verb>
|
||||
<verb>pull</verb>
|
||||
<verb>push</verb>
|
||||
</verbs>
|
||||
</role>
|
||||
<role>
|
||||
<name>HEALTH</name>
|
||||
<verbs>
|
||||
<verb>healthCheck</verb>
|
||||
</verbs>
|
||||
</role>
|
||||
<role>
|
||||
<name>OWNER</name>
|
||||
<verbs>
|
||||
<verb>*</verb>
|
||||
</verbs>
|
||||
</role>
|
||||
</roles>
|
||||
</repository-permissions>
|
||||
@@ -37,5 +37,45 @@
|
||||
}
|
||||
},
|
||||
"unknown": "Unknown permission"
|
||||
},
|
||||
"verbs": {
|
||||
"repository": {
|
||||
"read": {
|
||||
"displayName": "read",
|
||||
"description": "May see the repository inside the SCM-Manager"
|
||||
},
|
||||
"modify": {
|
||||
"displayName": "modify",
|
||||
"description": "May modify the properties of the repository"
|
||||
},
|
||||
"delete": {
|
||||
"displayName": "delete",
|
||||
"description": "May delete the repository"
|
||||
},
|
||||
"pull": {
|
||||
"displayName": "pull/checkout",
|
||||
"description": "May pull/checkout the repository"
|
||||
},
|
||||
"push": {
|
||||
"displayName": "push/commit",
|
||||
"description": "May change the content of the repository (push/commit)"
|
||||
},
|
||||
"permissionRead": {
|
||||
"displayName": "read permissions",
|
||||
"description": "May see the permissions of the repository"
|
||||
},
|
||||
"permissionWrite": {
|
||||
"displayName": "modify permissions",
|
||||
"description": "May modify the permissions of the repository"
|
||||
},
|
||||
"healthCheck": {
|
||||
"displayName": "health check",
|
||||
"description": "May run the health check for the repository"
|
||||
},
|
||||
"*": {
|
||||
"displayName": "overall",
|
||||
"description": "May change everything for the repository (includes all other permissions)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
@@ -29,10 +30,9 @@ import org.junit.jupiter.api.TestFactory;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -47,6 +47,8 @@ import java.util.stream.Stream;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
@@ -66,7 +68,7 @@ import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
|
||||
password = "secret",
|
||||
configuration = "classpath:sonia/scm/repository/shiro.ini"
|
||||
)
|
||||
public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
public class RepositoryPermissionRootResourceTest extends RepositoryTestBase {
|
||||
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;
|
||||
@@ -76,15 +78,15 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
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 String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"verbs\" : [\"read\",\"pull\"] }";
|
||||
private static final ArrayList<RepositoryPermission> TEST_PERMISSIONS = Lists
|
||||
.newArrayList(
|
||||
new RepositoryPermission("user_write", PermissionType.WRITE, false),
|
||||
new RepositoryPermission("user_read", PermissionType.READ, false),
|
||||
new RepositoryPermission("user_owner", PermissionType.OWNER, false),
|
||||
new RepositoryPermission("group_read", PermissionType.READ, true),
|
||||
new RepositoryPermission("group_write", PermissionType.WRITE, true),
|
||||
new RepositoryPermission("group_owner", PermissionType.OWNER, true)
|
||||
new RepositoryPermission("user_write", asList("read","modify"), false),
|
||||
new RepositoryPermission("user_read", singletonList("read"), false),
|
||||
new RepositoryPermission("user_owner", singletonList("*"), false),
|
||||
new RepositoryPermission("group_read", singletonList("read"), true),
|
||||
new RepositoryPermission("group_write", asList("read","modify"), true),
|
||||
new RepositoryPermission("group_owner", singletonList("*"), true)
|
||||
);
|
||||
private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest()
|
||||
.description("GET all permissions")
|
||||
@@ -124,11 +126,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
private RepositoryPermissionToRepositoryPermissionDtoMapperImpl permissionToPermissionDtoMapper;
|
||||
|
||||
@InjectMocks
|
||||
private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper;
|
||||
private RepositoryPermissionDtoToRepositoryPermissionMapperImpl permissionDtoToPermissionMapper;
|
||||
|
||||
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
|
||||
|
||||
private PermissionRootResource permissionRootResource;
|
||||
private RepositoryPermissionRootResource repositoryPermissionRootResource;
|
||||
|
||||
private final Subject subject = mock(Subject.class);
|
||||
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||
@@ -138,8 +140,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
public void prepareEnvironment() {
|
||||
initMocks(this);
|
||||
repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
|
||||
permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
|
||||
super.permissionRootResource = Providers.of(permissionRootResource);
|
||||
repositoryPermissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
|
||||
super.permissionRootResource = Providers.of(repositoryPermissionRootResource);
|
||||
dispatcher = createDispatcher(getRepositoryRootResource());
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
@@ -232,11 +234,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet400OnCreatingNewPermissionWithNotAllowedCharacters() throws URISyntaxException {
|
||||
// the @ character at the begin of the name is not allowed
|
||||
createUserWithRepository("user");
|
||||
String permissionJson = "{ \"name\": \"@permission\", \"type\": \"OWNER\" }";
|
||||
String permissionJson = "{ \"name\": \"@permission\", \"verbs\": [\"*\"] }";
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
|
||||
.content(permissionJson.getBytes())
|
||||
.contentType(VndMediaType.PERMISSION);
|
||||
.contentType(VndMediaType.REPOSITORY_PERMISSION);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
@@ -244,11 +246,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
assertEquals(400, response.getStatus());
|
||||
|
||||
// the whitespace at the begin opf the name is not allowed
|
||||
permissionJson = "{ \"name\": \" permission\", \"type\": \"OWNER\" }";
|
||||
permissionJson = "{ \"name\": \" permission\", \"verbs\": [\"*\"] }";
|
||||
request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
|
||||
.content(permissionJson.getBytes())
|
||||
.contentType(VndMediaType.PERMISSION);
|
||||
.contentType(VndMediaType.REPOSITORY_PERMISSION);
|
||||
response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
@@ -259,12 +261,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
@Test
|
||||
public void shouldGetCreatedPermissions() throws URISyntaxException {
|
||||
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
|
||||
RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", PermissionType.WRITE, true);
|
||||
RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", asList("read", "pull", "push"), true);
|
||||
ArrayList<RepositoryPermission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
|
||||
permissions.add(newPermission);
|
||||
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(permissions);
|
||||
assertExpectedRequest(requestPOSTPermission
|
||||
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}")
|
||||
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : true}")
|
||||
.expectedResponseStatus(201)
|
||||
.responseValidator(response -> assertThat(response.getContentAsString())
|
||||
.as("POST response has no body")
|
||||
@@ -278,7 +280,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
|
||||
RepositoryPermission newPermission = TEST_PERMISSIONS.get(0);
|
||||
assertExpectedRequest(requestPOSTPermission
|
||||
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : false}")
|
||||
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : false}")
|
||||
.expectedResponseStatus(409)
|
||||
);
|
||||
}
|
||||
@@ -288,10 +290,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
|
||||
RepositoryPermission modifiedPermission = TEST_PERMISSIONS.get(0);
|
||||
// modify the type to owner
|
||||
modifiedPermission.setType(PermissionType.OWNER);
|
||||
modifiedPermission.setVerbs(new ArrayList<>(singletonList("*")));
|
||||
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS);
|
||||
assertExpectedRequest(requestPUTPermission
|
||||
.content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}")
|
||||
.content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"verbs\" : [\"*\"], \"groupPermission\" : false}")
|
||||
.path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName())
|
||||
.expectedResponseStatus(204)
|
||||
.responseValidator(response -> assertThat(response.getContentAsString())
|
||||
@@ -353,7 +355,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
.map(hal -> {
|
||||
RepositoryPermissionDto result = new RepositoryPermissionDto();
|
||||
result.setName(hal.getAttribute("name").asText());
|
||||
result.setType(hal.getAttribute("type").asText());
|
||||
JsonNode attribute = hal.getAttribute("verbs");
|
||||
List<String> verbs = new ArrayList<>();
|
||||
attribute.iterator().forEachRemaining(v -> verbs.add(v.asText()));
|
||||
result.setVerbs(verbs);
|
||||
result.setGroupPermission(hal.getAttribute("groupPermission").asBoolean());
|
||||
result.add(hal.getLinks());
|
||||
return result;
|
||||
@@ -382,7 +387,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
RepositoryPermissionDto result = new RepositoryPermissionDto();
|
||||
result.setName(permission.getName());
|
||||
result.setGroupPermission(permission.isGroupPermission());
|
||||
result.setType(permission.getType().name());
|
||||
result.setVerbs(permission.getVerbs());
|
||||
String permissionName = Optional.of(permission.getName())
|
||||
.filter(p -> !permission.isGroupPermission())
|
||||
.orElse(GROUP_PREFIX + permission.getName());
|
||||
@@ -425,7 +430,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
|
||||
HttpRequest request = MockHttpRequest
|
||||
.create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path)
|
||||
.content(entry.content)
|
||||
.contentType(VndMediaType.PERMISSION);
|
||||
.contentType(VndMediaType.REPOSITORY_PERMISSION);
|
||||
dispatcher.invoke(request, response);
|
||||
log.info("Test the Request :{}", entry);
|
||||
assertThat(response.getStatus())
|
||||
@@ -8,11 +8,11 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.Silent.class)
|
||||
@@ -36,7 +36,7 @@ public class RepositoryPermissionToRepositoryPermissionDtoMapperTest {
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldMapGroupPermissionCorrectly() {
|
||||
Repository repository = getDummyRepository();
|
||||
RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, true);
|
||||
RepositoryPermission permission = new RepositoryPermission("42", asList("read","modify","delete"), true);
|
||||
|
||||
RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository);
|
||||
|
||||
@@ -48,7 +48,7 @@ public class RepositoryPermissionToRepositoryPermissionDtoMapperTest {
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldMapNonGroupPermissionCorrectly() {
|
||||
Repository repository = getDummyRepository();
|
||||
RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, false);
|
||||
RepositoryPermission permission = new RepositoryPermission("42", asList("read","modify","delete"), false);
|
||||
|
||||
RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.google.common.io.Resources;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
@@ -18,8 +17,6 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryIsNotArchivedException;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
@@ -41,15 +38,12 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentCaptor.forClass;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyObject;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -291,36 +285,14 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
Assertions.assertThat(createCaptor.getValue().getPermissions())
|
||||
assertThat(createCaptor.getValue().getPermissions())
|
||||
.hasSize(1)
|
||||
.allSatisfy(p -> {
|
||||
assertThat(p.getName()).isEqualTo("trillian");
|
||||
assertThat(p.getType()).isEqualTo(PermissionType.OWNER);
|
||||
assertThat(p.getVerbs()).containsExactly("*");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception {
|
||||
Repository existingRepository = mockRepository("space", "repo");
|
||||
existingRepository.setPermissions(singletonList(new RepositoryPermission("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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateArrayOfProtocolUrls() throws Exception {
|
||||
mockRepository("space", "repo");
|
||||
|
||||
@@ -16,7 +16,7 @@ public abstract class RepositoryTestBase {
|
||||
protected Provider<ChangesetRootResource> changesetRootResource;
|
||||
protected Provider<SourceRootResource> sourceRootResource;
|
||||
protected Provider<ContentResource> contentResource;
|
||||
protected Provider<PermissionRootResource> permissionRootResource;
|
||||
protected Provider<RepositoryPermissionRootResource> permissionRootResource;
|
||||
protected Provider<DiffRootResource> diffRootResource;
|
||||
protected Provider<ModificationsRootResource> modificationsRootResource;
|
||||
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
|
||||
|
||||
@@ -10,8 +10,6 @@ import org.junit.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.repository.HealthCheckFailure;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
@@ -238,7 +236,6 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
repository.setId("1");
|
||||
repository.setCreationDate(System.currentTimeMillis());
|
||||
repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure")));
|
||||
repository.setPermissions(singletonList(new RepositoryPermission("permission", PermissionType.READ)));
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ public class ResourceLinksMock {
|
||||
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
|
||||
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
|
||||
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
|
||||
when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo));
|
||||
|
||||
return resourceLinks;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ import sonia.scm.api.rest.ObjectMapperProvider;
|
||||
import sonia.scm.api.v2.resources.RepositoryDto;
|
||||
import sonia.scm.api.v2.resources.UserDto;
|
||||
import sonia.scm.api.v2.resources.UserToUserDtoMapperImpl;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
@@ -117,10 +116,6 @@ public class GitLfsITCase {
|
||||
|
||||
@Test
|
||||
public void testLfsAPIWithOwnerPermissions() throws IOException {
|
||||
uploadAndDownloadAsUser(PermissionType.OWNER);
|
||||
}
|
||||
|
||||
private void uploadAndDownloadAsUser(PermissionType permissionType) throws IOException {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setPassword("secret123");
|
||||
createUser(trillian);
|
||||
@@ -129,8 +124,8 @@ public class GitLfsITCase {
|
||||
String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref();
|
||||
IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl))
|
||||
.accept("*/*")
|
||||
.type(VndMediaType.PERMISSION)
|
||||
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"WRITE\"}");
|
||||
.type(VndMediaType.REPOSITORY_PERMISSION)
|
||||
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"*\"]}");
|
||||
|
||||
ScmClient client = new ScmClient(trillian.getId(), "secret123");
|
||||
|
||||
@@ -140,11 +135,6 @@ public class GitLfsITCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLfsAPIWithWritePermissions() throws IOException {
|
||||
uploadAndDownloadAsUser(PermissionType.WRITE);
|
||||
}
|
||||
|
||||
private void createUser(User user) {
|
||||
UserDto dto = new UserToUserDtoMapperImpl(){
|
||||
@Override
|
||||
@@ -175,8 +165,8 @@ public class GitLfsITCase {
|
||||
String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref();
|
||||
IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl))
|
||||
.accept("*/*")
|
||||
.type(VndMediaType.PERMISSION)
|
||||
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}");
|
||||
.type(VndMediaType.REPOSITORY_PERMISSION)
|
||||
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"read\"]}");
|
||||
|
||||
ScmClient client = new ScmClient(trillian.getId(), "secret123");
|
||||
uploadAndDownload(client);
|
||||
@@ -196,8 +186,8 @@ public class GitLfsITCase {
|
||||
String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref();
|
||||
IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl))
|
||||
.accept("*/*")
|
||||
.type(VndMediaType.PERMISSION)
|
||||
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}");
|
||||
.type(VndMediaType.REPOSITORY_PERMISSION)
|
||||
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"read\",\"pull\"]}");
|
||||
|
||||
// upload data as admin
|
||||
String data = UUID.randomUUID().toString();
|
||||
|
||||
@@ -111,7 +111,6 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
public void setUpObjectUnderTest(){
|
||||
when(repositoryHandler.getType()).thenReturn(new RepositoryType(REPOSITORY_TYPE, REPOSITORY_TYPE, Sets.newHashSet()));
|
||||
Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler);
|
||||
RepositoryMatcher repositoryMatcher = new RepositoryMatcher(Collections.<RepositoryPathMatcher>emptySet());
|
||||
NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class);
|
||||
repositoryManager = new DefaultRepositoryManager(
|
||||
configuration,
|
||||
@@ -138,7 +137,7 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
/**
|
||||
* Start performance test and ensure that the timeout is not reached.
|
||||
*/
|
||||
@Test(timeout = 6000l)
|
||||
@Test(timeout = 6000L)
|
||||
public void perfTestGetAll(){
|
||||
SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret"));
|
||||
|
||||
@@ -155,7 +154,7 @@ public class DefaultRepositoryManagerPerfTest {
|
||||
}
|
||||
|
||||
private long calculateAverage(List<Long> times) {
|
||||
Long sum = 0l;
|
||||
Long sum = 0L;
|
||||
if(!times.isEmpty()) {
|
||||
for (Long time : times) {
|
||||
sum += time;
|
||||
@@ -183,9 +182,8 @@ private long calculateAverage(List<Long> times) {
|
||||
}
|
||||
|
||||
private Repository createTestRepository(int number) {
|
||||
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number);
|
||||
repository.addPermission(new RepositoryPermission("trillian", PermissionType.READ));
|
||||
return repository;
|
||||
return new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number);
|
||||
|
||||
}
|
||||
|
||||
static class DummyRealm extends AuthorizingRealm {
|
||||
|
||||
@@ -32,14 +32,12 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupEvent;
|
||||
import sonia.scm.group.GroupModificationEvent;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryModificationEvent;
|
||||
@@ -50,6 +48,14 @@ import sonia.scm.user.UserEvent;
|
||||
import sonia.scm.user.UserModificationEvent;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AuthorizationChangedEventProducer}.
|
||||
*
|
||||
@@ -88,6 +94,11 @@ public class AuthorizationChangedEventProducerTest {
|
||||
assertEquals(username, producer.event.getNameOfAffectedUser());
|
||||
}
|
||||
|
||||
private void assertGlobalEventIsFired(){
|
||||
assertNotNull(producer.event);
|
||||
assertFalse(producer.event.isEveryUserAffected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)} with modified user.
|
||||
*/
|
||||
@@ -127,11 +138,6 @@ public class AuthorizationChangedEventProducerTest {
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
private void assertGlobalEventIsFired(){
|
||||
assertNotNull(producer.event);
|
||||
assertFalse(producer.event.isEveryUserAffected());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
|
||||
*/
|
||||
@@ -174,10 +180,10 @@ public class AuthorizationChangedEventProducerTest {
|
||||
{
|
||||
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
|
||||
repositoryModified.setName("test123");
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test")));
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false)));
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test")));
|
||||
repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false)));
|
||||
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository));
|
||||
assertEventIsNotFired();
|
||||
@@ -185,18 +191,18 @@ public class AuthorizationChangedEventProducerTest {
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertEventIsNotFired();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test")));
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false)));
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertEventIsNotFired();
|
||||
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123")));
|
||||
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123", singletonList("read"), false)));
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertGlobalEventIsFired();
|
||||
|
||||
resetStoredEvent();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new RepositoryPermission("test", PermissionType.READ, true))
|
||||
Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), true))
|
||||
);
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertGlobalEventIsFired();
|
||||
@@ -204,10 +210,19 @@ public class AuthorizationChangedEventProducerTest {
|
||||
resetStoredEvent();
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new RepositoryPermission("test", PermissionType.WRITE))
|
||||
Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false))
|
||||
);
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertGlobalEventIsFired();
|
||||
|
||||
resetStoredEvent();
|
||||
repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false)));
|
||||
|
||||
repositoryModified.setPermissions(
|
||||
Lists.newArrayList(new RepositoryPermission("test", asList("write", "read"), false))
|
||||
);
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertEventIsNotFired();
|
||||
}
|
||||
|
||||
private void resetStoredEvent(){
|
||||
|
||||
@@ -51,7 +51,6 @@ import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.GroupNames;
|
||||
import sonia.scm.repository.PermissionType;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
@@ -59,6 +58,7 @@ import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
@@ -225,10 +225,10 @@ public class DefaultAuthorizationCollectorTest {
|
||||
authenticate(UserTestData.createTrillian(), group);
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
heartOfGold.setId("one");
|
||||
heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian")));
|
||||
heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian", asList("read", "pull"), false)));
|
||||
Repository puzzle42 = RepositoryTestData.create42Puzzle();
|
||||
puzzle42.setId("two");
|
||||
RepositoryPermission permission = new RepositoryPermission(group, PermissionType.WRITE, true);
|
||||
RepositoryPermission permission = new RepositoryPermission(group, asList("read", "pull", "push"), true);
|
||||
puzzle42.setPermissions(Lists.newArrayList(permission));
|
||||
when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42));
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.util.ClassLoaders;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class RepositoryPermissionProviderTest {
|
||||
|
||||
private RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private String[] allVerbsFromRepositoryClass;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
PluginLoader pluginLoader = mock(PluginLoader.class);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
|
||||
repositoryPermissionProvider = new RepositoryPermissionProvider(pluginLoader);
|
||||
allVerbsFromRepositoryClass = Arrays.stream(RepositoryPermissions.class.getDeclaredFields())
|
||||
.filter(field -> field.getName().startsWith("ACTION_"))
|
||||
.map(this::getString)
|
||||
.filter(verb -> !"create".equals(verb))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAvailableRoles() {
|
||||
assertThat(repositoryPermissionProvider.availableRoles()).isNotEmpty();
|
||||
assertThat(repositoryPermissionProvider.availableRoles()).allSatisfy(this::containsOnlyAvailableVerbs);
|
||||
}
|
||||
|
||||
private void containsOnlyAvailableVerbs(RepositoryRole role) {
|
||||
assertThat(role.getVerbs()).isSubsetOf(repositoryPermissionProvider.availableVerbs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReadAvailableVerbsFromRepository() {
|
||||
assertThat(repositoryPermissionProvider.availableVerbs()).contains(allVerbsFromRepositoryClass);
|
||||
}
|
||||
|
||||
private String getString(Field field) {
|
||||
try {
|
||||
return (String) field.get(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
fail(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user