Merge permission endpoint into feature branch

This commit is contained in:
René Pfeuffer
2018-08-22 07:54:40 +02:00
15 changed files with 874 additions and 133 deletions

View File

@@ -1,32 +1,32 @@
/** /*
* Copyright (c) 2010, Sebastian Sdorra Copyright (c) 2010, Sebastian Sdorra
* All rights reserved. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this contributors may be used to endorse or promote products derived from this
* software without specific prior written permission. software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager http://bitbucket.org/sdorra/scm-manager
*
*/ */
@@ -56,10 +56,11 @@ import java.io.Serializable;
public class Permission implements PermissionObject, Serializable public class Permission implements PermissionObject, Serializable
{ {
/** Field description */
private static final long serialVersionUID = -2915175031430884040L; private static final long serialVersionUID = -2915175031430884040L;
//~--- constructors --------------------------------------------------------- private boolean groupPermission = false;
private String name;
private PermissionType type = PermissionType.READ;
/** /**
* Constructs a new {@link Permission}. * Constructs a new {@link Permission}.
@@ -152,12 +153,7 @@ public class Permission implements PermissionObject, Serializable
return Objects.hashCode(name, type, groupPermission); return Objects.hashCode(name, type, groupPermission);
} }
/**
* Method description
*
*
* @return
*/
@Override @Override
public String toString() public String toString()
{ {
@@ -241,15 +237,4 @@ public class Permission implements PermissionObject, Serializable
{ {
this.type = type; this.type = type;
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
private boolean groupPermission = false;
/** Field description */
private String name;
/** Field description */
private PermissionType type = PermissionType.READ;
} }

View File

@@ -16,6 +16,7 @@ public class VndMediaType {
public static final String USER = PREFIX + "user" + SUFFIX; public static final String USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX;
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;

View File

@@ -298,6 +298,24 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<!-- core plugins --> <!-- core plugins -->
<dependency> <dependency>

View File

@@ -1,30 +1,30 @@
/** /*
* Copyright (c) 2010, Sebastian Sdorra All rights reserved. Copyright (c) 2010, Sebastian Sdorra All rights reserved.
*
* Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer. 2. Redistributions in this list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. Neither the name of SCM-Manager; 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 nor the names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission. derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 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 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. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager http://bitbucket.org/sdorra/scm-manager
*
*/ */
@@ -36,11 +36,11 @@ package sonia.scm.api.rest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.ExceptionMapper;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -56,14 +56,14 @@ public class StatusExceptionMapper<E extends Throwable>
private static final Logger logger = private static final Logger logger =
LoggerFactory.getLogger(StatusExceptionMapper.class); LoggerFactory.getLogger(StatusExceptionMapper.class);
//~--- constructors --------------------------------------------------------- private final Response.Status status;
private final Class<E> type;
/** /**
* Constructs ... * Map an Exception to a HTTP Response
* *
* * @param type the exception class
* @param type * @param status the http status to be mapped
* @param status
*/ */
public StatusExceptionMapper(Class<E> type, Response.Status status) public StatusExceptionMapper(Class<E> type, Response.Status status)
{ {
@@ -71,15 +71,12 @@ public class StatusExceptionMapper<E extends Throwable>
this.status = status; this.status = status;
} }
//~--- methods --------------------------------------------------------------
/** /**
* Method description * provide a http responses from an exception
* *
* @param exception the thrown exception
* *
* @param exception * @return the http response with the exception presentation
*
* @return
*/ */
@Override @Override
public Response toResponse(E exception) public Response toResponse(E exception)
@@ -95,12 +92,4 @@ public class StatusExceptionMapper<E extends Throwable>
return Response.status(status).build(); return Response.status(status).build();
} }
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Response.Status status;
/** Field description */
private final Class<E> type;
} }

View File

@@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule {
bind(RepositoryTypeCollectionToDtoMapper.class); bind(RepositoryTypeCollectionToDtoMapper.class);
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass()); bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass());
bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass());
bind(UriInfoStore.class).in(ServletScopes.REQUEST); bind(UriInfoStore.class).in(ServletScopes.REQUEST);
} }

View File

