merge repository heads

This commit is contained in:
Sebastian Sdorra
2019-01-29 16:01:36 +01:00
61 changed files with 1715 additions and 1399 deletions

View File

@@ -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;
}

View File

@@ -68,7 +68,6 @@ import java.util.Set;
@XmlRootElement(name = "repositories") @XmlRootElement(name = "repositories")
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
private static final long serialVersionUID = 3486560714961909711L; private static final long serialVersionUID = 3486560714961909711L;
private String contact; private String contact;
@@ -81,6 +80,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified; private Long lastModified;
private String namespace; private String namespace;
private String name; private String name;
@XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>(); private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public") @XmlElement(name = "public")
private boolean publicReadable = false; private boolean publicReadable = false;

View File

@@ -37,12 +37,19 @@ package sonia.scm.repository;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import org.apache.commons.collections.CollectionUtils;
import sonia.scm.security.PermissionObject; import sonia.scm.security.PermissionObject;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable; 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 ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -60,54 +67,19 @@ public class RepositoryPermission implements PermissionObject, Serializable
private boolean groupPermission = false; private boolean groupPermission = false;
private String name; private String name;
private PermissionType type = PermissionType.READ; @XmlElement(name = "verb")
private Collection<String> verbs;
/** /**
* Constructs a new {@link RepositoryPermission}. * Constructs a new {@link RepositoryPermission}.
* This constructor is used by JAXB. * This constructor is used by JAXB and mapstruct.
*
*/ */
public RepositoryPermission() {} public RepositoryPermission() {}
/** public RepositoryPermission(String name, Collection<String> verbs, boolean groupPermission)
* Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ}
* for the specified user.
*
*
* @param name name of the user
*/
public RepositoryPermission(String name)
{ {
this();
this.name = name; this.name = name;
} this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs));
/**
* 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.groupPermission = groupPermission; this.groupPermission = groupPermission;
} }
@@ -137,7 +109,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
final RepositoryPermission other = (RepositoryPermission) obj; final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name) return Objects.equal(name, other.name)
&& Objects.equal(type, other.type) && CollectionUtils.isEqualCollection(verbs, other.verbs)
&& Objects.equal(groupPermission, other.groupPermission); && Objects.equal(groupPermission, other.groupPermission);
} }
@@ -150,7 +122,9 @@ public class RepositoryPermission implements PermissionObject, Serializable
@Override @Override
public int hashCode() 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- //J-
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("name", name) .add("name", name)
.add("type", type) .add("verbs", verbs)
.add("groupPermission", groupPermission) .add("groupPermission", groupPermission)
.toString(); .toString();
//J+ //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;
} }
} }

View File

@@ -39,12 +39,11 @@ import org.apache.shiro.subject.Subject;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.Changeset; import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.IncomingCommand; import sonia.scm.repository.spi.IncomingCommand;
import sonia.scm.repository.spi.IncomingCommandRequest; import sonia.scm.repository.spi.IncomingCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
@@ -94,8 +93,7 @@ public final class IncomingCommandBuilder
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
subject.checkPermission(new RepositoryPermission(remoteRepository, subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
PermissionType.READ));
request.setRemoteRepository(remoteRepository); request.setRemoteRepository(remoteRepository);

View File

@@ -34,12 +34,11 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.OutgoingCommand; import sonia.scm.repository.spi.OutgoingCommand;
import sonia.scm.repository.spi.OutgoingCommandRequest; import sonia.scm.repository.spi.OutgoingCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
@@ -84,8 +83,7 @@ public final class OutgoingCommandBuilder
{ {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
subject.checkPermission(new RepositoryPermission(remoteRepository, subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString());
PermissionType.READ));
request.setRemoteRepository(remoteRepository); request.setRemoteRepository(remoteRepository);

View File

@@ -38,11 +38,10 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.PullCommand; import sonia.scm.repository.spi.PullCommand;
import sonia.scm.repository.spi.PullCommandRequest; import sonia.scm.repository.spi.PullCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@@ -96,9 +95,7 @@ public final class PullCommandBuilder
public PullResponse pull(String url) throws IOException { public PullResponse pull(String url) throws IOException {
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.checkPermission( subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
new RepositoryPermission(localRepository, PermissionType.WRITE)
);
//J+ //J+
URL remoteUrl = new URL(url); URL remoteUrl = new URL(url);
@@ -124,12 +121,8 @@ public final class PullCommandBuilder
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.checkPermission( subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString());
new RepositoryPermission(localRepository, PermissionType.WRITE) subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
);
subject.checkPermission(
new RepositoryPermission(remoteRepository, PermissionType.READ)
);
//J+ //J+
request.reset(); request.reset();

View File

@@ -39,11 +39,10 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.spi.PushCommand; import sonia.scm.repository.spi.PushCommand;
import sonia.scm.repository.spi.PushCommandRequest; import sonia.scm.repository.spi.PushCommandRequest;
import sonia.scm.security.RepositoryPermission;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
@@ -92,9 +91,7 @@ public final class PushCommandBuilder
Subject subject = SecurityUtils.getSubject(); Subject subject = SecurityUtils.getSubject();
//J- //J-
subject.checkPermission( subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString());
new RepositoryPermission(remoteRepository, PermissionType.WRITE)
);
//J+ //J+
logger.info("push changes to repository {}", remoteRepository.getId()); logger.info("push changes to repository {}", remoteRepository.getId());

View File

@@ -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;
}

View File

@@ -20,7 +20,7 @@ public class VndMediaType {
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String MODIFICATIONS = PREFIX + "modifications" + 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 REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX;
public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX;
public static final String CONFIG = PREFIX + "config" + 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_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX;
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;

View File

@@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator
} }
else else
{ {
permitted = RepositoryPermissions.read(repository).isPermitted(); permitted = RepositoryPermissions.pull(repository).isPermitted();
} }
return permitted; return permitted;

View File

@@ -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);
}
}

View File

@@ -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)));
}
}

View File

@@ -8,5 +8,5 @@ unpriv = secret
[roles] [roles]
admin = * admin = *
user = something:* user = something:*
repo_read = "repository:read:1" repo_read = "repository:read,pull:1"
repo_write = "repository:push:1" repo_write = "repository:read,write,pull,push:1"

View File

@@ -16,6 +16,7 @@ import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryTestData;
import java.io.IOException; import java.io.IOException;
@@ -24,8 +25,10 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.Clock; import java.time.Clock;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@@ -70,9 +73,7 @@ class XmlRepositoryDAOTest {
Clock clock = mock(Clock.class); Clock clock = mock(Clock.class);
when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); when(clock.millis()).then(ic -> atomicClock.incrementAndGet());
XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem, clock); return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
return dao;
} }
@Test @Test
@@ -329,6 +330,21 @@ class XmlRepositoryDAOTest {
assertThat(content).contains("Awesome Spaceship"); 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 @Test
void shouldReadPathDatabaseAndMetadataOfRepositories() { void shouldReadPathDatabaseAndMetadataOfRepositories() {
Repository heartOfGold = createHeartOfGold(); Repository heartOfGold = createHeartOfGold();

View File

@@ -42,7 +42,6 @@ import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters; import org.junit.runners.Parameterized.Parameters;
import sonia.scm.it.utils.RepositoryUtil; import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.TestData; import sonia.scm.it.utils.TestData;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.repository.client.api.RepositoryClientException;
import sonia.scm.web.VndMediaType; 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.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.utils.RestUtil.given; import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes; 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.USER_SCM_ADMIN;
import static sonia.scm.it.utils.TestData.WRITE;
import static sonia.scm.it.utils.TestData.callRepository; import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class) @RunWith(Parameterized.class)
@@ -91,11 +93,11 @@ public class PermissionsITCase {
public void prepareEnvironment() { public void prepareEnvironment() {
TestData.createDefault(); TestData.createDefault();
TestData.createNotAdminUser(USER_READ, USER_PASS); 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.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.createNotAdminUser(USER_OWNER, USER_PASS);
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); TestData.createUserPermission(USER_OWNER, OWNER, repositoryType);
TestData.createNotAdminUser(USER_OTHER, USER_PASS); TestData.createNotAdminUser(USER_OTHER, USER_PASS);
createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER); createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER);
} }
@@ -109,7 +111,7 @@ public class PermissionsITCase {
@Test @Test
public void readUserShouldNotSeeBruteForcePermissions() { public void readUserShouldNotSeeBruteForcePermissions() {
given(VndMediaType.PERMISSION, USER_READ, USER_PASS) given(VndMediaType.REPOSITORY_PERMISSION, USER_READ, USER_PASS)
.when() .when()
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
.then() .then()
@@ -125,7 +127,7 @@ public class PermissionsITCase {
@Test @Test
public void writeUserShouldNotSeeBruteForcePermissions() { public void writeUserShouldNotSeeBruteForcePermissions() {
given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS) given(VndMediaType.REPOSITORY_PERMISSION, USER_WRITE, USER_PASS)
.when() .when()
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
.then() .then()
@@ -145,7 +147,7 @@ public class PermissionsITCase {
@Test @Test
public void otherUserShouldNotSeeBruteForcePermissions() { public void otherUserShouldNotSeeBruteForcePermissions() {
given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS) given(VndMediaType.REPOSITORY_PERMISSION, USER_OTHER, USER_PASS)
.when() .when()
.get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType))
.then() .then()

View File

@@ -4,15 +4,16 @@ import io.restassured.response.ValidatableResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.repository.PermissionType;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonObjectBuilder; import javax.json.JsonObjectBuilder;
import java.net.URI; import java.net.URI;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static sonia.scm.it.utils.RestUtil.createResourceUrl; 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_SCM_ADMIN = "scmadmin";
public static final String USER_ANONYMOUS = "anonymous"; 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 final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>(); private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
@@ -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); 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); LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
given(VndMediaType.PERMISSION) given(VndMediaType.REPOSITORY_PERMISSION)
.when() .when()
.content("{\n" + .content("{\n" +
"\t\"type\": \"" + permissionType.name() + "\",\n" + "\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" +
"\t\"name\": \"" + name + "\",\n" + "\t\"name\": \"" + name + "\",\n" +
"\t\"groupPermission\": false\n" + "\t\"groupPermission\": false\n" +
"\t\n" + "\t\n" +
@@ -106,7 +112,7 @@ public class TestData {
} }
public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { 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() .when()
.get(TestData.getDefaultPermissionUrl(username, password, repositoryType)) .get(TestData.getDefaultPermissionUrl(username, password, repositoryType))
.then() .then()

View File

@@ -1,7 +1,8 @@
//@flow //@flow
import React from "react"; import React from "react";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import SubmitButton, { type ButtonProps } from "./SubmitButton"; import { type ButtonProps } from "./Button";
import SubmitButton from "./SubmitButton";
import classNames from "classnames"; import classNames from "classnames";
const styles = { const styles = {

View File

@@ -54,7 +54,7 @@ class Select extends React.Component<Props> {
> >
{options.map(opt => { {options.map(opt => {
return ( return (
<option value={opt.value} key={opt.value}> <option value={opt.value} key={"KEY_" + opt.value}>
{opt.label} {opt.label}
</option> </option>
); );

View File

@@ -0,0 +1,11 @@
// @flow
export type RepositoryRole = {
name: string,
verbs: string[]
};
export type AvailableRepositoryPermissions = {
availableVerbs: string[],
availableRoles: RepositoryRole[]
};

View File

@@ -7,7 +7,7 @@ export type Permission = PermissionCreateEntry & {
export type PermissionCreateEntry = { export type PermissionCreateEntry = {
name: string, name: string,
type: string, verbs: string[],
groupPermission: boolean groupPermission: boolean
} }

View File

@@ -24,3 +24,5 @@ export type { Permission, PermissionCreateEntry, PermissionCollection } from "./
export type { SubRepository, File } from "./Sources"; export type { SubRepository, File } from "./Sources";
export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { SelectValue, AutocompleteObject } from "./Autocomplete";
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";

View File

@@ -91,14 +91,18 @@
"group": "Group", "group": "Group",
"error-title": "Error", "error-title": "Error",
"error-subtitle": "Unknown permissions error", "error-subtitle": "Unknown permissions error",
"name": "User or Group", "name": "User or group",
"type": "Type", "role": "Role",
"permissions": "Permissions",
"group-permission": "Group Permission", "group-permission": "Group Permission",
"user-permission": "User Permission", "user-permission": "User Permission",
"edit-permission": { "edit-permission": {
"delete-button": "Delete", "delete-button": "Delete",
"save-button": "Save Changes" "save-button": "Save Changes"
}, },
"advanced-button": {
"label": "Advanced"
},
"delete-permission-button": { "delete-permission-button": {
"label": "Delete", "label": "Delete",
"confirm-alert": { "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!" "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": { "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", "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": { "autocomplete": {
"no-group-options": "No group suggestion available", "no-group-options": "No group suggestion available",
@@ -124,6 +129,13 @@
"no-user-options": "No user suggestion available", "no-user-options": "No user suggestion available",
"user-placeholder": "Enter user", "user-placeholder": "Enter user",
"loading": "Loading..." "loading": "Loading..."
},
"advanced": {
"dialog": {
"title": "Advanced permissions",
"submit": "Submit",
"abort": "Abort"
}
} }
}, },
"help": { "help": {

View File

@@ -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);

View 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);

View File

@@ -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);

View File

@@ -18,7 +18,8 @@ describe("permission validation", () => {
name: "PermissionName", name: "PermissionName",
groupPermission: true, groupPermission: true,
type: "READ", type: "READ",
_links: {} _links: {},
verbs: []
} }
]; ];
const name = "PermissionName"; const name = "PermissionName";
@@ -35,7 +36,8 @@ describe("permission validation", () => {
name: "PermissionName", name: "PermissionName",
groupPermission: false, groupPermission: false,
type: "READ", type: "READ",
_links: {} _links: {},
verbs: []
} }
]; ];
const name = "PermissionName"; const name = "PermissionName";

View File

@@ -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);

View File

@@ -1,17 +1,26 @@
// @flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Autocomplete, Radio, SubmitButton } from "@scm-manager/ui-components"; import {
import TypeSelector from "./TypeSelector"; Autocomplete,
SubmitButton,
Button,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
import RoleSelector from "../components/RoleSelector";
import type { import type {
AvailableRepositoryPermissions,
PermissionCollection, PermissionCollection,
PermissionCreateEntry, PermissionCreateEntry,
SelectValue SelectValue
} from "@scm-manager/ui-types"; } 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 = { type Props = {
t: string => string, t: string => string,
availablePermissions: AvailableRepositoryPermissions,
createPermission: (permission: PermissionCreateEntry) => void, createPermission: (permission: PermissionCreateEntry) => void,
loading: boolean, loading: boolean,
currentPermissions: PermissionCollection, currentPermissions: PermissionCollection,
@@ -21,10 +30,11 @@ type Props = {
type State = { type State = {
name: string, name: string,
type: string, verbs: string[],
groupPermission: boolean, groupPermission: boolean,
valid: boolean, valid: boolean,
value?: SelectValue value?: SelectValue,
showAdvancedDialog: boolean
}; };
class CreatePermissionForm extends React.Component<Props, State> { class CreatePermissionForm extends React.Component<Props, State> {
@@ -33,10 +43,11 @@ class CreatePermissionForm extends React.Component<Props, State> {
this.state = { this.state = {
name: "", name: "",
type: "READ", verbs: props.availablePermissions.availableRoles[0].verbs,
groupPermission: false, groupPermission: false,
valid: true, valid: true,
value: undefined value: undefined,
showAdvancedDialog: false
}; };
} }
@@ -121,9 +132,23 @@ class CreatePermissionForm extends React.Component<Props, State> {
}; };
render() { 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 ( return (
<div> <div>
@@ -131,33 +156,58 @@ class CreatePermissionForm extends React.Component<Props, State> {
<h2 className="subtitle"> <h2 className="subtitle">
{t("permission.add-permission.add-permission-heading")} {t("permission.add-permission.add-permission-heading")}
</h2> </h2>
{advancedDialog}
<form onSubmit={this.submit}> <form onSubmit={this.submit}>
<Radio <div className="control">
<label className="radio">
<input
type="radio"
name="permission_scope" name="permission_scope"
value="USER_PERMISSION"
checked={!this.state.groupPermission} checked={!this.state.groupPermission}
label={t("permission.user-permission")} value="USER_PERMISSION"
onChange={this.permissionScopeChanged} onChange={this.permissionScopeChanged}
/> />
<Radio {t("permission.user-permission")}
</label>
<label className="radio">
<input
type="radio"
name="permission_scope" name="permission_scope"
value="GROUP_PERMISSION" value="GROUP_PERMISSION"
checked={this.state.groupPermission} checked={this.state.groupPermission}
label={t("permission.group-permission")}
onChange={this.permissionScopeChanged} onChange={this.permissionScopeChanged}
/> />
{t("permission.group-permission")}
</label>
</div>
<div className="columns"> <div className="columns">
<div className="column is-three-quarters"> <div className="column is-two-thirds">
{this.renderAutocompletionField()} {this.renderAutocompletionField()}
</div> </div>
<div className="column is-one-quarter"> <div className="column is-one-third">
<TypeSelector <div className="columns">
label={t("permission.type")} <div className="column is-half">
helpText={t("permission.help.typeHelpText")} <RoleSelector
handleTypeChange={this.handleTypeChange} availableRoles={availableRoleNames}
type={type ? type : "READ"} label={t("permission.role")}
helpText={t("permission.help.roleHelpText")}
handleRoleChange={this.handleRoleChange}
role={matchingRole}
/> />
</div> </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>
<div className="columns"> <div className="columns">
<div className="column"> <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 => { submit = e => {
this.props.createPermission({ this.props.createPermission({
name: this.state.name, name: this.state.name,
type: this.state.type, verbs: this.state.verbs,
groupPermission: this.state.groupPermission groupPermission: this.state.groupPermission
}); });
this.removeState(); this.removeState();
@@ -186,17 +251,24 @@ class CreatePermissionForm extends React.Component<Props, State> {
removeState = () => { removeState = () => {
this.setState({ this.setState({
name: "", name: "",
type: "READ", verbs: this.props.availablePermissions.availableRoles[0].verbs,
groupPermission: false, groupPermission: false,
valid: true valid: true
}); });
}; };
handleTypeChange = (type: string) => { handleRoleChange = (role: string) => {
const selectedRole = this.findAvailableRole(role);
this.setState({ 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); export default translate("repos")(CreatePermissionForm);

View File

@@ -3,8 +3,12 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { import {
fetchAvailablePermissionsIfNeeded,
fetchPermissions, fetchPermissions,
getFetchAvailablePermissionsFailure,
getAvailablePermissions,
getFetchPermissionsFailure, getFetchPermissionsFailure,
isFetchAvailablePermissionsPending,
isFetchPermissionsPending, isFetchPermissionsPending,
getPermissionsOfRepo, getPermissionsOfRepo,
hasCreatePermission, hasCreatePermission,
@@ -17,14 +21,19 @@ import {
modifyPermissionReset, modifyPermissionReset,
deletePermissionReset deletePermissionReset
} from "../modules/permissions"; } from "../modules/permissions";
import { Loading, ErrorPage } from "@scm-manager/ui-components"; import {
Loading,
ErrorPage,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
import type { import type {
AvailableRepositoryPermissions,
Permission, Permission,
PermissionCollection, PermissionCollection,
PermissionCreateEntry PermissionCreateEntry
} from "@scm-manager/ui-types"; } from "@scm-manager/ui-types";
import SinglePermission from "./SinglePermission"; import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "../components/CreatePermissionForm"; import CreatePermissionForm from "./CreatePermissionForm";
import type { History } from "history"; import type { History } from "history";
import { getPermissionsLink } from "../../modules/repos"; import { getPermissionsLink } from "../../modules/repos";
import { import {
@@ -33,6 +42,7 @@ import {
} from "../../../modules/indexResource"; } from "../../../modules/indexResource";
type Props = { type Props = {
availablePermissions: AvailableRepositoryPermissions,
namespace: string, namespace: string,
repoName: string, repoName: string,
loading: boolean, loading: boolean,
@@ -45,6 +55,7 @@ type Props = {
userAutoCompleteLink: string, userAutoCompleteLink: string,
//dispatch functions //dispatch functions
fetchAvailablePermissionsIfNeeded: () => void,
fetchPermissions: (link: string, namespace: string, repoName: string) => void, fetchPermissions: (link: string, namespace: string, repoName: string) => void,
createPermission: ( createPermission: (
link: string, link: string,
@@ -65,6 +76,7 @@ type Props = {
class Permissions extends React.Component<Props> { class Permissions extends React.Component<Props> {
componentDidMount() { componentDidMount() {
const { const {
fetchAvailablePermissionsIfNeeded,
fetchPermissions, fetchPermissions,
namespace, namespace,
repoName, repoName,
@@ -77,6 +89,7 @@ class Permissions extends React.Component<Props> {
createPermissionReset(namespace, repoName); createPermissionReset(namespace, repoName);
modifyPermissionReset(namespace, repoName); modifyPermissionReset(namespace, repoName);
deletePermissionReset(namespace, repoName); deletePermissionReset(namespace, repoName);
fetchAvailablePermissionsIfNeeded();
fetchPermissions(permissionsLink, namespace, repoName); fetchPermissions(permissionsLink, namespace, repoName);
} }
@@ -91,6 +104,7 @@ class Permissions extends React.Component<Props> {
render() { render() {
const { const {
availablePermissions,
loading, loading,
error, error,
permissions, permissions,
@@ -112,12 +126,13 @@ class Permissions extends React.Component<Props> {
); );
} }
if (loading || !permissions) { if (loading || !permissions || !availablePermissions) {
return <Loading />; return <Loading />;
} }
const createPermissionForm = hasPermissionToCreate ? ( const createPermissionForm = hasPermissionToCreate ? (
<CreatePermissionForm <CreatePermissionForm
availablePermissions={availablePermissions}
createPermission={permission => this.createPermission(permission)} createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission} loading={loadingCreatePermission}
currentPermissions={permissions} currentPermissions={permissions}
@@ -131,11 +146,30 @@ class Permissions extends React.Component<Props> {
<table className="has-background-light table is-hoverable is-fullwidth"> <table className="has-background-light table is-hoverable is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>{t("permission.name")}</th> <th>
<th className="is-hidden-mobile"> <LabelWithHelpIcon
{t("permission.group-permission")} 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>
<th>{t("permission.type")}</th>
<th /> <th />
</tr> </tr>
</thead> </thead>
@@ -143,6 +177,7 @@ class Permissions extends React.Component<Props> {
{permissions.map(permission => { {permissions.map(permission => {
return ( return (
<SinglePermission <SinglePermission
availablePermissions={availablePermissions}
key={permission.name + permission.groupPermission.toString()} key={permission.name + permission.groupPermission.toString()}
namespace={namespace} namespace={namespace}
repoName={repoName} repoName={repoName}
@@ -165,8 +200,11 @@ const mapStateToProps = (state, ownProps) => {
getFetchPermissionsFailure(state, namespace, repoName) || getFetchPermissionsFailure(state, namespace, repoName) ||
getCreatePermissionFailure(state, namespace, repoName) || getCreatePermissionFailure(state, namespace, repoName) ||
getDeletePermissionsFailure(state, namespace, repoName) || getDeletePermissionsFailure(state, namespace, repoName) ||
getModifyPermissionsFailure(state, namespace, repoName); getModifyPermissionsFailure(state, namespace, repoName) ||
const loading = isFetchPermissionsPending(state, namespace, repoName); getFetchAvailablePermissionsFailure(state);
const loading =
isFetchPermissionsPending(state, namespace, repoName) ||
isFetchAvailablePermissionsPending(state);
const permissions = getPermissionsOfRepo(state, namespace, repoName); const permissions = getPermissionsOfRepo(state, namespace, repoName);
const loadingCreatePermission = isCreatePermissionPending( const loadingCreatePermission = isCreatePermissionPending(
state, state,
@@ -177,7 +215,9 @@ const mapStateToProps = (state, ownProps) => {
const permissionsLink = getPermissionsLink(state, namespace, repoName); const permissionsLink = getPermissionsLink(state, namespace, repoName);
const groupAutoCompleteLink = getGroupAutoCompleteLink(state); const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
const userAutoCompleteLink = getUserAutoCompleteLink(state); const userAutoCompleteLink = getUserAutoCompleteLink(state);
const availablePermissions = getAvailablePermissions(state);
return { return {
availablePermissions,
namespace, namespace,
repoName, repoName,
error, error,
@@ -196,6 +236,9 @@ const mapDispatchToProps = dispatch => {
fetchPermissions: (link: string, namespace: string, repoName: string) => { fetchPermissions: (link: string, namespace: string, repoName: string) => {
dispatch(fetchPermissions(link, namespace, repoName)); dispatch(fetchPermissions(link, namespace, repoName));
}, },
fetchAvailablePermissionsIfNeeded: () => {
dispatch(fetchAvailablePermissionsIfNeeded());
},
createPermission: ( createPermission: (
link: string, link: string,
permission: PermissionCreateEntry, permission: PermissionCreateEntry,

View File

@@ -1,22 +1,28 @@
// @flow // @flow
import React from "react"; 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 { translate } from "react-i18next";
import { import {
modifyPermission, modifyPermission,
isModifyPermissionPending, isModifyPermissionPending,
deletePermission, deletePermission,
isDeletePermissionPending isDeletePermissionPending,
findMatchingRoleName
} from "../modules/permissions"; } from "../modules/permissions";
import { connect } from "react-redux"; import { connect } from "react-redux";
import type { History } from "history"; 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 DeletePermissionButton from "../components/buttons/DeletePermissionButton";
import TypeSelector from "../components/TypeSelector"; import RoleSelector from "../components/RoleSelector";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
type Props = { type Props = {
availablePermissions: AvailableRepositoryPermissions,
submitForm: Permission => void, submitForm: Permission => void,
modifyPermission: (Permission, string, string) => void, modifyPermission: (permission: Permission, namespace: string, name: string) => void,
permission: Permission, permission: Permission,
t: string => string, t: string => string,
namespace: string, namespace: string,
@@ -24,38 +30,53 @@ type Props = {
match: any, match: any,
history: History, history: History,
loading: boolean, loading: boolean,
deletePermission: (Permission, string, string) => void, deletePermission: (permission: Permission, namespace: string, name: string) => void,
deleteLoading: boolean deleteLoading: boolean
}; };
type State = { type State = {
permission: Permission role: string,
permission: Permission,
showAdvancedDialog: boolean
}; };
class SinglePermission extends React.Component<Props, State> { class SinglePermission extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const defaultPermission = props.availablePermissions.availableRoles
? props.availablePermissions.availableRoles[0]
: {};
this.state = { this.state = {
permission: { permission: {
name: "", name: "",
type: "READ", verbs: defaultPermission.verbs,
groupPermission: false, groupPermission: false,
_links: {} _links: {}
} },
role: defaultPermission.name,
showAdvancedDialog: false
}; };
} }
componentDidMount() { componentDidMount() {
const { permission } = this.props; const { availablePermissions, permission } = this.props;
const matchingRole = findMatchingRoleName(
availablePermissions,
permission.verbs
);
if (permission) { if (permission) {
this.setState({ this.setState({
permission: { permission: {
name: permission.name, name: permission.name,
type: permission.type, verbs: permission.verbs,
groupPermission: permission.groupPermission, groupPermission: permission.groupPermission,
_links: permission._links _links: permission._links
} },
role: matchingRole
}); });
} }
} }
@@ -69,28 +90,57 @@ class SinglePermission extends React.Component<Props, State> {
}; };
render() { render() {
const { permission } = this.state; const { role, permission, showAdvancedDialog } = this.state;
const { loading, namespace, repoName } = this.props; const {
const typeSelector = t,
this.props.permission._links && this.props.permission._links.update ? ( 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> <td>
<TypeSelector <RoleSelector
handleTypeChange={this.handleTypeChange} handleRoleChange={this.handleRoleChange}
type={permission.type ? permission.type : "READ"} availableRoles={availableRoleNames}
role={role}
loading={loading} loading={loading}
/> />
</td> </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 ( return (
<tr> <tr>
<td>{permission.name}</td> <td>{permission.name}</td>
<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> </td>
{typeSelector}
<td> <td>
<DeletePermissionButton <DeletePermissionButton
permission={permission} permission={permission}
@@ -99,39 +149,69 @@ class SinglePermission extends React.Component<Props, State> {
deletePermission={this.deletePermission} deletePermission={this.deletePermission}
loading={this.props.deleteLoading} loading={this.props.deleteLoading}
/> />
{advancedDialg}
</td> </td>
</tr> </tr>
); );
} }
handleTypeChange = (type: string) => { mayChangePermissions = () => {
this.setState({ return this.props.permission._links && this.props.permission._links.update;
permission: {
...this.state.permission,
type: type
}
});
this.modifyPermission(type);
}; };
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; let permission = this.state.permission;
permission.type = type; permission.verbs = verbs;
this.props.modifyPermission( this.props.modifyPermission(
permission, permission,
this.props.namespace, this.props.namespace,
this.props.repoName this.props.repoName
); );
}; };
createSelectOptions(types: string[]) {
return types.map(type => {
return {
label: type,
value: type
};
});
}
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {

View File

@@ -4,6 +4,7 @@ import type { Action } from "@scm-manager/ui-components";
import { apiClient } from "@scm-manager/ui-components"; import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../../modules/types"; import * as types from "../../../modules/types";
import type { import type {
AvailableRepositoryPermissions,
Permission, Permission,
PermissionCollection, PermissionCollection,
PermissionCreateEntry PermissionCreateEntry
@@ -11,7 +12,18 @@ import type {
import { isPending } from "../../../modules/pending"; import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure"; import { getFailure } from "../../../modules/failure";
import { Dispatch } from "redux"; 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 = "scm/permissions/FETCH_PERMISSIONS";
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${ export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
types.PENDING_SUFFIX types.PENDING_SUFFIX
@@ -62,7 +74,71 @@ export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
types.RESET_SUFFIX 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 // fetch permissions
@@ -368,6 +444,7 @@ export function deletePermissionReset(namespace: string, repoName: string) {
itemId: namespace + "/" + repoName itemId: namespace + "/" + repoName
}; };
} }
function deletePermissionFromState( function deletePermissionFromState(
oldPermissions: PermissionCollection, oldPermissions: PermissionCollection,
permission: Permission permission: Permission
@@ -399,12 +476,17 @@ export default function reducer(
return state; return state;
} }
switch (action.type) { switch (action.type) {
case FETCH_AVAILABLE_SUCCESS:
return {
...state,
available: action.payload
};
case FETCH_PERMISSIONS_SUCCESS: case FETCH_PERMISSIONS_SUCCESS:
return { return {
...state, ...state,
[action.itemId]: { [action.itemId]: {
entries: action.payload._embedded.permissions, entries: action.payload._embedded.permissions,
createPermission: action.payload._links.create ? true : false createPermission: !!action.payload._links.create
} }
}; };
case MODIFY_PERMISSION_SUCCESS: case MODIFY_PERMISSION_SUCCESS:
@@ -452,6 +534,12 @@ export default function reducer(
// selectors // selectors
export function getAvailablePermissions(state: Object) {
if (state.permissions) {
return state.permissions.available;
}
}
export function getPermissionsOfRepo( export function getPermissionsOfRepo(
state: Object, state: Object,
namespace: string, namespace: string,
@@ -463,6 +551,10 @@ export function getPermissionsOfRepo(
} }
} }
export function isFetchAvailablePermissionsPending(state: Object) {
return isPending(state, FETCH_AVAILABLE, "available");
}
export function isFetchPermissionsPending( export function isFetchPermissionsPending(
state: Object, state: Object,
namespace: string, namespace: string,
@@ -471,6 +563,10 @@ export function isFetchPermissionsPending(
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName); return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
} }
export function getFetchAvailablePermissionsFailure(state: Object) {
return getFailure(state, FETCH_AVAILABLE, "available");
}
export function getFetchPermissionsFailure( export function getFetchPermissionsFailure(
state: Object, state: Object,
namespace: string, namespace: string,
@@ -522,6 +618,7 @@ export function isCreatePermissionPending(
) { ) {
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName); return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
} }
export function getCreatePermissionFailure( export function getCreatePermissionFailure(
state: Object, state: Object,
namespace: string, namespace: string,
@@ -603,3 +700,33 @@ export function getModifyPermissionsFailure(
} }
return null; 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));
}

View File

@@ -59,7 +59,8 @@ const hitchhiker_puzzle42Permission_user_eins: Permission = {
href: href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins" "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
} }
} },
verbs: []
}; };
const hitchhiker_puzzle42Permission_user_zwei: Permission = { const hitchhiker_puzzle42Permission_user_zwei: Permission = {
@@ -79,7 +80,8 @@ const hitchhiker_puzzle42Permission_user_zwei: Permission = {
href: href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei" "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
} }
} },
verbs: []
}; };
const hitchhiker_puzzle42Permissions: PermissionCollection = [ const hitchhiker_puzzle42Permissions: PermissionCollection = [
@@ -175,8 +177,7 @@ describe("permission fetch", () => {
} }
); );
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins }; let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
editedPermission.type = "OWNER";
const store = mockStore({}); const store = mockStore({});
@@ -197,8 +198,7 @@ describe("permission fetch", () => {
} }
); );
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins }; let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
editedPermission.type = "OWNER";
const store = mockStore({}); const store = mockStore({});
@@ -227,8 +227,7 @@ describe("permission fetch", () => {
} }
); );
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins }; let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
editedPermission.type = "OWNER";
const store = mockStore({}); const store = mockStore({});
@@ -451,8 +450,7 @@ describe("permissions reducer", () => {
entries: [hitchhiker_puzzle42Permission_user_eins] entries: [hitchhiker_puzzle42Permission_user_eins]
} }
}; };
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins }; let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
permissionEdited.type = "OWNER";
let expectedState = { let expectedState = {
"hitchhiker/puzzle42": { "hitchhiker/puzzle42": {
entries: [permissionEdited] entries: [permissionEdited]

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -56,6 +56,7 @@ public class IndexDtoGenerator extends LinkAppenderMapper {
if (PermissionPermissions.list().isPermitted()) { if (PermissionPermissions.list().isPermitted()) {
builder.single(link("permissions", resourceLinks.permissions().self())); builder.single(link("permissions", resourceLinks.permissions().self()));
} }
builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self()));
} else { } else {
builder.single(link("login", resourceLinks.authentication().jsonLogin())); builder.single(link("login", resourceLinks.authentication().jsonLogin()));
} }

View File

@@ -25,7 +25,7 @@ public class MapperModule extends AbstractModule {
bind(RepositoryTypeCollectionToDtoMapper.class); bind(RepositoryTypeCollectionToDtoMapper.class);
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); bind(RepositoryPermissionDtoToRepositoryPermissionMapper.class).to(Mappers.getMapper(RepositoryPermissionDtoToRepositoryPermissionMapper.class).getClass());
bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass());
bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass()); bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass());

View File

@@ -6,10 +6,9 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -24,6 +23,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
public class RepositoryCollectionResource { public class RepositoryCollectionResource {
@@ -100,7 +100,7 @@ public class RepositoryCollectionResource {
private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) {
Repository repository = dtoToRepositoryMapper.map(repositoryDto, null); Repository repository = dtoToRepositoryMapper.map(repositoryDto, null);
repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), PermissionType.OWNER))); repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), singletonList("*"), false)));
return repository; return repository;
} }

View File

@@ -10,7 +10,6 @@ public abstract class RepositoryDtoToRepositoryMapper extends BaseDtoMapper {
@Mapping(target = "id", ignore = true) @Mapping(target = "id", ignore = true)
@Mapping(target = "publicReadable", ignore = true) @Mapping(target = "publicReadable", ignore = true)
@Mapping(target = "healthCheckFailures", ignore = true) @Mapping(target = "healthCheckFailures", ignore = true)
@Mapping(target = "permissions", ignore = true)
public abstract Repository map(RepositoryDto repositoryDto, @Context String id); public abstract Repository map(RepositoryDto repositoryDto, @Context String id);
@AfterMapping @AfterMapping

View File

@@ -7,9 +7,13 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
import java.util.Collection;
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN; import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
@Getter @Setter @ToString @NoArgsConstructor @Getter @Setter @ToString @NoArgsConstructor
@@ -20,16 +24,8 @@ public class RepositoryPermissionDto extends HalRepresentation {
@Pattern(regexp = USER_GROUP_PATTERN) @Pattern(regexp = USER_GROUP_PATTERN)
private String name; private String name;
/** @NotEmpty
* the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable private Collection<String> verbs;
* the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version
*
* see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19
*
**/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String type;
private boolean groupPermission = false; private boolean groupPermission = false;
@@ -38,7 +34,6 @@ public class RepositoryPermissionDto extends HalRepresentation {
this.groupPermission = groupPermission; this.groupPermission = groupPermission;
} }
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) { protected HalRepresentation add(Links links) {

View File

@@ -1,11 +1,12 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
@Mapper @Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE)
public abstract class PermissionDtoToPermissionMapper { public abstract class RepositoryPermissionDtoToRepositoryPermissionMapper {
public abstract RepositoryPermission map(RepositoryPermissionDto permissionDto); public abstract RepositoryPermission map(RepositoryPermissionDto permissionDto);

View File

@@ -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;
}
}

View File

@@ -35,18 +35,21 @@ import static sonia.scm.NotFoundException.notFound;
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Slf4j @Slf4j
public class PermissionRootResource { public class RepositoryPermissionRootResource {
private RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper;
private PermissionDtoToPermissionMapper dtoToModelMapper;
private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper; private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
private ResourceLinks resourceLinks; private ResourceLinks resourceLinks;
private final RepositoryManager manager; private final RepositoryManager manager;
@Inject @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.dtoToModelMapper = dtoToModelMapper;
this.modelToDtoMapper = modelToDtoMapper; this.modelToDtoMapper = modelToDtoMapper;
this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper; this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper;
@@ -54,7 +57,6 @@ public class PermissionRootResource {
this.manager = manager; this.manager = manager;
} }
/** /**
* Adds a new permission to the user or group managed by the repository * Adds a new permission to the user or group managed by the repository
* *
@@ -71,7 +73,7 @@ public class PermissionRootResource {
@ResponseCode(code = 409, condition = "conflict") @ResponseCode(code = 409, condition = "conflict")
}) })
@TypeHint(TypeHint.NO_CONTENT.class) @TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION) @Consumes(VndMediaType.REPOSITORY_PERMISSION)
@Path("") @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); log.info("try to add new permission: {}", permission);
@@ -84,7 +86,6 @@ public class PermissionRootResource {
return Response.created(URI.create(resourceLinks.repositoryPermission().self(namespace, name, urlPermissionName))).build(); return Response.created(URI.create(resourceLinks.repositoryPermission().self(namespace, name, urlPermissionName))).build();
} }
/** /**
* Get the searched permission with permission name related to a repository * 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 = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@Produces(VndMediaType.PERMISSION) @Produces(VndMediaType.REPOSITORY_PERMISSION)
@TypeHint(RepositoryPermissionDto.class) @TypeHint(RepositoryPermissionDto.class)
@Path("{permission-name}") @Path("{permission-name}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) { public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) {
@@ -115,7 +116,6 @@ public class PermissionRootResource {
).build(); ).build();
} }
/** /**
* Get all permissions related to a repository * Get all permissions related to a repository
* *
@@ -130,7 +130,7 @@ public class PermissionRootResource {
@ResponseCode(code = 404, condition = "not found"), @ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error") @ResponseCode(code = 500, condition = "internal server error")
}) })
@Produces(VndMediaType.PERMISSION) @Produces(VndMediaType.REPOSITORY_PERMISSION)
@TypeHint(RepositoryPermissionDto.class) @TypeHint(RepositoryPermissionDto.class)
@Path("") @Path("")
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) { 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(); return Response.ok(repositoryPermissionCollectionToDtoMapper.map(repository)).build();
} }
/** /**
* Update a permission to the user or group managed by the repository * 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) * 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") @ResponseCode(code = 500, condition = "internal server error")
}) })
@TypeHint(TypeHint.NO_CONTENT.class) @TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION) @Consumes(VndMediaType.REPOSITORY_PERMISSION)
@Path("{permission-name}") @Path("{permission-name}")
public Response update(@PathParam("namespace") String namespace, public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name, @PathParam("name") String name,
@@ -172,6 +171,7 @@ public class PermissionRootResource {
if (!extractedPermissionName.equals(permission.getName())) { if (!extractedPermissionName.equals(permission.getName())) {
checkPermissionAlreadyExists(permission, repository); checkPermissionAlreadyExists(permission, repository);
} }
RepositoryPermission existingPermission = repository.getPermissions() RepositoryPermission existingPermission = repository.getPermissions()
.stream() .stream()
.filter(filterPermission(permissionName)) .filter(filterPermission(permissionName))
@@ -208,17 +208,16 @@ public class PermissionRootResource {
.stream() .stream()
.filter(filterPermission(permissionName)) .filter(filterPermission(permissionName))
.findFirst() .findFirst()
.ifPresent(repository::removePermission) .ifPresent(repository::removePermission);
;
manager.modify(repository); manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName); log.info("the permission with name: {} is updated.", permissionName);
return Response.noContent().build(); return Response.noContent().build();
} }
Predicate<RepositoryPermission> filterPermission(String permissionName) { private Predicate<RepositoryPermission> filterPermission(String name) {
return permission -> getPermissionName(permissionName).equals(permission.getName()) return permission -> getPermissionName(name).equals(permission.getName())
&& &&
permission.isGroupPermission() == isGroupPermission(permissionName); permission.isGroupPermission() == isGroupPermission(name);
} }
private String getPermissionName(String permissionName) { private String getPermissionName(String permissionName) {
@@ -231,7 +230,6 @@ public class PermissionRootResource {
return permissionName.startsWith(GROUP_PREFIX); return permissionName.startsWith(GROUP_PREFIX);
} }
/** /**
* check if the actual user is permitted to manage the repository permissions * check if the actual user is permitted to manage the repository permissions
* return the repository if the user is permitted * return the repository if the user is permitted

View File

@@ -39,7 +39,7 @@ public class RepositoryResource {
private final Provider<ChangesetRootResource> changesetRootResource; private final Provider<ChangesetRootResource> changesetRootResource;
private final Provider<SourceRootResource> sourceRootResource; private final Provider<SourceRootResource> sourceRootResource;
private final Provider<ContentResource> contentResource; private final Provider<ContentResource> contentResource;
private final Provider<PermissionRootResource> permissionRootResource; private final Provider<RepositoryPermissionRootResource> permissionRootResource;
private final Provider<DiffRootResource> diffRootResource; private final Provider<DiffRootResource> diffRootResource;
private final Provider<ModificationsRootResource> modificationsRootResource; private final Provider<ModificationsRootResource> modificationsRootResource;
private final Provider<FileHistoryRootResource> fileHistoryRootResource; private final Provider<FileHistoryRootResource> fileHistoryRootResource;
@@ -54,7 +54,7 @@ public class RepositoryResource {
Provider<BranchRootResource> branchRootResource, Provider<BranchRootResource> branchRootResource,
Provider<ChangesetRootResource> changesetRootResource, Provider<ChangesetRootResource> changesetRootResource,
Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource, Provider<SourceRootResource> sourceRootResource, Provider<ContentResource> contentResource,
Provider<PermissionRootResource> permissionRootResource, Provider<RepositoryPermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource, Provider<DiffRootResource> diffRootResource,
Provider<ModificationsRootResource> modificationsRootResource, Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource, Provider<FileHistoryRootResource> fileHistoryRootResource,
@@ -154,7 +154,6 @@ public class RepositoryResource {
private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) { private Repository processUpdate(RepositoryDto repositoryDto, Repository existing) {
Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId()); Repository changedRepository = dtoToRepositoryMapper.map(repositoryDto, existing.getId());
changedRepository.setPermissions(existing.getPermissions());
return changedRepository; return changedRepository;
} }
@@ -194,7 +193,7 @@ public class RepositoryResource {
} }
@Path("permissions/") @Path("permissions/")
public PermissionRootResource permissions() { public RepositoryPermissionRootResource permissions() {
return permissionRootResource.get(); return permissionRootResource.get();
} }

View File

@@ -514,7 +514,7 @@ class ResourceLinks {
private final LinkBuilder permissionLinkBuilder; private final LinkBuilder permissionLinkBuilder;
RepositoryPermissionLinks(ScmPathInfo pathInfo) { 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) { String all(String namespace, String name) {
@@ -639,14 +639,30 @@ class ResourceLinks {
} }
static class PermissionsLinks { static class PermissionsLinks {
private final LinkBuilder permissionsLlinkBuilder; private final LinkBuilder permissionsLinkBuilder;
PermissionsLinks(ScmPathInfo scmPathInfo) { PermissionsLinks(ScmPathInfo scmPathInfo) {
this.permissionsLlinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class); this.permissionsLinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class);
} }
String self() { 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();
} }
} }
} }

View File

@@ -198,8 +198,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private void collectRepositoryPermissions(Builder<String> builder, private void collectRepositoryPermissions(Builder<String> builder,
Repository repository, User user, GroupNames groups) Repository repository, User user, GroupNames groups)
{ {
Collection<RepositoryPermission> repositoryPermissions Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions();
= repository.getPermissions();
if (Util.isNotEmpty(repositoryPermissions)) if (Util.isNotEmpty(repositoryPermissions))
{ {
@@ -207,9 +206,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
for (RepositoryPermission permission : repositoryPermissions) for (RepositoryPermission permission : repositoryPermissions)
{ {
hasPermission = isUserPermitted(user, groups, permission); 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()) if (logger.isTraceEnabled())
{ {
logger.trace("add repository permission {} for user {} at repository {}", logger.trace("add repository permission {} for user {} at repository {}",

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -4,6 +4,7 @@ import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.apache.shiro.authz.AuthorizationException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.PushStateDispatcher; import sonia.scm.PushStateDispatcher;
import sonia.scm.filter.WebElement; import sonia.scm.filter.WebElement;
@@ -74,6 +75,9 @@ public class HttpProtocolServlet extends HttpServlet {
} catch (NotFoundException e) { } catch (NotFoundException e) {
log.debug(e.getMessage()); log.debug(e.getMessage());
resp.setStatus(HttpStatus.SC_NOT_FOUND); resp.setStatus(HttpStatus.SC_NOT_FOUND);
} catch (AuthorizationException e) {
log.debug(e.getMessage());
resp.setStatus(HttpStatus.SC_FORBIDDEN);
} }
} }
} }

View File

@@ -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>

View File

@@ -37,5 +37,45 @@
} }
}, },
"unknown": "Unknown permission" "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)"
}
}
} }
} }

View File

@@ -1,5 +1,6 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
@@ -29,10 +30,9 @@ import org.junit.jupiter.api.TestFactory;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import java.io.IOException; 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.Link.link;
import static de.otto.edison.hal.Links.linkingTo; 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.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@@ -66,7 +68,7 @@ import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
password = "secret", password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini" 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_NAMESPACE = "repo_namespace";
private static final String REPOSITORY_NAME = "repo"; private static final String REPOSITORY_NAME = "repo";
private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME; 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 PERMISSION_NAME = "perm";
private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/"; 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 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 private static final ArrayList<RepositoryPermission> TEST_PERMISSIONS = Lists
.newArrayList( .newArrayList(
new RepositoryPermission("user_write", PermissionType.WRITE, false), new RepositoryPermission("user_write", asList("read","modify"), false),
new RepositoryPermission("user_read", PermissionType.READ, false), new RepositoryPermission("user_read", singletonList("read"), false),
new RepositoryPermission("user_owner", PermissionType.OWNER, false), new RepositoryPermission("user_owner", singletonList("*"), false),
new RepositoryPermission("group_read", PermissionType.READ, true), new RepositoryPermission("group_read", singletonList("read"), true),
new RepositoryPermission("group_write", PermissionType.WRITE, true), new RepositoryPermission("group_write", asList("read","modify"), true),
new RepositoryPermission("group_owner", PermissionType.OWNER, true) new RepositoryPermission("group_owner", singletonList("*"), true)
); );
private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest()
.description("GET all permissions") .description("GET all permissions")
@@ -124,11 +126,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
private RepositoryPermissionToRepositoryPermissionDtoMapperImpl permissionToPermissionDtoMapper; private RepositoryPermissionToRepositoryPermissionDtoMapperImpl permissionToPermissionDtoMapper;
@InjectMocks @InjectMocks
private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; private RepositoryPermissionDtoToRepositoryPermissionMapperImpl permissionDtoToPermissionMapper;
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
private PermissionRootResource permissionRootResource; private RepositoryPermissionRootResource repositoryPermissionRootResource;
private final Subject subject = mock(Subject.class); private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject); private final ThreadState subjectThreadState = new SubjectThreadState(subject);
@@ -138,8 +140,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
public void prepareEnvironment() { public void prepareEnvironment() {
initMocks(this); initMocks(this);
repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager); repositoryPermissionRootResource = new RepositoryPermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
super.permissionRootResource = Providers.of(permissionRootResource); super.permissionRootResource = Providers.of(repositoryPermissionRootResource);
dispatcher = createDispatcher(getRepositoryRootResource()); dispatcher = createDispatcher(getRepositoryRootResource());
subjectThreadState.bind(); subjectThreadState.bind();
ThreadContext.bind(subject); ThreadContext.bind(subject);
@@ -232,11 +234,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
public void shouldGet400OnCreatingNewPermissionWithNotAllowedCharacters() throws URISyntaxException { public void shouldGet400OnCreatingNewPermissionWithNotAllowedCharacters() throws URISyntaxException {
// the @ character at the begin of the name is not allowed // the @ character at the begin of the name is not allowed
createUserWithRepository("user"); createUserWithRepository("user");
String permissionJson = "{ \"name\": \"@permission\", \"type\": \"OWNER\" }"; String permissionJson = "{ \"name\": \"@permission\", \"verbs\": [\"*\"] }";
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS) .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content(permissionJson.getBytes()) .content(permissionJson.getBytes())
.contentType(VndMediaType.PERMISSION); .contentType(VndMediaType.REPOSITORY_PERMISSION);
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -244,11 +246,11 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
assertEquals(400, response.getStatus()); assertEquals(400, response.getStatus());
// the whitespace at the begin opf the name is not allowed // the whitespace at the begin opf the name is not allowed
permissionJson = "{ \"name\": \" permission\", \"type\": \"OWNER\" }"; permissionJson = "{ \"name\": \" permission\", \"verbs\": [\"*\"] }";
request = MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS) .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
.content(permissionJson.getBytes()) .content(permissionJson.getBytes())
.contentType(VndMediaType.PERMISSION); .contentType(VndMediaType.REPOSITORY_PERMISSION);
response = new MockHttpResponse(); response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -259,12 +261,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test @Test
public void shouldGetCreatedPermissions() throws URISyntaxException { public void shouldGetCreatedPermissions() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); 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); ArrayList<RepositoryPermission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
permissions.add(newPermission); permissions.add(newPermission);
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(permissions); ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(permissions);
assertExpectedRequest(requestPOSTPermission assertExpectedRequest(requestPOSTPermission
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") .content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : true}")
.expectedResponseStatus(201) .expectedResponseStatus(201)
.responseValidator(response -> assertThat(response.getContentAsString()) .responseValidator(response -> assertThat(response.getContentAsString())
.as("POST response has no body") .as("POST response has no body")
@@ -278,7 +280,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
RepositoryPermission newPermission = TEST_PERMISSIONS.get(0); RepositoryPermission newPermission = TEST_PERMISSIONS.get(0);
assertExpectedRequest(requestPOSTPermission assertExpectedRequest(requestPOSTPermission
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : false}") .content("{\"name\" : \"" + newPermission.getName() + "\" , \"verbs\" : [\"read\",\"pull\",\"push\"], \"groupPermission\" : false}")
.expectedResponseStatus(409) .expectedResponseStatus(409)
); );
} }
@@ -288,10 +290,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
RepositoryPermission modifiedPermission = TEST_PERMISSIONS.get(0); RepositoryPermission modifiedPermission = TEST_PERMISSIONS.get(0);
// modify the type to owner // modify the type to owner
modifiedPermission.setType(PermissionType.OWNER); modifiedPermission.setVerbs(new ArrayList<>(singletonList("*")));
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS);
assertExpectedRequest(requestPUTPermission assertExpectedRequest(requestPUTPermission
.content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"verbs\" : [\"*\"], \"groupPermission\" : false}")
.path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName())
.expectedResponseStatus(204) .expectedResponseStatus(204)
.responseValidator(response -> assertThat(response.getContentAsString()) .responseValidator(response -> assertThat(response.getContentAsString())
@@ -353,7 +355,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
.map(hal -> { .map(hal -> {
RepositoryPermissionDto result = new RepositoryPermissionDto(); RepositoryPermissionDto result = new RepositoryPermissionDto();
result.setName(hal.getAttribute("name").asText()); 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.setGroupPermission(hal.getAttribute("groupPermission").asBoolean());
result.add(hal.getLinks()); result.add(hal.getLinks());
return result; return result;
@@ -382,7 +387,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
RepositoryPermissionDto result = new RepositoryPermissionDto(); RepositoryPermissionDto result = new RepositoryPermissionDto();
result.setName(permission.getName()); result.setName(permission.getName());
result.setGroupPermission(permission.isGroupPermission()); result.setGroupPermission(permission.isGroupPermission());
result.setType(permission.getType().name()); result.setVerbs(permission.getVerbs());
String permissionName = Optional.of(permission.getName()) String permissionName = Optional.of(permission.getName())
.filter(p -> !permission.isGroupPermission()) .filter(p -> !permission.isGroupPermission())
.orElse(GROUP_PREFIX + permission.getName()); .orElse(GROUP_PREFIX + permission.getName());
@@ -425,7 +430,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
HttpRequest request = MockHttpRequest HttpRequest request = MockHttpRequest
.create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path)
.content(entry.content) .content(entry.content)
.contentType(VndMediaType.PERMISSION); .contentType(VndMediaType.REPOSITORY_PERMISSION);
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
log.info("Test the Request :{}", entry); log.info("Test the Request :{}", entry);
assertThat(response.getStatus()) assertThat(response.getStatus())

View File

@@ -8,11 +8,11 @@ import org.junit.runner.RunWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import java.net.URI; import java.net.URI;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@@ -36,7 +36,7 @@ public class RepositoryPermissionToRepositoryPermissionDtoMapperTest {
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldMapGroupPermissionCorrectly() { public void shouldMapGroupPermissionCorrectly() {
Repository repository = getDummyRepository(); 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); RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository);
@@ -48,7 +48,7 @@ public class RepositoryPermissionToRepositoryPermissionDtoMapperTest {
@SubjectAware(username = "trillian", password = "secret") @SubjectAware(username = "trillian", password = "secret")
public void shouldMapNonGroupPermissionCorrectly() { public void shouldMapNonGroupPermissionCorrectly() {
Repository repository = getDummyRepository(); 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); RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository);

View File

@@ -6,7 +6,6 @@ import com.google.common.io.Resources;
import com.google.inject.util.Providers; import com.google.inject.util.Providers;
import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.assertj.core.api.Assertions;
import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse; import org.jboss.resteasy.mock.MockHttpResponse;
@@ -18,8 +17,6 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import sonia.scm.PageResult; import sonia.scm.PageResult;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryIsNotArchivedException;
import sonia.scm.repository.RepositoryManager; 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_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -291,36 +285,14 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
Assertions.assertThat(createCaptor.getValue().getPermissions()) assertThat(createCaptor.getValue().getPermissions())
.hasSize(1) .hasSize(1)
.allSatisfy(p -> { .allSatisfy(p -> {
assertThat(p.getName()).isEqualTo("trillian"); 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 @Test
public void shouldCreateArrayOfProtocolUrls() throws Exception { public void shouldCreateArrayOfProtocolUrls() throws Exception {
mockRepository("space", "repo"); mockRepository("space", "repo");

View File

@@ -16,7 +16,7 @@ public abstract class RepositoryTestBase {
protected Provider<ChangesetRootResource> changesetRootResource; protected Provider<ChangesetRootResource> changesetRootResource;
protected Provider<SourceRootResource> sourceRootResource; protected Provider<SourceRootResource> sourceRootResource;
protected Provider<ContentResource> contentResource; protected Provider<ContentResource> contentResource;
protected Provider<PermissionRootResource> permissionRootResource; protected Provider<RepositoryPermissionRootResource> permissionRootResource;
protected Provider<DiffRootResource> diffRootResource; protected Provider<DiffRootResource> diffRootResource;
protected Provider<ModificationsRootResource> modificationsRootResource; protected Provider<ModificationsRootResource> modificationsRootResource;
protected Provider<FileHistoryRootResource> fileHistoryRootResource; protected Provider<FileHistoryRootResource> fileHistoryRootResource;

View File

@@ -10,8 +10,6 @@ import org.junit.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.HealthCheckFailure;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command; import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryService;
@@ -238,7 +236,6 @@ public class RepositoryToRepositoryDtoMapperTest {
repository.setId("1"); repository.setId("1");
repository.setCreationDate(System.currentTimeMillis()); repository.setCreationDate(System.currentTimeMillis());
repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure"))); repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure")));
repository.setPermissions(singletonList(new RepositoryPermission("permission", PermissionType.READ)));
return repository; return repository;
} }

View File

@@ -42,6 +42,7 @@ public class ResourceLinksMock {
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo));
return resourceLinks; return resourceLinks;
} }

View File

@@ -50,7 +50,6 @@ import sonia.scm.api.rest.ObjectMapperProvider;
import sonia.scm.api.v2.resources.RepositoryDto; import sonia.scm.api.v2.resources.RepositoryDto;
import sonia.scm.api.v2.resources.UserDto; import sonia.scm.api.v2.resources.UserDto;
import sonia.scm.api.v2.resources.UserToUserDtoMapperImpl; import sonia.scm.api.v2.resources.UserToUserDtoMapperImpl;
import sonia.scm.repository.PermissionType;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserTestData; import sonia.scm.user.UserTestData;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
@@ -117,10 +116,6 @@ public class GitLfsITCase {
@Test @Test
public void testLfsAPIWithOwnerPermissions() throws IOException { public void testLfsAPIWithOwnerPermissions() throws IOException {
uploadAndDownloadAsUser(PermissionType.OWNER);
}
private void uploadAndDownloadAsUser(PermissionType permissionType) throws IOException {
User trillian = UserTestData.createTrillian(); User trillian = UserTestData.createTrillian();
trillian.setPassword("secret123"); trillian.setPassword("secret123");
createUser(trillian); createUser(trillian);
@@ -129,8 +124,8 @@ public class GitLfsITCase {
String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref();
IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl))
.accept("*/*") .accept("*/*")
.type(VndMediaType.PERMISSION) .type(VndMediaType.REPOSITORY_PERMISSION)
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"WRITE\"}"); .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"*\"]}");
ScmClient client = new ScmClient(trillian.getId(), "secret123"); 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) { private void createUser(User user) {
UserDto dto = new UserToUserDtoMapperImpl(){ UserDto dto = new UserToUserDtoMapperImpl(){
@Override @Override
@@ -175,8 +165,8 @@ public class GitLfsITCase {
String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref();
IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl))
.accept("*/*") .accept("*/*")
.type(VndMediaType.PERMISSION) .type(VndMediaType.REPOSITORY_PERMISSION)
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"read\"]}");
ScmClient client = new ScmClient(trillian.getId(), "secret123"); ScmClient client = new ScmClient(trillian.getId(), "secret123");
uploadAndDownload(client); uploadAndDownload(client);
@@ -196,8 +186,8 @@ public class GitLfsITCase {
String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref();
IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl))
.accept("*/*") .accept("*/*")
.type(VndMediaType.PERMISSION) .type(VndMediaType.REPOSITORY_PERMISSION)
.post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"verbs\":[\"read\",\"pull\"]}");
// upload data as admin // upload data as admin
String data = UUID.randomUUID().toString(); String data = UUID.randomUUID().toString();

View File

@@ -111,7 +111,6 @@ public class DefaultRepositoryManagerPerfTest {
public void setUpObjectUnderTest(){ public void setUpObjectUnderTest(){
when(repositoryHandler.getType()).thenReturn(new RepositoryType(REPOSITORY_TYPE, REPOSITORY_TYPE, Sets.newHashSet())); when(repositoryHandler.getType()).thenReturn(new RepositoryType(REPOSITORY_TYPE, REPOSITORY_TYPE, Sets.newHashSet()));
Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler); Set<RepositoryHandler> handlerSet = ImmutableSet.of(repositoryHandler);
RepositoryMatcher repositoryMatcher = new RepositoryMatcher(Collections.<RepositoryPathMatcher>emptySet());
NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class);
repositoryManager = new DefaultRepositoryManager( repositoryManager = new DefaultRepositoryManager(
configuration, configuration,
@@ -138,7 +137,7 @@ public class DefaultRepositoryManagerPerfTest {
/** /**
* Start performance test and ensure that the timeout is not reached. * Start performance test and ensure that the timeout is not reached.
*/ */
@Test(timeout = 6000l) @Test(timeout = 6000L)
public void perfTestGetAll(){ public void perfTestGetAll(){
SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret")); SecurityUtils.getSubject().login(new UsernamePasswordToken("trillian", "secret"));
@@ -155,7 +154,7 @@ public class DefaultRepositoryManagerPerfTest {
} }
private long calculateAverage(List<Long> times) { private long calculateAverage(List<Long> times) {
Long sum = 0l; Long sum = 0L;
if(!times.isEmpty()) { if(!times.isEmpty()) {
for (Long time : times) { for (Long time : times) {
sum += time; sum += time;
@@ -183,9 +182,8 @@ private long calculateAverage(List<Long> times) {
} }
private Repository createTestRepository(int number) { private Repository createTestRepository(int number) {
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number); return new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number);
repository.addPermission(new RepositoryPermission("trillian", PermissionType.READ));
return repository;
} }
static class DummyRealm extends AuthorizingRealm { static class DummyRealm extends AuthorizingRealm {

View File

@@ -32,14 +32,12 @@
package sonia.scm.security; package sonia.scm.security;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Test;
import sonia.scm.HandlerEventType; import sonia.scm.HandlerEventType;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent; import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupModificationEvent; import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent; import sonia.scm.repository.RepositoryModificationEvent;
@@ -50,6 +48,14 @@ import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent; import sonia.scm.user.UserModificationEvent;
import sonia.scm.user.UserTestData; 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}. * Unit tests for {@link AuthorizationChangedEventProducer}.
* *
@@ -88,6 +94,11 @@ public class AuthorizationChangedEventProducerTest {
assertEquals(username, producer.event.getNameOfAffectedUser()); 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. * Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.user.UserEvent)} with modified user.
*/ */
@@ -127,11 +138,6 @@ public class AuthorizationChangedEventProducerTest {
assertGlobalEventIsFired(); assertGlobalEventIsFired();
} }
private void assertGlobalEventIsFired(){
assertNotNull(producer.event);
assertFalse(producer.event.isEveryUserAffected());
}
/** /**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups. * Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.group.GroupEvent)} with modified groups.
*/ */
@@ -174,10 +180,10 @@ public class AuthorizationChangedEventProducerTest {
{ {
Repository repositoryModified = RepositoryTestData.createHeartOfGold(); Repository repositoryModified = RepositoryTestData.createHeartOfGold();
repositoryModified.setName("test123"); 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 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)); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository));
assertEventIsNotFired(); assertEventIsNotFired();
@@ -185,18 +191,18 @@ public class AuthorizationChangedEventProducerTest {
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired(); 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)); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired(); 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)); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired(); assertGlobalEventIsFired();
resetStoredEvent(); resetStoredEvent();
repositoryModified.setPermissions( 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)); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired(); assertGlobalEventIsFired();
@@ -204,10 +210,19 @@ public class AuthorizationChangedEventProducerTest {
resetStoredEvent(); resetStoredEvent();
repositoryModified.setPermissions( 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)); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired(); 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(){ private void resetStoredEvent(){

View File

@@ -51,7 +51,6 @@ import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames; import sonia.scm.group.GroupNames;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO; import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
@@ -59,6 +58,7 @@ import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserTestData; import sonia.scm.user.UserTestData;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
@@ -225,10 +225,10 @@ public class DefaultAuthorizationCollectorTest {
authenticate(UserTestData.createTrillian(), group); authenticate(UserTestData.createTrillian(), group);
Repository heartOfGold = RepositoryTestData.createHeartOfGold(); Repository heartOfGold = RepositoryTestData.createHeartOfGold();
heartOfGold.setId("one"); 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(); Repository puzzle42 = RepositoryTestData.create42Puzzle();
puzzle42.setId("two"); 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)); puzzle42.setPermissions(Lists.newArrayList(permission));
when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42)); when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42));

View File

@@ -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;
}
}
}