From f79975b18d6608a75f8d6fa95b66a828ca619c51 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Mon, 20 Aug 2018 18:16:14 +0200 Subject: [PATCH] #8771 Permission endpoints --- .../PermissionAlreadyExistsException.java | 11 + .../PermissionNotFoundException.java | 12 + .../main/java/sonia/scm/web/VndMediaType.java | 1 + scm-webapp/pom.xml | 20 +- .../scm/api/rest/StatusExceptionMapper.java | 81 ++-- .../AuthorizationExceptionMapper.java | 50 ++ .../scm/api/v2/resources/MapperModule.java | 2 + ...ermissionAlreadyExistsExceptionMapper.java | 50 ++ .../PermissionCollectionResource.java | 18 - .../scm/api/v2/resources/PermissionDto.java | 17 +- .../PermissionDtoToPermissionMapper.java | 21 + .../PermissionNotFoundExceptionMapper.java | 50 ++ .../v2/resources/PermissionRootResource.java | 230 ++++++++- .../PermissionToPermissionDtoMapper.java | 37 ++ .../api/v2/resources/PermissionTypeDto.java | 26 -- .../RepositoryNotFoundExceptionMapper.java | 50 ++ .../RepositoryToRepositoryDtoMapper.java | 2 +- .../scm/api/v2/resources/ResourceLinks.java | 31 +- .../resources/PermissionRootResourceTest.java | 437 ++++++++++++++++++ .../api/v2/resources/ResourceLinksMock.java | 2 +- 20 files changed, 1035 insertions(+), 113 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java create mode 100644 scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java new file mode 100644 index 0000000000..aeaf64a3e9 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionAlreadyExistsException.java @@ -0,0 +1,11 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionAlreadyExistsException extends RepositoryException { + + public PermissionAlreadyExistsException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} is already exists", permissionName, repository.getNamespace(), repository.getName())); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java new file mode 100644 index 0000000000..9e1b644faa --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/PermissionNotFoundException.java @@ -0,0 +1,12 @@ +package sonia.scm.repository; + +import java.text.MessageFormat; + +public class PermissionNotFoundException extends RepositoryException{ + + + public PermissionNotFoundException(Repository repository, String permissionName) { + super(MessageFormat.format("the permission {0} of the repository {1}/{2} does not exists", permissionName,repository.getNamespace(), repository.getName() )); + } + +} 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 1e439a6a16..afb1417670 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -16,6 +16,7 @@ public class VndMediaType { public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; + public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index d0003c1f7c..c622042061 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -276,7 +276,25 @@ ${jersey-client.version} test - + + + org.junit.jupiter + junit-jupiter-api + 5.2.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.2.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.2.0 + test + diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java index 08d6b984d2..70b0d0ce2e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java @@ -1,30 +1,30 @@ -/** - * 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 - * +/* + 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 + */ @@ -56,14 +56,14 @@ public class StatusExceptionMapper private static final Logger logger = LoggerFactory.getLogger(StatusExceptionMapper.class); - //~--- constructors --------------------------------------------------------- + private final Response.Status status; + private final Class type; /** - * Constructs ... + * Map an Exception to a HTTP Response * - * - * @param type - * @param status + * @param type the exception class + * @param status the http status to be mapped */ public StatusExceptionMapper(Class type, Response.Status status) { @@ -71,15 +71,12 @@ public class StatusExceptionMapper this.status = status; } - //~--- methods -------------------------------------------------------------- - /** - * Method description + * provide a http responses from an exception * + * @param exception the thrown exception * - * @param exception - * - * @return + * @return the http response with the exception presentation */ @Override public Response toResponse(E exception) @@ -95,12 +92,4 @@ public class StatusExceptionMapper return Response.status(status).build(); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final Response.Status status; - - /** Field description */ - private final Class type; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java new file mode 100644 index 0000000000..bf00bbfc5e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AuthorizationExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import org.apache.shiro.authz.AuthorizationException; +import sonia.scm.api.rest.StatusExceptionMapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class AuthorizationExceptionMapper extends StatusExceptionMapper { + + public AuthorizationExceptionMapper() { + super(AuthorizationException.class, Response.Status.UNAUTHORIZED); + } +} 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 0ac6929689..0605d943e7 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 @@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule { bind(RepositoryTypeCollectionToDtoMapper.class); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); + bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass()); + bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass()); bind(UriInfoStore.class).in(ServletScopes.REQUEST); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java new file mode 100644 index 0000000000..0cf83f097a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionAlreadyExistsExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionAlreadyExistsException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class PermissionAlreadyExistsExceptionMapper extends StatusExceptionMapper { + + public PermissionAlreadyExistsExceptionMapper() { + super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java deleted file mode 100644 index 6c4b52c16d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java +++ /dev/null @@ -1,18 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.ws.rs.DefaultValue; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; - -public class PermissionCollectionResource { - @GET - @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); - } -} 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/PermissionDto.java index ebd49423ab..b184bc3934 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/PermissionDto.java @@ -5,16 +5,25 @@ import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; +import lombok.ToString; -@Getter @Setter +@Getter @Setter @ToString public class PermissionDto extends HalRepresentation { - @JsonInclude(JsonInclude.Include.NON_NULL) - private PermissionTypeDto type = PermissionTypeDto.READ; - @JsonInclude(JsonInclude.Include.NON_NULL) private String name; + /** + * the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable + * the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version + * + * see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19 + * + **/ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String type ; + + private boolean groupPermission = false; 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 new file mode 100644 index 0000000000..9128479836 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Permission; + +@Mapper +public abstract class PermissionDtoToPermissionMapper { + + public abstract Permission map(PermissionDto permissionDto); + + /** + * this method is needed to modify an existing permission object + * + * @param target the target permission + * @param permissionDto the source dto + * @return the mapped target permission object + */ + public abstract Permission map(@MappingTarget Permission target, PermissionDto permissionDto); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java new file mode 100644 index 0000000000..42e341ce0d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.PermissionNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class PermissionNotFoundExceptionMapper extends StatusExceptionMapper { + + public PermissionNotFoundExceptionMapper() { + super(PermissionNotFoundException.class, Response.Status.NOT_FOUND); + } +} 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 cd1e970e43..e9e1d38d95 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 @@ -1,20 +1,234 @@ package sonia.scm.api.v2.resources; -import javax.inject.Inject; -import javax.inject.Provider; -import javax.ws.rs.Path; +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 lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import sonia.scm.repository.*; +import sonia.scm.web.VndMediaType; +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j public class PermissionRootResource { - private final Provider permissionCollectionResource; + private PermissionDtoToPermissionMapper dtoToModelMapper; + private PermissionToPermissionDtoMapper modelToDtoMapper; + private ResourceLinks resourceLinks; + private final RepositoryManager manager; + @Inject - public PermissionRootResource(Provider permissionCollectionResource) { - this.permissionCollectionResource = permissionCollectionResource; + public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) { + this.dtoToModelMapper = dtoToModelMapper; + this.modelToDtoMapper = modelToDtoMapper; + this.resourceLinks = resourceLinks; + this.manager = manager; } + + /** + * Adds a new permission to the user or group managed by the repository + * + * @param permission permission to add + * @return a web response with the status code 201 and the url to GET the added permission + */ + @POST + @StatusCodes({ + @ResponseCode(code = 201, condition = "creates", additionalHeaders = { + @ResponseHeader(name = "Location", description = "uri of the created permission") + }), + @ResponseCode(code = 500, condition = "internal server error"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 409, condition = "conflict") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException { + log.info("try to add new permission: {}", permission); + Repository repository = checkPermission(namespace, name); + checkPermissionAlreadyExists(permission, repository); + repository.getPermissions().add(dtoToModelMapper.map(permission)); + manager.modify(repository); + return Response.created(URI.create(resourceLinks.permission().self(namespace,name,permission.getName()))).build(); + } + + + /** + * Get the searched permission with permission name related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) + @Path("{permission-name}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException { + Repository repository = checkPermission(namespace, name); + return Response.ok( + repository.getPermissions() + .stream() + .filter(permission -> StringUtils.isNotBlank(permission.getName()) && permission.getName().equals(permissionName)) + .map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .findFirst() + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ).build(); + } + + + /** + * Get all permissions related to a repository + * + * @param namespace the repository namespace + * @param name the repository name + * @return the http response with a list of permissionDto objects + * @throws RepositoryNotFoundException if the repository does not exists + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "ok"), + @ResponseCode(code = 404, condition = "not found"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PERMISSION) + @TypeHint(PermissionDto.class) @Path("") - public PermissionCollectionResource getPermissionCollectionResource() { - return permissionCollectionResource.get(); + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + Repository repository = checkPermission(namespace, name); + List permissionDtoList = repository.getPermissions() + .stream() + .map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(),repository.getName()))) + .collect(Collectors.toList()); + return Response.ok(permissionDtoList).build(); + } + + + /** + * Update a permission to the user or group managed by the repository + * + * @param permission permission to modify + * @param permissionName permission to modify + * @return a web response with the status code 204 + */ + @PUT + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PERMISSION) + @Path("{permission-name}") + public Response update(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName, + PermissionDto permission) throws RepositoryException { + log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); + Repository repository = checkPermission(namespace, name); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .map(p -> dtoToModelMapper.map(p, permission)) + .orElseThrow(() -> new PermissionNotFoundException(repository, permissionName)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + /** + * Update a permission to the user or group managed by the repository + * + * @param permissionName permission to delete + * @return a web response with the status code 204 + */ + @DELETE + @StatusCodes({ + @ResponseCode(code = 204, condition = "delete success or nothing to delete"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Path("{permission-name}") + public Response delete(@PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("permission-name") String permissionName) throws RepositoryException { + log.info("try to delete the permission with name: {}.", permissionName); + Repository repository = checkPermission(namespace, name); + repository.getPermissions() + .stream() + .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .findFirst() + .ifPresent(p -> repository.getPermissions().remove(p)) + ; + manager.modify(repository); + log.info("the permission with name: {} is updated.", permissionName); + return Response.noContent().build(); + } + + + + /** + * check if the actual user is permitted to manage the repository permissions + * return the repository if the user is permitted + * + * @param namespace the repository namespace + * @param name the repository name + * @return the repository if the user is permitted + * @throws RepositoryNotFoundException if the repository does not exists + */ + private Repository checkPermission(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException { + return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) + .filter(repository -> { + checkUserPermitted(repository); + return true; + }) + .orElseThrow(() -> new RepositoryNotFoundException(name)); + } + + + /** + * throw exception if the user is not permitted + * @param repository + */ + protected void checkUserPermitted(Repository repository) { + RepositoryPermissions.modify(repository).check(); + } + + + /** + * check if the permission already exists in the repository + * + * @param permission the searched permission + * @param repository the repository to be inspected + * @throws PermissionAlreadyExistsException if the permission already exists in the repository + */ + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws PermissionAlreadyExistsException { + boolean isPermissionAlreadyExist = repository.getPermissions() + .stream() + .anyMatch(p -> p.getName().equals(permission.getName())); + if (isPermissionAlreadyExist) { + throw new PermissionAlreadyExistsException(repository, permission.getName()); + } } } + + 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/PermissionToPermissionDtoMapper.java new file mode 100644 index 0000000000..49adb0e1c0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -0,0 +1,37 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.*; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Permission; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class PermissionToPermissionDtoMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract PermissionDto map(Permission permission, @Context NamespaceAndName namespaceAndName); + + /** + * Add the self, update and delete links. + * + * @param target the mapped dto + * @param namespaceAndName the repository namespace and name + */ + @AfterMapping + void appendLinks(@MappingTarget PermissionDto target, @Context NamespaceAndName namespaceAndName) { + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.permission().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); + linksBuilder.single(link("update", resourceLinks.permission().update(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + linksBuilder.single(link("delete", resourceLinks.permission().delete(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()))); + target.add(linksBuilder.build()); + } +} + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java deleted file mode 100644 index 7b280f9f1f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionTypeDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package sonia.scm.api.v2.resources; - -/** - * Type of permissionPrefix for a {@link RepositoryDto}. - * - * @author mkarray - */ - -public enum PermissionTypeDto { - - /** - * read permission - */ - READ, - - /** - * read and write permission - */ - WRITE, - - /** - * read, write and manage the properties and permissions - */ - OWNER - -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java new file mode 100644 index 0000000000..2116b8e31c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + Copyright (c) 2014, Sebastian Sdorra All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. 2. Redistributions in + binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. 3. Neither the name of SCM-Manager; + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + http://bitbucket.org/sdorra/scm-manager + + */ + + +package sonia.scm.api.v2.resources; + + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.RepositoryNotFoundException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +/** + * @author mkarray + * @since 2.0.0 + */ +@Provider +public class RepositoryNotFoundExceptionMapper extends StatusExceptionMapper { + + public RepositoryNotFoundExceptionMapper() { + super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND); + } +} 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 f46bc22cdf..0263b81048 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 @@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper TEST_PERMISSIONS = Lists + .newArrayList( + new Permission("user_write", PermissionType.WRITE, false), + new Permission("user_read", PermissionType.READ, false), + new Permission("user_owner", PermissionType.OWNER, false), + new Permission("group_read", PermissionType.READ, true), + new Permission("group_write", PermissionType.WRITE, true), + new Permission("group_owner", PermissionType.OWNER, true) + ); + private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest() + .description("GET all permissions") + .method("GET") + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestPOSTPermission = new ExpectedRequest() + .description("create new permission") + .method("POST") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ALL_PERMISSIONS); + private final ExpectedRequest requestGETPermission = new ExpectedRequest() + .description("GET permission") + .method("GET") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestDELETEPermission = new ExpectedRequest() + .description("delete permission") + .method("DELETE") + .path(PATH_OF_ONE_PERMISSION); + private final ExpectedRequest requestPUTPermission = new ExpectedRequest() + .description("update permission") + .method("PUT") + .content(PERMISSION_TEST_PAYLOAD) + .path(PATH_OF_ONE_PERMISSION); + + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Mock + private RepositoryManager repositoryManager; + + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper; + + @InjectMocks + private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper; + + + private PermissionRootResource permissionRootResource; + + + @BeforeEach + @Before + public void prepareEnvironment() { + initMocks(this); + permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager)); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider + .of(new RepositoryResource(null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(PermissionAlreadyExistsExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + } + + + @Test + public void shouldGetAllPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + List actualPermissionDtos = mapper.readValue(body, new TypeReference>() { + }); + assertThat(actualPermissionDtos) + .as("response payload match permission object models") + .hasSize(TEST_PERMISSIONS.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(TEST_PERMISSIONS)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + @Test + public void shouldGetPermissionByName() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission expectedPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestGETPermission + .expectedResponseStatus(200) + .path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName()) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class); + assertThat(actualPermissionDto) + .as("response payload match permission object model") + .isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission)) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + @Test + public void shouldGetCreatedPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true); + ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); + permissions.add(newPermission); + ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(201) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("POST response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + @Test + public void shouldNotAddExistingPermission() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission newPermission = TEST_PERMISSIONS.get(0); + assertExpectedRequest(requestPOSTPermission + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .expectedResponseStatus(409) + ); + } + + + @Test + public void shouldGetUpdatedPermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission modifiedPermission = TEST_PERMISSIONS.get(0); + // modify the type to owner + modifiedPermission.setType(PermissionType.OWNER); + ImmutableList expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); + assertExpectedRequest(requestPUTPermission + .content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}") + .path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("PUT response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + + @Test + public void shouldDeletePermissions() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission 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) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + @Test + public void deletingNotExistingPermissionShouldProcess() { + authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS); + Permission 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) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + assertExpectedRequest(requestDELETEPermission + .path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName()) + .expectedResponseStatus(204) + .responseValidator(response -> assertThat(response.getContentAsString()) + .as("DELETE response has no body") + .isBlank()) + ); + assertGettingExpectedPermissions(expectedPermissions); + } + + private void assertGettingExpectedPermissions(ImmutableList expectedPermissions) { + assertExpectedRequest(requestGETAllPermissions + .expectedResponseStatus(200) + .responseValidator((response) -> { + String body = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + try { + List actualPermissionDtos = mapper.readValue(body, new TypeReference>() { + }); + assertThat(actualPermissionDtos) + .as("response payload match permission object models") + .hasSize(expectedPermissions.size()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions))) + ; + } catch (IOException e) { + fail(); + } + }) + ); + } + + + private PermissionDto[] getExpectedPermissionDtos(ArrayList permissions) { + return permissions + .stream() + .map(this::getExpectedPermissionDto) + .toArray(PermissionDto[]::new); + } + + private PermissionDto getExpectedPermissionDto(Permission permission) { + PermissionDto result = new PermissionDto(); + result.setName(permission.getName()); + result.setGroupPermission(permission.isGroupPermission()); + result.setType(permission.getType().name()); + String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName(); + result.add(linkingTo() + .self(permissionHref) + .single(link("update", permissionHref)) + .single(link("delete", permissionHref)) + .build()); + return result; + } + + @TestFactory + @DisplayName("test endpoints on missing repository and user is Admin") + Stream missedRepositoryTestFactory() { + return createDynamicTestsToAssertResponses( + requestGETAllPermissions.expectedResponseStatus(404), + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(404), + requestDELETEPermission.expectedResponseStatus(404), + requestPUTPermission.expectedResponseStatus(404)); + } + + + @TestFactory + @DisplayName("test endpoints on missing permission and user is Admin") + Stream missedPermissionTestFactory() { + authorizedUserHasARepository(); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(404), + requestPOSTPermission.expectedResponseStatus(201), + requestGETAllPermissions.expectedResponseStatus(200), + requestDELETEPermission.expectedResponseStatus(204), + requestPUTPermission.expectedResponseStatus(404)); + } + + private Repository authorizedUserHasARepository() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE); + when(mockRepository.getName()).thenReturn(REPOSITORY_NAME); + doNothing().when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return mockRepository; + } + + private void authorizedUserHasARepositoryWithPermissions(ArrayList permissions) { + when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions); + } + + + @TestFactory + @DisplayName("test endpoints on missing permission and user is not Admin") + Stream missedPermissionUserForbiddenTestFactory() { + Repository mockRepository = mock(Repository.class); + when(mockRepository.getId()).thenReturn(REPOSITORY_NAME); + doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository); + when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(401), + requestPOSTPermission.expectedResponseStatus(401), + requestGETAllPermissions.expectedResponseStatus(401), + requestDELETEPermission.expectedResponseStatus(401), + requestPUTPermission.expectedResponseStatus(401)); + } + + + private Stream createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) { + + return Stream.of(expectedRequests) + .map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry))); + } + + private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) { + MockHttpResponse response = new MockHttpResponse(); + HttpRequest request = null; + try { + request = MockHttpRequest + .create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path) + .content(entry.content) + .contentType(VndMediaType.PERMISSION); + } catch (URISyntaxException e) { + fail(e.getMessage()); + } + dispatcher.invoke(request, response); + log.info("Test the Request :{}", entry); + assertThat(entry.expectedResponseStatus) + .as("assert status code") + .isEqualTo(response.getStatus()); + if (entry.responseValidator != null) { + entry.responseValidator.accept(response); + } + return response; + } + + @ToString + public class ExpectedRequest { + private String description; + private String method; + private String path; + private int expectedResponseStatus; + private byte[] content = new byte[]{}; + private Consumer responseValidator; + + public ExpectedRequest description(String description) { + this.description = description; + return this; + } + + public ExpectedRequest method(String method) { + this.method = method; + return this; + } + + public ExpectedRequest path(String path) { + this.path = path; + return this; + } + + public ExpectedRequest content(String content) { + if (content != null) { + this.content = content.getBytes(); + } + return this; + } + + public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) { + this.expectedResponseStatus = expectedResponseStatus; + return this; + } + + public ExpectedRequest responseValidator(Consumer responseValidator) { + this.responseValidator = responseValidator; + return this; + } + } + +} 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 882d754329..6df5ac1a7a 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 @@ -23,7 +23,7 @@ public class ResourceLinksMock { when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); - when(resourceLinks.permissionCollection()).thenReturn(new ResourceLinks.PermissionCollectionLinks(uriInfo)); + when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));