@@ -0,0 +1,49 @@
/*
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.NotFoundException;
import sonia.scm.api.rest.StatusExceptionMapper;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @since 2.0.0
*/
@Provider
public class NotFoundExceptionMapper extends StatusExceptionMapper<NotFoundException> {
public NotFoundExceptionMapper() {
super(NotFoundException.class, Response.Status.NOT_FOUND);
}
}

View File

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

View File

@@ -0,0 +1,35 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.annotation.JsonInclude;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter @ToString
public class PermissionDto extends HalRepresentation {
@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;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -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 void modify(@MappingTarget Permission target, PermissionDto permissionDto);
}

View File

@@ -1,20 +1,231 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import javax.inject.Inject; import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import javax.inject.Provider; import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import javax.ws.rs.Path; 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.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryNotFoundException;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
public class PermissionRootResource { public class PermissionRootResource {
private final Provider<PermissionCollectionResource> permissionCollectionResource; private PermissionDtoToPermissionMapper dtoToModelMapper;
private PermissionToPermissionDtoMapper modelToDtoMapper;
private ResourceLinks resourceLinks;
private final RepositoryManager manager;
@Inject @Inject
public PermissionRootResource(Provider<PermissionCollectionResource> permissionCollectionResource) { public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) {
this.permissionCollectionResource = permissionCollectionResource; 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 Exception {
log.info("try to add new permission: {}", permission);
Repository repository = load(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 NotFoundException 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 NotFoundException {
Repository repository = load(namespace, name);
return Response.ok(
repository.getPermissions()
.stream()
.filter(permission -> permissionName.equals(permission.getName()))
.map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(), repository.getName())))
.findFirst()
.orElseThrow(NotFoundException::new)
).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 NotFoundException 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("") @Path("")
public PermissionCollectionResource getPermissionCollectionResource() { public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException {
return permissionCollectionResource.get(); Repository repository = load(namespace, name);
List<PermissionDto> 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 Exception {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = load(namespace, name);
Permission existingPermission = repository.getPermissions()
.stream()
.filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName))
.findFirst()
.orElseThrow(() -> new NotFoundException());
dtoToModelMapper.modify(existingPermission, permission);
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 Exception {
log.info("try to delete the permission with name: {}.", permissionName);
Repository repository = load(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 load(String namespace, String name) throws RepositoryNotFoundException {
return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name)))
.orElseThrow(() -> new RepositoryNotFoundException(name));
}
/**
* check if the permission already exists in the repository
*
* @param permission the searched permission
* @param repository the repository to be inspected
* @throws AlreadyExistsException if the permission already exists in the repository
*/
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException {
boolean isPermissionAlreadyExist = repository.getPermissions()
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()));
if (isPermissionAlreadyExist) {
throw new AlreadyExistsException();
}
} }
} }

View File

@@ -0,0 +1,41 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
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());
}
}

View File

@@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
} }
if (RepositoryPermissions.modify(repository).isPermitted()) { if (RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName()))); linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName())));
linksBuilder.single(link("permissions", resourceLinks.permissionCollection().self(target.getNamespace(), target.getName()))); linksBuilder.single(link("permissions", resourceLinks.permission().all(target.getNamespace(), target.getName())));
} }
try (RepositoryService repositoryService = serviceFactory.create(repository)) { try (RepositoryService repositoryService = serviceFactory.create(repository)) {
if (repositoryService.isSupported(Command.TAGS)) { if (repositoryService.isSupported(Command.TAGS)) {

View File

@@ -298,20 +298,35 @@ class ResourceLinks {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href(); return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href();
} }
} }
public PermissionLinks permission() {
public PermissionCollectionLinks permissionCollection() { return new PermissionLinks(uriInfoStore.get());
return new PermissionCollectionLinks(uriInfoStore.get());
} }
static class PermissionCollectionLinks { static class PermissionLinks {
private final LinkBuilder permissionLinkBuilder; private final LinkBuilder permissionLinkBuilder;
PermissionCollectionLinks(UriInfo uriInfo) { PermissionLinks(UriInfo uriInfo) {
permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class, PermissionCollectionResource.class); permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
} }
String self(String namespace, String name) { String all(String namespace, String name) {
return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getPermissionCollectionResource").parameters().method("getAll").parameters().href(); return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getAll").parameters().href();
}
String self(String repositoryNamespace, String repositoryName, String permissionName) {
return getLink(repositoryNamespace, repositoryName, permissionName, "get");
}
String update(String repositoryNamespace, String repositoryName, String permissionName) {
return getLink(repositoryNamespace, repositoryName, permissionName, "update");
}
String delete(String repositoryNamespace, String repositoryName, String permissionName) {
return getLink(repositoryNamespace, repositoryName, permissionName, "delete");
}
private String getLink(String repositoryNamespace, String repositoryName, String permissionName, String methodName) {
return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method(methodName).parameters(permissionName).href();
} }
} }
} }

