From 32b034164939a1d847b0c2f470e96032ff8bbe12 Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Thu, 20 Dec 2018 17:56:28 +0100 Subject: [PATCH 01/28] Security System: Query permission.xmls from uber classloader. Allows for finding permission.xmls from plugins. Adds an examplary permission.xml for git plugin. --- .../resources/META-INF/scm/permissions.xml | 48 +++++++++++++++++++ .../scm/security/DefaultSecuritySystem.java | 34 ++++++------- .../security/DefaultSecuritySystemTest.java | 35 +++++++------- 3 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..a61077f61a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,48 @@ + + + + + + Git config (read) + Read access to git config + configuration:read:git + + + + Git config (write) + Write access to git config + configuration:write:git + + + diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index e93d4de597..780b3832bc 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -36,7 +36,6 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.github.legman.Subscribe; - import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Strings; @@ -44,30 +43,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.HandlerEventType; import sonia.scm.event.ScmEventBus; import sonia.scm.group.GroupEvent; +import sonia.scm.plugin.PluginLoader; import sonia.scm.store.ConfigurationEntryStore; import sonia.scm.store.ConfigurationEntryStoreFactory; import sonia.scm.user.UserEvent; -import sonia.scm.util.ClassLoaders; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.net.URL; - -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Map.Entry; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @@ -75,6 +60,14 @@ 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.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map.Entry; + +//~--- JDK imports ------------------------------------------------------------ /** * TODO add events @@ -99,6 +92,8 @@ public class DefaultSecuritySystem implements SecuritySystem private static final Logger logger = LoggerFactory.getLogger(DefaultSecuritySystem.class); + private PluginLoader pluginLoader; + //~--- constructors --------------------------------------------------------- /** @@ -109,12 +104,13 @@ public class DefaultSecuritySystem implements SecuritySystem */ @Inject @SuppressWarnings("unchecked") - public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory) + public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory, PluginLoader pluginLoader) { store = storeFactory .withType(AssignedPermission.class) .withName(NAME) .build(); + this.pluginLoader = pluginLoader; readAvailablePermissions(); } @@ -409,9 +405,9 @@ public class DefaultSecuritySystem implements SecuritySystem JAXBContext context = JAXBContext.newInstance(PermissionDescriptors.class); + // Querying permissions from uberClassLoader returns also the permissions from plugin Enumeration descirptorEnum = - ClassLoaders.getContextClassLoader( - DefaultSecuritySystem.class).getResources(PERMISSION_DESCRIPTOR); + pluginLoader.getUberClassLoader().getResources(PERMISSION_DESCRIPTOR); while (descirptorEnum.hasMoreElements()) { diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index 2ccdb2b28a..efae4b8ee5 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -35,26 +35,28 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Predicate; - import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; - import org.junit.Before; import org.junit.Test; - +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; import sonia.scm.AbstractTestBase; +import sonia.scm.plugin.PluginLoader; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; +import sonia.scm.util.ClassLoaders; import sonia.scm.util.MockUtil; -import static org.hamcrest.Matchers.*; +import java.util.List; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; //~--- JDK imports ------------------------------------------------------------ -import java.util.List; - /** * * @author Sebastian Sdorra @@ -62,6 +64,12 @@ import java.util.List; public class DefaultSecuritySystemTest extends AbstractTestBase { + private JAXBConfigurationEntryStoreFactory jaxbConfigurationEntryStoreFactory; + private PluginLoader pluginLoader; + @InjectMocks + private DefaultSecuritySystem securitySystem; + + /** * Method description * @@ -69,12 +77,12 @@ public class DefaultSecuritySystemTest extends AbstractTestBase @Before public void createSecuritySystem() { - JAXBConfigurationEntryStoreFactory factory = - new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() ); + jaxbConfigurationEntryStoreFactory = + spy(new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() ) {}); + pluginLoader = mock(PluginLoader.class); + when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); - securitySystem = new DefaultSecuritySystem(factory); - - // ScmEventBus.getInstance().register(listener); + MockitoAnnotations.initMocks(this); } /** @@ -325,9 +333,4 @@ public class DefaultSecuritySystemTest extends AbstractTestBase setSubject(MockUtil.createUserSubject(sm)); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private DefaultSecuritySystem securitySystem; } From 43232e0c5900379e4a1b0475f408bd0f526d6bdb Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Thu, 20 Dec 2018 17:57:35 +0100 Subject: [PATCH 02/28] Adds Global Permission Proof of Concept --- .../GlobalPermissionPocResource.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java new file mode 100644 index 0000000000..6c564a43bd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java @@ -0,0 +1,94 @@ +package sonia.scm.api.v2.resources; + +import lombok.extern.slf4j.Slf4j; +import sonia.scm.security.AssignedPermission; +import sonia.scm.security.SecuritySystem; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Global Permission Proof of Concept (POC). + * TODO Extend or delete this during implementation! + */ +@Path("v2/permissions") +@Slf4j +public class GlobalPermissionPocResource { + + private SecuritySystem securitySystem; + + @Inject + public GlobalPermissionPocResource(SecuritySystem securitySystem) { + this.securitySystem = securitySystem; + } + + + /** + + How to use this proof of concept? + + curl -vu scmadmin:scmadmin --data '{ + "active": true, + "admin": false, + "displayName": "arthur", + "mail": "x@abcde.cd", + "name": "arthur", + "password": "scmadmin", + "type": "xml" + }' \ + --header "Content-Type: application/vnd.scmm-user+json;v=2" http://localhost:8081/scm/api/v2/users/ + + curl -vu scmadmin:scmadmin --data '{ + "description": "descr", + "name": "configurers", + "members": [ "arthur" ] + }' \ + --header "Content-Type: application/vnd.scmm-group+json" http://localhost:8081/scm/api/v2/groups/ + + # not allowed + curl -vu arthur:scmadmin http://localhost:8081/scm/api/v2/config + # not allowed (empty) + curl -vu arthur:scmadmin "http://localhost:8081/scm/api/v2/groups/?sortBy=name&desc=true" | jq + + # Assign permissions (call this resource) + curl -X POST -vu scmadmin:scmadmin http://localhost:8081/scm/api/v2/permissions + + # Now allowed via individual permission + curl -vu arthur:scmadmin "http://localhost:8081/scm/api/v2/groups/?sortBy=name&desc=true" | jq + # allowed via group permission + curl -vu arthur:scmadmin http://localhost:8081/scm/api/v2/config | jq + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("") + public Response create() { + + // Should contain all permissions defined in permissions.xmls on the classpath. + // Core: scm-webapp/src/main/resources/META-INF/scm/permissions.xml + // Plugins, e.g. scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml + log.info("{} Available permissions: {}", securitySystem.getAvailablePermissions().size(), securitySystem.getAvailablePermissions()); + // Should contain all stored permissions. See assignExemplaryPermissions() for example. + log.info("{} All permissions: {}", securitySystem.getAllPermissions().size(), securitySystem.getAllPermissions()); + + assignExemplaryPermissions(); + + // TODO use created() + return Response.noContent().build(); + } + + protected void assignExemplaryPermissions() { + AssignedPermission groupPermission = new AssignedPermission("configurers", true,"configuration:*"); + log.info("try to add new permission: {}", groupPermission); + securitySystem.addPermission(groupPermission); + + AssignedPermission userPermission = new AssignedPermission("arthur", "group:*"); + log.info("try to add new permission: {}", userPermission); + securitySystem.addPermission(userPermission); + } +} + + From f1692aa1c75a876495ee32fc1acf745dc7c1200d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 16 Jan 2019 14:19:11 +0100 Subject: [PATCH 03/28] Cleanup security system - remove probably unused methods - use sets instead of lists - remove old REST resource --- .../src/main/java/sonia/scm/ScmState.java | 6 +- .../main/java/sonia/scm/ScmStateFactory.java | 4 +- .../scm/security/PermissionDescriptor.java | 50 +-- .../sonia/scm/security/SecuritySystem.java | 31 +- .../resources/AbstractPermissionResource.java | 298 ------------------ .../resources/GroupPermissionResource.java | 127 -------- .../resources/SecuritySystemResource.java | 106 ------- .../resources/UserPermissionResource.java | 127 -------- .../GlobalPermissionPocResource.java | 15 +- .../api/v2/resources/PerminssionListDto.java | 15 + .../scm/api/v2/resources/UserResource.java | 29 +- .../DefaultAuthorizationCollector.java | 2 +- .../scm/security/DefaultSecuritySystem.java | 97 +----- .../resources/META-INF/scm/permissions.xml | 10 +- .../v2/resources/UserRootResourceTest.java | 5 +- .../security/DefaultSecuritySystemTest.java | 88 ++---- 16 files changed, 105 insertions(+), 905 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java diff --git a/scm-core/src/main/java/sonia/scm/ScmState.java b/scm-core/src/main/java/sonia/scm/ScmState.java index cd36aa707c..84fb07a1f7 100644 --- a/scm-core/src/main/java/sonia/scm/ScmState.java +++ b/scm-core/src/main/java/sonia/scm/ScmState.java @@ -85,7 +85,7 @@ public final class ScmState public ScmState(String version, User user, Collection groups, String token, Collection repositoryTypes, String defaultUserType, ScmClientConfig clientConfig, List assignedPermission, - List availablePermissions) + Collection availablePermissions) { this.version = version; this.user = user; @@ -119,7 +119,7 @@ public final class ScmState * @return available global permissions * @since 1.31 */ - public List getAvailablePermissions() + public Collection getAvailablePermissions() { return availablePermissions; } @@ -232,7 +232,7 @@ public final class ScmState * Avaliable global permission * @since 1.31 */ - private List availablePermissions; + private Collection availablePermissions; /** Field description */ private ScmClientConfig clientConfig; diff --git a/scm-core/src/main/java/sonia/scm/ScmStateFactory.java b/scm-core/src/main/java/sonia/scm/ScmStateFactory.java index 41502a6850..e839a0ddcc 100644 --- a/scm-core/src/main/java/sonia/scm/ScmStateFactory.java +++ b/scm-core/src/main/java/sonia/scm/ScmStateFactory.java @@ -134,7 +134,7 @@ public final class ScmStateFactory User user = collection.oneByType(User.class); GroupNames groups = collection.oneByType(GroupNames.class); - List ap = Collections.EMPTY_LIST; + Collection ap = Collections.EMPTY_LIST; if (subject.hasRole(Role.ADMIN)) { @@ -150,7 +150,7 @@ public final class ScmStateFactory private ScmState createState(User user, Collection groups, String token, List assignedPermissions, - List availablePermissions) + Collection availablePermissions) { User u = user.clone(); diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index 20d95958a1..a005583256 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -67,19 +67,8 @@ public class PermissionDescriptor implements Serializable */ public PermissionDescriptor() {} - /** - * Constructs ... - * - * - * @param displayName - * @param description - * @param value - */ - public PermissionDescriptor(String displayName, String description, - String value) + public PermissionDescriptor(String value) { - this.displayName = displayName; - this.description = description; this.value = value; } @@ -103,9 +92,7 @@ public class PermissionDescriptor implements Serializable final PermissionDescriptor other = (PermissionDescriptor) obj; - return Objects.equal(displayName, other.displayName) - && Objects.equal(description, other.description) - && Objects.equal(value, other.value); + return Objects.equal(value, other.value); } /** @@ -114,7 +101,7 @@ public class PermissionDescriptor implements Serializable @Override public int hashCode() { - return Objects.hashCode(displayName, description, value); + return value.hashCode(); } /** @@ -126,8 +113,6 @@ public class PermissionDescriptor implements Serializable //J- return MoreObjects.toStringHelper(this) - .add("displayName", displayName) - .add("description", description) .add("value", value) .toString(); @@ -136,28 +121,6 @@ public class PermissionDescriptor implements Serializable //~--- get methods ---------------------------------------------------------- - /** - * Returns the description of the permission. - * - * - * @return description - */ - public String getDescription() - { - return description; - } - - /** - * Returns the display name of the permission. - * - * - * @return display name - */ - public String getDisplayName() - { - return displayName; - } - /** * Returns the string representation of the permission. * @@ -171,13 +134,6 @@ public class PermissionDescriptor implements Serializable //~--- fields --------------------------------------------------------------- - /** description */ - private String description; - - /** display name */ - @XmlElement(name = "display-name") - private String displayName; - /** value */ private String value; } diff --git a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java index f43afcb7f2..f99625a5a9 100644 --- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java +++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java @@ -38,6 +38,7 @@ import com.google.common.base.Predicate; //~--- JDK imports ------------------------------------------------------------ +import java.util.Collection; import java.util.List; /** @@ -75,41 +76,15 @@ public interface SecuritySystem */ public void deletePermission(String id); - /** - * Modify stored permission. - * - * - * @param permission stored permisison - */ - public void modifyPermission(StoredAssignedPermission permission); - //~--- get methods ---------------------------------------------------------- - /** - * Return all stored permissions. - * - * - * @return stored permission - */ - public List getAllPermissions(); - /** * Return all available permissions. * * * @return available permissions */ - public List getAvailablePermissions(); - - /** - * Return the stored permission which is stored with the given id. - * - * - * @param id id of the stored permission - * - * @return stored permission - */ - public StoredAssignedPermission getPermission(String id); + public Collection getAvailablePermissions(); /** * Returns all stored permissions which are matched by the given @@ -120,6 +95,6 @@ public interface SecuritySystem * * @return filtered permissions */ - public List getPermissions( + public Collection getPermissions( Predicate predicate); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java deleted file mode 100644 index 040eb6b2dc..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java +++ /dev/null @@ -1,298 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import com.google.common.collect.Lists; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.ResponseHeader; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; - -import sonia.scm.api.rest.Permission; -import sonia.scm.security.AssignedPermission; -import sonia.scm.security.SecuritySystem; -import sonia.scm.security.StoredAssignedPermission; - -//~--- JDK imports ------------------------------------------------------------ - -import java.net.URI; - -import java.util.List; - -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriInfo; - -/** - * Abstract base class for global permission resources. - * - * @author Sebastian Sdorra - * @since 1.31 - */ -public abstract class AbstractPermissionResource -{ - - /** - * Constructs a new {@link AbstractPermissionResource}. - * - * - * @param securitySystem security system - * @param name name of the user or group - */ - protected AbstractPermissionResource(SecuritySystem securitySystem, - String name) - { - this.securitySystem = securitySystem; - this.name = name; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Transforms a {@link Permission} to a {@link AssignedPermission}. - * - * - * @param permission permission object to transform - * - * @return transformed {@link AssignedPermission} - */ - protected abstract AssignedPermission transformPermission( - Permission permission); - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns a {@link Predicate} to filter permissions. - * - * - * @return {@link Predicate} to filter permissions - */ - protected abstract Predicate getPredicate(); - - //~--- methods -------------------------------------------------------------- - - /** - * Adds a new permission to the user or group managed by the resource. - * - * @param uriInfo uri informations - * @param permission permission to add - * - * @return web response - */ - @POST - @StatusCodes({ - @ResponseCode(code = 201, condition = "creates", additionalHeaders = { - @ResponseHeader(name = "Location", description = "uri to new create permission") - }), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response add(@Context UriInfo uriInfo, Permission permission) - { - AssignedPermission ap = transformPermission(permission); - StoredAssignedPermission sap = securitySystem.addPermission(ap); - URI uri = uriInfo.getAbsolutePathBuilder().path(sap.getId()).build(); - - return Response.created(uri).build(); - } - - /** - * Deletes a permission from the user or group managed by the resource. - * - * @param id id of the permission - * - * @return web response - */ - @DELETE - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"), - @ResponseCode(code = 404, condition = "not found, no permission with the specified id available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - public Response delete(@PathParam("id") String id) - { - StoredAssignedPermission sap = getPermission(id); - - securitySystem.deletePermission(sap); - - return Response.noContent().build(); - } - - /** - * Updates the specified permission on the user or group managed by the resource. - * - * @param id id of the permission - * @param permission updated permission - * - * @return web response - */ - @PUT - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"), - @ResponseCode(code = 404, condition = "not found, no permission with the specified id available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response update(@PathParam("id") String id, Permission permission) - { - StoredAssignedPermission sap = getPermission(id); - - securitySystem.modifyPermission(new StoredAssignedPermission(sap.getId(), - transformPermission(permission))); - - return Response.noContent().build(); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns the {@link Permission} with the specified id. - * - * @param id id of the {@link Permission} - * - * @return {@link Permission} with the specified id - */ - @GET - @Path("{id}") - @StatusCodes({ - @ResponseCode(code = 204, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"), - @ResponseCode(code = 404, condition = "not found, no permission with the specified id available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Permission get(@PathParam("id") String id) - { - StoredAssignedPermission sap = getPermission(id); - - return new Permission(sap.getId(), sap.getPermission()); - } - - /** - * Returns all permissions of the user or group managed by the resource. - * - * @return all permissions of the user or group - */ - @GET - @StatusCodes({ - @ResponseCode(code = 204, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public List getAll() - { - return getPermissions(getPredicate()); - } - - /** - * Returns the {@link StoredAssignedPermission} with the given id. - * - * - * @param id id of the stored permission - * - * @return {@link StoredAssignedPermission} with the given id - */ - private StoredAssignedPermission getPermission(String id) - { - StoredAssignedPermission sap = securitySystem.getPermission(id); - - if (sap == null) - { - throw new WebApplicationException(Status.NOT_FOUND); - } - - if (!getPredicate().apply(sap)) - { - throw new WebApplicationException(Status.BAD_REQUEST); - } - - return sap; - } - - /** - * Returns all permissions which matches the given {@link Predicate}. - * - * - * @param predicate predicate for filtering - * - * @return all permissions which matches the given {@link Predicate} - */ - private List getPermissions( - Predicate predicate) - { - List permissions = - securitySystem.getPermissions(predicate); - - return Lists.transform(permissions, - new Function() - { - - @Override - public Permission apply(StoredAssignedPermission mgp) - { - return new Permission(mgp.getId(), mgp.getPermission()); - } - }); - } - - //~--- fields --------------------------------------------------------------- - - /** name of the user or the group */ - protected String name; - - /** security system */ - private SecuritySystem securitySystem; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java deleted file mode 100644 index b591d6a2a3..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java +++ /dev/null @@ -1,127 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; - -import sonia.scm.api.rest.Permission; -import sonia.scm.security.AssignedPermission; -import sonia.scm.security.SecuritySystem; - -/** - * Resource to manage global group permission for a specified group. - * - * @author Sebastian Sdorra - * @since 1.31 - */ -public class GroupPermissionResource extends AbstractPermissionResource -{ - - /** - * Constructs a new group permissions resource - * - * - * @param securitySystem security system - * @param name name of the group - */ - public GroupPermissionResource(SecuritySystem securitySystem, String name) - { - super(securitySystem, name); - } - - //~--- methods -------------------------------------------------------------- - - /** - * {@inheritDoc} - */ - @Override - protected AssignedPermission transformPermission(Permission permission) - { - return new AssignedPermission(name, true, permission.getValue()); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * {@inheritDoc} - */ - @Override - protected Predicate getPredicate() - { - return new GroupPredicate(name); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Group predicate to filter permissions. - */ - private static class GroupPredicate implements Predicate - { - - /** - * Constructs a new group predicate - * - * - * @param name name of the group - */ - public GroupPredicate(String name) - { - this.name = name; - } - - //~--- methods ------------------------------------------------------------ - - /** - * Returns true if the permission is a group permission and the name is - * equals. - * - * @param input permission - * - * @return true if the permission is a group permission and the name is - * equals - */ - @Override - public boolean apply(AssignedPermission input) - { - return input.isGroupPermission() && input.getName().equals(name); - } - - //~--- fields ------------------------------------------------------------- - - /** name of the group */ - private String name; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java deleted file mode 100644 index f9e95232db..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java +++ /dev/null @@ -1,106 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; - -import org.apache.shiro.SecurityUtils; - -import sonia.scm.security.Role; -import sonia.scm.security.SecuritySystem; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; - -/** - * Resource for managing system security permissions. - * - * @author Sebastian Sdorra - */ -@Path("security/permission") -public class SecuritySystemResource -{ - - /** - * Constructs ... - * - * - * @param system - */ - @Inject - public SecuritySystemResource(SecuritySystem system) - { - this.system = system; - - // only administrators can use this resource - SecurityUtils.getSubject().checkRole(Role.ADMIN); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Returns group permission sub resource. - * - * @param group name of group - * - * @return sub resource - */ - @Path("group/{group}") - public GroupPermissionResource getGroupSubResource(@PathParam("group") String group) - { - return new GroupPermissionResource(system, group); - } - - /** - * Returns user permission sub resource. - * - * - * @param user name of user - * - * @return sub resource - */ - @Path("user/{user}") - public UserPermissionResource getUserSubResource(@PathParam("user") String user) - { - return new UserPermissionResource(system, user); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final SecuritySystem system; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java deleted file mode 100644 index 5f82fb98eb..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java +++ /dev/null @@ -1,127 +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.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; - -import sonia.scm.api.rest.Permission; -import sonia.scm.security.AssignedPermission; -import sonia.scm.security.SecuritySystem; - -/** - * Resource to manage global user permission for a specified user. - * - * @author Sebastian Sdorra - * @since 1.31 - */ -public class UserPermissionResource extends AbstractPermissionResource -{ - - /** - * Constructs a new user permission resource. - * - * - * @param securitySystem security system - * @param name name of the user - */ - public UserPermissionResource(SecuritySystem securitySystem, String name) - { - super(securitySystem, name); - } - - //~--- methods -------------------------------------------------------------- - - /** - * {@inheritDoc} - */ - @Override - protected AssignedPermission transformPermission(Permission permission) - { - return new AssignedPermission(name, permission.getValue()); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * {@inheritDoc} - */ - @Override - protected Predicate getPredicate() - { - return new UserPredicate(name); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * User predicate to filter permissions. - */ - private static class UserPredicate implements Predicate - { - - /** - * Constructs a new user predicate. - * - * - * @param name name of the user - */ - public UserPredicate(String name) - { - this.name = name; - } - - //~--- methods ------------------------------------------------------------ - - /** - * Returns true if the permission is a user permission and the name is - * equals. - * - * @param input permission - * - * @return true if the permission is a user permission and the name is - * equals - */ - @Override - public boolean apply(AssignedPermission input) - { - return !input.isGroupPermission() && input.getName().equals(name); - } - - //~--- fields ------------------------------------------------------------- - - /** name of the user */ - private String name; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java index 6c564a43bd..369fd376c7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java @@ -2,12 +2,15 @@ package sonia.scm.api.v2.resources; import lombok.extern.slf4j.Slf4j; import sonia.scm.security.AssignedPermission; +import sonia.scm.security.PermissionDescriptor; import sonia.scm.security.SecuritySystem; import javax.inject.Inject; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -71,8 +74,6 @@ public class GlobalPermissionPocResource { // Core: scm-webapp/src/main/resources/META-INF/scm/permissions.xml // Plugins, e.g. scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml log.info("{} Available permissions: {}", securitySystem.getAvailablePermissions().size(), securitySystem.getAvailablePermissions()); - // Should contain all stored permissions. See assignExemplaryPermissions() for example. - log.info("{} All permissions: {}", securitySystem.getAllPermissions().size(), securitySystem.getAllPermissions()); assignExemplaryPermissions(); @@ -80,12 +81,20 @@ public class GlobalPermissionPocResource { return Response.noContent().build(); } + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("") + public Response getAll() { + String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); + return Response.ok(new PerminssionListDto(permissions)).build(); + } + protected void assignExemplaryPermissions() { AssignedPermission groupPermission = new AssignedPermission("configurers", true,"configuration:*"); log.info("try to add new permission: {}", groupPermission); securitySystem.addPermission(groupPermission); - AssignedPermission userPermission = new AssignedPermission("arthur", "group:*"); + AssignedPermission userPermission = new AssignedPermission("rene", "group:*"); log.info("try to add new permission: {}", userPermission); securitySystem.addPermission(userPermission); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java new file mode 100644 index 0000000000..3752d089f7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java @@ -0,0 +1,15 @@ +package sonia.scm.api.v2.resources; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class PerminssionListDto { + + private String[] permissions; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 0076d057ca..70381c3304 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -4,12 +4,14 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.SecuritySystem; +import sonia.scm.security.StoredAssignedPermission; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -28,14 +30,16 @@ public class UserResource { private final IdResourceManagerAdapter adapter; private final UserManager userManager; private final PasswordService passwordService; + private final SecuritySystem securitySystem; @Inject - public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) { + public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService, SecuritySystem securitySystem) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; this.adapter = new IdResourceManagerAdapter<>(manager, User.class); this.userManager = manager; this.passwordService = passwordService; + this.securitySystem = securitySystem; } /** @@ -132,4 +136,25 @@ public class UserResource { userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword())); return Response.noContent().build(); } + + /** + * Returns permissions for a user. + * + * @param id the id/name of the user + */ + @GET + @Path("permissions") + @Produces(VndMediaType.USER) + @TypeHint(UserDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the user"), + @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getPermissions(@PathParam("id") String id) { + String[] permissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(StoredAssignedPermission::getPermission).toArray(String[]::new); + return Response.ok(new PerminssionListDto(permissions)).build(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 8a79293642..0a347a1a03 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -175,7 +175,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectGlobalPermissions(Builder builder, final User user, final GroupNames groups) { - List globalPermissions = + Collection globalPermissions = securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input)); for (StoredAssignedPermission gp : globalPermissions) diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index 780b3832bc..1869558345 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -39,8 +39,8 @@ import com.github.legman.Subscribe; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableSet.Builder; +import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; import org.apache.shiro.SecurityUtils; @@ -62,6 +62,7 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.IOException; import java.net.URL; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.List; @@ -92,8 +93,6 @@ public class DefaultSecuritySystem implements SecuritySystem private static final Logger logger = LoggerFactory.getLogger(DefaultSecuritySystem.class); - private PluginLoader pluginLoader; - //~--- constructors --------------------------------------------------------- /** @@ -110,8 +109,7 @@ public class DefaultSecuritySystem implements SecuritySystem .withType(AssignedPermission.class) .withName(NAME) .build(); - this.pluginLoader = pluginLoader; - readAvailablePermissions(); + this.availablePermissions = readAvailablePermissions(pluginLoader); } //~--- methods -------------------------------------------------------------- @@ -228,31 +226,6 @@ public class DefaultSecuritySystem implements SecuritySystem } } - /** - * Method description - * - * - * @param permission - */ - @Override - public void modifyPermission(StoredAssignedPermission permission) - { - assertIsAdmin(); - validatePermission(permission); - - synchronized (store) - { - store.remove(permission.getId()); - store.put(permission.getId(), new AssignedPermission(permission)); - } - - //J- - ScmEventBus.getInstance().post( - new StoredAssignedPermissionEvent(HandlerEventType.CREATE, permission) - ); - //J+ - } - //~--- get methods ---------------------------------------------------------- /** @@ -262,49 +235,13 @@ public class DefaultSecuritySystem implements SecuritySystem * @return */ @Override - public List getAllPermissions() - { - return getPermissions(null); - } - - /** - * Method description - * - * - * @return - */ - @Override - public List getAvailablePermissions() + public Collection getAvailablePermissions() { assertIsAdmin(); return availablePermissions; } - /** - * Method description - * - * - * @param id - * - * @return - */ - @Override - public StoredAssignedPermission getPermission(String id) - { - assertIsAdmin(); - - StoredAssignedPermission sap = null; - AssignedPermission ap = store.get(id); - - if (ap != null) - { - sap = new StoredAssignedPermission(id, ap); - } - - return sap; - } - /** * Method description * @@ -314,10 +251,9 @@ public class DefaultSecuritySystem implements SecuritySystem * @return */ @Override - public List getPermissions( - Predicate predicate) + public Collection getPermissions(Predicate predicate) { - Builder permissions = ImmutableList.builder(); + Builder permissions = ImmutableSet.builder(); for (Entry e : store.getAll().entrySet()) { @@ -349,7 +285,7 @@ public class DefaultSecuritySystem implements SecuritySystem */ private void deletePermissions(Predicate predicate) { - List permissions = getPermissions(predicate); + Collection permissions = getPermissions(predicate); for (StoredAssignedPermission permission : permissions) { @@ -367,7 +303,7 @@ public class DefaultSecuritySystem implements SecuritySystem * @return */ @SuppressWarnings("unchecked") - private List parsePermissionDescriptor( + private static List parsePermissionDescriptor( JAXBContext context, URL descriptorUrl) { List descriptors = Collections.EMPTY_LIST; @@ -395,10 +331,11 @@ public class DefaultSecuritySystem implements SecuritySystem /** * Method description * + * @param pluginLoader */ - private void readAvailablePermissions() + private static ImmutableSet readAvailablePermissions(PluginLoader pluginLoader) { - Builder builder = ImmutableList.builder(); + ImmutableSet.Builder builder = ImmutableSet.builder(); try { @@ -428,7 +365,7 @@ public class DefaultSecuritySystem implements SecuritySystem "could not create jaxb context to read permission descriptors", ex); } - availablePermissions = builder.build(); + return builder.build(); } /** @@ -455,12 +392,6 @@ public class DefaultSecuritySystem implements SecuritySystem private static class PermissionDescriptors { - /** - * Constructs ... - * - */ - public PermissionDescriptors() {} - //~--- get methods -------------------------------------------------------- /** @@ -494,5 +425,5 @@ public class DefaultSecuritySystem implements SecuritySystem private final ConfigurationEntryStore store; /** Field description */ - private List availablePermissions; + private final ImmutableSet availablePermissions; } diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 537ada0bce..8978b050ca 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -34,21 +34,15 @@ - All Repository (read) - Read access to all repositories - repository:*:READ + repository:read:* - All Repository (write) - Write access to all repositories repository:*:WRITE - All Repository (owner) - Owner access to all repositories repository:*:OWNER - + diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 284e7d1d7b..06376675df 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -17,6 +17,7 @@ import org.mockito.Mock; import sonia.scm.ContextEntry; import sonia.scm.NotFoundException; import sonia.scm.PageResult; +import sonia.scm.security.SecuritySystem; import sonia.scm.user.ChangePasswordNotAllowedException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -59,6 +60,8 @@ public class UserRootResourceTest { private PasswordService passwordService; @Mock private UserManager userManager; + @Mock + private SecuritySystem securitySystem; @InjectMocks private UserDtoToUserMapperImpl dtoToUserMapper; @InjectMocks @@ -80,7 +83,7 @@ public class UserRootResourceTest { UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks); UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, userCollectionToDtoMapper, resourceLinks, passwordService); - UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService); + UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService, securitySystem); UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource), Providers.of(userResource)); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index efae4b8ee5..ed6bac7bea 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -32,9 +32,6 @@ package sonia.scm.security; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; @@ -48,14 +45,15 @@ import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.util.ClassLoaders; import sonia.scm.util.MockUtil; +import java.util.Collection; import java.util.List; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.greaterThan; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; /** * @@ -111,10 +109,10 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - List list = securitySystem.getAvailablePermissions(); + Collection list = securitySystem.getAvailablePermissions(); assertNotNull(list); - assertThat(list.size(), greaterThan(0)); + assertThat(list).isNotEmpty(); } /** @@ -131,7 +129,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase securitySystem.deletePermission(sap); - assertNull(securitySystem.getPermission(sap.getId())); + assertThat(securitySystem.getPermissions(p -> p.getName().equals("trillian"))).isEmpty(); } /** @@ -150,10 +148,10 @@ public class DefaultSecuritySystemTest extends AbstractTestBase StoredAssignedPermission marvin = createPermission("marvin", false, "repository:*:READ"); - List all = securitySystem.getAllPermissions(); + List all = securitySystem.getPermissions(p -> true); assertEquals(3, all.size()); - assertThat(all, containsInAnyOrder(trillian, dent, marvin)); + assertThat(all).contains(trillian, dent, marvin); } /** @@ -168,10 +166,9 @@ public class DefaultSecuritySystemTest extends AbstractTestBase StoredAssignedPermission sap = createPermission("trillian", false, "repository:*:READ"); - StoredAssignedPermission other = securitySystem.getPermission(sap.getId()); + List other = securitySystem.getPermissions(p -> p.getName().equals("trillian")); - assertEquals(sap.getId(), other.getId()); - assertEquals(sap, other); + assertThat(other).containsExactly(sap); } /** @@ -191,41 +188,11 @@ public class DefaultSecuritySystemTest extends AbstractTestBase createPermission("hitchhiker", true, "repository:*:READ"); List filtered = - securitySystem.getPermissions(new Predicate() - { + securitySystem.getPermissions(p -> !p.isGroupPermission()); - @Override - public boolean apply(AssignedPermission input) - { - return !input.isGroupPermission(); - } - }); - - assertEquals(2, filtered.size()); - assertThat(filtered, containsInAnyOrder(trillian, dent)); - } - - /** - * Method description - * - */ - @Test - public void testModifyPermission() - { - setAdminSubject(); - - StoredAssignedPermission sap = createPermission("trillian", false, - "repository:*:READ"); - StoredAssignedPermission modified = - new StoredAssignedPermission(sap.getId(), - new AssignedPermission("trillian", "repository:*:WRITE")); - - securitySystem.modifyPermission(modified); - - sap = securitySystem.getPermission(modified.getId()); - - assertEquals(modified.getId(), sap.getId()); - assertEquals(modified, sap); + assertThat(filtered) + .hasSize(2) + .contains(trillian, dent); } /** @@ -268,24 +235,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase "repository:*:READ"); setUserSubject(); - securitySystem.getPermission(sap.getId()); - } - - /** - * Method description - * - */ - @Test(expected = UnauthorizedException.class) - public void testUnauthorizedModifyPermission() - { - setAdminSubject(); - - StoredAssignedPermission sap = createPermission("trillian", false, - "repository:*:READ"); - - setUserSubject(); - - securitySystem.modifyPermission(sap); + securitySystem.getPermissions(p -> true); } /** From 5e364e1043b37a663a0736f1f02fc1979ea81ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 16 Jan 2019 16:03:02 +0100 Subject: [PATCH 04/28] Do not expose StoredAssignedPermission --- ...vent.java => AssignedPermissionEvent.java} | 16 ++-- .../sonia/scm/security/SecuritySystem.java | 22 +---- .../security/StoredAssignedPermission.java | 2 + .../scm/api/v2/resources/UserResource.java | 5 +- .../AuthorizationChangedEventProducer.java | 16 ++-- .../DefaultAuthorizationCollector.java | 4 +- .../scm/security/DefaultSecuritySystem.java | 86 ++++++------------- ...AuthorizationChangedEventProducerTest.java | 12 +-- .../security/DefaultSecuritySystemTest.java | 40 ++++----- 9 files changed, 78 insertions(+), 125 deletions(-) rename scm-core/src/main/java/sonia/scm/security/{StoredAssignedPermissionEvent.java => AssignedPermissionEvent.java} (90%) diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java similarity index 90% rename from scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java rename to scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java index ad93bf25a9..9ebea488a5 100644 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java @@ -51,7 +51,7 @@ import java.io.Serializable; * @since 1.31 */ @Event -public final class StoredAssignedPermissionEvent implements Serializable +public final class AssignedPermissionEvent implements Serializable { /** serial version uid */ @@ -60,14 +60,14 @@ public final class StoredAssignedPermissionEvent implements Serializable //~--- constructors --------------------------------------------------------- /** - * Constructs a new StoredAssignedPermissionEvent. + * Constructs a new AssignedPermissionEvent. * * * @param type type of the event * @param permission permission object which has changed */ - public StoredAssignedPermissionEvent(HandlerEventType type, - StoredAssignedPermission permission) + public AssignedPermissionEvent(HandlerEventType type, + AssignedPermission permission) { this.type = type; this.permission = permission; @@ -91,8 +91,8 @@ public final class StoredAssignedPermissionEvent implements Serializable return false; } - final StoredAssignedPermissionEvent other = - (StoredAssignedPermissionEvent) obj; + final AssignedPermissionEvent other = + (AssignedPermissionEvent) obj; return Objects.equal(type, other.type) && Objects.equal(permission, other.permission); @@ -140,7 +140,7 @@ public final class StoredAssignedPermissionEvent implements Serializable * * @return changed permission */ - public StoredAssignedPermission getPermission() + public AssignedPermission getPermission() { return permission; } @@ -148,7 +148,7 @@ public final class StoredAssignedPermissionEvent implements Serializable //~--- fields --------------------------------------------------------------- /** changed permission */ - private StoredAssignedPermission permission; + private AssignedPermission permission; /** type of the event */ private HandlerEventType type; diff --git a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java index f99625a5a9..49f27be3e8 100644 --- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java +++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java @@ -32,14 +32,8 @@ package sonia.scm.security; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Predicate; - -//~--- JDK imports ------------------------------------------------------------ - import java.util.Collection; -import java.util.List; +import java.util.function.Predicate; /** * The SecuritySystem manages global permissions. @@ -58,7 +52,7 @@ public interface SecuritySystem * * @return stored permission */ - public StoredAssignedPermission addPermission(AssignedPermission permission); + public void addPermission(AssignedPermission permission); /** * Delete stored permission. @@ -66,15 +60,7 @@ public interface SecuritySystem * * @param permission permission to be deleted */ - public void deletePermission(StoredAssignedPermission permission); - - /** - * Delete stored permission. - * - * - * @param id id of the permission - */ - public void deletePermission(String id); + public void deletePermission(AssignedPermission permission); //~--- get methods ---------------------------------------------------------- @@ -95,6 +81,6 @@ public interface SecuritySystem * * @return filtered permissions */ - public Collection getPermissions( + public Collection getPermissions( Predicate predicate); } diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java index 903f86df90..4b2e46b665 100644 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java @@ -34,6 +34,8 @@ package sonia.scm.security; //~--- JDK imports ------------------------------------------------------------ +import com.google.common.base.Objects; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 70381c3304..be97af2677 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -4,9 +4,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; -import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.AssignedPermission; import sonia.scm.security.SecuritySystem; -import sonia.scm.security.StoredAssignedPermission; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -154,7 +153,7 @@ public class UserResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { - String[] permissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(StoredAssignedPermission::getPermission).toArray(String[]::new); + String[] permissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).toArray(String[]::new); return Response.ok(new PerminssionListDto(permissions)).build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index cf4c980625..0586db2bb3 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -189,9 +189,9 @@ public class AuthorizationChangedEventProducer { * @param event permission event */ @Subscribe - public void onEvent(StoredAssignedPermissionEvent event) { + public void onEvent(AssignedPermissionEvent event) { if (event.getEventType().isPost()) { - StoredAssignedPermission permission = event.getPermission(); + AssignedPermission permission = event.getPermission(); if (permission.isGroupPermission()) { handleGroupPermissionChange(permission); } else { @@ -200,18 +200,18 @@ public class AuthorizationChangedEventProducer { } } - private void handleGroupPermissionChange(StoredAssignedPermission permission) { + private void handleGroupPermissionChange(AssignedPermission permission) { logger.debug( - "fire authorization changed event, because global group permission {} has changed", - permission.getId() + "fire authorization changed event for group {}, because permission {} has changed", + permission.getName(), permission.getPermission() ); fireEventForEveryUser(); } - private void handleUserPermissionChange(StoredAssignedPermission permission) { + private void handleUserPermissionChange(AssignedPermission permission) { logger.debug( - "fire authorization changed event for user {}, because permission {} has changed", - permission.getName(), permission.getId() + "fire authorization changed event for user {}, because permission {} has changed", + permission.getName(), permission.getPermission() ); fireEventForUser(permission.getName()); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 0a347a1a03..903445df3a 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -175,10 +175,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectGlobalPermissions(Builder builder, final User user, final GroupNames groups) { - Collection globalPermissions = + Collection globalPermissions = securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input)); - for (StoredAssignedPermission gp : globalPermissions) + for (AssignedPermission gp : globalPermissions) { String permission = gp.getPermission(); diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index 1869558345..a8a9cc7f8b 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -36,8 +36,8 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.github.legman.Subscribe; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.ImmutableSet; @@ -67,6 +67,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Map.Entry; +import java.util.function.Predicate; //~--- JDK imports ------------------------------------------------------------ @@ -123,7 +124,7 @@ public class DefaultSecuritySystem implements SecuritySystem * @return */ @Override - public StoredAssignedPermission addPermission(AssignedPermission permission) + public void addPermission(AssignedPermission permission) { assertIsAdmin(); validatePermission(permission); @@ -134,11 +135,9 @@ public class DefaultSecuritySystem implements SecuritySystem //J- ScmEventBus.getInstance().post( - new StoredAssignedPermissionEvent(HandlerEventType.CREATE, sap) + new AssignedPermissionEvent(HandlerEventType.CREATE, permission) ); //J+ - - return sap; } /** @@ -148,33 +147,16 @@ public class DefaultSecuritySystem implements SecuritySystem * @param permission */ @Override - public void deletePermission(StoredAssignedPermission permission) + public void deletePermission(AssignedPermission permission) { assertIsAdmin(); - store.remove(permission.getId()); - //J- - ScmEventBus.getInstance().post( - new StoredAssignedPermissionEvent(HandlerEventType.CREATE, permission) - ); - //J+ - } - - /** - * Method description - * - * - * @param id - */ - @Override - public void deletePermission(String id) - { - assertIsAdmin(); - - AssignedPermission ap = store.get(id); - - if (ap != null) - { - deletePermission(new StoredAssignedPermission(id, ap)); + boolean deleted = deletePermissions(sap -> Objects.equal(sap.getName(), permission.getName()) + && Objects.equal(sap.isGroupPermission(), permission.isGroupPermission()) + && Objects.equal(sap.getPermission(), permission.getPermission())); + if (deleted) { + ScmEventBus.getInstance().post( + new AssignedPermissionEvent(HandlerEventType.DELETE, permission) + ); } } @@ -189,16 +171,8 @@ public class DefaultSecuritySystem implements SecuritySystem { if (event.getEventType() == HandlerEventType.DELETE) { - deletePermissions(new Predicate() - { - - @Override - public boolean apply(AssignedPermission p) - { - return !p.isGroupPermission() - && event.getItem().getName().equals(p.getName()); - } - }); + deletePermissions(p -> !p.isGroupPermission() + && event.getItem().getName().equals(p.getName())); } } @@ -213,16 +187,8 @@ public class DefaultSecuritySystem implements SecuritySystem { if (event.getEventType() == HandlerEventType.DELETE) { - deletePermissions(new Predicate() - { - - @Override - public boolean apply(AssignedPermission p) - { - return p.isGroupPermission() - && event.getItem().getName().equals(p.getName()); - } - }); + deletePermissions(p -> p.isGroupPermission() + && event.getItem().getName().equals(p.getName())); } } @@ -251,13 +217,13 @@ public class DefaultSecuritySystem implements SecuritySystem * @return */ @Override - public Collection getPermissions(Predicate predicate) + public Collection getPermissions(Predicate predicate) { - Builder permissions = ImmutableSet.builder(); + Builder permissions = ImmutableSet.builder(); for (Entry e : store.getAll().entrySet()) { - if ((predicate == null) || predicate.apply(e.getValue())) + if ((predicate == null) || predicate.test(e.getValue())) { permissions.add(new StoredAssignedPermission(e.getKey(), e.getValue())); } @@ -283,14 +249,16 @@ public class DefaultSecuritySystem implements SecuritySystem * * @param predicate */ - private void deletePermissions(Predicate predicate) + private boolean deletePermissions(Predicate predicate) { - Collection permissions = getPermissions(predicate); - - for (StoredAssignedPermission permission : permissions) - { - deletePermission(permission); + boolean found = false; + for (Entry e : store.getAll().entrySet()) { + if ((predicate == null) || predicate.test(e.getValue())) { + store.remove(e.getKey()); + found = true; + } } + return found; } /** diff --git a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java index b45e0d72e9..17e0a8ed52 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java @@ -214,7 +214,7 @@ public class AuthorizationChangedEventProducerTest { } /** - * Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}. + * Tests {@link AuthorizationChangedEventProducer#onEvent(AssignedPermissionEvent)}. */ @Test public void testOnStoredAssignedPermissionEvent() @@ -222,10 +222,10 @@ public class AuthorizationChangedEventProducerTest { StoredAssignedPermission groupPermission = new StoredAssignedPermission( "123", new AssignedPermission("_authenticated", true, "repository:read:*") ); - producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission)); + producer.onEvent(new AssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission)); assertEventIsNotFired(); - producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, groupPermission)); + producer.onEvent(new AssignedPermissionEvent(HandlerEventType.CREATE, groupPermission)); assertGlobalEventIsFired(); resetStoredEvent(); @@ -233,12 +233,12 @@ public class AuthorizationChangedEventProducerTest { StoredAssignedPermission userPermission = new StoredAssignedPermission( "123", new AssignedPermission("trillian", false, "repository:read:*") ); - producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission)); + producer.onEvent(new AssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission)); assertEventIsNotFired(); resetStoredEvent(); - producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, userPermission)); + producer.onEvent(new AssignedPermissionEvent(HandlerEventType.CREATE, userPermission)); assertUserEventIsFired("trillian"); } @@ -253,4 +253,4 @@ public class AuthorizationChangedEventProducerTest { } -} \ No newline at end of file +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index ed6bac7bea..74214376ec 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -32,6 +32,7 @@ package sonia.scm.security; +import com.google.common.base.Objects; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; @@ -46,7 +47,6 @@ import sonia.scm.util.ClassLoaders; import sonia.scm.util.MockUtil; import java.util.Collection; -import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @@ -92,8 +92,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission sap = createPermission("trillian", false, - "repository:*:READ"); + AssignedPermission sap = createPermission("trillian", false, "repository:*:READ"); assertEquals("trillian", sap.getName()); assertEquals("repository:*:READ", sap.getPermission()); @@ -124,7 +123,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission sap = createPermission("trillian", false, + AssignedPermission sap = createPermission("trillian", false, "repository:*:READ"); securitySystem.deletePermission(sap); @@ -141,14 +140,14 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission trillian = createPermission("trillian", false, + AssignedPermission trillian = createPermission("trillian", false, "repository:*:READ"); - StoredAssignedPermission dent = createPermission("dent", false, + AssignedPermission dent = createPermission("dent", false, "repository:*:READ"); - StoredAssignedPermission marvin = createPermission("marvin", false, + AssignedPermission marvin = createPermission("marvin", false, "repository:*:READ"); - List all = securitySystem.getPermissions(p -> true); + Collection all = securitySystem.getPermissions(p -> true); assertEquals(3, all.size()); assertThat(all).contains(trillian, dent, marvin); @@ -163,10 +162,10 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission sap = createPermission("trillian", false, + AssignedPermission sap = createPermission("trillian", false, "repository:*:READ"); - List other = securitySystem.getPermissions(p -> p.getName().equals("trillian")); + Collection other = securitySystem.getPermissions(p -> p.getName().equals("trillian")); assertThat(other).containsExactly(sap); } @@ -180,14 +179,14 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission trillian = createPermission("trillian", false, + AssignedPermission trillian = createPermission("trillian", false, "repository:*:READ"); - StoredAssignedPermission dent = createPermission("dent", false, + AssignedPermission dent = createPermission("dent", false, "repository:*:READ"); createPermission("hitchhiker", true, "repository:*:READ"); - List filtered = + Collection filtered = securitySystem.getPermissions(p -> !p.isGroupPermission()); assertThat(filtered) @@ -215,7 +214,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission sap = createPermission("trillian", false, + AssignedPermission sap = createPermission("trillian", false, "repository:*:READ"); setUserSubject(); @@ -231,7 +230,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase { setAdminSubject(); - StoredAssignedPermission sap = createPermission("trillian", false, + createPermission("trillian", false, "repository:*:READ"); setUserSubject(); @@ -248,17 +247,16 @@ public class DefaultSecuritySystemTest extends AbstractTestBase * * @return */ - private StoredAssignedPermission createPermission(String name, + private AssignedPermission createPermission(String name, boolean groupPermission, String value) { AssignedPermission ap = new AssignedPermission(name, groupPermission, value); - StoredAssignedPermission sap = securitySystem.addPermission(ap); + securitySystem.addPermission(ap); - assertNotNull(sap); - assertNotNull(sap.getId()); - - return sap; + return securitySystem.getPermissions(permission -> Objects.equal(name, permission.getName()) + && Objects.equal(groupPermission, permission.isGroupPermission()) + && Objects.equal(value, permission.getPermission())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found")); } //~--- set methods ---------------------------------------------------------- From 7462613c16088bf6e8fa4bbe34924a61c998860d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 16 Jan 2019 16:55:24 +0100 Subject: [PATCH 05/28] Add permission for permissions --- .../src/main/java/sonia/scm/security/Permission.java | 12 ++++++++++++ .../sonia/scm/security/DefaultSecuritySystem.java | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/Permission.java diff --git a/scm-core/src/main/java/sonia/scm/security/Permission.java b/scm-core/src/main/java/sonia/scm/security/Permission.java new file mode 100644 index 0000000000..ef6b350c09 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/Permission.java @@ -0,0 +1,12 @@ +package sonia.scm.security; + +import com.github.sdorra.ssp.PermissionObject; +import com.github.sdorra.ssp.StaticPermissions; + +@StaticPermissions( + value = "permission", + permissions = {}, + globalPermissions = {"list", "assign"} +) +public interface Permission extends PermissionObject { +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index a8a9cc7f8b..26bf704775 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -126,7 +126,7 @@ public class DefaultSecuritySystem implements SecuritySystem @Override public void addPermission(AssignedPermission permission) { - assertIsAdmin(); + assertHasPermission(); validatePermission(permission); String id = store.put(permission); @@ -149,7 +149,7 @@ public class DefaultSecuritySystem implements SecuritySystem @Override public void deletePermission(AssignedPermission permission) { - assertIsAdmin(); + assertHasPermission(); boolean deleted = deletePermissions(sap -> Objects.equal(sap.getName(), permission.getName()) && Objects.equal(sap.isGroupPermission(), permission.isGroupPermission()) && Objects.equal(sap.getPermission(), permission.getPermission())); @@ -203,7 +203,7 @@ public class DefaultSecuritySystem implements SecuritySystem @Override public Collection getAvailablePermissions() { - assertIsAdmin(); + assertHasPermission(); return availablePermissions; } @@ -238,9 +238,9 @@ public class DefaultSecuritySystem implements SecuritySystem * Method description * */ - private void assertIsAdmin() + private void assertHasPermission() { - SecurityUtils.getSubject().checkRole(Role.ADMIN); + PermissionPermissions.assign().check(); } /** From ad65c8cd02f9522e9d105e0a2b1f1cdfa8b92e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 17 Jan 2019 13:21:20 +0100 Subject: [PATCH 06/28] Use PermissionDescriptor instead of String --- .../scm/security/AssignedPermission.java | 23 +++++--- .../sonia/scm/security/SecuritySystem.java | 9 ++- .../JAXBConfigurationEntryStoreTest.java | 2 +- .../InMemoryConfigurationEntryStore.java | 52 +++++++++++++++++ ...nMemoryConfigurationEntryStoreFactory.java | 28 +++++++++ .../GlobalPermissionPocResource.java | 6 +- ...ionListDto.java => PermissionListDto.java} | 2 +- .../scm/api/v2/resources/UserResource.java | 32 ++++++++++- .../DefaultAuthorizationCollector.java | 2 +- .../scm/security/DefaultSecuritySystem.java | 23 ++++---- .../scm/security/PermissionAssigner.java | 35 ++++++++++++ .../DefaultAuthorizationCollectorTest.java | 5 +- .../security/DefaultSecuritySystemTest.java | 4 +- .../scm/security/PermissionAssignerTest.java | 57 +++++++++++++++++++ 14 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java create mode 100644 scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{PerminssionListDto.java => PermissionListDto.java} (87%) create mode 100644 scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java create mode 100644 scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java index 56b8d04a41..c98d81f8ba 100644 --- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java @@ -89,8 +89,12 @@ public class AssignedPermission implements PermissionObject, Serializable */ public AssignedPermission(String name, String permission) { - this.name = name; - this.permission = permission; + this(name, new PermissionDescriptor(permission)); + } + + public AssignedPermission(String name, PermissionDescriptor permission) + { + this(name, false, permission); } /** @@ -103,6 +107,12 @@ public class AssignedPermission implements PermissionObject, Serializable */ public AssignedPermission(String name, boolean groupPermission, String permission) + { + this(name, groupPermission, new PermissionDescriptor(permission)); + } + + public AssignedPermission(String name, boolean groupPermission, + PermissionDescriptor permission) { this.name = name; this.groupPermission = groupPermission; @@ -173,12 +183,9 @@ public class AssignedPermission implements PermissionObject, Serializable } /** - * Returns the string representation of the permission. - * - * - * @return string representation of the permission + * Returns the description of the permission. */ - public String getPermission() + public PermissionDescriptor getPermission() { return permission; } @@ -205,5 +212,5 @@ public class AssignedPermission implements PermissionObject, Serializable private String name; /** string representation of the permission */ - private String permission; + private PermissionDescriptor permission; } diff --git a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java index 49f27be3e8..174b64f5e6 100644 --- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java +++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java @@ -52,7 +52,7 @@ public interface SecuritySystem * * @return stored permission */ - public void addPermission(AssignedPermission permission); + void addPermission(AssignedPermission permission); /** * Delete stored permission. @@ -60,7 +60,7 @@ public interface SecuritySystem * * @param permission permission to be deleted */ - public void deletePermission(AssignedPermission permission); + void deletePermission(AssignedPermission permission); //~--- get methods ---------------------------------------------------------- @@ -70,7 +70,7 @@ public interface SecuritySystem * * @return available permissions */ - public Collection getAvailablePermissions(); + Collection getAvailablePermissions(); /** * Returns all stored permissions which are matched by the given @@ -81,6 +81,5 @@ public interface SecuritySystem * * @return filtered permissions */ - public Collection getPermissions( - Predicate predicate); + Collection getPermissions(Predicate predicate); } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java index ae84f9d768..3d9fa3f283 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java @@ -141,7 +141,7 @@ public class JAXBConfigurationEntryStoreTest assertNotNull(ap); assertEquals("tuser4", ap.getName()); - assertEquals("repository:create", ap.getPermission()); + assertEquals("repository:create", ap.getPermission().getValue()); } @Test diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java new file mode 100644 index 0000000000..40124dd717 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java @@ -0,0 +1,52 @@ +package sonia.scm.store; + +import com.google.common.base.Predicate; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class InMemoryConfigurationEntryStore implements ConfigurationEntryStore { + + private final Map values = new HashMap<>(); + + @Override + public Collection getMatchingValues(Predicate predicate) { + return values.values().stream().filter(predicate).collect(Collectors.toList()); + } + + @Override + public String put(V item) { + String key = UUID.randomUUID().toString(); + values.put(key, item); + return key; + } + + @Override + public void put(String id, V item) { + values.put(id, item); + } + + @Override + public Map getAll() { + return Collections.unmodifiableMap(values); + } + + @Override + public void clear() { + values.clear(); + } + + @Override + public void remove(String id) { + values.remove(id); + } + + @Override + public V get(String id) { + return values.get(id); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java new file mode 100644 index 0000000000..48e60684b6 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java @@ -0,0 +1,28 @@ +package sonia.scm.store; + +public class InMemoryConfigurationEntryStoreFactory implements ConfigurationEntryStoreFactory { + + + + + private ConfigurationEntryStore store; + + public static ConfigurationEntryStoreFactory create() { + return new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore()); + } + + public InMemoryConfigurationEntryStoreFactory() { + } + + public InMemoryConfigurationEntryStoreFactory(ConfigurationEntryStore store) { + this.store = store; + } + + @Override + public ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) { + if (store != null) { + return store; + } + return new InMemoryConfigurationEntryStore<>(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java index 369fd376c7..845aaddd07 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java @@ -86,15 +86,15 @@ public class GlobalPermissionPocResource { @Path("") public Response getAll() { String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); - return Response.ok(new PerminssionListDto(permissions)).build(); + return Response.ok(new PermissionListDto(permissions)).build(); } protected void assignExemplaryPermissions() { - AssignedPermission groupPermission = new AssignedPermission("configurers", true,"configuration:*"); + AssignedPermission groupPermission = new AssignedPermission("configurers", true, new PermissionDescriptor("configuration:*")); log.info("try to add new permission: {}", groupPermission); securitySystem.addPermission(groupPermission); - AssignedPermission userPermission = new AssignedPermission("rene", "group:*"); + AssignedPermission userPermission = new AssignedPermission("rene", new PermissionDescriptor("group:*")); log.info("try to add new permission: {}", userPermission); securitySystem.addPermission(userPermission); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java similarity index 87% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java index 3752d089f7..807330dd97 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java @@ -9,7 +9,7 @@ import lombok.Setter; @Setter @AllArgsConstructor @NoArgsConstructor -public class PerminssionListDto { +public class PermissionListDto { private String[] permissions; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index be97af2677..479e4094fb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.security.AssignedPermission; +import sonia.scm.security.PermissionDescriptor; import sonia.scm.security.SecuritySystem; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -153,7 +154,34 @@ public class UserResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { - String[] permissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).toArray(String[]::new); - return Response.ok(new PerminssionListDto(permissions)).build(); + String[] permissions = + securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)) + .stream() + .map(AssignedPermission::getPermission) + .map(PermissionDescriptor::getValue) + .toArray(String[]::new); + return Response.ok(new PermissionListDto(permissions)).build(); + } + + /** + * Sets permissions for a user. Overwrites all existing permissions. + * + * @param id id of the user to be modified + * @param newPermissions New list of permissions for the user + */ + @PUT + @Path("permissions") + @Consumes(VndMediaType.PASSWORD_OVERWRITE) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the correct privilege"), + @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) { + return Response.noContent().build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 903445df3a..5560d77e4a 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -180,7 +180,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector for (AssignedPermission gp : globalPermissions) { - String permission = gp.getPermission(); + String permission = gp.getPermission().getValue(); logger.trace("add permission {} for user {}", permission, user.getName()); builder.add(permission); diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index 26bf704775..0064a46228 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -39,11 +39,10 @@ import com.github.legman.Subscribe; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSet.Builder; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.HandlerEventType; @@ -68,6 +67,9 @@ import java.util.Enumeration; import java.util.List; import java.util.Map.Entry; import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.Objects.isNull; //~--- JDK imports ------------------------------------------------------------ @@ -251,14 +253,13 @@ public class DefaultSecuritySystem implements SecuritySystem */ private boolean deletePermissions(Predicate predicate) { - boolean found = false; - for (Entry e : store.getAll().entrySet()) { - if ((predicate == null) || predicate.test(e.getValue())) { - store.remove(e.getKey()); - found = true; - } - } - return found; + List> toRemove = + store.getAll() + .entrySet() + .stream() + .filter(e -> (predicate == null) || predicate.test(e.getValue())).collect(Collectors.toList()); + toRemove.forEach(e -> store.remove(e.getKey())); + return !toRemove.isEmpty(); } /** @@ -346,7 +347,7 @@ public class DefaultSecuritySystem implements SecuritySystem { Preconditions.checkArgument(!Strings.isNullOrEmpty(perm.getName()), "name is required"); - Preconditions.checkArgument(!Strings.isNullOrEmpty(perm.getPermission()), + Preconditions.checkArgument(!isNull(perm.getPermission()), "permission is required"); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java new file mode 100644 index 0000000000..24895dce8c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -0,0 +1,35 @@ +package sonia.scm.security; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class PermissionAssigner { + + private final SecuritySystem securitySystem; + + public PermissionAssigner(SecuritySystem securitySystem) { + this.securitySystem = securitySystem; + } + + public Collection getAvailablePermissions() { + return securitySystem.getAvailablePermissions(); + } + + public Collection readPermissionsForUser(String id) { + return securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).collect(Collectors.toSet()); + } + + public void setPermissionsForUser(String id, Collection permissions) { + Collection existingPermissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)); + List toRemove = existingPermissions.stream() + .filter(p -> !permissions.contains(p.getPermission())) + .collect(Collectors.toList()); + toRemove.forEach(securitySystem::deletePermission); + + permissions.stream() + .map(p -> new AssignedPermission(id, false, p)) + .filter(p -> !existingPermissions.contains(p)) + .forEach(securitySystem::addPermission); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 2f455469dd..92e3ba71e6 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -33,7 +33,6 @@ package sonia.scm.security; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; -import com.google.common.base.Predicate; import com.google.common.collect.Lists; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -219,7 +218,7 @@ public class DefaultAuthorizationCollectorTest { StoredAssignedPermission p1 = new StoredAssignedPermission("one", new AssignedPermission("one", "one:one")); StoredAssignedPermission p2 = new StoredAssignedPermission("two", new AssignedPermission("two", "two:two")); - when(securitySystem.getPermissions(Mockito.any(Predicate.class))).thenReturn(Lists.newArrayList(p1, p2)); + when(securitySystem.getPermissions(any())).thenReturn(Lists.newArrayList(p1, p2)); // execute and assert AuthorizationInfo authInfo = collector.collect(); @@ -246,7 +245,7 @@ public class DefaultAuthorizationCollectorTest { verify(cache).clear(); collector.invalidateCache(AuthorizationChangedEvent.createForUser("dent")); - verify(cache).removeAll(Mockito.any(Predicate.class)); + verify(cache).removeAll(any()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index 74214376ec..e9b4a0ae00 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -95,7 +95,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase AssignedPermission sap = createPermission("trillian", false, "repository:*:READ"); assertEquals("trillian", sap.getName()); - assertEquals("repository:*:READ", sap.getPermission()); + assertEquals("repository:*:READ", sap.getPermission().getValue()); assertEquals(false, sap.isGroupPermission()); } @@ -256,7 +256,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase return securitySystem.getPermissions(permission -> Objects.equal(name, permission.getName()) && Objects.equal(groupPermission, permission.isGroupPermission()) - && Objects.equal(value, permission.getPermission())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found")); + && Objects.equal(value, permission.getPermission().getValue())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found")); } //~--- set methods ---------------------------------------------------------- diff --git a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java new file mode 100644 index 0000000000..f698f24bfb --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java @@ -0,0 +1,57 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; +import sonia.scm.util.ClassLoaders; + +import java.util.Collection; + +import static java.util.Arrays.asList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini", username = "dent", password = "secret") +public class PermissionAssignerTest { + + @Rule + public ShiroRule shiroRule = new ShiroRule(); + + private DefaultSecuritySystem securitySystem; + private PermissionAssigner permissionAssigner; + + @Before + public void init() { + PluginLoader pluginLoader = mock(PluginLoader.class); + when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); + + securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader); + + securitySystem.addPermission(new AssignedPermission("1", "perm:read:1")); + securitySystem.addPermission(new AssignedPermission("1", "perm:read:2")); + securitySystem.addPermission(new AssignedPermission("2", "perm:read:2")); + securitySystem.addPermission(new AssignedPermission("1", true, "perm:read:2")); + permissionAssigner = new PermissionAssigner(securitySystem); + } + + @Test + public void shouldFindUserPermissions() { + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } + + @Test + public void shouldOverwriteUserPermissions() { + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:3"), new PermissionDescriptor("perm:read:4"))); + + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("2"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } +} From 783c425b1ec6a28b62943f7fde51b6d840b99c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 17 Jan 2019 14:25:49 +0100 Subject: [PATCH 07/28] Rename Permission -> RepositoryPermission --- .../java/sonia/scm/repository/Repository.java | 12 ++-- ...mission.java => RepositoryPermission.java} | 28 ++++---- .../scm/api/v2/resources/MapperModule.java | 2 +- .../PermissionDtoToPermissionMapper.java | 8 +-- .../v2/resources/PermissionRootResource.java | 39 +++++------ .../RepositoryCollectionResource.java | 4 +- ...itoryPermissionCollectionToDtoMapper.java} | 18 ++--- ...nDto.java => RepositoryPermissionDto.java} | 4 +- ...ssionToRepositoryPermissionDtoMapper.java} | 18 ++--- .../DefaultAuthorizationCollector.java | 7 +- .../scm/security/PermissionAssigner.java | 2 + .../resources/PermissionRootResourceTest.java | 70 +++++++++---------- ...oryRepositoryPermissionDtoMapperTest.java} | 24 +++---- .../resources/RepositoryRootResourceTest.java | 4 +- .../RepositoryToRepositoryDtoMapperTest.java | 4 +- .../DefaultRepositoryManagerPerfTest.java | 2 +- ...AuthorizationChangedEventProducerTest.java | 13 ++-- .../DefaultAuthorizationCollectorTest.java | 5 +- 18 files changed, 133 insertions(+), 131 deletions(-) rename scm-core/src/main/java/sonia/scm/repository/{Permission.java => RepositoryPermission.java} (84%) rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{PermissionCollectionToDtoMapper.java => RepositoryPermissionCollectionToDtoMapper.java} (61%) rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{PermissionDto.java => RepositoryPermissionDto.java} (89%) rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{PermissionToPermissionDtoMapper.java => RepositoryPermissionToRepositoryPermissionDtoMapper.java} (69%) rename scm-webapp/src/test/java/sonia/scm/api/v2/resources/{PermissionToPermissionDtoMapperTest.java => RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java} (54%) 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 fd8f07df8d..622eed6ad6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -81,7 +81,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per private Long lastModified; private String namespace; private String name; - private final Set permissions = new HashSet<>(); + private final Set permissions = new HashSet<>(); @XmlElement(name = "public") private boolean publicReadable = false; private boolean archived = false; @@ -122,7 +122,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per * @param permissions permissions for specific users and groups. */ public Repository(String id, String type, String namespace, String name, String contact, - String description, Permission... permissions) { + String description, RepositoryPermission... permissions) { this.id = id; this.type = type; this.namespace = namespace; @@ -201,7 +201,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return new NamespaceAndName(getNamespace(), getName()); } - public Collection getPermissions() { + public Collection getPermissions() { return Collections.unmodifiableCollection(permissions); } @@ -297,16 +297,16 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per this.name = name; } - public void setPermissions(Collection permissions) { + public void setPermissions(Collection permissions) { this.permissions.clear(); this.permissions.addAll(permissions); } - public void addPermission(Permission newPermission) { + public void addPermission(RepositoryPermission newPermission) { this.permissions.add(newPermission); } - public void removePermission(Permission permission) { + public void removePermission(RepositoryPermission permission) { this.permissions.remove(permission); } diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java similarity index 84% rename from scm-core/src/main/java/sonia/scm/repository/Permission.java rename to scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java index 20cdc83cef..0aff771fce 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -53,7 +53,7 @@ import java.io.Serializable; */ @XmlRootElement(name = "permissions") @XmlAccessorType(XmlAccessType.FIELD) -public class Permission implements PermissionObject, Serializable +public class RepositoryPermission implements PermissionObject, Serializable { private static final long serialVersionUID = -2915175031430884040L; @@ -63,41 +63,41 @@ public class Permission implements PermissionObject, Serializable private PermissionType type = PermissionType.READ; /** - * Constructs a new {@link Permission}. + * Constructs a new {@link RepositoryPermission}. * This constructor is used by JAXB. * */ - public Permission() {} + public RepositoryPermission() {} /** - * Constructs a new {@link Permission} with type = {@link PermissionType#READ} + * Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ} * for the specified user. * * * @param name name of the user */ - public Permission(String name) + public RepositoryPermission(String name) { this(); this.name = name; } /** - * Constructs a new {@link Permission} with the specified type for + * 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 Permission(String name, PermissionType type) + public RepositoryPermission(String name, PermissionType type) { this(name); this.type = type; } /** - * Constructs a new {@link Permission} with the specified type for + * Constructs a new {@link RepositoryPermission} with the specified type for * the given user or group. * * @@ -105,7 +105,7 @@ public class Permission implements PermissionObject, Serializable * @param type type of the permission * @param groupPermission true if the permission is a permission for a group */ - public Permission(String name, PermissionType type, boolean groupPermission) + public RepositoryPermission(String name, PermissionType type, boolean groupPermission) { this(name, type); this.groupPermission = groupPermission; @@ -114,12 +114,12 @@ public class Permission implements PermissionObject, Serializable //~--- methods -------------------------------------------------------------- /** - * Returns true if the {@link Permission} is the same as the obj argument. + * Returns true if the {@link RepositoryPermission} is the same as the obj argument. * * * @param obj the reference object with which to compare * - * @return true if the {@link Permission} is the same as the obj argument + * @return true if the {@link RepositoryPermission} is the same as the obj argument */ @Override public boolean equals(Object obj) @@ -134,7 +134,7 @@ public class Permission implements PermissionObject, Serializable return false; } - final Permission other = (Permission) obj; + final RepositoryPermission other = (RepositoryPermission) obj; return Objects.equal(name, other.name) && Objects.equal(type, other.type) @@ -142,10 +142,10 @@ public class Permission implements PermissionObject, Serializable } /** - * Returns the hash code value for the {@link Permission}. + * Returns the hash code value for the {@link RepositoryPermission}. * * - * @return the hash code value for the {@link Permission} + * @return the hash code value for the {@link RepositoryPermission} */ @Override public int hashCode() diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 66eadaad7d..0be607def8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -27,7 +27,7 @@ public class MapperModule extends AbstractModule { bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); - bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); + bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass()); bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass()); bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java index 1e90c23aa7..8d9761c28c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -2,20 +2,20 @@ package sonia.scm.api.v2.resources; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; @Mapper public abstract class PermissionDtoToPermissionMapper { - public abstract Permission map(PermissionDto permissionDto); + public abstract RepositoryPermission map(RepositoryPermissionDto permissionDto); /** * this method is needed to modify an existing permission object * * @param target the target permission - * @param permissionDto the source dto + * @param repositoryPermissionDto the source dto * @return the mapped target permission object */ - public abstract void modify(@MappingTarget Permission target, PermissionDto permissionDto); + public abstract void modify(@MappingTarget RepositoryPermission target, RepositoryPermissionDto repositoryPermissionDto); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 127a3f450e..1aa67bb4aa 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -8,14 +8,13 @@ import lombok.extern.slf4j.Slf4j; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -33,24 +32,24 @@ import java.util.function.Predicate; import static sonia.scm.AlreadyExistsException.alreadyExists; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; -import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; +import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; @Slf4j public class PermissionRootResource { private PermissionDtoToPermissionMapper dtoToModelMapper; - private PermissionToPermissionDtoMapper modelToDtoMapper; - private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper; + private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; private ResourceLinks resourceLinks; private final RepositoryManager manager; @Inject - public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper, RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { this.dtoToModelMapper = dtoToModelMapper; this.modelToDtoMapper = modelToDtoMapper; - this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper; + this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper; this.resourceLinks = resourceLinks; this.manager = manager; } @@ -74,7 +73,7 @@ public class PermissionRootResource { @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PERMISSION) @Path("") - public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) { + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid RepositoryPermissionDto permission) { log.info("try to add new permission: {}", permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); @@ -101,7 +100,7 @@ public class PermissionRootResource { @ResponseCode(code = 500, condition = "internal server error") }) @Produces(VndMediaType.PERMISSION) - @TypeHint(PermissionDto.class) + @TypeHint(RepositoryPermissionDto.class) @Path("{permission-name}") public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) { Repository repository = load(namespace, name); @@ -112,7 +111,7 @@ public class PermissionRootResource { .filter(filterPermission(permissionName)) .map(permission -> modelToDtoMapper.map(permission, repository)) .findFirst() - .orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name))) + .orElseThrow(() -> notFound(entity(RepositoryPermission.class, namespace).in(Repository.class, namespace + "/" + name))) ).build(); } @@ -132,12 +131,12 @@ public class PermissionRootResource { @ResponseCode(code = 500, condition = "internal server error") }) @Produces(VndMediaType.PERMISSION) - @TypeHint(PermissionDto.class) + @TypeHint(RepositoryPermissionDto.class) @Path("") public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) { Repository repository = load(namespace, name); RepositoryPermissions.permissionRead(repository).check(); - return Response.ok(permissionCollectionToDtoMapper.map(repository)).build(); + return Response.ok(repositoryPermissionCollectionToDtoMapper.map(repository)).build(); } @@ -161,23 +160,23 @@ public class PermissionRootResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName, - @Valid PermissionDto permission) { + @Valid RepositoryPermissionDto permission) { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); String extractedPermissionName = getPermissionName(permissionName); - if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) { - throw notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name)); + if (!isPermissionExist(new RepositoryPermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) { + throw notFound(entity(RepositoryPermission.class, namespace).in(Repository.class, namespace + "/" + name)); } permission.setGroupPermission(isGroupPermission(permissionName)); if (!extractedPermissionName.equals(permission.getName())) { checkPermissionAlreadyExists(permission, repository); } - Permission existingPermission = repository.getPermissions() + RepositoryPermission existingPermission = repository.getPermissions() .stream() .filter(filterPermission(permissionName)) .findFirst() - .orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name))); + .orElseThrow(() -> notFound(entity(RepositoryPermission.class, namespace).in(Repository.class, namespace + "/" + name))); dtoToModelMapper.modify(existingPermission, permission); manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); @@ -216,7 +215,7 @@ public class PermissionRootResource { return Response.noContent().build(); } - Predicate filterPermission(String permissionName) { + Predicate filterPermission(String permissionName) { return permission -> getPermissionName(permissionName).equals(permission.getName()) && permission.isGroupPermission() == isGroupPermission(permissionName); @@ -255,13 +254,13 @@ public class PermissionRootResource { * @param repository the repository to be inspected * @throws AlreadyExistsException if the permission already exists in the repository */ - private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) { + private void checkPermissionAlreadyExists(RepositoryPermissionDto permission, Repository repository) { if (isPermissionExist(permission, repository)) { throw alreadyExists(entity("permission", permission.getName()).in(repository)); } } - private boolean isPermissionExist(PermissionDto permission, Repository repository) { + private boolean isPermissionExist(RepositoryPermissionDto permission, Repository repository) { return repository.getPermissions() .stream() .anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index d8d5280456..420e08fe96 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -6,7 +6,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.SecurityUtils; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -100,7 +100,7 @@ public class RepositoryCollectionResource { private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { Repository repository = dtoToRepositoryMapper.map(repositoryDto, null); - repository.setPermissions(singletonList(new Permission(currentUser(), PermissionType.OWNER))); + repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), PermissionType.OWNER))); return repository; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java similarity index 61% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java index 4789915f3d..9faad89473 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java @@ -14,23 +14,23 @@ import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; import static java.util.stream.Collectors.toList; -public class PermissionCollectionToDtoMapper { +public class RepositoryPermissionCollectionToDtoMapper { private final ResourceLinks resourceLinks; - private final PermissionToPermissionDtoMapper permissionToPermissionDtoMapper; + private final RepositoryPermissionToRepositoryPermissionDtoMapper repositoryPermissionToRepositoryPermissionDtoMapper; @Inject - public PermissionCollectionToDtoMapper(PermissionToPermissionDtoMapper permissionToPermissionDtoMapper, ResourceLinks resourceLinks) { + public RepositoryPermissionCollectionToDtoMapper(RepositoryPermissionToRepositoryPermissionDtoMapper repositoryPermissionToRepositoryPermissionDtoMapper, ResourceLinks resourceLinks) { this.resourceLinks = resourceLinks; - this.permissionToPermissionDtoMapper = permissionToPermissionDtoMapper; + this.repositoryPermissionToRepositoryPermissionDtoMapper = repositoryPermissionToRepositoryPermissionDtoMapper; } public HalRepresentation map(Repository repository) { - List permissionDtoList = repository.getPermissions() + List repositoryPermissionDtoList = repository.getPermissions() .stream() - .map(permission -> permissionToPermissionDtoMapper.map(permission, repository)) + .map(permission -> repositoryPermissionToRepositoryPermissionDtoMapper.map(permission, repository)) .collect(toList()); - return new HalRepresentation(createLinks(repository), embedDtos(permissionDtoList)); + return new HalRepresentation(createLinks(repository), embedDtos(repositoryPermissionDtoList)); } private Links createLinks(Repository repository) { @@ -43,9 +43,9 @@ public class PermissionCollectionToDtoMapper { return linksBuilder.build(); } - private Embedded embedDtos(List permissionDtoList) { + private Embedded embedDtos(List repositoryPermissionDtoList) { return embeddedBuilder() - .with("permissions", permissionDtoList) + .with("permissions", repositoryPermissionDtoList) .build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java similarity index 89% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index 82405a6ac2..6e6b9fd7fc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -13,7 +13,7 @@ import javax.validation.constraints.Pattern; import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN; @Getter @Setter @ToString @NoArgsConstructor -public class PermissionDto extends HalRepresentation { +public class RepositoryPermissionDto extends HalRepresentation { public static final String GROUP_PREFIX = "@"; @@ -33,7 +33,7 @@ public class PermissionDto extends HalRepresentation { private boolean groupPermission = false; - public PermissionDto(String permissionName, boolean groupPermission) { + public RepositoryPermissionDto(String permissionName, boolean groupPermission) { name = permissionName; this.groupPermission = groupPermission; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java similarity index 69% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java index d6ab3721cf..772495beec 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java @@ -7,7 +7,7 @@ import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; @@ -16,16 +16,16 @@ import java.util.Optional; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; -import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; +import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; @Mapper -public abstract class PermissionToPermissionDtoMapper { +public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper { @Inject private ResourceLinks resourceLinks; @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract PermissionDto map(Permission permission, @Context Repository repository); + public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Repository repository); @BeforeMapping @@ -40,7 +40,7 @@ public abstract class PermissionToPermissionDtoMapper { * @param repository the repository */ @AfterMapping - void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) { + void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Repository repository) { String permissionName = getUrlPermissionName(target); Links.Builder linksBuilder = linkingTo() .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName)); @@ -51,9 +51,9 @@ public abstract class PermissionToPermissionDtoMapper { target.add(linksBuilder.build()); } - public String getUrlPermissionName(PermissionDto permissionDto) { - return Optional.of(permissionDto.getName()) - .filter(p -> !permissionDto.isGroupPermission()) - .orElse(GROUP_PREFIX + permissionDto.getName()); + public String getUrlPermissionName(RepositoryPermissionDto repositoryPermissionDto) { + return Optional.of(repositoryPermissionDto.getName()) + .filter(p -> !repositoryPermissionDto.isGroupPermission()) + .orElse(GROUP_PREFIX + repositoryPermissionDto.getName()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 5560d77e4a..0eb9ba2b0d 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -54,7 +54,7 @@ import sonia.scm.cache.CacheManager; import sonia.scm.group.GroupNames; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.user.User; @@ -62,7 +62,6 @@ import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; import java.util.Collection; -import java.util.List; import java.util.Set; //~--- JDK imports ------------------------------------------------------------ @@ -199,13 +198,13 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectRepositoryPermissions(Builder builder, Repository repository, User user, GroupNames groups) { - Collection repositoryPermissions + Collection repositoryPermissions = repository.getPermissions(); if (Util.isNotEmpty(repositoryPermissions)) { boolean hasPermission = false; - for (sonia.scm.repository.Permission permission : repositoryPermissions) + for (RepositoryPermission permission : repositoryPermissions) { hasPermission = isUserPermitted(user, groups, permission); if (hasPermission) diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java index 24895dce8c..54a82607ab 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -1,5 +1,6 @@ package sonia.scm.security; +import javax.inject.Inject; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -8,6 +9,7 @@ public class PermissionAssigner { private final SecuritySystem securitySystem; + @Inject public PermissionAssigner(SecuritySystem securitySystem) { this.securitySystem = securitySystem; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index c008e0b8db..012656c4cd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -29,7 +29,7 @@ import org.junit.jupiter.api.TestFactory; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -58,7 +58,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; -import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; +import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX; @Slf4j @SubjectAware( @@ -77,14 +77,14 @@ public class PermissionRootResourceTest extends RepositoryTestBase { private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/"; private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME; private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }"; - private static final ArrayList TEST_PERMISSIONS = Lists + private static final ArrayList TEST_PERMISSIONS = Lists .newArrayList( - new Permission("user_write", PermissionType.WRITE, false), - new Permission("user_read", PermissionType.READ, false), - new Permission("user_owner", PermissionType.OWNER, false), - new Permission("group_read", PermissionType.READ, true), - new Permission("group_write", PermissionType.WRITE, true), - new Permission("group_owner", PermissionType.OWNER, true) + new RepositoryPermission("user_write", PermissionType.WRITE, false), + new RepositoryPermission("user_read", PermissionType.READ, false), + new RepositoryPermission("user_owner", PermissionType.OWNER, false), + new RepositoryPermission("group_read", PermissionType.READ, true), + new RepositoryPermission("group_write", PermissionType.WRITE, true), + new RepositoryPermission("group_owner", PermissionType.OWNER, true) ); private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() .description("GET all permissions") @@ -121,12 +121,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase { private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks - private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper; + private RepositoryPermissionToRepositoryPermissionDtoMapperImpl permissionToPermissionDtoMapper; @InjectMocks private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; - private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper; private PermissionRootResource permissionRootResource; @@ -137,8 +137,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() { initMocks(this); - permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); - permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); + repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); + permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager); super.permissionRootResource = Providers.of(permissionRootResource); dispatcher = createDispatcher(getRepositoryRootResource()); subjectThreadState.bind(); @@ -207,7 +207,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void shouldGetPermissionByName() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ); - Permission expectedPermission = TEST_PERMISSIONS.get(0); + RepositoryPermission expectedPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestGETPermission .expectedResponseStatus(200) .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) @@ -215,8 +215,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase { String body = response.getContentAsString(); ObjectMapper mapper = new ObjectMapper(); try { - PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class); - assertThat(actualPermissionDto) + RepositoryPermissionDto actualRepositoryPermissionDto = mapper.readValue(body, RepositoryPermissionDto.class); + assertThat(actualRepositoryPermissionDto) .as("response payload match permission object model") .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission, PERMISSION_READ)) ; @@ -259,10 +259,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void shouldGetCreatedPermissions() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); - Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); - ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); + RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", PermissionType.WRITE, true); + ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); permissions.add(newPermission); - ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); + ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); assertExpectedRequest(requestPOSTPermission .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") .expectedResponseStatus(201) @@ -276,7 +276,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void shouldNotAddExistingPermission() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); - Permission newPermission = TEST_PERMISSIONS.get(0); + RepositoryPermission newPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestPOSTPermission .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : false}") .expectedResponseStatus(409) @@ -286,10 +286,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void shouldGetUpdatedPermissions() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); - Permission modifiedPermission = TEST_PERMISSIONS.get(0); + RepositoryPermission modifiedPermission = TEST_PERMISSIONS.get(0); // modify the type to owner modifiedPermission.setType(PermissionType.OWNER); - ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); assertExpectedRequest(requestPUTPermission .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) @@ -305,8 +305,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void shouldDeletePermissions() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER); - Permission deletedPermission = TEST_PERMISSIONS.get(0); - ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + RepositoryPermission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) @@ -320,8 +320,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase { @Test public void deletingNotExistingPermissionShouldProcess() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER); - Permission deletedPermission = TEST_PERMISSIONS.get(0); - ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + RepositoryPermission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); assertExpectedRequest(requestDELETEPermission .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) .expectedResponseStatus(204) @@ -340,7 +340,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ); } - private void assertGettingExpectedPermissions(ImmutableList expectedPermissions, String userPermission) throws URISyntaxException { + private void assertGettingExpectedPermissions(ImmutableList expectedPermissions, String userPermission) throws URISyntaxException { assertExpectedRequest(requestGETAllPermissions .expectedResponseStatus(200) .responseValidator((response) -> { @@ -349,16 +349,16 @@ public class PermissionRootResourceTest extends RepositoryTestBase { try { HalRepresentation halRepresentation = mapper.readValue(body, HalRepresentation.class); List actualPermissionDtos = halRepresentation.getEmbedded().getItemsBy("permissions", HalRepresentation.class); - List permissionDtoStream = actualPermissionDtos.stream() + List repositoryPermissionDtoStream = actualPermissionDtos.stream() .map(hal -> { - PermissionDto result = new PermissionDto(); + RepositoryPermissionDto result = new RepositoryPermissionDto(); result.setName(hal.getAttribute("name").asText()); result.setType(hal.getAttribute("type").asText()); result.setGroupPermission(hal.getAttribute("groupPermission").asBoolean()); result.add(hal.getLinks()); return result; }).collect(Collectors.toList()); - assertThat(permissionDtoStream) + assertThat(repositoryPermissionDtoStream) .as("response payload match permission object models") .hasSize(expectedPermissions.size()) .usingRecursiveFieldByFieldElementComparator() @@ -371,15 +371,15 @@ public class PermissionRootResourceTest extends RepositoryTestBase { ); } - private PermissionDto[] getExpectedPermissionDtos(ArrayList permissions, String userPermission) { + private RepositoryPermissionDto[] getExpectedPermissionDtos(ArrayList permissions, String userPermission) { return permissions .stream() .map(p -> getExpectedPermissionDto(p, userPermission)) - .toArray(PermissionDto[]::new); + .toArray(RepositoryPermissionDto[]::new); } - private PermissionDto getExpectedPermissionDto(Permission permission, String userPermission) { - PermissionDto result = new PermissionDto(); + private RepositoryPermissionDto getExpectedPermissionDto(RepositoryPermission permission, String userPermission) { + RepositoryPermissionDto result = new RepositoryPermissionDto(); result.setName(permission.getName()); result.setGroupPermission(permission.isGroupPermission()); result.setType(permission.getType().name()); @@ -411,7 +411,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase { return mockRepository; } - private void createUserWithRepositoryAndPermissions(ArrayList permissions, String userPermission) { + private void createUserWithRepositoryAndPermissions(ArrayList permissions, String userPermission) { createUserWithRepository(userPermission).setPermissions(permissions); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java similarity index 54% rename from scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java rename to scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java index 31c2f0ec31..9a1ab148a3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java @@ -7,7 +7,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SubjectAware( configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class PermissionToPermissionDtoMapperTest { +public class RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest { @Rule public ShiroRule shiro = new ShiroRule(); @@ -30,31 +30,31 @@ public class PermissionToPermissionDtoMapperTest { private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks - PermissionToPermissionDtoMapperImpl mapper; + RepositoryPermissionToRepositoryPermissionDtoMapperImpl mapper; @Test @SubjectAware(username = "trillian", password = "secret") public void shouldMapGroupPermissionCorrectly() { Repository repository = getDummyRepository(); - Permission permission = new Permission("42", PermissionType.OWNER, true); + RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, true); - PermissionDto permissionDto = mapper.map(permission, repository); + RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository); - assertThat(permissionDto.getLinks().getLinkBy("self").isPresent()).isTrue(); - assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).contains("@42"); + assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").isPresent()).isTrue(); + assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").get().getHref()).contains("@42"); } @Test @SubjectAware(username = "trillian", password = "secret") public void shouldMapNonGroupPermissionCorrectly() { Repository repository = getDummyRepository(); - Permission permission = new Permission("42", PermissionType.OWNER, false); + RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, false); - PermissionDto permissionDto = mapper.map(permission, repository); + RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository); - assertThat(permissionDto.getLinks().getLinkBy("self").isPresent()).isTrue(); - assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).contains("42"); - assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).doesNotContain("@"); + assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").isPresent()).isTrue(); + assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").get().getHref()).contains("42"); + assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").get().getHref()).doesNotContain("@"); } private Repository getDummyRepository() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index fe403088f2..1677be95b1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -18,7 +18,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.NamespaceAndName; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryIsNotArchivedException; @@ -302,7 +302,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Test public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception { Repository existingRepository = mockRepository("space", "repo"); - existingRepository.setPermissions(singletonList(new Permission("user", PermissionType.READ))); + 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); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 1ddae1d107..9bf70093fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -10,7 +10,7 @@ import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; -import sonia.scm.repository.Permission; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; @@ -238,7 +238,7 @@ public class RepositoryToRepositoryDtoMapperTest { repository.setId("1"); repository.setCreationDate(System.currentTimeMillis()); repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure"))); - repository.setPermissions(singletonList(new Permission("permission", PermissionType.READ))); + repository.setPermissions(singletonList(new RepositoryPermission("permission", PermissionType.READ))); return repository; } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 9d03fa02ca..146810e787 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -184,7 +184,7 @@ private long calculateAverage(List times) { private Repository createTestRepository(int number) { Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number); - repository.addPermission(new Permission("trillian", PermissionType.READ)); + repository.addPermission(new RepositoryPermission("trillian", PermissionType.READ)); return repository; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java index 17e0a8ed52..ab8ce5dce8 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java @@ -43,6 +43,7 @@ import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryEvent; import sonia.scm.repository.RepositoryModificationEvent; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserEvent; @@ -173,10 +174,10 @@ public class AuthorizationChangedEventProducerTest { { Repository repositoryModified = RepositoryTestData.createHeartOfGold(); repositoryModified.setName("test123"); - repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test"))); + repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test"))); Repository repository = RepositoryTestData.createHeartOfGold(); - repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test"))); + repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test"))); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository)); assertEventIsNotFired(); @@ -184,18 +185,18 @@ public class AuthorizationChangedEventProducerTest { producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertEventIsNotFired(); - repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test"))); + repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test"))); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertEventIsNotFired(); - repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123"))); + repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123"))); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertGlobalEventIsFired(); resetStoredEvent(); repositoryModified.setPermissions( - Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true)) + Lists.newArrayList(new RepositoryPermission("test", PermissionType.READ, true)) ); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertGlobalEventIsFired(); @@ -203,7 +204,7 @@ public class AuthorizationChangedEventProducerTest { resetStoredEvent(); repositoryModified.setPermissions( - Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE)) + Lists.newArrayList(new RepositoryPermission("test", PermissionType.WRITE)) ); producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository)); assertGlobalEventIsFired(); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 92e3ba71e6..532768f39f 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -52,6 +52,7 @@ import sonia.scm.group.GroupNames; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; @@ -192,10 +193,10 @@ public class DefaultAuthorizationCollectorTest { authenticate(UserTestData.createTrillian(), group); Repository heartOfGold = RepositoryTestData.createHeartOfGold(); heartOfGold.setId("one"); - heartOfGold.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("trillian"))); + heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian"))); Repository puzzle42 = RepositoryTestData.create42Puzzle(); puzzle42.setId("two"); - sonia.scm.repository.Permission permission = new sonia.scm.repository.Permission(group, PermissionType.WRITE, true); + RepositoryPermission permission = new RepositoryPermission(group, PermissionType.WRITE, true); puzzle42.setPermissions(Lists.newArrayList(permission)); when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42)); From aa26a9c0e3e13b4a8818a90ec33dd0a687e574e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 17 Jan 2019 15:29:21 +0100 Subject: [PATCH 08/28] Fix REST endpoint for user permissions --- .../java/sonia/scm/security/Permission.java | 2 +- .../main/java/sonia/scm/web/VndMediaType.java | 1 + .../PermissionCollectionToDtoMapper.java | 40 ++++++++++ .../api/v2/resources/PermissionListDto.java | 10 ++- .../scm/api/v2/resources/ResourceLinks.java | 20 +++++ .../v2/resources/UserPermissionResource.java | 79 +++++++++++++++++++ .../scm/api/v2/resources/UserResource.java | 60 +++----------- .../api/v2/resources/UserToUserDtoMapper.java | 4 + .../api/v2/resources/ResourceLinksMock.java | 1 + .../v2/resources/UserRootResourceTest.java | 58 ++++++++++++-- 10 files changed, 215 insertions(+), 60 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java diff --git a/scm-core/src/main/java/sonia/scm/security/Permission.java b/scm-core/src/main/java/sonia/scm/security/Permission.java index ef6b350c09..1b7c34f740 100644 --- a/scm-core/src/main/java/sonia/scm/security/Permission.java +++ b/scm-core/src/main/java/sonia/scm/security/Permission.java @@ -6,7 +6,7 @@ import com.github.sdorra.ssp.StaticPermissions; @StaticPermissions( value = "permission", permissions = {}, - globalPermissions = {"list", "assign"} + globalPermissions = {"list", "read", "assign"} ) public interface Permission extends PermissionObject { } 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 2a409482c8..8596bab754 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -41,6 +41,7 @@ public class VndMediaType { public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX; @SuppressWarnings("squid:S2068") public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX; + public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX; public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java new file mode 100644 index 0000000000..093b2930e9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.Context; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.PermissionPermissions; + +import javax.inject.Inject; +import java.util.Collection; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +public class PermissionCollectionToDtoMapper { + + private final ResourceLinks resourceLinks; + + @Inject + public PermissionCollectionToDtoMapper(ResourceLinks resourceLinks) { + this.resourceLinks = resourceLinks; + } + + public PermissionListDto map(Collection permissions, @Context String userId) { + String[] permissionStrings = permissions + .stream() + .map(PermissionDescriptor::getValue) + .toArray(String[]::new); + PermissionListDto target = new PermissionListDto(permissionStrings); + + Links.Builder linksBuilder = linkingTo().self(resourceLinks.userPermissions().permissions(userId)); + + if (PermissionPermissions.assign().isPermitted()) { + linksBuilder.single(link("overwrite", resourceLinks.userPermissions().overwritePermissions(userId))); + } + + target.add(linksBuilder.build()); + + return target; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java index 807330dd97..23d57f4d8e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java @@ -1,5 +1,7 @@ package sonia.scm.api.v2.resources; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,7 +11,13 @@ import lombok.Setter; @Setter @AllArgsConstructor @NoArgsConstructor -public class PermissionListDto { +public class PermissionListDto extends HalRepresentation { private String[] permissions; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 0b09f0c5d7..3f7536e2e8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -96,6 +96,26 @@ class ResourceLinks { } } + UserPermissionLinks userPermissions() { + return new UserPermissionLinks(scmPathInfoStore.get()); + } + + static class UserPermissionLinks { + private final LinkBuilder userPermissionLinkBuilder; + + UserPermissionLinks(ScmPathInfo pathInfo) { + this.userPermissionLinkBuilder = new LinkBuilder(pathInfo, UserRootResource.class, UserResource.class, UserPermissionResource.class); + } + + public String permissions(String name) { + return userPermissionLinkBuilder.method("getUserResource").parameters(name).method("permissions").parameters().method("getPermissions").parameters().href(); + } + + public String overwritePermissions(String name) { + return userPermissionLinkBuilder.method("getUserResource").parameters(name).method("permissions").parameters().method("overwritePermissions").parameters().href(); + } + } + MeLinks me() { return new MeLinks(scmPathInfoStore.get(), this.user()); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java new file mode 100644 index 0000000000..9bd5396aa6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -0,0 +1,79 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +public class UserPermissionResource { + + private final PermissionAssigner permissionAssigner; + private final PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + + @Inject + public UserPermissionResource(PermissionAssigner permissionAssigner, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper) { + this.permissionAssigner = permissionAssigner; + this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper; + } + + /** + * Returns permissions for a user. + * + * @param id the id/name of the user + */ + @GET + @Path("") + @Produces(VndMediaType.USER) + @TypeHint(UserDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the user"), + @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getPermissions(@PathParam("id") String id) { + Collection permissions = permissionAssigner.readPermissionsForUser(id); + return Response.ok(permissionCollectionToDtoMapper.map(permissions, id)).build(); + } + + /** + * Sets permissions for a user. Overwrites all existing permissions. + * + * @param id id of the user to be modified + * @param newPermissions New list of permissions for the user + */ + @PUT + @Path("") + @Consumes(VndMediaType.PERMISSION_COLLECTION) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the correct privilege"), + @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) { + Collection permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + permissionAssigner.setPermissionsForUser(id, permissionDescriptors); + return Response.noContent().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 479e4094fb..6e90b4e6ec 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -4,9 +4,6 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; -import sonia.scm.security.AssignedPermission; -import sonia.scm.security.PermissionDescriptor; -import sonia.scm.security.SecuritySystem; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -30,16 +27,20 @@ public class UserResource { private final IdResourceManagerAdapter adapter; private final UserManager userManager; private final PasswordService passwordService; - private final SecuritySystem securitySystem; + private final UserPermissionResource userPermissionResource; @Inject - public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService, SecuritySystem securitySystem) { + public UserResource( + UserDtoToUserMapper dtoToUserMapper, + UserToUserDtoMapper userToDtoMapper, + UserManager manager, + PasswordService passwordService, UserPermissionResource userPermissionResource) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; this.adapter = new IdResourceManagerAdapter<>(manager, User.class); this.userManager = manager; this.passwordService = passwordService; - this.securitySystem = securitySystem; + this.userPermissionResource = userPermissionResource; } /** @@ -137,51 +138,8 @@ public class UserResource { return Response.noContent().build(); } - /** - * Returns permissions for a user. - * - * @param id the id/name of the user - */ - @GET @Path("permissions") - @Produces(VndMediaType.USER) - @TypeHint(UserDto.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the user"), - @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - public Response getPermissions(@PathParam("id") String id) { - String[] permissions = - securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)) - .stream() - .map(AssignedPermission::getPermission) - .map(PermissionDescriptor::getValue) - .toArray(String[]::new); - return Response.ok(new PermissionListDto(permissions)).build(); - } - - /** - * Sets permissions for a user. Overwrites all existing permissions. - * - * @param id id of the user to be modified - * @param newPermissions New list of permissions for the user - */ - @PUT - @Path("permissions") - @Consumes(VndMediaType.PASSWORD_OVERWRITE) - @StatusCodes({ - @ResponseCode(code = 204, condition = "update success"), - @ResponseCode(code = 400, condition = "Invalid body"), - @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user does not have the correct privilege"), - @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @TypeHint(TypeHint.NO_CONTENT.class) - public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) { - return Response.noContent().build(); + public UserPermissionResource permissions() { + return userPermissionResource; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java index 5874e5767a..3c7e9fd7f1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java @@ -5,6 +5,7 @@ import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; +import sonia.scm.security.PermissionPermissions; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; @@ -42,6 +43,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper { linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName()))); } } + if (PermissionPermissions.read().isPermitted()) { + linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(target.getName()))); + } appendLinks(new EdisonLinkAppender(linksBuilder), user); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 435d8b4673..96587271e3 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -16,6 +16,7 @@ public class ResourceLinksMock { when(resourceLinks.user()).thenReturn(userLinks); when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks)); when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); + when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(uriInfo)); when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo)); when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 06376675df..88142e4d50 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -14,10 +14,12 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import sonia.scm.ContextEntry; import sonia.scm.NotFoundException; import sonia.scm.PageResult; -import sonia.scm.security.SecuritySystem; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; import sonia.scm.user.ChangePasswordNotAllowedException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -27,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Collection; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; @@ -61,11 +64,13 @@ public class UserRootResourceTest { @Mock private UserManager userManager; @Mock - private SecuritySystem securitySystem; + private PermissionAssigner permissionAssigner; @InjectMocks private UserDtoToUserMapperImpl dtoToUserMapper; @InjectMocks private UserToUserDtoMapperImpl userToDtoMapper; + @InjectMocks + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); private User originalUser; @@ -83,7 +88,8 @@ public class UserRootResourceTest { UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks); UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, userCollectionToDtoMapper, resourceLinks, passwordService); - UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService, securitySystem); + UserPermissionResource userPermissionResource = new UserPermissionResource(permissionAssigner, permissionCollectionToDtoMapper); + UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService, userPermissionResource); UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource), Providers.of(userResource)); @@ -333,8 +339,6 @@ public class UserRootResourceTest { dispatcher.invoke(request, response); - System.out.println(response.getContentAsString()); - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertTrue(response.getContentAsString().contains("\"name\":\"Neo\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/?page=0")); @@ -351,8 +355,6 @@ public class UserRootResourceTest { dispatcher.invoke(request, response); - System.out.println(response.getContentAsString()); - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertTrue(response.getContentAsString().contains("\"name\":\"Neo\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/?page=1")); @@ -362,6 +364,48 @@ public class UserRootResourceTest { assertTrue(response.getContentAsString().contains("\"last\":{\"href\":\"/v2/users/?page=2")); } + @Test + public void shouldGetPermissionLink() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertTrue(response.getContentAsString().contains("\"permissions\":{")); + } + + @Test + public void shouldGetPermissions() throws URISyntaxException { + when(permissionAssigner.readPermissionsForUser("Neo")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); + MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertTrue(response.getContentAsString().contains("\"permissions\":[\"something:*\"]")); + } + + @Test + public void shouldSetPermissions() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .put("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions") + .contentType(VndMediaType.PERMISSION_COLLECTION) + .content("{\"permissions\":[\"other:*\"]}".getBytes()); + MockHttpResponse response = new MockHttpResponse(); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + doNothing().when(permissionAssigner).setPermissionsForUser(eq("Neo"), captor.capture()); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + assertEquals("other:*", captor.getValue().iterator().next().getValue()); + } + private PageResult createSingletonPageResult(int overallCount) { return new PageResult<>(singletonList(createDummyUser("Neo")), overallCount); } From f3531f6715b367590c5bbf34b17dd4b5ba071943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 10:48:57 +0100 Subject: [PATCH 09/28] Add link to permissions to index --- .../api/v2/resources/IndexDtoGenerator.java | 4 +++ .../v2/resources/PermissionRootResource.java | 2 +- ...sitoryPermissionCollectionToDtoMapper.java | 4 +-- ...issionToRepositoryPermissionDtoMapper.java | 6 ++--- .../RepositoryToRepositoryDtoMapper.java | 2 +- .../scm/api/v2/resources/ResourceLinks.java | 25 ++++++++++++++++--- .../api/v2/resources/ResourceLinksMock.java | 3 ++- 7 files changed, 34 insertions(+), 12 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 108d6fae5d..6377f21163 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -7,6 +7,7 @@ import org.apache.shiro.SecurityUtils; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.group.GroupPermissions; +import sonia.scm.security.PermissionPermissions; import sonia.scm.user.UserPermissions; import javax.inject.Inject; @@ -52,6 +53,9 @@ public class IndexDtoGenerator extends LinkAppenderMapper { builder.single(link("config", resourceLinks.config().self())); } builder.single(link("repositories", resourceLinks.repositoryCollection().self())); + if (PermissionPermissions.list().isPermitted()) { + builder.single(link("permissions", resourceLinks.permissions().self())); + } } else { builder.single(link("login", resourceLinks.authentication().jsonLogin())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 1aa67bb4aa..cb3821cd67 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -81,7 +81,7 @@ public class PermissionRootResource { repository.addPermission(dtoToModelMapper.map(permission)); manager.modify(repository); String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission); - return Response.created(URI.create(resourceLinks.permission().self(namespace, name, urlPermissionName))).build(); + return Response.created(URI.create(resourceLinks.repositoryPermission().self(namespace, name, urlPermissionName))).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java index 9faad89473..5e678212e8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java @@ -36,9 +36,9 @@ public class RepositoryPermissionCollectionToDtoMapper { private Links createLinks(Repository repository) { RepositoryPermissions.permissionRead(repository).check(); Links.Builder linksBuilder = linkingTo() - .with(Links.linkingTo().self(resourceLinks.permission().all(repository.getNamespace(), repository.getName())).build()); + .with(Links.linkingTo().self(resourceLinks.repositoryPermission().all(repository.getNamespace(), repository.getName())).build()); if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { - linksBuilder.single(link("create", resourceLinks.permission().create(repository.getNamespace(), repository.getName()))); + linksBuilder.single(link("create", resourceLinks.repositoryPermission().create(repository.getNamespace(), repository.getName()))); } return linksBuilder.build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java index 772495beec..9f9971ffce 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java @@ -43,10 +43,10 @@ public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper { void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Repository repository) { String permissionName = getUrlPermissionName(target); Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName)); + .self(resourceLinks.repositoryPermission().self(repository.getNamespace(), repository.getName(), permissionName)); if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), permissionName))); - linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), permissionName))); + linksBuilder.single(link("update", resourceLinks.repositoryPermission().update(repository.getNamespace(), repository.getName(), permissionName))); + linksBuilder.single(link("delete", resourceLinks.repositoryPermission().delete(repository.getNamespace(), repository.getName(), permissionName))); } target.add(linksBuilder.build()); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 743474825b..f15f7c4b00 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -41,7 +41,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper Date: Fri, 18 Jan 2019 10:49:35 +0100 Subject: [PATCH 10/28] Add interface to modify permissions for a user --- scm-ui/public/locales/en/permissions.json | 22 ++ scm-ui/public/locales/en/users.json | 6 + .../users/components/PermissionCheckbox.js | 32 +++ .../users/components/SetUserPermissions.js | 196 ++++++++++++++++++ .../navLinks/SetPermissionsNavLink.js | 28 +++ .../navLinks/SetPermissionsNavLink.test.js | 31 +++ scm-ui/src/users/components/navLinks/index.js | 1 + scm-ui/src/users/components/setPermissions.js | 13 ++ scm-ui/src/users/containers/SingleUser.js | 12 +- .../resources/META-INF/scm/permissions.xml | 4 +- 10 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 scm-ui/public/locales/en/permissions.json create mode 100644 scm-ui/src/users/components/PermissionCheckbox.js create mode 100644 scm-ui/src/users/components/SetUserPermissions.js create mode 100644 scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js create mode 100644 scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js create mode 100644 scm-ui/src/users/components/setPermissions.js diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json new file mode 100644 index 0000000000..71c2bcc9e4 --- /dev/null +++ b/scm-ui/public/locales/en/permissions.json @@ -0,0 +1,22 @@ +{ + "repository": { + "read": { + "*": { + "displayName": "Read all repositories", + "description": "Read access to all repositories" + } + }, + "write": { + "*": { + "displayName": "Modify all repositories", + "description": "May modify/configure all repositories" + } + } + }, + "user":{ + "*": { + "displayName": "Administer users", + "description": "May administer all users" + } + } +} diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index 2a9ee7b79d..aa7ed4e3fc 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -32,6 +32,9 @@ "set-password-button": { "label": "Set password" }, + "set-permissions-button": { + "label": "Set permissions" + }, "user-form": { "submit": "Submit" }, @@ -55,6 +58,9 @@ "password": { "set-password-successful": "Password successfully set" }, + "permissions": { + "set-permissions-successful": "Permissions successfully set" + }, "help": { "usernameHelpText": "Unique name of the user.", "displayNameHelpText": "Display name of the user.", diff --git a/scm-ui/src/users/components/PermissionCheckbox.js b/scm-ui/src/users/components/PermissionCheckbox.js new file mode 100644 index 0000000000..d45128134d --- /dev/null +++ b/scm-ui/src/users/components/PermissionCheckbox.js @@ -0,0 +1,32 @@ +// @flow + +import React from "react"; +import { translate } from "react-i18next"; +import { Checkbox } from "@scm-manager/ui-components"; + +type Props = { + permission: string, + checked: boolean, + onChange: (value: boolean, name: string) => void, + disabled: boolean, + t: string => string +}; + +class PermissionCheckbox extends React.Component { + render() { + const { t, permission, checked, onChange, disabled } = this.props; + const key = permission.split(":").join("."); + return ( + + ); + } +} + +export default translate("permissions")(PermissionCheckbox); diff --git a/scm-ui/src/users/components/SetUserPermissions.js b/scm-ui/src/users/components/SetUserPermissions.js new file mode 100644 index 0000000000..204381e9e4 --- /dev/null +++ b/scm-ui/src/users/components/SetUserPermissions.js @@ -0,0 +1,196 @@ +// @flow +import React from "react"; +import type { User } from "@scm-manager/ui-types"; +import { + Notification, + ErrorNotification, + SubmitButton +} from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; +import { setPermissions } from "./setPermissions"; +import { apiClient } from "@scm-manager/ui-components"; +import PermissionCheckbox from "./PermissionCheckbox"; +import { connect } from "react-redux"; +import { getLink } from "../../modules/indexResource"; + +type Props = { + user: User, + t: string => string, + permissionLink: string +}; + +type State = { + permissions: { [string]: boolean }, + loading: boolean, + error?: Error, + permissionsChanged: boolean, + permissionsSubmitted: boolean, + modifiable: boolean +}; + +class SetUserPermissions extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + permissions: { perm1: false, perm2: false }, + loading: true, + permissionsChanged: false, + permissionsSubmitted: false, + modifiable: false + }; + } + + setLoadingState = () => { + this.setState({ + ...this.state, + loading: true + }); + }; + + setErrorState = (error: Error) => { + this.setState({ + ...this.state, + error: error, + loading: false + }); + }; + + setSuccessfulState = () => { + this.setState({ + ...this.state, + loading: false, + permissionsSubmitted: true, + permissionsChanged: false + }); + }; + + componentDidMount(): void { + apiClient + .get(this.props.permissionLink) + .then(response => { + return response.json(); + }) + .then(response => { + const availablePermissions = response.permissions; + const permissions = {}; + availablePermissions.forEach(p => { + permissions[p] = false; + }); + this.setState({ permissions }, this.loadPermissionsForUser); + }); + } + + loadPermissionsForUser = () => { + apiClient + .get(this.props.user._links.permissions.href) + .then(response => { + return response.json(); + }) + .then(response => { + const checkedPermissions = response.permissions; + const modifiable = !!response._links.overwrite; + this.setState(state => { + const newPermissions = state.permissions; + checkedPermissions.forEach(name => (newPermissions[name] = true)); + return { + loading: false, + modifiable: modifiable, + permissions: newPermissions + }; + }); + }); + }; + + submit = (event: Event) => { + event.preventDefault(); + if (this.state.permissions) { + const { user } = this.props; + const { permissions } = this.state; + this.setLoadingState(); + const selectedPermissions = Object.entries(permissions) + .filter(e => e[1]) + .map(e => e[0]); + setPermissions(user._links.permissions.href, selectedPermissions) + .then(result => { + if (result.error) { + this.setErrorState(result.error); + } else { + this.setSuccessfulState(); + } + }) + .catch(err => {}); + } + }; + + render() { + const { t } = this.props; + const { loading, permissionsSubmitted, error } = this.state; + + let message = null; + + if (permissionsSubmitted) { + message = ( + this.onClose()} + /> + ); + } else if (error) { + message = ; + } + + return ( +
+ {message} + {this.renderPermissions()} + + + ); + } + + renderPermissions = () => { + const { modifiable, permissions } = this.state; + return Object.keys(permissions).map(p => ( +
+ +
+ )); + }; + + valueChanged = (value: boolean, name: string) => { + this.setState(state => { + const newPermissions = state.permissions; + newPermissions[name] = value; + return { + permissions: newPermissions, + permissionsChanged: true + }; + }); + }; + + onClose = () => { + this.setState({ + permissionsSubmitted: false + }); + }; +} + +const mapStateToProps = state => { + const permissionLink = getLink(state, "permissions"); + return { + permissionLink + }; +}; + +export default connect(mapStateToProps)(translate("users")(SetUserPermissions)); diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js new file mode 100644 index 0000000000..cfdb1775a3 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { User } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + user: User, + permissionsUrl: String +}; + +class ChangePermissionNavLink extends React.Component { + render() { + const { t, permissionsUrl } = this.props; + + if (!this.hasPermissionToSetPermission()) { + return null; + } + return ; + } + + hasPermissionToSetPermission = () => { + return this.props.user._links.permissions; + }; +} + +export default translate("users")(ChangePermissionNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js new file mode 100644 index 0000000000..0e5fcf4125 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js @@ -0,0 +1,31 @@ +import React from "react"; +import { shallow } from "enzyme"; +import "../../../tests/enzyme"; +import "../../../tests/i18n"; +import SetPermissionsNavLink from "./SetPermissionsNavLink"; + +it("should render nothing, if the permissions link is missing", () => { + const user = { + _links: {} + }; + + const navLink = shallow( + + ); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + permissions: { + href: "/permissions" + } + } + }; + + const navLink = shallow( + + ); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index a6d8370c00..eb39bb6726 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,3 +1,4 @@ export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; +export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/users/components/setPermissions.js b/scm-ui/src/users/components/setPermissions.js new file mode 100644 index 0000000000..4c54036fab --- /dev/null +++ b/scm-ui/src/users/components/setPermissions.js @@ -0,0 +1,13 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; + +export const CONTENT_TYPE_PERMISSIONS = + "application/vnd.scmm-permissionCollection+json;v=2"; + +export function setPermissions(url: string, permissions: string[]) { + return apiClient + .put(url, { permissions: permissions }, CONTENT_TYPE_PERMISSIONS) + .then(response => { + return response; + }); +} diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index 5f20598962..033c0aa0a2 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -27,11 +27,13 @@ import { import { DeleteUserNavLink, EditUserNavLink, - SetPasswordNavLink + SetPasswordNavLink, + SetPermissionsNavLink } from "./../components/navLinks"; import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; +import SetUserPermissions from "../components/SetUserPermissions"; type Props = { name: string, @@ -106,6 +108,10 @@ class SingleUser extends React.Component { path={`${url}/password`} component={() => } /> + } + />
@@ -119,6 +125,10 @@ class SingleUser extends React.Component { user={user} passwordUrl={`${url}/password`} /> +
diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 8978b050ca..55c8cc41a6 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -38,11 +38,11 @@ - repository:*:WRITE + repository:write:* - repository:*:OWNER + user:* From 19975f3f4bb74e08164ef14fcfe3e32b0b1f5469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 11:25:24 +0100 Subject: [PATCH 11/28] Remove POC status --- .../GlobalPermissionPocResource.java | 103 ------------------ .../resources/GlobalPermissionResource.java | 37 +++++++ .../scm/api/v2/resources/ResourceLinks.java | 2 +- 3 files changed, 38 insertions(+), 104 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java deleted file mode 100644 index 845aaddd07..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java +++ /dev/null @@ -1,103 +0,0 @@ -package sonia.scm.api.v2.resources; - -import lombok.extern.slf4j.Slf4j; -import sonia.scm.security.AssignedPermission; -import sonia.scm.security.PermissionDescriptor; -import sonia.scm.security.SecuritySystem; - -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -/** - * Global Permission Proof of Concept (POC). - * TODO Extend or delete this during implementation! - */ -@Path("v2/permissions") -@Slf4j -public class GlobalPermissionPocResource { - - private SecuritySystem securitySystem; - - @Inject - public GlobalPermissionPocResource(SecuritySystem securitySystem) { - this.securitySystem = securitySystem; - } - - - /** - - How to use this proof of concept? - - curl -vu scmadmin:scmadmin --data '{ - "active": true, - "admin": false, - "displayName": "arthur", - "mail": "x@abcde.cd", - "name": "arthur", - "password": "scmadmin", - "type": "xml" - }' \ - --header "Content-Type: application/vnd.scmm-user+json;v=2" http://localhost:8081/scm/api/v2/users/ - - curl -vu scmadmin:scmadmin --data '{ - "description": "descr", - "name": "configurers", - "members": [ "arthur" ] - }' \ - --header "Content-Type: application/vnd.scmm-group+json" http://localhost:8081/scm/api/v2/groups/ - - # not allowed - curl -vu arthur:scmadmin http://localhost:8081/scm/api/v2/config - # not allowed (empty) - curl -vu arthur:scmadmin "http://localhost:8081/scm/api/v2/groups/?sortBy=name&desc=true" | jq - - # Assign permissions (call this resource) - curl -X POST -vu scmadmin:scmadmin http://localhost:8081/scm/api/v2/permissions - - # Now allowed via individual permission - curl -vu arthur:scmadmin "http://localhost:8081/scm/api/v2/groups/?sortBy=name&desc=true" | jq - # allowed via group permission - curl -vu arthur:scmadmin http://localhost:8081/scm/api/v2/config | jq - */ - @POST - @Consumes(MediaType.APPLICATION_JSON) - @Path("") - public Response create() { - - // Should contain all permissions defined in permissions.xmls on the classpath. - // Core: scm-webapp/src/main/resources/META-INF/scm/permissions.xml - // Plugins, e.g. scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml - log.info("{} Available permissions: {}", securitySystem.getAvailablePermissions().size(), securitySystem.getAvailablePermissions()); - - assignExemplaryPermissions(); - - // TODO use created() - return Response.noContent().build(); - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("") - public Response getAll() { - String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); - return Response.ok(new PermissionListDto(permissions)).build(); - } - - protected void assignExemplaryPermissions() { - AssignedPermission groupPermission = new AssignedPermission("configurers", true, new PermissionDescriptor("configuration:*")); - log.info("try to add new permission: {}", groupPermission); - securitySystem.addPermission(groupPermission); - - AssignedPermission userPermission = new AssignedPermission("rene", new PermissionDescriptor("group:*")); - log.info("try to add new permission: {}", userPermission); - securitySystem.addPermission(userPermission); - } -} - - diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java new file mode 100644 index 0000000000..6ef0015e8f --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java @@ -0,0 +1,37 @@ +package sonia.scm.api.v2.resources; + +import lombok.extern.slf4j.Slf4j; +import sonia.scm.security.AssignedPermission; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.security.SecuritySystem; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path("v2/permissions") +public class GlobalPermissionResource { + + private SecuritySystem securitySystem; + + @Inject + public GlobalPermissionResource(SecuritySystem securitySystem) { + this.securitySystem = securitySystem; + } + + @GET + @Produces(VndMediaType.PERMISSION_COLLECTION) + @Path("") + public Response getAll() { + String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); + return Response.ok(new PermissionListDto(permissions)).build(); + } +} + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 7126ff5b94..1bc5f584a9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -616,7 +616,7 @@ class ResourceLinks { private final LinkBuilder permissionsLlinkBuilder; PermissionsLinks(ScmPathInfo scmPathInfo) { - this.permissionsLlinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionPocResource.class); + this.permissionsLlinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class); } String self() { From 5e7405b52d812c54321ceb6cccca937cb2e40853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 11:28:20 +0100 Subject: [PATCH 12/28] Cleanup class --- .../scm/api/v2/resources/GlobalPermissionResource.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java index 6ef0015e8f..ac73865910 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java @@ -1,18 +1,13 @@ package sonia.scm.api.v2.resources; -import lombok.extern.slf4j.Slf4j; -import sonia.scm.security.AssignedPermission; import sonia.scm.security.PermissionDescriptor; import sonia.scm.security.SecuritySystem; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.Consumes; import javax.ws.rs.GET; -import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Path("v2/permissions") @@ -33,5 +28,3 @@ public class GlobalPermissionResource { return Response.ok(new PermissionListDto(permissions)).build(); } } - - From 8000731ab709c0983b96ca126b186a65ece85b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 12:06:37 +0100 Subject: [PATCH 13/28] Add REST resource for group permissions --- .../resources/GlobalPermissionResource.java | 10 +-- .../v2/resources/GroupPermissionResource.java | 79 +++++++++++++++++++ .../scm/api/v2/resources/GroupResource.java | 10 ++- .../v2/resources/GroupToGroupDtoMapper.java | 4 + .../scm/api/v2/resources/ResourceLinks.java | 20 +++++ .../v2/resources/UserPermissionResource.java | 4 +- .../scm/security/PermissionAssigner.java | 36 ++++++++- .../v2/resources/GroupRootResourceTest.java | 53 ++++++++++++- .../api/v2/resources/ResourceLinksMock.java | 1 + 9 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java index ac73865910..f9cd015f45 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2.resources; +import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; -import sonia.scm.security.SecuritySystem; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -13,18 +13,18 @@ import javax.ws.rs.core.Response; @Path("v2/permissions") public class GlobalPermissionResource { - private SecuritySystem securitySystem; + private PermissionAssigner permissionAssigner; @Inject - public GlobalPermissionResource(SecuritySystem securitySystem) { - this.securitySystem = securitySystem; + public GlobalPermissionResource(PermissionAssigner permissionAssigner) { + this.permissionAssigner = permissionAssigner; } @GET @Produces(VndMediaType.PERMISSION_COLLECTION) @Path("") public Response getAll() { - String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); + String[] permissions = permissionAssigner.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); return Response.ok(new PermissionListDto(permissions)).build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java new file mode 100644 index 0000000000..888e527eee --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -0,0 +1,79 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +public class GroupPermissionResource { + + private final PermissionAssigner permissionAssigner; + private final PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + + @Inject + public GroupPermissionResource(PermissionAssigner permissionAssigner, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper) { + this.permissionAssigner = permissionAssigner; + this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper; + } + + /** + * Returns permissions for a group. + * + * @param id the id/name of the group + */ + @GET + @Path("") + @Produces(VndMediaType.PERMISSION_COLLECTION) + @TypeHint(PermissionListDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the group"), + @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getPermissions(@PathParam("id") String id) { + Collection permissions = permissionAssigner.readPermissionsForGroup(id); + return Response.ok(permissionCollectionToDtoMapper.map(permissions, id)).build(); + } + + /** + * Sets permissions for a group. Overwrites all existing permissions. + * + * @param id id of the group to be modified + * @param newPermissions New list of permissions for the group + */ + @PUT + @Path("") + @Consumes(VndMediaType.PERMISSION_COLLECTION) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current group does not have the correct privilege"), + @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) { + Collection permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + permissionAssigner.setPermissionsForGroup(id, permissionDescriptors); + return Response.noContent().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 6d0b921d02..cfc1916fc1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -8,7 +8,6 @@ import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -24,13 +23,15 @@ public class GroupResource { private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupDtoToGroupMapper dtoToGroupMapper; private final IdResourceManagerAdapter adapter; + private final GroupPermissionResource groupPermissionResource; @Inject public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper, - GroupDtoToGroupMapper groupDtoToGroupMapper) { + GroupDtoToGroupMapper groupDtoToGroupMapper, GroupPermissionResource groupPermissionResource) { this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.dtoToGroupMapper = groupDtoToGroupMapper; this.adapter = new IdResourceManagerAdapter<>(manager, Group.class); + this.groupPermissionResource = groupPermissionResource; } /** @@ -100,4 +101,9 @@ public class GroupResource { public Response update(@PathParam("id") String name, @Valid GroupDto group) { return adapter.update(name, existing -> dtoToGroupMapper.map(group)); } + + @Path("permissions") + public GroupPermissionResource permissions() { + return groupPermissionResource; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java index 9a25e711cd..bf866af350 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java @@ -6,6 +6,7 @@ import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import sonia.scm.group.Group; import sonia.scm.group.GroupPermissions; +import sonia.scm.security.PermissionPermissions; import javax.inject.Inject; import java.util.List; @@ -31,6 +32,9 @@ public abstract class GroupToGroupDtoMapper extends BaseMapper if (GroupPermissions.modify(group).isPermitted()) { linksBuilder.single(link("update", resourceLinks.group().update(target.getName()))); } + if (PermissionPermissions.read().isPermitted()) { + linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(target.getName()))); + } appendLinks(new EdisonLinkAppender(linksBuilder), group); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1bc5f584a9..c62a34f093 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -116,6 +116,26 @@ class ResourceLinks { } } + GroupPermissionLinks groupPermissions() { + return new GroupPermissionLinks(scmPathInfoStore.get()); + } + + static class GroupPermissionLinks { + private final LinkBuilder groupPermissionLinkBuilder; + + GroupPermissionLinks(ScmPathInfo pathInfo) { + this.groupPermissionLinkBuilder = new LinkBuilder(pathInfo, GroupRootResource.class, GroupResource.class, GroupPermissionResource.class); + } + + public String permissions(String name) { + return groupPermissionLinkBuilder.method("getGroupResource").parameters(name).method("permissions").parameters().method("getPermissions").parameters().href(); + } + + public String overwritePermissions(String name) { + return groupPermissionLinkBuilder.method("getGroupResource").parameters(name).method("permissions").parameters().method("overwritePermissions").parameters().href(); + } + } + MeLinks me() { return new MeLinks(scmPathInfoStore.get(), this.user()); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index 9bd5396aa6..35988b7fb2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -37,8 +37,8 @@ public class UserPermissionResource { */ @GET @Path("") - @Produces(VndMediaType.USER) - @TypeHint(UserDto.class) + @Produces(VndMediaType.PERMISSION_COLLECTION) + @TypeHint(PermissionListDto.class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java index 54a82607ab..2821460e55 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -3,6 +3,8 @@ package sonia.scm.security; import javax.inject.Inject; import java.util.Collection; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; public class PermissionAssigner { @@ -19,18 +21,46 @@ public class PermissionAssigner { } public Collection readPermissionsForUser(String id) { - return securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).collect(Collectors.toSet()); + return readPermissions(filterForUser(id)); + } + + public Collection readPermissionsForGroup(String id) { + return readPermissions(filterForGroup(id)); + } + + private Predicate filterForUser(String id) { + return p -> !p.isGroupPermission() && p.getName().equals(id); + } + + private Predicate filterForGroup(String id) { + return p -> p.isGroupPermission() && p.getName().equals(id); + } + + private Set readPermissions(Predicate predicate) { + return securitySystem.getPermissions(predicate) + .stream() + .map(AssignedPermission::getPermission) + .collect(Collectors.toSet()); } public void setPermissionsForUser(String id, Collection permissions) { - Collection existingPermissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)); + Collection existingPermissions = securitySystem.getPermissions(filterForUser(id)); + adaptPermissions(id, false, permissions, existingPermissions); + } + + public void setPermissionsForGroup(String id, Collection permissions) { + Collection existingPermissions = securitySystem.getPermissions(filterForGroup(id)); + adaptPermissions(id, true, permissions, existingPermissions); + } + + private void adaptPermissions(String id, boolean groupPermission, Collection permissions, Collection existingPermissions) { List toRemove = existingPermissions.stream() .filter(p -> !permissions.contains(p.getPermission())) .collect(Collectors.toList()); toRemove.forEach(securitySystem::deletePermission); permissions.stream() - .map(p -> new AssignedPermission(id, false, p)) + .map(p -> new AssignedPermission(id, groupPermission, p)) .filter(p -> !existingPermissions.contains(p)) .forEach(securitySystem::addPermission); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index f28cf49d03..646e9d0839 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -18,6 +18,8 @@ import sonia.scm.api.rest.JSONContextResolver; import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; @@ -25,6 +27,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Collection; import java.util.Collections; import static java.util.Collections.singletonList; @@ -54,10 +57,15 @@ public class GroupRootResourceTest { @Mock private GroupManager groupManager; + @Mock + private PermissionAssigner permissionAssigner; @InjectMocks private GroupDtoToGroupMapperImpl dtoToGroupMapper; @InjectMocks private GroupToGroupDtoMapperImpl groupToDtoMapper; + @InjectMocks + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + private ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); @@ -73,7 +81,8 @@ public class GroupRootResourceTest { GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks); - GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper); + GroupPermissionResource groupPermissionResource = new GroupPermissionResource(permissionAssigner, permissionCollectionToDtoMapper); + GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper, groupPermissionResource); GroupRootResource groupRootResource = new GroupRootResource(Providers.of(groupCollectionResource), Providers.of(groupResource)); dispatcher = createDispatcher(groupRootResource); @@ -307,6 +316,48 @@ public class GroupRootResourceTest { assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}")); } + @Test + public void shouldGetPermissionLink() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertTrue(response.getContentAsString().contains("\"permissions\":{")); + } + + @Test + public void shouldGetPermissions() throws URISyntaxException { + when(permissionAssigner.readPermissionsForGroup("admin")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); + MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertTrue(response.getContentAsString().contains("\"permissions\":[\"something:*\"]")); + } + + @Test + public void shouldSetPermissions() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .put("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions") + .contentType(VndMediaType.PERMISSION_COLLECTION) + .content("{\"permissions\":[\"other:*\"]}".getBytes()); + MockHttpResponse response = new MockHttpResponse(); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + doNothing().when(permissionAssigner).setPermissionsForGroup(eq("admin"), captor.capture()); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + assertEquals("other:*", captor.getValue().iterator().next().getValue()); + } + private Group createDummyGroup() { Group group = new Group(); group.setName("admin"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 268652833a..655d00fc10 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -20,6 +20,7 @@ public class ResourceLinksMock { when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo)); when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); + when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo)); when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); From 0711475d78e9529d2e0286b5a496671b1361ce4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 13:21:54 +0100 Subject: [PATCH 14/28] Copy user permission administration for groups --- scm-ui/public/locales/en/groups.json | 6 + .../groups/components/SetGroupPermissions.js | 198 ++++++++++++++++++ .../navLinks/SetPermissionsNavLink.js | 28 +++ .../src/groups/components/navLinks/index.js | 3 +- .../src/groups/components/setPermissions.js | 13 ++ scm-ui/src/groups/containers/SingleGroup.js | 16 +- 6 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 scm-ui/src/groups/components/SetGroupPermissions.js create mode 100644 scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js create mode 100644 scm-ui/src/groups/components/setPermissions.js diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index f1ebb95e18..7caa89e5ee 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -63,5 +63,11 @@ "submit": "Yes", "cancel": "No" } + }, + "set-permissions-button": { + "label": "Set permissions" + }, + "permissions": { + "set-permissions-successful": "Permissions successfully set" } } diff --git a/scm-ui/src/groups/components/SetGroupPermissions.js b/scm-ui/src/groups/components/SetGroupPermissions.js new file mode 100644 index 0000000000..ae4fb1f8d8 --- /dev/null +++ b/scm-ui/src/groups/components/SetGroupPermissions.js @@ -0,0 +1,198 @@ +// @flow +import React from "react"; +import type { Group } from "@scm-manager/ui-types"; +import { + Notification, + ErrorNotification, + SubmitButton +} from "@scm-manager/ui-components"; +import { translate } from "react-i18next"; +import { setPermissions } from "./setPermissions"; +import { apiClient } from "@scm-manager/ui-components"; +import PermissionCheckbox from "../../users/components/PermissionCheckbox"; +import { connect } from "react-redux"; +import { getLink } from "../../modules/indexResource"; + +type Props = { + group: Group, + t: string => string, + permissionLink: string +}; + +type State = { + permissions: { [string]: boolean }, + loading: boolean, + error?: Error, + permissionsChanged: boolean, + permissionsSubmitted: boolean, + modifiable: boolean +}; + +class SetGroupPermissions extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + permissions: { perm1: false, perm2: false }, + loading: true, + permissionsChanged: false, + permissionsSubmitted: false, + modifiable: false + }; + } + + setLoadingState = () => { + this.setState({ + ...this.state, + loading: true + }); + }; + + setErrorState = (error: Error) => { + this.setState({ + ...this.state, + error: error, + loading: false + }); + }; + + setSuccessfulState = () => { + this.setState({ + ...this.state, + loading: false, + permissionsSubmitted: true, + permissionsChanged: false + }); + }; + + componentDidMount(): void { + apiClient + .get(this.props.permissionLink) + .then(response => { + return response.json(); + }) + .then(response => { + const availablePermissions = response.permissions; + const permissions = {}; + availablePermissions.forEach(p => { + permissions[p] = false; + }); + this.setState({ permissions }, this.loadPermissionsForGroup); + }); + } + + loadPermissionsForGroup = () => { + apiClient + .get(this.props.group._links.permissions.href) + .then(response => { + return response.json(); + }) + .then(response => { + const checkedPermissions = response.permissions; + const modifiable = !!response._links.overwrite; + this.setState(state => { + const newPermissions = state.permissions; + checkedPermissions.forEach(name => (newPermissions[name] = true)); + return { + loading: false, + modifiable: modifiable, + permissions: newPermissions + }; + }); + }); + }; + + submit = (event: Event) => { + event.preventDefault(); + if (this.state.permissions) { + const { group } = this.props; + const { permissions } = this.state; + this.setLoadingState(); + const selectedPermissions = Object.entries(permissions) + .filter(e => e[1]) + .map(e => e[0]); + setPermissions(group._links.permissions.href, selectedPermissions) + .then(result => { + if (result.error) { + this.setErrorState(result.error); + } else { + this.setSuccessfulState(); + } + }) + .catch(err => {}); + } + }; + + render() { + const { t } = this.props; + const { loading, permissionsSubmitted, error } = this.state; + + let message = null; + + if (permissionsSubmitted) { + message = ( + this.onClose()} + /> + ); + } else if (error) { + message = ; + } + + return ( +
+ {message} + {this.renderPermissions()} + + + ); + } + + renderPermissions = () => { + const { modifiable, permissions } = this.state; + return Object.keys(permissions).map(p => ( +
+ +
+ )); + }; + + valueChanged = (value: boolean, name: string) => { + this.setState(state => { + const newPermissions = state.permissions; + newPermissions[name] = value; + return { + permissions: newPermissions, + permissionsChanged: true + }; + }); + }; + + onClose = () => { + this.setState({ + permissionsSubmitted: false + }); + }; +} + +const mapStateToProps = state => { + const permissionLink = getLink(state, "permissions"); + return { + permissionLink + }; +}; + +export default connect(mapStateToProps)( + translate("groups")(SetGroupPermissions) +); diff --git a/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js new file mode 100644 index 0000000000..41bc57da30 --- /dev/null +++ b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js @@ -0,0 +1,28 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import type { Group } from "@scm-manager/ui-types"; +import { NavLink } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + group: Group, + permissionsUrl: String +}; + +class ChangePermissionNavLink extends React.Component { + render() { + const { t, permissionsUrl } = this.props; + + if (!this.hasPermissionToSetPermission()) { + return null; + } + return ; + } + + hasPermissionToSetPermission = () => { + return this.props.group._links.permissions; + }; +} + +export default translate("groups")(ChangePermissionNavLink); diff --git a/scm-ui/src/groups/components/navLinks/index.js b/scm-ui/src/groups/components/navLinks/index.js index 30fdd34b6d..e589e5b6c9 100644 --- a/scm-ui/src/groups/components/navLinks/index.js +++ b/scm-ui/src/groups/components/navLinks/index.js @@ -1,2 +1,3 @@ export { default as DeleteGroupNavLink } from "./DeleteGroupNavLink"; -export { default as EditGroupNavLink } from "./EditGroupNavLink"; +export { default as EditGroupNavLink } from "./EditGroupNavLink"; +export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/groups/components/setPermissions.js b/scm-ui/src/groups/components/setPermissions.js new file mode 100644 index 0000000000..4c54036fab --- /dev/null +++ b/scm-ui/src/groups/components/setPermissions.js @@ -0,0 +1,13 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; + +export const CONTENT_TYPE_PERMISSIONS = + "application/vnd.scmm-permissionCollection+json;v=2"; + +export function setPermissions(url: string, permissions: string[]) { + return apiClient + .put(url, { permissions: permissions }, CONTENT_TYPE_PERMISSIONS) + .then(response => { + return response; + }); +} diff --git a/scm-ui/src/groups/containers/SingleGroup.js b/scm-ui/src/groups/containers/SingleGroup.js index 1dd4aa569f..ce60898700 100644 --- a/scm-ui/src/groups/containers/SingleGroup.js +++ b/scm-ui/src/groups/containers/SingleGroup.js @@ -11,7 +11,11 @@ import { } from "@scm-manager/ui-components"; import { Route } from "react-router"; import { Details } from "./../components/table"; -import { DeleteGroupNavLink, EditGroupNavLink } from "./../components/navLinks"; +import { + DeleteGroupNavLink, + EditGroupNavLink, + SetPermissionsNavLink +} from "./../components/navLinks"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; import { @@ -27,6 +31,7 @@ import { import { translate } from "react-i18next"; import EditGroup from "./EditGroup"; import { getGroupsLink } from "../../modules/indexResource"; +import SetGroupPermissions from "../components/SetGroupPermissions"; type Props = { name: string, @@ -102,6 +107,11 @@ class SingleGroup extends React.Component { exact component={() => } /> + } + />
@@ -110,6 +120,10 @@ class SingleGroup extends React.Component { to={`${url}`} label={t("single-group.information-label")} /> +
Date: Fri, 18 Jan 2019 14:16:26 +0100 Subject: [PATCH 15/28] Extract single permission editor --- scm-ui/public/locales/en/groups.json | 3 - scm-ui/public/locales/en/permissions.json | 6 + scm-ui/public/locales/en/users.json | 3 - .../groups/components/SetGroupPermissions.js | 198 ------------------ scm-ui/src/groups/containers/SingleGroup.js | 6 +- .../components/PermissionCheckbox.js | 2 +- .../components/SetPermissions.js} | 64 +++--- .../components/setPermissions.js | 0 scm-ui/src/users/components/setPermissions.js | 13 -- scm-ui/src/users/containers/SingleUser.js | 8 +- 10 files changed, 52 insertions(+), 251 deletions(-) delete mode 100644 scm-ui/src/groups/components/SetGroupPermissions.js rename scm-ui/src/{users => permissions}/components/PermissionCheckbox.js (89%) rename scm-ui/src/{users/components/SetUserPermissions.js => permissions/components/SetPermissions.js} (73%) rename scm-ui/src/{groups => permissions}/components/setPermissions.js (100%) delete mode 100644 scm-ui/src/users/components/setPermissions.js diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index 7caa89e5ee..3fbe088029 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -66,8 +66,5 @@ }, "set-permissions-button": { "label": "Set permissions" - }, - "permissions": { - "set-permissions-successful": "Permissions successfully set" } } diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json index 71c2bcc9e4..3e929434cb 100644 --- a/scm-ui/public/locales/en/permissions.json +++ b/scm-ui/public/locales/en/permissions.json @@ -18,5 +18,11 @@ "displayName": "Administer users", "description": "May administer all users" } + }, + "form": { + "submit-button": { + "label": "Set permissions" + }, + "set-permissions-successful": "Permissions set successfully" } } diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index aa7ed4e3fc..afe86deb9b 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -58,9 +58,6 @@ "password": { "set-password-successful": "Password successfully set" }, - "permissions": { - "set-permissions-successful": "Permissions successfully set" - }, "help": { "usernameHelpText": "Unique name of the user.", "displayNameHelpText": "Display name of the user.", diff --git a/scm-ui/src/groups/components/SetGroupPermissions.js b/scm-ui/src/groups/components/SetGroupPermissions.js deleted file mode 100644 index ae4fb1f8d8..0000000000 --- a/scm-ui/src/groups/components/SetGroupPermissions.js +++ /dev/null @@ -1,198 +0,0 @@ -// @flow -import React from "react"; -import type { Group } from "@scm-manager/ui-types"; -import { - Notification, - ErrorNotification, - SubmitButton -} from "@scm-manager/ui-components"; -import { translate } from "react-i18next"; -import { setPermissions } from "./setPermissions"; -import { apiClient } from "@scm-manager/ui-components"; -import PermissionCheckbox from "../../users/components/PermissionCheckbox"; -import { connect } from "react-redux"; -import { getLink } from "../../modules/indexResource"; - -type Props = { - group: Group, - t: string => string, - permissionLink: string -}; - -type State = { - permissions: { [string]: boolean }, - loading: boolean, - error?: Error, - permissionsChanged: boolean, - permissionsSubmitted: boolean, - modifiable: boolean -}; - -class SetGroupPermissions extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - permissions: { perm1: false, perm2: false }, - loading: true, - permissionsChanged: false, - permissionsSubmitted: false, - modifiable: false - }; - } - - setLoadingState = () => { - this.setState({ - ...this.state, - loading: true - }); - }; - - setErrorState = (error: Error) => { - this.setState({ - ...this.state, - error: error, - loading: false - }); - }; - - setSuccessfulState = () => { - this.setState({ - ...this.state, - loading: false, - permissionsSubmitted: true, - permissionsChanged: false - }); - }; - - componentDidMount(): void { - apiClient - .get(this.props.permissionLink) - .then(response => { - return response.json(); - }) - .then(response => { - const availablePermissions = response.permissions; - const permissions = {}; - availablePermissions.forEach(p => { - permissions[p] = false; - }); - this.setState({ permissions }, this.loadPermissionsForGroup); - }); - } - - loadPermissionsForGroup = () => { - apiClient - .get(this.props.group._links.permissions.href) - .then(response => { - return response.json(); - }) - .then(response => { - const checkedPermissions = response.permissions; - const modifiable = !!response._links.overwrite; - this.setState(state => { - const newPermissions = state.permissions; - checkedPermissions.forEach(name => (newPermissions[name] = true)); - return { - loading: false, - modifiable: modifiable, - permissions: newPermissions - }; - }); - }); - }; - - submit = (event: Event) => { - event.preventDefault(); - if (this.state.permissions) { - const { group } = this.props; - const { permissions } = this.state; - this.setLoadingState(); - const selectedPermissions = Object.entries(permissions) - .filter(e => e[1]) - .map(e => e[0]); - setPermissions(group._links.permissions.href, selectedPermissions) - .then(result => { - if (result.error) { - this.setErrorState(result.error); - } else { - this.setSuccessfulState(); - } - }) - .catch(err => {}); - } - }; - - render() { - const { t } = this.props; - const { loading, permissionsSubmitted, error } = this.state; - - let message = null; - - if (permissionsSubmitted) { - message = ( - this.onClose()} - /> - ); - } else if (error) { - message = ; - } - - return ( -
- {message} - {this.renderPermissions()} - - - ); - } - - renderPermissions = () => { - const { modifiable, permissions } = this.state; - return Object.keys(permissions).map(p => ( -
- -
- )); - }; - - valueChanged = (value: boolean, name: string) => { - this.setState(state => { - const newPermissions = state.permissions; - newPermissions[name] = value; - return { - permissions: newPermissions, - permissionsChanged: true - }; - }); - }; - - onClose = () => { - this.setState({ - permissionsSubmitted: false - }); - }; -} - -const mapStateToProps = state => { - const permissionLink = getLink(state, "permissions"); - return { - permissionLink - }; -}; - -export default connect(mapStateToProps)( - translate("groups")(SetGroupPermissions) -); diff --git a/scm-ui/src/groups/containers/SingleGroup.js b/scm-ui/src/groups/containers/SingleGroup.js index ce60898700..7e8011e2de 100644 --- a/scm-ui/src/groups/containers/SingleGroup.js +++ b/scm-ui/src/groups/containers/SingleGroup.js @@ -31,7 +31,7 @@ import { import { translate } from "react-i18next"; import EditGroup from "./EditGroup"; import { getGroupsLink } from "../../modules/indexResource"; -import SetGroupPermissions from "../components/SetGroupPermissions"; +import SetPermissions from "../../permissions/components/SetPermissions"; type Props = { name: string, @@ -110,7 +110,9 @@ class SingleGroup extends React.Component { } + component={() => ( + + )} />
diff --git a/scm-ui/src/users/components/PermissionCheckbox.js b/scm-ui/src/permissions/components/PermissionCheckbox.js similarity index 89% rename from scm-ui/src/users/components/PermissionCheckbox.js rename to scm-ui/src/permissions/components/PermissionCheckbox.js index d45128134d..5f9b2774a4 100644 --- a/scm-ui/src/users/components/PermissionCheckbox.js +++ b/scm-ui/src/permissions/components/PermissionCheckbox.js @@ -2,7 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; -import { Checkbox } from "@scm-manager/ui-components"; +import { Checkbox } from "../../../../scm-ui-components/packages/ui-components/src"; type Props = { permission: string, diff --git a/scm-ui/src/users/components/SetUserPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js similarity index 73% rename from scm-ui/src/users/components/SetUserPermissions.js rename to scm-ui/src/permissions/components/SetPermissions.js index 204381e9e4..5d0d2b7a1f 100644 --- a/scm-ui/src/users/components/SetUserPermissions.js +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -1,6 +1,6 @@ // @flow import React from "react"; -import type { User } from "@scm-manager/ui-types"; +import type { Link } from "@scm-manager/ui-types"; import { Notification, ErrorNotification, @@ -14,9 +14,9 @@ import { connect } from "react-redux"; import { getLink } from "../../modules/indexResource"; type Props = { - user: User, t: string => string, - permissionLink: string + availablePermissionLink: string, + selectedPermissionsLink: Link }; type State = { @@ -25,19 +25,20 @@ type State = { error?: Error, permissionsChanged: boolean, permissionsSubmitted: boolean, - modifiable: boolean + overwritePermissionsLink?: Link }; -class SetUserPermissions extends React.Component { +class SetPermissions extends React.Component { constructor(props: Props) { super(props); this.state = { - permissions: { perm1: false, perm2: false }, + permissions: {}, loading: true, permissionsChanged: false, permissionsSubmitted: false, - modifiable: false + modifiable: false, + overwritePermissionsLink: undefined }; } @@ -67,7 +68,7 @@ class SetUserPermissions extends React.Component { componentDidMount(): void { apiClient - .get(this.props.permissionLink) + .get(this.props.availablePermissionLink) .then(response => { return response.json(); }) @@ -83,20 +84,19 @@ class SetUserPermissions extends React.Component { loadPermissionsForUser = () => { apiClient - .get(this.props.user._links.permissions.href) + .get(this.props.selectedPermissionsLink.href) .then(response => { return response.json(); }) .then(response => { const checkedPermissions = response.permissions; - const modifiable = !!response._links.overwrite; this.setState(state => { const newPermissions = state.permissions; checkedPermissions.forEach(name => (newPermissions[name] = true)); return { loading: false, - modifiable: modifiable, - permissions: newPermissions + permissions: newPermissions, + overwritePermissionsLink: response._links.overwrite }; }); }); @@ -105,21 +105,25 @@ class SetUserPermissions extends React.Component { submit = (event: Event) => { event.preventDefault(); if (this.state.permissions) { - const { user } = this.props; const { permissions } = this.state; this.setLoadingState(); const selectedPermissions = Object.entries(permissions) .filter(e => e[1]) .map(e => e[0]); - setPermissions(user._links.permissions.href, selectedPermissions) - .then(result => { - if (result.error) { - this.setErrorState(result.error); - } else { - this.setSuccessfulState(); - } - }) - .catch(err => {}); + if (this.state.overwritePermissionsLink) { + setPermissions( + this.state.overwritePermissionsLink.href, + selectedPermissions + ) + .then(result => { + if (result.error) { + this.setErrorState(result.error); + } else { + this.setSuccessfulState(); + } + }) + .catch(err => {}); + } } }; @@ -133,7 +137,7 @@ class SetUserPermissions extends React.Component { message = ( this.onClose()} /> ); @@ -148,21 +152,21 @@ class SetUserPermissions extends React.Component { ); } renderPermissions = () => { - const { modifiable, permissions } = this.state; + const { overwritePermissionsLink, permissions } = this.state; return Object.keys(permissions).map(p => (
)); @@ -187,10 +191,12 @@ class SetUserPermissions extends React.Component { } const mapStateToProps = state => { - const permissionLink = getLink(state, "permissions"); + const availablePermissionLink = getLink(state, "permissions"); return { - permissionLink + availablePermissionLink }; }; -export default connect(mapStateToProps)(translate("users")(SetUserPermissions)); +export default connect(mapStateToProps)( + translate("permissions")(SetPermissions) +); diff --git a/scm-ui/src/groups/components/setPermissions.js b/scm-ui/src/permissions/components/setPermissions.js similarity index 100% rename from scm-ui/src/groups/components/setPermissions.js rename to scm-ui/src/permissions/components/setPermissions.js diff --git a/scm-ui/src/users/components/setPermissions.js b/scm-ui/src/users/components/setPermissions.js deleted file mode 100644 index 4c54036fab..0000000000 --- a/scm-ui/src/users/components/setPermissions.js +++ /dev/null @@ -1,13 +0,0 @@ -//@flow -import { apiClient } from "@scm-manager/ui-components"; - -export const CONTENT_TYPE_PERMISSIONS = - "application/vnd.scmm-permissionCollection+json;v=2"; - -export function setPermissions(url: string, permissions: string[]) { - return apiClient - .put(url, { permissions: permissions }, CONTENT_TYPE_PERMISSIONS) - .then(response => { - return response; - }); -} diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index 033c0aa0a2..bd172f3f41 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -33,7 +33,7 @@ import { import { translate } from "react-i18next"; import { getUsersLink } from "../../modules/indexResource"; import SetUserPassword from "../components/SetUserPassword"; -import SetUserPermissions from "../components/SetUserPermissions"; +import SetPermissions from "../../permissions/components/SetPermissions"; type Props = { name: string, @@ -110,7 +110,11 @@ class SingleUser extends React.Component { /> } + component={() => ( + + )} />
From 05f9483a9c72a8915422f3dec40db61fd296117c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 14:17:07 +0100 Subject: [PATCH 16/28] Fix permission overwrite links for groups --- .../api/v2/resources/GroupPermissionResource.java | 2 +- .../resources/PermissionCollectionToDtoMapper.java | 14 +++++++++++--- .../sonia/scm/api/v2/resources/ResourceLinks.java | 10 ++++++++-- .../api/v2/resources/UserPermissionResource.java | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java index 888e527eee..11934abcb0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -48,7 +48,7 @@ public class GroupPermissionResource { }) public Response getPermissions(@PathParam("id") String id) { Collection permissions = permissionAssigner.readPermissionsForGroup(id); - return Response.ok(permissionCollectionToDtoMapper.map(permissions, id)).build(); + return Response.ok(permissionCollectionToDtoMapper.mapForGroup(permissions, id)).build(); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java index 093b2930e9..87d1aeca9f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -20,17 +20,25 @@ public class PermissionCollectionToDtoMapper { this.resourceLinks = resourceLinks; } - public PermissionListDto map(Collection permissions, @Context String userId) { + public PermissionListDto mapForUser(Collection permissions, String userId) { + return map(permissions, userId, resourceLinks.userPermissions()); + } + + public PermissionListDto mapForGroup(Collection permissions, String groupId) { + return map(permissions, groupId, resourceLinks.groupPermissions()); + } + + private PermissionListDto map(Collection permissions, String id, ResourceLinks.WithPermissionLinks links) { String[] permissionStrings = permissions .stream() .map(PermissionDescriptor::getValue) .toArray(String[]::new); PermissionListDto target = new PermissionListDto(permissionStrings); - Links.Builder linksBuilder = linkingTo().self(resourceLinks.userPermissions().permissions(userId)); + Links.Builder linksBuilder = linkingTo().self(links.permissions(id)); if (PermissionPermissions.assign().isPermitted()) { - linksBuilder.single(link("overwrite", resourceLinks.userPermissions().overwritePermissions(userId))); + linksBuilder.single(link("overwrite", links.overwritePermissions(id))); } target.add(linksBuilder.build()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index c62a34f093..be036007be 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -96,11 +96,17 @@ class ResourceLinks { } } + interface WithPermissionLinks { + String permissions(String name); + + String overwritePermissions(String name); + } + UserPermissionLinks userPermissions() { return new UserPermissionLinks(scmPathInfoStore.get()); } - static class UserPermissionLinks { + static class UserPermissionLinks implements WithPermissionLinks { private final LinkBuilder userPermissionLinkBuilder; UserPermissionLinks(ScmPathInfo pathInfo) { @@ -120,7 +126,7 @@ class ResourceLinks { return new GroupPermissionLinks(scmPathInfoStore.get()); } - static class GroupPermissionLinks { + static class GroupPermissionLinks implements WithPermissionLinks { private final LinkBuilder groupPermissionLinkBuilder; GroupPermissionLinks(ScmPathInfo pathInfo) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index 35988b7fb2..2b02104646 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -48,7 +48,7 @@ public class UserPermissionResource { }) public Response getPermissions(@PathParam("id") String id) { Collection permissions = permissionAssigner.readPermissionsForUser(id); - return Response.ok(permissionCollectionToDtoMapper.map(permissions, id)).build(); + return Response.ok(permissionCollectionToDtoMapper.mapForUser(permissions, id)).build(); } /** From f781778908517853cf1a3ccf608c0efa38bbdebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 14:52:17 +0100 Subject: [PATCH 17/28] Fix permission checks --- .../scm/security/PermissionAssigner.java | 3 ++ .../security/DefaultSecuritySystemTest.java | 16 -------- .../scm/security/PermissionAssignerTest.java | 40 +++++++++++++++++-- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java index 2821460e55..b7874add69 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -17,6 +17,7 @@ public class PermissionAssigner { } public Collection getAvailablePermissions() { + PermissionPermissions.read().check(); return securitySystem.getAvailablePermissions(); } @@ -37,6 +38,7 @@ public class PermissionAssigner { } private Set readPermissions(Predicate predicate) { + PermissionPermissions.read().check(); return securitySystem.getPermissions(predicate) .stream() .map(AssignedPermission::getPermission) @@ -54,6 +56,7 @@ public class PermissionAssigner { } private void adaptPermissions(String id, boolean groupPermission, Collection permissions, Collection existingPermissions) { + PermissionPermissions.assign().check(); List toRemove = existingPermissions.stream() .filter(p -> !permissions.contains(p.getPermission())) .collect(Collectors.toList()); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index e9b4a0ae00..457bb96ae4 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -221,22 +221,6 @@ public class DefaultSecuritySystemTest extends AbstractTestBase securitySystem.deletePermission(sap); } - /** - * Method description - * - */ - @Test(expected = UnauthorizedException.class) - public void testUnauthorizedGetPermission() - { - setAdminSubject(); - - createPermission("trillian", false, - "repository:*:READ"); - - setUserSubject(); - securitySystem.getPermissions(p -> true); - } - /** * Method description * diff --git a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java index f698f24bfb..a5eb32b594 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java @@ -2,10 +2,12 @@ package sonia.scm.security; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.authz.UnauthorizedException; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import sonia.scm.plugin.PluginLoader; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.util.ClassLoaders; @@ -22,6 +24,9 @@ public class PermissionAssignerTest { @Rule public ShiroRule shiroRule = new ShiroRule(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private DefaultSecuritySystem securitySystem; private PermissionAssigner permissionAssigner; @@ -32,10 +37,14 @@ public class PermissionAssignerTest { securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader); - securitySystem.addPermission(new AssignedPermission("1", "perm:read:1")); - securitySystem.addPermission(new AssignedPermission("1", "perm:read:2")); - securitySystem.addPermission(new AssignedPermission("2", "perm:read:2")); - securitySystem.addPermission(new AssignedPermission("1", true, "perm:read:2")); + try { + securitySystem.addPermission(new AssignedPermission("1", "perm:read:1")); + securitySystem.addPermission(new AssignedPermission("1", "perm:read:2")); + securitySystem.addPermission(new AssignedPermission("2", "perm:read:2")); + securitySystem.addPermission(new AssignedPermission("1", true, "perm:read:2")); + } catch (UnauthorizedException e) { + // ignore for tests with limited privileges + } permissionAssigner = new PermissionAssigner(securitySystem); } @@ -46,6 +55,21 @@ public class PermissionAssignerTest { Assertions.assertThat(permissionDescriptors).hasSize(2); } + @Test + public void shouldFindGroupPermissions() { + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldNotReadUserPermissionsForUnprivilegedUser() { + expectedException.expect(UnauthorizedException.class); + + permissionAssigner.readPermissionsForUser("1"); + } + @Test public void shouldOverwriteUserPermissions() { permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:3"), new PermissionDescriptor("perm:read:4"))); @@ -54,4 +78,12 @@ public class PermissionAssignerTest { Assertions.assertThat(permissionDescriptors).hasSize(2); } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldNotOverwriteUserPermissionsForUnprivilegedUser() { + expectedException.expect(UnauthorizedException.class); + + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:3"), new PermissionDescriptor("perm:read:4"))); + } } From 80466c9700f535289de83ae9ddb4160f99992885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 15:33:33 +0100 Subject: [PATCH 18/28] Collect descriptions from plugins --- .../resources/META-INF/scm/permissions.xml | 4 ---- .../main/resources/locales/en/plugins.json | 16 +++++++++++++ scm-ui/public/locales/en/permissions.json | 20 ---------------- .../components/PermissionCheckbox.js | 7 +++--- .../main/resources/locales/en/plugins.json | 24 +++++++++++++++++++ 5 files changed, 44 insertions(+), 27 deletions(-) create mode 100644 scm-webapp/src/main/resources/locales/en/plugins.json diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml index a61077f61a..2f97eb5762 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -34,14 +34,10 @@ - Git config (read) - Read access to git config configuration:read:git - Git config (write) - Write access to git config configuration:write:git diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index a84f44726f..68194e3a43 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -33,5 +33,21 @@ }, "success": "Default branch changed!" } + }, + "permissions" : { + "configuration": { + "read": { + "git": { + "displayName": "Read git configuration", + "description": "May read the git configuration" + } + }, + "write": { + "git": { + "displayName": "Write git configuration", + "description": "May change the git configuration" + } + } + } } } diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json index 3e929434cb..52059db60a 100644 --- a/scm-ui/public/locales/en/permissions.json +++ b/scm-ui/public/locales/en/permissions.json @@ -1,24 +1,4 @@ { - "repository": { - "read": { - "*": { - "displayName": "Read all repositories", - "description": "Read access to all repositories" - } - }, - "write": { - "*": { - "displayName": "Modify all repositories", - "description": "May modify/configure all repositories" - } - } - }, - "user":{ - "*": { - "displayName": "Administer users", - "description": "May administer all users" - } - }, "form": { "submit-button": { "label": "Set permissions" diff --git a/scm-ui/src/permissions/components/PermissionCheckbox.js b/scm-ui/src/permissions/components/PermissionCheckbox.js index 5f9b2774a4..4fca7bba51 100644 --- a/scm-ui/src/permissions/components/PermissionCheckbox.js +++ b/scm-ui/src/permissions/components/PermissionCheckbox.js @@ -16,17 +16,18 @@ class PermissionCheckbox extends React.Component { render() { const { t, permission, checked, onChange, disabled } = this.props; const key = permission.split(":").join("."); + console.log("permissions." + key + ".displayName"); return ( ); } } -export default translate("permissions")(PermissionCheckbox); +export default translate("plugins")(PermissionCheckbox); diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json new file mode 100644 index 0000000000..2df9fc2981 --- /dev/null +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -0,0 +1,24 @@ +{ + "permissions": { + "repository": { + "read": { + "*": { + "displayName": "Read all repositories", + "description": "Read access to all repositories" + } + }, + "write": { + "*": { + "displayName": "Modify all repositories", + "description": "May modify/configure all repositories" + } + } + }, + "user": { + "*": { + "displayName": "Administer users", + "description": "May administer all users" + } + } + } +} From 40909653249521325464c258aabdd8edcc3c6fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 19 Jan 2019 19:55:15 +0100 Subject: [PATCH 19/28] Fix renaming error --- ...epositoryPermissionToRepositoryPermissionDtoMapperTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename scm-webapp/src/test/java/sonia/scm/api/v2/resources/{RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java => RepositoryPermissionToRepositoryPermissionDtoMapperTest.java} (96%) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java similarity index 96% rename from scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java rename to scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java index 9a1ab148a3..a6ab00db58 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java @@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SubjectAware( configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class RepositoryPermissionToRepositoryRepositoryPermissionDtoMapperTest { +public class RepositoryPermissionToRepositoryPermissionDtoMapperTest { @Rule public ShiroRule shiro = new ShiroRule(); From d790d2e7e118740f981dde50747a512e4475cbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 19 Jan 2019 20:05:19 +0100 Subject: [PATCH 20/28] Move api usage to separate file --- .../permissions/components/SetPermissions.js | 33 +++++++++---------- ...setPermissions.js => handlePermissions.js} | 12 +++++++ 2 files changed, 27 insertions(+), 18 deletions(-) rename scm-ui/src/permissions/components/{setPermissions.js => handlePermissions.js} (57%) diff --git a/scm-ui/src/permissions/components/SetPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js index 5d0d2b7a1f..10639a9516 100644 --- a/scm-ui/src/permissions/components/SetPermissions.js +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -7,8 +7,11 @@ import { SubmitButton } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; -import { setPermissions } from "./setPermissions"; -import { apiClient } from "@scm-manager/ui-components"; +import { + loadAvailablePermissions, + loadPermissionsForEntity, + setPermissions +} from "./handlePermissions"; import PermissionCheckbox from "./PermissionCheckbox"; import { connect } from "react-redux"; import { getLink } from "../../modules/indexResource"; @@ -67,28 +70,21 @@ class SetPermissions extends React.Component { }; componentDidMount(): void { - apiClient - .get(this.props.availablePermissionLink) - .then(response => { - return response.json(); - }) - .then(response => { + loadAvailablePermissions(this.props.availablePermissionLink).then( + response => { const availablePermissions = response.permissions; const permissions = {}; availablePermissions.forEach(p => { permissions[p] = false; }); - this.setState({ permissions }, this.loadPermissionsForUser); - }); + this.setState({ permissions }, this.loadPermissionsForEntity); + } + ); } - loadPermissionsForUser = () => { - apiClient - .get(this.props.selectedPermissionsLink.href) - .then(response => { - return response.json(); - }) - .then(response => { + loadPermissionsForEntity = () => { + loadPermissionsForEntity(this.props.selectedPermissionsLink.href).then( + response => { const checkedPermissions = response.permissions; this.setState(state => { const newPermissions = state.permissions; @@ -99,7 +95,8 @@ class SetPermissions extends React.Component { overwritePermissionsLink: response._links.overwrite }; }); - }); + } + ); }; submit = (event: Event) => { diff --git a/scm-ui/src/permissions/components/setPermissions.js b/scm-ui/src/permissions/components/handlePermissions.js similarity index 57% rename from scm-ui/src/permissions/components/setPermissions.js rename to scm-ui/src/permissions/components/handlePermissions.js index 4c54036fab..4ef9507ce7 100644 --- a/scm-ui/src/permissions/components/setPermissions.js +++ b/scm-ui/src/permissions/components/handlePermissions.js @@ -11,3 +11,15 @@ export function setPermissions(url: string, permissions: string[]) { return response; }); } + +export function loadPermissionsForEntity(url: string) { + return apiClient.get(url).then(response => { + return response.json(); + }); +} + +export function loadAvailablePermissions(url: string) { + return apiClient.get(url).then(response => { + return response.json(); + }); +} From 54ea940c6402ccdbbdde85b85dee8423c8ddf507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Sat, 19 Jan 2019 20:06:03 +0100 Subject: [PATCH 21/28] Remove debug console log --- scm-ui/src/permissions/components/PermissionCheckbox.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scm-ui/src/permissions/components/PermissionCheckbox.js b/scm-ui/src/permissions/components/PermissionCheckbox.js index 4fca7bba51..8e14bc0418 100644 --- a/scm-ui/src/permissions/components/PermissionCheckbox.js +++ b/scm-ui/src/permissions/components/PermissionCheckbox.js @@ -16,7 +16,6 @@ class PermissionCheckbox extends React.Component { render() { const { t, permission, checked, onChange, disabled } = this.props; const key = permission.split(":").join("."); - console.log("permissions." + key + ".displayName"); return ( Date: Sat, 19 Jan 2019 20:27:25 +0100 Subject: [PATCH 22/28] Fail assignment on not existing permission --- .../sonia/scm/security/PermissionAssigner.java | 15 +++++++++++++++ .../scm/security/PermissionAssignerTest.java | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java index b7874add69..22b3cd1e2c 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -1,5 +1,8 @@ package sonia.scm.security; +import sonia.scm.ContextEntry; +import sonia.scm.NotFoundException; + import javax.inject.Inject; import java.util.Collection; import java.util.List; @@ -62,9 +65,21 @@ public class PermissionAssigner { .collect(Collectors.toList()); toRemove.forEach(securitySystem::deletePermission); + Collection availablePermissions = this.getAvailablePermissions(); + permissions.stream() + .filter(permissionExists(availablePermissions)) .map(p -> new AssignedPermission(id, groupPermission, p)) .filter(p -> !existingPermissions.contains(p)) .forEach(securitySystem::addPermission); } + + private Predicate permissionExists(Collection availablePermissions) { + return p -> { + if (!availablePermissions.contains(p)) { + throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("permission", p.getValue())); + } + return true; + }; + } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java index a5eb32b594..8ab2ef8c8e 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java @@ -8,11 +8,14 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import sonia.scm.NotFoundException; import sonia.scm.plugin.PluginLoader; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.util.ClassLoaders; +import java.util.Arrays; import java.util.Collection; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static org.mockito.Mockito.mock; @@ -35,7 +38,14 @@ public class PermissionAssignerTest { PluginLoader pluginLoader = mock(PluginLoader.class); when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); - securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader); + securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader) { + @Override + public Collection getAvailablePermissions() { + return Arrays.stream(new String[]{"perm:read:1", "perm:read:2", "perm:read:3", "perm:read:4"}) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + } + }; try { securitySystem.addPermission(new AssignedPermission("1", "perm:read:1")); @@ -86,4 +96,10 @@ public class PermissionAssignerTest { permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:3"), new PermissionDescriptor("perm:read:4"))); } + + @Test + public void shouldFailForNotExistingPermissions() { + expectedException.expect(NotFoundException.class); + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:5"), new PermissionDescriptor("perm:read:4"))); + } } From 760a37409bf9cd4825332c333e0121e7cca75761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 21 Jan 2019 09:17:43 +0100 Subject: [PATCH 23/28] Specify global permissions --- .../resources/META-INF/scm/permissions.xml | 1 - .../resources/META-INF/scm/permissions.xml | 43 +++++++++++++++++++ .../main/resources/locales/en/plugins.json | 16 +++++++ .../resources/META-INF/scm/permissions.xml | 43 +++++++++++++++++++ .../main/resources/locales/en/plugins.json | 18 +++++++- .../resources/META-INF/scm/permissions.xml | 11 ++++- .../main/resources/locales/en/plugins.json | 16 +++++++ 7 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml create mode 100644 scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml index 2f97eb5762..da11b5164a 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -36,7 +36,6 @@ configuration:read:git - configuration:write:git diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..205e8cc770 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ + + + + + + configuration:read:hg + + + configuration:write:hg + + + diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index 504e7d3815..48f3bd57d6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -24,5 +24,21 @@ "disabledHelpText": "Enable or disable the Mercurial plugin.", "required": "This configuration value is required" } + }, + "permissions" : { + "configuration": { + "read": { + "hg": { + "displayName": "Read Mercurial configuration", + "description": "May read the Mercurial configuration" + } + }, + "write": { + "hg": { + "displayName": "Write Mercurial configuration", + "description": "May change the Mercurial configuration" + } + } + } } } diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..3da3526f93 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ + + + + + + configuration:read:svn + + + configuration:write:svn + + + diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json index a446bb6f26..2a363c77cd 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json @@ -1,7 +1,7 @@ { "scm-svn-plugin": { "information": { - "checkout" : "Checkout repository" + "checkout": "Checkout repository" }, "config": { "link": "Subversion", @@ -22,5 +22,21 @@ "disabledHelpText": "Enable or disable the Git plugin", "required": "This configuration value is required" } + }, + "permissions": { + "configuration": { + "read": { + "svn": { + "displayName": "Read Subversion configuration", + "description": "May read the Subversion configuration" + } + }, + "write": { + "svn": { + "displayName": "Write Subversion configuration", + "description": "May change the Subversion configuration" + } + } + } } } diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 55c8cc41a6..5800f354d3 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -36,13 +36,20 @@ repository:read:* - repository:write:* - + + repository:*:* + + + repository:create + user:* + + group:* + diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 2df9fc2981..1c0b84b8d7 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -12,6 +12,16 @@ "displayName": "Modify all repositories", "description": "May modify/configure all repositories" } + }, + "*": { + "*": { + "displayName": "Own all repositories", + "description": "Can modify/configure and delete all repositories" + } + }, + "create": { + "displayName": "Create repositories", + "description": "Can create repositories" } }, "user": { @@ -19,6 +29,12 @@ "displayName": "Administer users", "description": "May administer all users" } + }, + "group": { + "*": { + "displayName": "Administer groups", + "description": "May administer all groups" + } } } } From cda8c59c2de3372eb3da1d39176445b2443c1998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 21 Jan 2019 10:01:29 +0100 Subject: [PATCH 24/28] Accept already assigned permissions even when they are not available --- .../main/java/sonia/scm/security/PermissionAssigner.java | 6 +++--- .../java/sonia/scm/security/PermissionAssignerTest.java | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java index 22b3cd1e2c..faa25d7817 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -68,15 +68,15 @@ public class PermissionAssigner { Collection availablePermissions = this.getAvailablePermissions(); permissions.stream() - .filter(permissionExists(availablePermissions)) + .filter(permissionExists(availablePermissions, existingPermissions)) .map(p -> new AssignedPermission(id, groupPermission, p)) .filter(p -> !existingPermissions.contains(p)) .forEach(securitySystem::addPermission); } - private Predicate permissionExists(Collection availablePermissions) { + private Predicate permissionExists(Collection availablePermissions, Collection existingPermissions) { return p -> { - if (!availablePermissions.contains(p)) { + if (!availablePermissions.contains(p) && existingPermissions.stream().map(AssignedPermission::getPermission).noneMatch(e -> e.equals(p))) { throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("permission", p.getValue())); } return true; diff --git a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java index 8ab2ef8c8e..366c16f6b8 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java @@ -100,6 +100,13 @@ public class PermissionAssignerTest { @Test public void shouldFailForNotExistingPermissions() { expectedException.expect(NotFoundException.class); - permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:5"), new PermissionDescriptor("perm:read:4"))); + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:4"), new PermissionDescriptor("perm:read:5"))); + } + + @Test + public void shouldAcceptNotExistingPermissionsWhenTheyWereAssignedBefore() { + securitySystem.addPermission(new AssignedPermission("2", "perm:read:5")); + + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:5"))); } } From 66d024177297019ba1b41735d64dab420aa7ec38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 21 Jan 2019 10:17:59 +0100 Subject: [PATCH 25/28] Display unknown permissions without translation keys --- .../components/PermissionCheckbox.js | 19 +++++++++++++++++-- .../main/resources/locales/en/plugins.json | 3 ++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scm-ui/src/permissions/components/PermissionCheckbox.js b/scm-ui/src/permissions/components/PermissionCheckbox.js index 8e14bc0418..f4ddcae397 100644 --- a/scm-ui/src/permissions/components/PermissionCheckbox.js +++ b/scm-ui/src/permissions/components/PermissionCheckbox.js @@ -19,14 +19,29 @@ class PermissionCheckbox extends React.Component { return ( ); } + + translateOrDefault = (key: string, defaultText: string) => { + const translation = this.props.t(key); + if (translation === key) { + return defaultText; + } else { + return translation; + } + }; } export default translate("plugins")(PermissionCheckbox); diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 1c0b84b8d7..cfe69c03e3 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -35,6 +35,7 @@ "displayName": "Administer groups", "description": "May administer all groups" } - } + }, + "unknown": "Unknown permission" } } From bc4028ea9db55e62ab192dce06dffc98c25dfe13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 21 Jan 2019 10:50:22 +0100 Subject: [PATCH 26/28] Handle errors in frontend --- .../src/permissions/components/SetPermissions.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/scm-ui/src/permissions/components/SetPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js index 10639a9516..d0b4a28d3a 100644 --- a/scm-ui/src/permissions/components/SetPermissions.js +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -47,14 +47,12 @@ class SetPermissions extends React.Component { setLoadingState = () => { this.setState({ - ...this.state, loading: true }); }; setErrorState = (error: Error) => { this.setState({ - ...this.state, error: error, loading: false }); @@ -62,8 +60,8 @@ class SetPermissions extends React.Component { setSuccessfulState = () => { this.setState({ - ...this.state, loading: false, + error: undefined, permissionsSubmitted: true, permissionsChanged: false }); @@ -113,13 +111,11 @@ class SetPermissions extends React.Component { selectedPermissions ) .then(result => { - if (result.error) { - this.setErrorState(result.error); - } else { - this.setSuccessfulState(); - } + this.setSuccessfulState(); }) - .catch(err => {}); + .catch(err => { + this.setErrorState(err); + }); } } }; From 746061d8790514a2854b93f276818ba28d26a59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 21 Jan 2019 11:10:46 +0100 Subject: [PATCH 27/28] Adapt available permissions to repository roles --- .../main/resources/META-INF/scm/permissions.xml | 4 ++-- .../src/main/resources/locales/en/plugins.json | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 5800f354d3..b86199d700 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -34,10 +34,10 @@ - repository:read:* + repository:read,pull:* - repository:write:* + repository:read,pull,push:* repository:*:* diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index cfe69c03e3..7c75539005 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -1,27 +1,27 @@ { "permissions": { "repository": { - "read": { + "read,pull": { "*": { "displayName": "Read all repositories", - "description": "Read access to all repositories" + "description": "May see and clone all repositories" } }, - "write": { + "read,pull,push": { "*": { - "displayName": "Modify all repositories", - "description": "May modify/configure all repositories" + "displayName": "Write all repositories", + "description": "May see, clone and push to all repositories" } }, "*": { "*": { "displayName": "Own all repositories", - "description": "Can modify/configure and delete all repositories" + "description": "May see, clone, push to, configure and delete all repositories" } }, "create": { "displayName": "Create repositories", - "description": "Can create repositories" + "description": "May create repositories" } }, "user": { From db57a49738085848bbe2b9b7be48f3c09ce380db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 21 Jan 2019 13:05:05 +0100 Subject: [PATCH 28/28] Refactor fetching permissions --- .../permissions/components/SetPermissions.js | 39 +++-------- .../components/handlePermissions.js | 26 ++++--- .../components/handlePermissions.test.js | 67 +++++++++++++++++++ 3 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 scm-ui/src/permissions/components/handlePermissions.test.js diff --git a/scm-ui/src/permissions/components/SetPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js index d0b4a28d3a..e7e561d739 100644 --- a/scm-ui/src/permissions/components/SetPermissions.js +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -8,7 +8,6 @@ import { } from "@scm-manager/ui-components"; import { translate } from "react-i18next"; import { - loadAvailablePermissions, loadPermissionsForEntity, setPermissions } from "./handlePermissions"; @@ -68,35 +67,19 @@ class SetPermissions extends React.Component { }; componentDidMount(): void { - loadAvailablePermissions(this.props.availablePermissionLink).then( - response => { - const availablePermissions = response.permissions; - const permissions = {}; - availablePermissions.forEach(p => { - permissions[p] = false; - }); - this.setState({ permissions }, this.loadPermissionsForEntity); - } - ); + loadPermissionsForEntity( + this.props.availablePermissionLink, + this.props.selectedPermissionsLink.href + ).then(response => { + const { permissions, overwriteLink } = response; + this.setState({ + permissions: permissions, + loading: false, + overwritePermissionsLink: overwriteLink + }); + }); } - loadPermissionsForEntity = () => { - loadPermissionsForEntity(this.props.selectedPermissionsLink.href).then( - response => { - const checkedPermissions = response.permissions; - this.setState(state => { - const newPermissions = state.permissions; - checkedPermissions.forEach(name => (newPermissions[name] = true)); - return { - loading: false, - permissions: newPermissions, - overwritePermissionsLink: response._links.overwrite - }; - }); - } - ); - }; - submit = (event: Event) => { event.preventDefault(); if (this.state.permissions) { diff --git a/scm-ui/src/permissions/components/handlePermissions.js b/scm-ui/src/permissions/components/handlePermissions.js index 4ef9507ce7..6e48127d6d 100644 --- a/scm-ui/src/permissions/components/handlePermissions.js +++ b/scm-ui/src/permissions/components/handlePermissions.js @@ -12,14 +12,22 @@ export function setPermissions(url: string, permissions: string[]) { }); } -export function loadPermissionsForEntity(url: string) { - return apiClient.get(url).then(response => { - return response.json(); - }); -} - -export function loadAvailablePermissions(url: string) { - return apiClient.get(url).then(response => { - return response.json(); +export function loadPermissionsForEntity( + availableUrl: string, + userUrl: string +) { + return Promise.all([ + apiClient.get(availableUrl).then(response => { + return response.json(); + }), + apiClient.get(userUrl).then(response => { + return response.json(); + }) + ]).then(values => { + const [availablePermissions, checkedPermissions] = values; + const permissions = {}; + availablePermissions.permissions.forEach(p => (permissions[p] = false)); + checkedPermissions.permissions.forEach(p => (permissions[p] = true)); + return { permissions, overwriteLink: checkedPermissions._links.overwrite }; }); } diff --git a/scm-ui/src/permissions/components/handlePermissions.test.js b/scm-ui/src/permissions/components/handlePermissions.test.js new file mode 100644 index 0000000000..9fb697e938 --- /dev/null +++ b/scm-ui/src/permissions/components/handlePermissions.test.js @@ -0,0 +1,67 @@ +//@flow +import fetchMock from "fetch-mock"; +import { loadPermissionsForEntity } from "./handlePermissions"; + +describe("load permissions for entity", () => { + const AVAILABLE_PERMISSIONS_URL = "/permissions"; + const USER_PERMISSIONS_URL = "/user/scmadmin/permissions"; + + const availablePermissions = `{ + "permissions": [ + "repository:read,pull:*", + "repository:read,pull,push:*", + "repository:*:*" + ] + }`; + const userPermissions = `{ + "permissions": [ + "repository:read,pull:*" + ], + "_links": { + "self": { + "href": "/api/v2/users/rene/permissions" + }, + "overwrite": { + "href": "/api/v2/users/rene/permissions" + } + } + }`; + + beforeEach(() => { + fetchMock.getOnce( + "/api/v2" + AVAILABLE_PERMISSIONS_URL, + availablePermissions + ); + fetchMock.getOnce("/api/v2" + USER_PERMISSIONS_URL, userPermissions); + }); + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should return permissions array", done => { + loadPermissionsForEntity( + AVAILABLE_PERMISSIONS_URL, + USER_PERMISSIONS_URL + ).then(result => { + const { permissions } = result; + expect(Object.entries(permissions).length).toBe(3); + expect(permissions["repository:read,pull:*"]).toBe(true); + expect(permissions["repository:read,pull,push:*"]).toBe(false); + expect(permissions["repository:*:*"]).toBe(false); + done(); + }); + }); + + it("should return overwrite link", done => { + loadPermissionsForEntity( + AVAILABLE_PERMISSIONS_URL, + USER_PERMISSIONS_URL + ).then(result => { + const { overwriteLink } = result; + expect(overwriteLink.href).toBe("/api/v2/users/rene/permissions"); + done(); + }); + }); +});