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