diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java b/scm-core/src/main/java/sonia/scm/repository/PermissionType.java deleted file mode 100644 index bba0d44f3d..0000000000 --- a/scm-core/src/main/java/sonia/scm/repository/PermissionType.java +++ /dev/null @@ -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; -} diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 622eed6ad6..568b75e525 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -68,7 +68,6 @@ import java.util.Set; @XmlRootElement(name = "repositories") public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ - private static final long serialVersionUID = 3486560714961909711L; private String contact; @@ -81,6 +80,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per private Long lastModified; private String namespace; private String name; + @XmlElement(name = "permission") private final Set permissions = new HashSet<>(); @XmlElement(name = "public") private boolean publicReadable = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java index 0aff771fce..9e132ef93c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -37,12 +37,19 @@ package sonia.scm.repository; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; +import org.apache.commons.collections.CollectionUtils; import sonia.scm.security.PermissionObject; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedHashSet; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableCollection; //~--- JDK imports ------------------------------------------------------------ @@ -60,54 +67,19 @@ public class RepositoryPermission implements PermissionObject, Serializable private boolean groupPermission = false; private String name; - private PermissionType type = PermissionType.READ; + @XmlElement(name = "verb") + private Collection verbs; /** * Constructs a new {@link RepositoryPermission}. - * This constructor is used by JAXB. - * + * This constructor is used by JAXB and mapstruct. */ public RepositoryPermission() {} - /** - * Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ} - * for the specified user. - * - * - * @param name name of the user - */ - public RepositoryPermission(String name) + public RepositoryPermission(String name, Collection verbs, boolean groupPermission) { - this(); this.name = name; - } - - /** - * Constructs a new {@link RepositoryPermission} with the specified type for - * the given user. - * - * - * @param name name of the user - * @param type type of the permission - */ - public RepositoryPermission(String name, PermissionType type) - { - this(name); - this.type = type; - } - - /** - * Constructs a new {@link RepositoryPermission} with the specified type for - * the given user or group. - * - * - * @param name name of the user or group - * @param type type of the permission - * @param groupPermission true if the permission is a permission for a group - */ - public RepositoryPermission(String name, PermissionType type, boolean groupPermission) - { - this(name, type); + this.verbs = unmodifiableCollection(new LinkedHashSet<>(verbs)); this.groupPermission = groupPermission; } @@ -137,7 +109,7 @@ public class RepositoryPermission implements PermissionObject, Serializable final RepositoryPermission other = (RepositoryPermission) obj; return Objects.equal(name, other.name) - && Objects.equal(type, other.type) + && CollectionUtils.isEqualCollection(verbs, other.verbs) && Objects.equal(groupPermission, other.groupPermission); } @@ -150,7 +122,9 @@ public class RepositoryPermission implements PermissionObject, Serializable @Override public int hashCode() { - return Objects.hashCode(name, type, groupPermission); + // Normally we do not have a log of repository permissions having the same size of verbs, but different content. + // Therefore we do not use the verbs themselves for the hash code but only the number of verbs. + return Objects.hashCode(name, verbs.size(), groupPermission); } @@ -160,7 +134,7 @@ public class RepositoryPermission implements PermissionObject, Serializable //J- return MoreObjects.toStringHelper(this) .add("name", name) - .add("type", type) + .add("verbs", verbs) .add("groupPermission", groupPermission) .toString(); //J+ @@ -181,14 +155,14 @@ public class RepositoryPermission implements PermissionObject, Serializable } /** - * Returns the {@link PermissionType} of the permission. + * Returns the verb of the permission. * * - * @return {@link PermissionType} of the permission + * @return verb of the permission */ - public PermissionType getType() + public Collection 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 verbs) { - this.type = type; + this.verbs = verbs; } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java index 6c7c620fa4..6098bdf92b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/IncomingCommandBuilder.java @@ -39,12 +39,11 @@ import org.apache.shiro.subject.Subject; import sonia.scm.cache.CacheManager; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.IncomingCommand; import sonia.scm.repository.spi.IncomingCommandRequest; -import sonia.scm.security.RepositoryPermission; import java.io.IOException; @@ -94,8 +93,7 @@ public final class IncomingCommandBuilder { Subject subject = SecurityUtils.getSubject(); - subject.checkPermission(new RepositoryPermission(remoteRepository, - PermissionType.READ)); + subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString()); request.setRemoteRepository(remoteRepository); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java index 2753128eac..d39c95e0e2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/OutgoingCommandBuilder.java @@ -34,12 +34,11 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import sonia.scm.cache.CacheManager; import sonia.scm.repository.ChangesetPagingResult; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.OutgoingCommand; import sonia.scm.repository.spi.OutgoingCommandRequest; -import sonia.scm.security.RepositoryPermission; import java.io.IOException; @@ -84,8 +83,7 @@ public final class OutgoingCommandBuilder { Subject subject = SecurityUtils.getSubject(); - subject.checkPermission(new RepositoryPermission(remoteRepository, - PermissionType.READ)); + subject.isPermitted(RepositoryPermissions.pull(remoteRepository).asShiroString()); request.setRemoteRepository(remoteRepository); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java index a0f5ff4115..969ec6ef11 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PullCommandBuilder.java @@ -38,11 +38,10 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.PullCommand; import sonia.scm.repository.spi.PullCommandRequest; -import sonia.scm.security.RepositoryPermission; import java.io.IOException; import java.net.URL; @@ -96,9 +95,7 @@ public final class PullCommandBuilder public PullResponse pull(String url) throws IOException { Subject subject = SecurityUtils.getSubject(); //J- - subject.checkPermission( - new RepositoryPermission(localRepository, PermissionType.WRITE) - ); + subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString()); //J+ URL remoteUrl = new URL(url); @@ -124,12 +121,8 @@ public final class PullCommandBuilder Subject subject = SecurityUtils.getSubject(); //J- - subject.checkPermission( - new RepositoryPermission(localRepository, PermissionType.WRITE) - ); - subject.checkPermission( - new RepositoryPermission(remoteRepository, PermissionType.READ) - ); + subject.isPermitted(RepositoryPermissions.push(localRepository).asShiroString()); + subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString()); //J+ request.reset(); diff --git a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java index 7b318e49ec..a734225281 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/PushCommandBuilder.java @@ -39,11 +39,10 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.PushCommand; import sonia.scm.repository.spi.PushCommandRequest; -import sonia.scm.security.RepositoryPermission; import java.io.IOException; import java.net.URL; @@ -92,9 +91,7 @@ public final class PushCommandBuilder Subject subject = SecurityUtils.getSubject(); //J- - subject.checkPermission( - new RepositoryPermission(remoteRepository, PermissionType.WRITE) - ); + subject.isPermitted(RepositoryPermissions.push(remoteRepository).asShiroString()); //J+ logger.info("push changes to repository {}", remoteRepository.getId()); diff --git a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java b/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java deleted file mode 100644 index 1b0229d6f5..0000000000 --- a/scm-core/src/main/java/sonia/scm/security/RepositoryPermission.java +++ /dev/null @@ -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; -} diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 8596bab754..19859b876b 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -20,7 +20,7 @@ public class VndMediaType { public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; - public static final String PERMISSION = PREFIX + "permission" + SUFFIX; + public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX; @@ -33,6 +33,7 @@ public class VndMediaType { public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; public static final String BRANCH_COLLECTION = PREFIX + "branchCollection" + SUFFIX; public static final String CONFIG = PREFIX + "config" + SUFFIX; + public static final String REPOSITORY_PERMISSION_COLLECTION = PREFIX + "repositoryPermissionCollection" + SUFFIX; public static final String REPOSITORY_TYPE_COLLECTION = PREFIX + "repositoryTypeCollection" + SUFFIX; public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index 328494a626..a062fdb360 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -252,7 +252,7 @@ public abstract class PermissionFilter extends ScmProviderHttpServletDecorator } else { - permitted = RepositoryPermissions.read(repository).isPermitted(); + permitted = RepositoryPermissions.pull(repository).isPermitted(); } return permitted; diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java new file mode 100644 index 0000000000..2e9383b2e2 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionTest.java @@ -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); + } +} diff --git a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java b/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java deleted file mode 100644 index e8180ca24a..0000000000 --- a/scm-core/src/test/java/sonia/scm/security/RepositoryPermissionTest.java +++ /dev/null @@ -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))); - } -} diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini index fbdd35ba50..fda268ec83 100644 --- a/scm-core/src/test/resources/sonia/scm/shiro.ini +++ b/scm-core/src/test/resources/sonia/scm/shiro.ini @@ -8,5 +8,5 @@ unpriv = secret [roles] admin = * user = something:* -repo_read = "repository:read:1" -repo_write = "repository:push:1" +repo_read = "repository:read,pull:1" +repo_write = "repository:read,write,pull,push:1" diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java index 6330db56a0..aebdf010e2 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java @@ -16,6 +16,7 @@ import sonia.scm.io.FileSystem; import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryTestData; import java.io.IOException; @@ -24,8 +25,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Clock; import java.util.Collection; +import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -70,9 +73,7 @@ class XmlRepositoryDAOTest { Clock clock = mock(Clock.class); when(clock.millis()).then(ic -> atomicClock.incrementAndGet()); - XmlRepositoryDAO dao = new XmlRepositoryDAO(context, locationResolver, fileSystem, clock); - - return dao; + return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock); } @Test @@ -329,6 +330,21 @@ class XmlRepositoryDAOTest { assertThat(content).contains("Awesome Spaceship"); } + @Test + void shouldPersistPermissions() throws IOException { + Repository heartOfGold = createHeartOfGold(); + heartOfGold.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", Collections.singletonList("delete"), true))); + dao.add(heartOfGold); + + Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId()); + Path metadataPath = dao.resolveMetadataPath(repositoryDirectory); + + String content = content(metadataPath); + System.out.println(content); + assertThat(content).containsSubsequence("trillian", "read", "write"); + assertThat(content).containsSubsequence("vogons", "delete"); + } + @Test void shouldReadPathDatabaseAndMetadataOfRepositories() { Repository heartOfGold = createHeartOfGold(); diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index aa91e67022..926be5459f 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -42,7 +42,6 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import sonia.scm.it.utils.RepositoryUtil; import sonia.scm.it.utils.TestData; -import sonia.scm.repository.PermissionType; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClientException; import sonia.scm.web.VndMediaType; @@ -59,7 +58,10 @@ import static org.junit.Assert.assertNull; import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile; import static sonia.scm.it.utils.RestUtil.given; import static sonia.scm.it.utils.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.TestData.OWNER; +import static sonia.scm.it.utils.TestData.READ; import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; +import static sonia.scm.it.utils.TestData.WRITE; import static sonia.scm.it.utils.TestData.callRepository; @RunWith(Parameterized.class) @@ -91,11 +93,11 @@ public class PermissionsITCase { public void prepareEnvironment() { TestData.createDefault(); TestData.createNotAdminUser(USER_READ, USER_PASS); - TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType); + TestData.createUserPermission(USER_READ, READ, repositoryType); TestData.createNotAdminUser(USER_WRITE, USER_PASS); - TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType); + TestData.createUserPermission(USER_WRITE, WRITE, repositoryType); TestData.createNotAdminUser(USER_OWNER, USER_PASS); - TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType); + TestData.createUserPermission(USER_OWNER, OWNER, repositoryType); TestData.createNotAdminUser(USER_OTHER, USER_PASS); createdPermissions = asList(USER_READ, USER_WRITE, USER_OWNER); } @@ -109,7 +111,7 @@ public class PermissionsITCase { @Test public void readUserShouldNotSeeBruteForcePermissions() { - given(VndMediaType.PERMISSION, USER_READ, USER_PASS) + given(VndMediaType.REPOSITORY_PERMISSION, USER_READ, USER_PASS) .when() .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .then() @@ -125,7 +127,7 @@ public class PermissionsITCase { @Test public void writeUserShouldNotSeeBruteForcePermissions() { - given(VndMediaType.PERMISSION, USER_WRITE, USER_PASS) + given(VndMediaType.REPOSITORY_PERMISSION, USER_WRITE, USER_PASS) .when() .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .then() @@ -145,7 +147,7 @@ public class PermissionsITCase { @Test public void otherUserShouldNotSeeBruteForcePermissions() { - given(VndMediaType.PERMISSION, USER_OTHER, USER_PASS) + given(VndMediaType.REPOSITORY_PERMISSION, USER_OTHER, USER_PASS) .when() .get(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) .then() diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index 48605437c6..584737221f 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -4,15 +4,16 @@ import io.restassured.response.ValidatableResponse; import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.repository.PermissionType; import sonia.scm.web.VndMediaType; import javax.json.Json; import javax.json.JsonObjectBuilder; import java.net.URI; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static sonia.scm.it.utils.RestUtil.createResourceUrl; @@ -25,6 +26,11 @@ public class TestData { public static final String USER_SCM_ADMIN = "scmadmin"; public static final String USER_ANONYMOUS = "anonymous"; + + public static final Collection READ = asList("read", "pull"); + public static final Collection WRITE = asList("read", "write", "pull", "push"); + public static final Collection OWNER = asList("*"); + private static final List PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static Map 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 permissionType, String repositoryType) { String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl); - given(VndMediaType.PERMISSION) + given(VndMediaType.REPOSITORY_PERMISSION) .when() .content("{\n" + - "\t\"type\": \"" + permissionType.name() + "\",\n" + + "\t\"verbs\": " + permissionType.stream().collect(Collectors.joining("\",\"", "[\"", "\"]")) + ",\n" + "\t\"name\": \"" + name + "\",\n" + "\t\"groupPermission\": false\n" + "\t\n" + @@ -106,7 +112,7 @@ public class TestData { } public static ValidatableResponse callUserPermissions(String username, String password, String repositoryType, int expectedStatusCode) { - return given(VndMediaType.PERMISSION, username, password) + return given(VndMediaType.REPOSITORY_PERMISSION, username, password) .when() .get(TestData.getDefaultPermissionUrl(username, password, repositoryType)) .then() diff --git a/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js b/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js index b39098d3a1..3df3e78680 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js +++ b/scm-ui-components/packages/ui-components/src/buttons/CreateButton.js @@ -1,27 +1,28 @@ -//@flow -import React from "react"; -import injectSheet from "react-jss"; -import SubmitButton, { type ButtonProps } from "./SubmitButton"; -import classNames from "classnames"; - -const styles = { - spacing: { - marginTop: "2em", - border: "2px solid #e9f7fd", - padding: "1em 1em" - } - -}; - -class CreateButton extends React.Component { - render() { - const { classes } = this.props; - return ( -
- -
- ); - } -} - -export default injectSheet(styles)(CreateButton); +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import { type ButtonProps } from "./Button"; +import SubmitButton from "./SubmitButton"; +import classNames from "classnames"; + +const styles = { + spacing: { + marginTop: "2em", + border: "2px solid #e9f7fd", + padding: "1em 1em" + } + +}; + +class CreateButton extends React.Component { + render() { + const { classes } = this.props; + return ( +
+ +
+ ); + } +} + +export default injectSheet(styles)(CreateButton); diff --git a/scm-ui-components/packages/ui-components/src/forms/Select.js b/scm-ui-components/packages/ui-components/src/forms/Select.js index ccb82e62da..38e1cdab33 100644 --- a/scm-ui-components/packages/ui-components/src/forms/Select.js +++ b/scm-ui-components/packages/ui-components/src/forms/Select.js @@ -54,7 +54,7 @@ class Select extends React.Component { > {options.map(opt => { return ( - ); diff --git a/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js new file mode 100644 index 0000000000..ab7e8d82e4 --- /dev/null +++ b/scm-ui-components/packages/ui-types/src/AvailableRepositoryPermissions.js @@ -0,0 +1,11 @@ +// @flow + +export type RepositoryRole = { + name: string, + verbs: string[] +}; + +export type AvailableRepositoryPermissions = { + availableVerbs: string[], + availableRoles: RepositoryRole[] +}; diff --git a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js index 4352c21da6..ed3c925283 100644 --- a/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js +++ b/scm-ui-components/packages/ui-types/src/RepositoryPermissions.js @@ -7,7 +7,7 @@ export type Permission = PermissionCreateEntry & { export type PermissionCreateEntry = { name: string, - type: string, + verbs: string[], groupPermission: boolean } diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index cf739f747d..f7b375ac98 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -24,3 +24,5 @@ export type { Permission, PermissionCreateEntry, PermissionCollection } from "./ export type { SubRepository, File } from "./Sources"; export type { SelectValue, AutocompleteObject } from "./Autocomplete"; + +export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions"; diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index d6cdaa1d8d..7eef6f79b1 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -87,44 +87,56 @@ "label": "Branches" }, "permission": { - "user": "User", - "group": "Group", - "error-title": "Error", - "error-subtitle": "Unknown permissions error", - "name": "User or Group", - "type": "Type", - "group-permission": "Group Permission", - "user-permission": "User Permission", - "edit-permission": { - "delete-button": "Delete", - "save-button": "Save Changes" - }, - "delete-permission-button": { - "label": "Delete", - "confirm-alert": { - "title": "Delete permission", - "message": "Do you really want to delete the permission?", - "submit": "Yes", - "cancel": "No" - } - }, - "add-permission": { - "add-permission-heading": "Add new Permission", - "submit-button": "Submit", - "name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!" - }, - "help": { - "groupPermissionHelpText": "States if a permission is a group permission.", - "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" - }, - "autocomplete": { - "no-group-options": "No group suggestion available", - "group-placeholder": "Enter group", - "no-user-options": "No user suggestion available", - "user-placeholder": "Enter user", - "loading": "Loading..." + "user": "User", + "group": "Group", + "error-title": "Error", + "error-subtitle": "Unknown permissions error", + "name": "User or group", + "role": "Role", + "permissions": "Permissions", + "group-permission": "Group Permission", + "user-permission": "User Permission", + "edit-permission": { + "delete-button": "Delete", + "save-button": "Save Changes" + }, + "advanced-button": { + "label": "Advanced" + }, + "delete-permission-button": { + "label": "Delete", + "confirm-alert": { + "title": "Delete permission", + "message": "Do you really want to delete the permission?", + "submit": "Yes", + "cancel": "No" } + }, + "add-permission": { + "add-permission-heading": "Add new Permission", + "submit-button": "Submit", + "name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!" + }, + "help": { + "groupPermissionHelpText": "States if a permission is a group permission. If this is not checked, it is a user permission.", + "nameHelpText": "Manage permissions for a specific user or group", + "roleHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions. If nothing is selected here, use the 'Advanced' Button to see detailed permissions.", + "permissionsHelpText": "Use this to specify your own set of permissions regardless of predefined roles" + }, + "autocomplete": { + "no-group-options": "No group suggestion available", + "group-placeholder": "Enter group", + "no-user-options": "No user suggestion available", + "user-placeholder": "Enter user", + "loading": "Loading..." + }, + "advanced": { + "dialog": { + "title": "Advanced permissions", + "submit": "Submit", + "abort": "Abort" + } + } }, "help": { "nameHelpText": "The name of the repository. This name will be part of the repository url.", diff --git a/scm-ui/src/repos/permissions/components/PermissionCheckbox.js b/scm-ui/src/repos/permissions/components/PermissionCheckbox.js new file mode 100644 index 0000000000..4ee6b6b768 --- /dev/null +++ b/scm-ui/src/repos/permissions/components/PermissionCheckbox.js @@ -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 { + render() { + const { t } = this.props; + return ( + + ); + } +} + +export default translate("plugins")(PermissionCheckbox); diff --git a/scm-ui/src/repos/permissions/components/RoleSelector.js b/scm-ui/src/repos/permissions/components/RoleSelector.js new file mode 100644 index 0000000000..d472f17c4b --- /dev/null +++ b/scm-ui/src/repos/permissions/components/RoleSelector.js @@ -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 { + render() { + const { + availableRoles, + role, + handleRoleChange, + loading, + label, + helpText + } = this.props; + + if (!availableRoles) return null; + + const options = role + ? this.createSelectOptions(availableRoles) + : ["", ...this.createSelectOptions(availableRoles)]; + + return ( + - ); - } - - createSelectOptions(types: string[]) { - return types.map(type => { - return { - label: type, - value: type - }; - }); - } -} - -export default translate("repos")(TypeSelector); diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.test.js b/scm-ui/src/repos/permissions/components/permissionValidation.test.js index b2e7e8be68..1149075537 100644 --- a/scm-ui/src/repos/permissions/components/permissionValidation.test.js +++ b/scm-ui/src/repos/permissions/components/permissionValidation.test.js @@ -18,7 +18,8 @@ describe("permission validation", () => { name: "PermissionName", groupPermission: true, type: "READ", - _links: {} + _links: {}, + verbs: [] } ]; const name = "PermissionName"; @@ -35,7 +36,8 @@ describe("permission validation", () => { name: "PermissionName", groupPermission: false, type: "READ", - _links: {} + _links: {}, + verbs: [] } ]; const name = "PermissionName"; diff --git a/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js b/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js new file mode 100644 index 0000000000..0d844fdf3f --- /dev/null +++ b/scm-ui/src/repos/permissions/containers/AdvancedPermissionsDialog.js @@ -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 { + 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 => ( + + )); + + const submitButton = !readOnly ? ( + + ) : null; + + return ( +
+
+
+
+

+ {t("permission.advanced.dialog.title")} +

+
+
+
{verbSelectBoxes}
+
+ {submitButton} +
+
+
+ ); + } + + 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); diff --git a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js similarity index 53% rename from scm-ui/src/repos/permissions/components/CreatePermissionForm.js rename to scm-ui/src/repos/permissions/containers/CreatePermissionForm.js index 488ed7ce2b..362b4aa48a 100644 --- a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js +++ b/scm-ui/src/repos/permissions/containers/CreatePermissionForm.js @@ -1,17 +1,26 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { Autocomplete, Radio, SubmitButton } from "@scm-manager/ui-components"; -import TypeSelector from "./TypeSelector"; +import { + Autocomplete, + SubmitButton, + Button, + LabelWithHelpIcon +} from "@scm-manager/ui-components"; +import RoleSelector from "../components/RoleSelector"; import type { + AvailableRepositoryPermissions, PermissionCollection, PermissionCreateEntry, SelectValue } from "@scm-manager/ui-types"; -import * as validator from "./permissionValidation"; +import * as validator from "../components/permissionValidation"; +import { findMatchingRoleName } from "../modules/permissions"; +import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; type Props = { t: string => string, + availablePermissions: AvailableRepositoryPermissions, createPermission: (permission: PermissionCreateEntry) => void, loading: boolean, currentPermissions: PermissionCollection, @@ -21,10 +30,11 @@ type Props = { type State = { name: string, - type: string, + verbs: string[], groupPermission: boolean, valid: boolean, - value?: SelectValue + value?: SelectValue, + showAdvancedDialog: boolean }; class CreatePermissionForm extends React.Component { @@ -33,10 +43,11 @@ class CreatePermissionForm extends React.Component { this.state = { name: "", - type: "READ", + verbs: props.availablePermissions.availableRoles[0].verbs, groupPermission: false, valid: true, - value: undefined + value: undefined, + showAdvancedDialog: false }; } @@ -121,9 +132,23 @@ class CreatePermissionForm extends React.Component { }; 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 ? ( + + ) : null; return (
@@ -131,32 +156,57 @@ class CreatePermissionForm extends React.Component {

{t("permission.add-permission.add-permission-heading")}

+ {advancedDialog}
- - +
+ + +
+
-
+
{this.renderAutocompletionField()}
-
- +
+
+
+ +
+
+ +
+
@@ -173,10 +223,25 @@ class CreatePermissionForm extends React.Component { ); } + handleDetailedPermissionsPressed = () => { + this.setState({ showAdvancedDialog: true }); + }; + + closeAdvancedPermissionsDialog = () => { + this.setState({ showAdvancedDialog: false }); + }; + + submitAdvancedPermissionsDialog = (newVerbs: string[]) => { + this.setState({ + showAdvancedDialog: false, + verbs: newVerbs + }); + }; + submit = e => { this.props.createPermission({ name: this.state.name, - type: this.state.type, + verbs: this.state.verbs, groupPermission: this.state.groupPermission }); this.removeState(); @@ -186,17 +251,24 @@ class CreatePermissionForm extends React.Component { removeState = () => { this.setState({ name: "", - type: "READ", + verbs: this.props.availablePermissions.availableRoles[0].verbs, groupPermission: false, valid: true }); }; - handleTypeChange = (type: string) => { + handleRoleChange = (role: string) => { + const selectedRole = this.findAvailableRole(role); this.setState({ - type: type + verbs: selectedRole.verbs }); }; + + findAvailableRole = (roleName: string) => { + return this.props.availablePermissions.availableRoles.find( + role => role.name === roleName + ); + }; } export default translate("repos")(CreatePermissionForm); diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js index 48f4e585f7..7c20f503c5 100644 --- a/scm-ui/src/repos/permissions/containers/Permissions.js +++ b/scm-ui/src/repos/permissions/containers/Permissions.js @@ -1,225 +1,268 @@ -//@flow -import React from "react"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; -import { - fetchPermissions, - getFetchPermissionsFailure, - isFetchPermissionsPending, - getPermissionsOfRepo, - hasCreatePermission, - createPermission, - isCreatePermissionPending, - getCreatePermissionFailure, - createPermissionReset, - getDeletePermissionsFailure, - getModifyPermissionsFailure, - modifyPermissionReset, - deletePermissionReset -} from "../modules/permissions"; -import { Loading, ErrorPage } from "@scm-manager/ui-components"; -import type { - Permission, - PermissionCollection, - PermissionCreateEntry -} from "@scm-manager/ui-types"; -import SinglePermission from "./SinglePermission"; -import CreatePermissionForm from "../components/CreatePermissionForm"; -import type { History } from "history"; -import { getPermissionsLink } from "../../modules/repos"; -import { - getGroupAutoCompleteLink, - getUserAutoCompleteLink -} from "../../../modules/indexResource"; - -type Props = { - namespace: string, - repoName: string, - loading: boolean, - error: Error, - permissions: PermissionCollection, - hasPermissionToCreate: boolean, - loadingCreatePermission: boolean, - permissionsLink: string, - groupAutoCompleteLink: string, - userAutoCompleteLink: string, - - //dispatch functions - fetchPermissions: (link: string, namespace: string, repoName: string) => void, - createPermission: ( - link: string, - permission: PermissionCreateEntry, - namespace: string, - repoName: string, - callback?: () => void - ) => void, - createPermissionReset: (string, string) => void, - modifyPermissionReset: (string, string) => void, - deletePermissionReset: (string, string) => void, - // context props - t: string => string, - match: any, - history: History -}; - -class Permissions extends React.Component { - componentDidMount() { - const { - fetchPermissions, - namespace, - repoName, - modifyPermissionReset, - createPermissionReset, - deletePermissionReset, - permissionsLink - } = this.props; - - createPermissionReset(namespace, repoName); - modifyPermissionReset(namespace, repoName); - deletePermissionReset(namespace, repoName); - fetchPermissions(permissionsLink, namespace, repoName); - } - - createPermission = (permission: Permission) => { - this.props.createPermission( - this.props.permissionsLink, - permission, - this.props.namespace, - this.props.repoName - ); - }; - - render() { - const { - loading, - error, - permissions, - t, - namespace, - repoName, - loadingCreatePermission, - hasPermissionToCreate, - userAutoCompleteLink, - groupAutoCompleteLink - } = this.props; - if (error) { - return ( - - ); - } - - if (loading || !permissions) { - return ; - } - - const createPermissionForm = hasPermissionToCreate ? ( - this.createPermission(permission)} - loading={loadingCreatePermission} - currentPermissions={permissions} - userAutoCompleteLink={userAutoCompleteLink} - groupAutoCompleteLink={groupAutoCompleteLink} - /> - ) : null; - - return ( -
- - - - - - - - - - {permissions.map(permission => { - return ( - - ); - })} - -
{t("permission.name")} - {t("permission.group-permission")} - {t("permission.type")} -
- {createPermissionForm} -
- ); - } -} - -const mapStateToProps = (state, ownProps) => { - const namespace = ownProps.namespace; - const repoName = ownProps.repoName; - const error = - getFetchPermissionsFailure(state, namespace, repoName) || - getCreatePermissionFailure(state, namespace, repoName) || - getDeletePermissionsFailure(state, namespace, repoName) || - getModifyPermissionsFailure(state, namespace, repoName); - const loading = isFetchPermissionsPending(state, namespace, repoName); - const permissions = getPermissionsOfRepo(state, namespace, repoName); - const loadingCreatePermission = isCreatePermissionPending( - state, - namespace, - repoName - ); - const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); - const permissionsLink = getPermissionsLink(state, namespace, repoName); - const groupAutoCompleteLink = getGroupAutoCompleteLink(state); - const userAutoCompleteLink = getUserAutoCompleteLink(state); - return { - namespace, - repoName, - error, - loading, - permissions, - hasPermissionToCreate, - loadingCreatePermission, - permissionsLink, - groupAutoCompleteLink, - userAutoCompleteLink - }; -}; - -const mapDispatchToProps = dispatch => { - return { - fetchPermissions: (link: string, namespace: string, repoName: string) => { - dispatch(fetchPermissions(link, namespace, repoName)); - }, - createPermission: ( - link: string, - permission: PermissionCreateEntry, - namespace: string, - repoName: string, - callback?: () => void - ) => { - dispatch( - createPermission(link, permission, namespace, repoName, callback) - ); - }, - createPermissionReset: (namespace: string, repoName: string) => { - dispatch(createPermissionReset(namespace, repoName)); - }, - modifyPermissionReset: (namespace: string, repoName: string) => { - dispatch(modifyPermissionReset(namespace, repoName)); - }, - deletePermissionReset: (namespace: string, repoName: string) => { - dispatch(deletePermissionReset(namespace, repoName)); - } - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("repos")(Permissions)); +//@flow +import React from "react"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { + fetchAvailablePermissionsIfNeeded, + fetchPermissions, + getFetchAvailablePermissionsFailure, + getAvailablePermissions, + getFetchPermissionsFailure, + isFetchAvailablePermissionsPending, + isFetchPermissionsPending, + getPermissionsOfRepo, + hasCreatePermission, + createPermission, + isCreatePermissionPending, + getCreatePermissionFailure, + createPermissionReset, + getDeletePermissionsFailure, + getModifyPermissionsFailure, + modifyPermissionReset, + deletePermissionReset +} from "../modules/permissions"; +import { + Loading, + ErrorPage, + LabelWithHelpIcon +} from "@scm-manager/ui-components"; +import type { + AvailableRepositoryPermissions, + Permission, + PermissionCollection, + PermissionCreateEntry +} from "@scm-manager/ui-types"; +import SinglePermission from "./SinglePermission"; +import CreatePermissionForm from "./CreatePermissionForm"; +import type { History } from "history"; +import { getPermissionsLink } from "../../modules/repos"; +import { + getGroupAutoCompleteLink, + getUserAutoCompleteLink +} from "../../../modules/indexResource"; + +type Props = { + availablePermissions: AvailableRepositoryPermissions, + namespace: string, + repoName: string, + loading: boolean, + error: Error, + permissions: PermissionCollection, + hasPermissionToCreate: boolean, + loadingCreatePermission: boolean, + permissionsLink: string, + groupAutoCompleteLink: string, + userAutoCompleteLink: string, + + //dispatch functions + fetchAvailablePermissionsIfNeeded: () => void, + fetchPermissions: (link: string, namespace: string, repoName: string) => void, + createPermission: ( + link: string, + permission: PermissionCreateEntry, + namespace: string, + repoName: string, + callback?: () => void + ) => void, + createPermissionReset: (string, string) => void, + modifyPermissionReset: (string, string) => void, + deletePermissionReset: (string, string) => void, + // context props + t: string => string, + match: any, + history: History +}; + +class Permissions extends React.Component { + componentDidMount() { + const { + fetchAvailablePermissionsIfNeeded, + fetchPermissions, + namespace, + repoName, + modifyPermissionReset, + createPermissionReset, + deletePermissionReset, + permissionsLink + } = this.props; + + createPermissionReset(namespace, repoName); + modifyPermissionReset(namespace, repoName); + deletePermissionReset(namespace, repoName); + fetchAvailablePermissionsIfNeeded(); + fetchPermissions(permissionsLink, namespace, repoName); + } + + createPermission = (permission: Permission) => { + this.props.createPermission( + this.props.permissionsLink, + permission, + this.props.namespace, + this.props.repoName + ); + }; + + render() { + const { + availablePermissions, + loading, + error, + permissions, + t, + namespace, + repoName, + loadingCreatePermission, + hasPermissionToCreate, + userAutoCompleteLink, + groupAutoCompleteLink + } = this.props; + if (error) { + return ( + + ); + } + + if (loading || !permissions || !availablePermissions) { + return ; + } + + const createPermissionForm = hasPermissionToCreate ? ( + this.createPermission(permission)} + loading={loadingCreatePermission} + currentPermissions={permissions} + userAutoCompleteLink={userAutoCompleteLink} + groupAutoCompleteLink={groupAutoCompleteLink} + /> + ) : null; + + return ( +
+ + + + + + + + + + + {permissions.map(permission => { + return ( + + ); + })} + +
+ + + + + + + + +
+ {createPermissionForm} +
+ ); + } +} + +const mapStateToProps = (state, ownProps) => { + const namespace = ownProps.namespace; + const repoName = ownProps.repoName; + const error = + getFetchPermissionsFailure(state, namespace, repoName) || + getCreatePermissionFailure(state, namespace, repoName) || + getDeletePermissionsFailure(state, namespace, repoName) || + getModifyPermissionsFailure(state, namespace, repoName) || + getFetchAvailablePermissionsFailure(state); + const loading = + isFetchPermissionsPending(state, namespace, repoName) || + isFetchAvailablePermissionsPending(state); + const permissions = getPermissionsOfRepo(state, namespace, repoName); + const loadingCreatePermission = isCreatePermissionPending( + state, + namespace, + repoName + ); + const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName); + const permissionsLink = getPermissionsLink(state, namespace, repoName); + const groupAutoCompleteLink = getGroupAutoCompleteLink(state); + const userAutoCompleteLink = getUserAutoCompleteLink(state); + const availablePermissions = getAvailablePermissions(state); + return { + availablePermissions, + namespace, + repoName, + error, + loading, + permissions, + hasPermissionToCreate, + loadingCreatePermission, + permissionsLink, + groupAutoCompleteLink, + userAutoCompleteLink + }; +}; + +const mapDispatchToProps = dispatch => { + return { + fetchPermissions: (link: string, namespace: string, repoName: string) => { + dispatch(fetchPermissions(link, namespace, repoName)); + }, + fetchAvailablePermissionsIfNeeded: () => { + dispatch(fetchAvailablePermissionsIfNeeded()); + }, + createPermission: ( + link: string, + permission: PermissionCreateEntry, + namespace: string, + repoName: string, + callback?: () => void + ) => { + dispatch( + createPermission(link, permission, namespace, repoName, callback) + ); + }, + createPermissionReset: (namespace: string, repoName: string) => { + dispatch(createPermissionReset(namespace, repoName)); + }, + modifyPermissionReset: (namespace: string, repoName: string) => { + dispatch(modifyPermissionReset(namespace, repoName)); + }, + deletePermissionReset: (namespace: string, repoName: string) => { + dispatch(deletePermissionReset(namespace, repoName)); + } + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("repos")(Permissions)); diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js index 62380128be..b16d26e903 100644 --- a/scm-ui/src/repos/permissions/containers/SinglePermission.js +++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js @@ -1,176 +1,256 @@ -// @flow -import React from "react"; -import type { Permission } from "@scm-manager/ui-types"; -import { translate } from "react-i18next"; -import { - modifyPermission, - isModifyPermissionPending, - deletePermission, - isDeletePermissionPending -} from "../modules/permissions"; -import { connect } from "react-redux"; -import type { History } from "history"; -import { Checkbox } from "@scm-manager/ui-components"; -import DeletePermissionButton from "../components/buttons/DeletePermissionButton"; -import TypeSelector from "../components/TypeSelector"; - -type Props = { - submitForm: Permission => void, - modifyPermission: (Permission, string, string) => void, - permission: Permission, - t: string => string, - namespace: string, - repoName: string, - match: any, - history: History, - loading: boolean, - deletePermission: (Permission, string, string) => void, - deleteLoading: boolean -}; - -type State = { - permission: Permission -}; - -class SinglePermission extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - permission: { - name: "", - type: "READ", - groupPermission: false, - _links: {} - } - }; - } - - componentDidMount() { - const { permission } = this.props; - if (permission) { - this.setState({ - permission: { - name: permission.name, - type: permission.type, - groupPermission: permission.groupPermission, - _links: permission._links - } - }); - } - } - - deletePermission = () => { - this.props.deletePermission( - this.props.permission, - this.props.namespace, - this.props.repoName - ); - }; - - render() { - const { permission } = this.state; - const { loading, namespace, repoName } = this.props; - const typeSelector = - this.props.permission._links && this.props.permission._links.update ? ( - - - - ) : ( - {permission.type} - ); - - return ( - - {permission.name} - - - - {typeSelector} - - - - - ); - } - - handleTypeChange = (type: string) => { - this.setState({ - permission: { - ...this.state.permission, - type: type - } - }); - this.modifyPermission(type); - }; - - modifyPermission = (type: string) => { - let permission = this.state.permission; - permission.type = type; - this.props.modifyPermission( - permission, - this.props.namespace, - this.props.repoName - ); - }; - - createSelectOptions(types: string[]) { - return types.map(type => { - return { - label: type, - value: type - }; - }); - } -} - -const mapStateToProps = (state, ownProps) => { - const permission = ownProps.permission; - const loading = isModifyPermissionPending( - state, - ownProps.namespace, - ownProps.repoName, - permission - ); - const deleteLoading = isDeletePermissionPending( - state, - ownProps.namespace, - ownProps.repoName, - permission - ); - - return { loading, deleteLoading }; -}; - -const mapDispatchToProps = dispatch => { - return { - modifyPermission: ( - permission: Permission, - namespace: string, - repoName: string - ) => { - dispatch(modifyPermission(permission, namespace, repoName)); - }, - deletePermission: ( - permission: Permission, - namespace: string, - repoName: string - ) => { - dispatch(deletePermission(permission, namespace, repoName)); - } - }; -}; -export default connect( - mapStateToProps, - mapDispatchToProps -)(translate("repos")(SinglePermission)); +// @flow +import React from "react"; +import type { + AvailableRepositoryPermissions, + Permission +} from "@scm-manager/ui-types"; +import { translate } from "react-i18next"; +import { + modifyPermission, + isModifyPermissionPending, + deletePermission, + isDeletePermissionPending, + findMatchingRoleName +} from "../modules/permissions"; +import { connect } from "react-redux"; +import type { History } from "history"; +import { Button, Checkbox } from "@scm-manager/ui-components"; +import DeletePermissionButton from "../components/buttons/DeletePermissionButton"; +import RoleSelector from "../components/RoleSelector"; +import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog"; + +type Props = { + availablePermissions: AvailableRepositoryPermissions, + submitForm: Permission => void, + modifyPermission: (permission: Permission, namespace: string, name: string) => void, + permission: Permission, + t: string => string, + namespace: string, + repoName: string, + match: any, + history: History, + loading: boolean, + deletePermission: (permission: Permission, namespace: string, name: string) => void, + deleteLoading: boolean +}; + +type State = { + role: string, + permission: Permission, + showAdvancedDialog: boolean +}; + +class SinglePermission extends React.Component { + constructor(props: Props) { + super(props); + + const defaultPermission = props.availablePermissions.availableRoles + ? props.availablePermissions.availableRoles[0] + : {}; + + this.state = { + permission: { + name: "", + verbs: defaultPermission.verbs, + groupPermission: false, + _links: {} + }, + role: defaultPermission.name, + showAdvancedDialog: false + }; + } + + componentDidMount() { + const { availablePermissions, permission } = this.props; + + const matchingRole = findMatchingRoleName( + availablePermissions, + permission.verbs + ); + + if (permission) { + this.setState({ + permission: { + name: permission.name, + verbs: permission.verbs, + groupPermission: permission.groupPermission, + _links: permission._links + }, + role: matchingRole + }); + } + } + + deletePermission = () => { + this.props.deletePermission( + this.props.permission, + this.props.namespace, + this.props.repoName + ); + }; + + render() { + const { role, permission, showAdvancedDialog } = this.state; + const { + t, + availablePermissions, + loading, + namespace, + repoName + } = this.props; + const availableRoleNames = availablePermissions.availableRoles.map( + r => r.name + ); + const readOnly = !this.mayChangePermissions(); + const roleSelector = readOnly ? ( + {role} + ) : ( + + + + ); + + const advancedDialg = showAdvancedDialog ? ( + + ) : null; + + return ( + + {permission.name} + + + + {roleSelector} + +