View File

@@ -0,0 +1,392 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.spi.HttpRequest;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
import sonia.scm.api.rest.AuthorizationExceptionMapper;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@Slf4j
public class PermissionRootResourceTest {
private static final String REPOSITORY_NAMESPACE = "repo_namespace";
private static final String REPOSITORY_NAME = "repo";
private static final String PERMISSION_NAME = "perm";
private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/";
private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME;
private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }";
private static final ArrayList<Permission> 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();
@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 = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager);
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider
.of(new RepositoryResource(null, null, null, null, null, null, null,null, MockProvider.of(permissionRootResource))), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AlreadyExistsExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
}
@TestFactory
@DisplayName("test endpoints on missing repository and user is Admin")
Stream<DynamicTest> 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<DynamicTest> missedPermissionTestFactory() {
authorizedUserHasARepository();
return createDynamicTestsToAssertResponses(
requestGETPermission.expectedResponseStatus(404),
requestPOSTPermission.expectedResponseStatus(201),
requestGETAllPermissions.expectedResponseStatus(200),
requestDELETEPermission.expectedResponseStatus(204),
requestPUTPermission.expectedResponseStatus(404));
}
@TestFactory
@DisplayName("test endpoints on missing permission and user is not Admin")
Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() {
Repository mockRepository = mock(Repository.class);
when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class));
return createDynamicTestsToAssertResponses(
requestGETPermission.expectedResponseStatus(403),
requestPOSTPermission.expectedResponseStatus(403),
requestGETAllPermissions.expectedResponseStatus(403),
requestDELETEPermission.expectedResponseStatus(403),
requestPUTPermission.expectedResponseStatus(403));
}
@Test
public void shouldGetAllPermissions() throws URISyntaxException {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
assertGettingExpectedPermissions(ImmutableList.copyOf(TEST_PERMISSIONS));
}
@Test
public void shouldGetPermissionByName() throws URISyntaxException {
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() throws URISyntaxException {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true);
ArrayList<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
permissions.add(newPermission);
ImmutableList<Permission> 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() throws URISyntaxException {
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() throws URISyntaxException {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission modifiedPermission = TEST_PERMISSIONS.get(0);
// modify the type to owner
modifiedPermission.setType(PermissionType.OWNER);
ImmutableList<Permission> 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() throws URISyntaxException {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<Permission> 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() throws URISyntaxException {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<Permission> 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<Permission> expectedPermissions) throws URISyntaxException {
assertExpectedRequest(requestGETAllPermissions
.expectedResponseStatus(200)
.responseValidator((response) -> {
String body = response.getContentAsString();
ObjectMapper mapper = new ObjectMapper();
try {
List<PermissionDto> actualPermissionDtos = mapper.readValue(body, new TypeReference<List<PermissionDto>>() {
});
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<Permission> 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;
}
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);
when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository);
return mockRepository;
}
private void authorizedUserHasARepositoryWithPermissions(ArrayList<Permission> permissions) {
when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions);
}
private Stream<DynamicTest> 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) throws URISyntaxException {
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = null;
request = MockHttpRequest
.create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path)
.content(entry.content)
.contentType(VndMediaType.PERMISSION);
dispatcher.invoke(request, response);
log.info("Test the Request :{}", entry);
assertThat(response.getStatus())
.as("assert status code")
.isEqualTo(entry.expectedResponseStatus);
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<MockHttpResponse> 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<MockHttpResponse> responseValidator) {
this.responseValidator = responseValidator;
return this;
}
}
}

View File

@@ -23,7 +23,7 @@ public class ResourceLinksMock {
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(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.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo));
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo));
when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));