Add REST endpoint for namespace permissions

This commit is contained in:
René Pfeuffer
2020-09-16 07:32:16 +02:00
parent 603fffc64b
commit 2000730c8d
9 changed files with 744 additions and 47 deletions

View File

@@ -0,0 +1,338 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.NotFoundException;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespaceManager;
import sonia.scm.repository.NamespacePermissions;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
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.Collection;
import java.util.Optional;
import java.util.function.Predicate;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Slf4j
public class NamespacePermissionResource {
private RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper;
private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
private ResourceLinks resourceLinks;
private final NamespaceManager manager;
@Inject
public NamespacePermissionResource(
RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper,
RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper,
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper,
ResourceLinks resourceLinks,
NamespaceManager manager) {
this.dtoToModelMapper = dtoToModelMapper;
this.modelToDtoMapper = modelToDtoMapper;
this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper;
this.resourceLinks = resourceLinks;
this.manager = manager;
}
/**
* Adds a new namespace permission for the user or group
*
* @param permission permission to add
* @return a web response with the status code 201 and the url to GET the added permission
*/
@POST
@Path("")
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
@Operation(summary = "Create namespace-specific permission", description = "Adds a new permission to the namespace for the user or group.", tags = {"Namespace", "Permissions"})
@ApiResponse(
responseCode = "201",
description = "creates",
headers = @Header(
name = "Location",
description = "uri of the created permission",
schema = @Schema(type = "string")
)
)
@ApiResponse(
responseCode = "404",
description = "not found",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(responseCode = "409", description = "conflict")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public Response create(@PathParam("namespace") String namespaceName, @Valid RepositoryPermissionDto permission) {
log.info("try to add new permission: {}", permission);
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionWrite().check();
checkPermissionAlreadyExists(permission, namespace);
namespace.addPermission(dtoToModelMapper.map(permission));
manager.modify(namespace);
String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission);
return Response.created(URI.create(resourceLinks.namespacePermission().self(namespaceName, urlPermissionName))).build();
}
/**
* Get the searched permission with permission name related to a namespace
*
* @param namespaceName the name of the namespace
* @return the http response with a list of permissionDto objects
* @throws NotFoundException if the namespace or the permission does not exists
*/
@GET
@Path("{permission-name}")
@Produces(VndMediaType.REPOSITORY_PERMISSION)
@Operation(summary = "Get single repository-specific permission", description = "Get the searched permission with permission name related to a repository.", tags = {"Repository", "Permissions"})
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.REPOSITORY_PERMISSION,
schema = @Schema(implementation = RepositoryPermissionDto.class)
)
)
@ApiResponse(
responseCode = "404",
description = "not found",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public RepositoryPermissionDto get(@PathParam("namespace") String namespaceName, @PathParam("permission-name") String permissionName) {
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionRead().check();
return
namespace.getPermissions()
.stream()
.filter(filterPermission(permissionName))
.map(permission -> modelToDtoMapper.map(permission, namespace))
.findFirst()
.orElseThrow(() -> notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName)));
}
/**
* Get all permissions related to a namespace
*
* @param namespaceMame the name of the namespace
* @return the http response with a list of permissionDto objects
* @throws NotFoundException if the namespace does not exists
*/
@GET
@Path("")
@Produces(VndMediaType.REPOSITORY_PERMISSION)
@Operation(summary = "List of namespace-specific permissions", description = "Get all permissions related to a namespace.", tags = {"Repository", "Permissions"})
@ApiResponse(
responseCode = "200",
description = "success",
content = @Content(
mediaType = VndMediaType.REPOSITORY_PERMISSION,
schema = @Schema(implementation = RepositoryPermissionDto.class)
)
)
@ApiResponse(
responseCode = "404",
description = "not found",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public HalRepresentation getAll(@PathParam("namespace") String namespaceMame) {
Namespace namespace = load(namespaceMame);
NamespacePermissions.permissionRead().check();
return repositoryPermissionCollectionToDtoMapper.map(namespace);
}
/**
* Update a permission to the user or group managed by the repository
* ignore the user input for groupPermission and take it from the path parameter (if the group prefix (@) exists it is a group permission)
*
* @param permission permission to modify
* @param permissionName permission to modify
* @return a web response with the status code 204
*/
@PUT
@Path("{permission-name}")
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
@Operation(summary = "Update repository-specific permission", description = "Update a permission to the user or group managed by the repository.", tags = {"Repository", "Permissions"})
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public void update(@PathParam("namespace") String namespaceName,
@PathParam("permission-name") String permissionName,
@Valid RepositoryPermissionDto permission) {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionWrite().check();
String extractedPermissionName = getPermissionName(permissionName);
if (!isPermissionExist(new RepositoryPermissionDto(extractedPermissionName, isGroupPermission(permissionName)), namespace)) {
throw notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName));
}
permission.setGroupPermission(isGroupPermission(permissionName));
if (!extractedPermissionName.equals(permission.getName())) {
checkPermissionAlreadyExists(permission, namespace);
}
RepositoryPermission existingPermission = namespace.getPermissions()
.stream()
.filter(filterPermission(permissionName))
.findFirst()
.orElseThrow(() -> notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName)));
RepositoryPermission newPermission = dtoToModelMapper.map(permission);
if (!namespace.removePermission(existingPermission)) {
throw new IllegalStateException(String.format("could not delete modified permission %s from namespace %s", existingPermission, namespaceName));
}
namespace.addPermission(newPermission);
manager.modify(namespace);
log.info("the permission with name: {} is updated.", permissionName);
}
/**
* 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
@Path("{permission-name}")
@Operation(summary = "Delete repository-specific permission", description = "Delete a permission with the given name.", tags = {"Repository", "Permissions"})
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public void delete(@PathParam("namespace") String namespaceName,
@PathParam("permission-name") String permissionName) {
log.info("try to delete the permission with name: {}.", permissionName);
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionWrite().check();
namespace.getPermissions()
.stream()
.filter(filterPermission(permissionName))
.findFirst()
.ifPresent(permission -> {
namespace.removePermission(permission);
manager.modify(namespace);
});
log.info("the permission with name: {} is deleted.", permissionName);
}
private Predicate<RepositoryPermission> filterPermission(String name) {
return permission -> getPermissionName(name).equals(permission.getName())
&&
permission.isGroupPermission() == isGroupPermission(name);
}
private String getPermissionName(String permissionName) {
return Optional.of(permissionName)
.filter(p -> !isGroupPermission(permissionName))
.orElse(permissionName.substring(1));
}
private boolean isGroupPermission(String permissionName) {
return permissionName.startsWith(GROUP_PREFIX);
}
private Namespace load(String namespaceMame) {
return manager.get(namespaceMame)
.orElseThrow(() -> notFound(entity("Namespace", namespaceMame)));
}
private void checkPermissionAlreadyExists(RepositoryPermissionDto permission, Namespace namespace) {
if (isPermissionExist(permission, namespace)) {
throw alreadyExists(entity("Permission", permission.getName()).in(Namespace.class, namespace.getNamespace()));
}
}
private boolean isPermissionExist(RepositoryPermissionDto permission, Namespace namespace) {
return namespace.getPermissions()
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission());
}
}

View File

@@ -32,6 +32,7 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -44,11 +45,13 @@ public class NamespaceResource {
private final RepositoryManager manager;
private final NamespaceToNamespaceDtoMapper namespaceMapper;
private final Provider<NamespacePermissionResource> namespacePermissionResource;
@Inject
public NamespaceResource(RepositoryManager manager, NamespaceToNamespaceDtoMapper namespaceMapper) {
public NamespaceResource(RepositoryManager manager, NamespaceToNamespaceDtoMapper namespaceMapper, Provider<NamespacePermissionResource> namespacePermissionResource) {
this.manager = manager;
this.namespaceMapper = namespaceMapper;
this.namespacePermissionResource = namespacePermissionResource;
}
/**
@@ -97,4 +100,8 @@ public class NamespaceResource {
.orElseThrow(() -> notFound(entity("Namespace", namespace)));
}
@Path("permissions")
public NamespacePermissionResource permissions() {
return namespacePermissionResource.get();
}
}

View File

@@ -24,6 +24,9 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import sonia.scm.repository.NamespacePermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
@@ -39,12 +42,15 @@ class NamespaceToNamespaceDtoMapper {
}
NamespaceDto map(String namespace) {
return new NamespaceDto(
namespace,
linkingTo()
.self(links.namespace().self(namespace))
.single(link("repositories", links.repositoryCollection().forNamespace(namespace)))
.build()
);
Links.Builder linkingTo = linkingTo();
linkingTo
.self(links.namespace().self(namespace))
.single(link("repositories", links.repositoryCollection().forNamespace(namespace)));
if (NamespacePermissions.permissionRead().isPermitted()) {
linkingTo
.single(link("permissions", links.namespacePermission().all(namespace)));
}
return new NamespaceDto(namespace, linkingTo.build());
}
}

View File

@@ -21,13 +21,15 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespacePermissions;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
@@ -57,6 +59,14 @@ public class RepositoryPermissionCollectionToDtoMapper {
return new HalRepresentation(createLinks(repository), embedDtos(repositoryPermissionDtoList));
}
public HalRepresentation map(Namespace namespace) {
List<RepositoryPermissionDto> repositoryPermissionDtoList = namespace.getPermissions()
.stream()
.map(permission -> repositoryPermissionToRepositoryPermissionDtoMapper.map(permission, namespace))
.collect(toList());
return new HalRepresentation(createLinks(namespace), embedDtos(repositoryPermissionDtoList));
}
private Links createLinks(Repository repository) {
RepositoryPermissions.permissionRead(repository).check();
Links.Builder linksBuilder = linkingTo()
@@ -67,6 +77,16 @@ public class RepositoryPermissionCollectionToDtoMapper {
return linksBuilder.build();
}
private Links createLinks(Namespace namespace) {
NamespacePermissions.permissionRead().check();
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(resourceLinks.namespacePermission().all(namespace.getNamespace())).build());
if (NamespacePermissions.permissionWrite().isPermitted()) {
linksBuilder.single(link("create", resourceLinks.namespacePermission().create(namespace.getNamespace())));
}
return linksBuilder.build();
}
private Embedded embedDtos(List<RepositoryPermissionDto> repositoryPermissionDtoList) {
return embeddedBuilder()
.with("permissions", repositoryPermissionDtoList)

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
@@ -31,6 +31,8 @@ import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespacePermissions;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
@@ -51,18 +53,19 @@ public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper {
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Repository repository);
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Namespace namespace);
@BeforeMapping
void validatePermissions(@Context Repository repository) {
RepositoryPermissions.permissionRead(repository).check();
}
/**
* Add the self, update and delete links.
*
* @param target the mapped dto
* @param repository the repository
*/
@BeforeMapping
void validatePermissions(@Context Namespace namespace) {
NamespacePermissions.permissionRead().check();
}
@AfterMapping
void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Repository repository) {
String permissionName = getUrlPermissionName(target);
@@ -75,6 +78,18 @@ public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper {
target.add(linksBuilder.build());
}
@AfterMapping
void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Namespace namespace) {
String permissionName = getUrlPermissionName(target);
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.namespacePermission().self(namespace.getNamespace(), permissionName));
if (NamespacePermissions.permissionWrite().isPermitted()) {
linksBuilder.single(link("update", resourceLinks.namespacePermission().update(namespace.getNamespace(), permissionName)));
linksBuilder.single(link("delete", resourceLinks.namespacePermission().delete(namespace.getNamespace(), permissionName)));
}
target.add(linksBuilder.build());
}
public String getUrlPermissionName(RepositoryPermissionDto repositoryPermissionDto) {
return Optional.of(repositoryPermissionDto.getName())
.filter(p -> !repositoryPermissionDto.isGroupPermission())

View File

@@ -916,4 +916,40 @@ class ResourceLinks {
return namespaceLinkBuilder.method("getNamespaceResource").parameters().method("get").parameters(namespace).href();
}
}
public NamespacePermissionLinks namespacePermission() {
return new NamespacePermissionLinks(scmPathInfoStore.get());
}
static class NamespacePermissionLinks {
private final LinkBuilder permissionLinkBuilder;
NamespacePermissionLinks(ScmPathInfo pathInfo) {
permissionLinkBuilder = new LinkBuilder(pathInfo, NamespaceRootResource.class, NamespaceResource.class, NamespacePermissionResource.class);
}
String all(String namespace) {
return permissionLinkBuilder.method("getNamespaceResource").parameters(namespace).method("permissions").parameters().method("getAll").parameters().href();
}
String create(String namespace) {
return permissionLinkBuilder.method("getNamespaceResource").parameters(namespace).method("permissions").parameters().method("create").parameters().href();
}
String self(String namespace, String permissionName) {
return getLink(namespace, permissionName, "get");
}
String update(String namespace, String permissionName) {
return getLink(namespace, permissionName, "update");
}
String delete(String namespace, String permissionName) {
return getLink(namespace, permissionName, "delete");
}
private String getLink(String namespace, String permissionName, String methodName) {
return permissionLinkBuilder.method("getNamespaceResource").parameters(namespace).method("permissions").parameters().method(methodName).parameters(permissionName).href();
}
}
}

View File

@@ -64,10 +64,12 @@ import sonia.scm.net.ahc.XmlContentTransformer;
import sonia.scm.plugin.DefaultPluginManager;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.PluginManager;
import sonia.scm.repository.DefaultNamespaceManager;
import sonia.scm.repository.DefaultRepositoryManager;
import sonia.scm.repository.DefaultRepositoryProvider;
import sonia.scm.repository.DefaultRepositoryRoleManager;
import sonia.scm.repository.HealthCheckContextListener;
import sonia.scm.repository.NamespaceManager;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.NamespaceStrategyProvider;
import sonia.scm.repository.Repository;
@@ -191,6 +193,7 @@ class ScmServletModule extends ServletModule {
bindDecorated(GroupManager.class, DefaultGroupManager.class,
GroupManagerProvider.class);
bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class);
bind(NamespaceManager.class, DefaultNamespaceManager.class);
bind(GroupCollector.class, DefaultGroupCollector.class);
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);

View File

@@ -24,83 +24,354 @@
package sonia.scm.api.v2.resources;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespaceManager;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.web.RestDispatcher;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
import static com.google.inject.util.Providers.of;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class NamespaceRootResourceTest {
@Mock
RepositoryManager repositoryManager;
@Mock
NamespaceManager namespaceManager;
@Mock
Subject subject;
RestDispatcher dispatcher = new RestDispatcher();
MockHttpResponse response = new MockHttpResponse();
ResourceLinks links = ResourceLinksMock.createMock(URI.create("/"));
@InjectMocks
RepositoryPermissionToRepositoryPermissionDtoMapperImpl repositoryPermissionToRepositoryPermissionDtoMapper;
@BeforeEach
void mockSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void unbindSubject() {
ThreadContext.unbindSubject();
}
@BeforeEach
void setUpResources() {
NamespaceToNamespaceDtoMapper namespaceMapper = new NamespaceToNamespaceDtoMapper(links);
NamespaceCollectionToDtoMapper namespaceCollectionToDtoMapper = new NamespaceCollectionToDtoMapper(namespaceMapper, links);
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(repositoryPermissionToRepositoryPermissionDtoMapper, links);
RepositoryPermissionDtoToRepositoryPermissionMapperImpl dtoToModelMapper = new RepositoryPermissionDtoToRepositoryPermissionMapperImpl();
NamespaceCollectionResource namespaceCollectionResource = new NamespaceCollectionResource(repositoryManager, namespaceCollectionToDtoMapper);
NamespaceResource namespaceResource = new NamespaceResource(repositoryManager, namespaceMapper);
NamespacePermissionResource namespacePermissionResource = new NamespacePermissionResource(dtoToModelMapper, repositoryPermissionToRepositoryPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, links, namespaceManager);
NamespaceResource namespaceResource = new NamespaceResource(repositoryManager, namespaceMapper, of(namespacePermissionResource));
dispatcher.addSingletonResource(new NamespaceRootResource(of(namespaceCollectionResource), of(namespaceResource)));
}
@Test
void shouldReturnAllNamespaces() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"/v2/namespaces/\"}")
.contains("\"_embedded\"")
.contains("\"namespace\":\"hitchhiker\"")
.contains("\"namespace\":\"space\"");
@BeforeEach
void mockExistingNamespaces() {
lenient().when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
Namespace hitchhikerNamespace = new Namespace("hitchhiker");
hitchhikerNamespace.setPermissions(singleton(new RepositoryPermission("humans", "READ", true)));
Namespace spaceNamespace = new Namespace("space");
lenient().when(namespaceManager.getAll()).thenReturn(asList(hitchhikerNamespace, spaceNamespace));
lenient().when(namespaceManager.get("hitchhiker")).thenReturn(Optional.of(hitchhikerNamespace));
lenient().when(namespaceManager.get("space")).thenReturn(Optional.of(spaceNamespace));
}
@Test
void shouldReturnSingleNamespace() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
@Nested
class WithoutSpecialPermission {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
@BeforeEach
void mockNoPermissions() {
lenient().when(subject.isPermitted(anyString())).thenReturn(false);
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionRead");
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionWrite");
}
dispatcher.invoke(request, response);
@Test
void shouldReturnAllNamespaces() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"namespace\":\"space\"")
.contains("\"self\":{\"href\":\"/v2/namespaces/space\"}");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"/v2/namespaces/\"}")
.contains("\"_embedded\"")
.contains("\"namespace\":\"hitchhiker\"")
.contains("\"namespace\":\"space\"");
}
@Test
void shouldReturnSingleNamespace() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"namespace\":\"space\"")
.contains("\"self\":{\"href\":\"/v2/namespaces/space\"}")
.doesNotContain("permissions");
}
@Test
void shouldHandleUnknownNamespace() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "unknown");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void shouldNotReturnPermissions() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(403);
}
}
@Test
void shouldHandleUnknownNamespace() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
@Nested
class WithReadPermission {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "unknown");
@BeforeEach
void grantReadPermission() {
lenient().when(subject.isPermitted("namespace:permissionRead")).thenReturn(true);
lenient().when(subject.isPermitted("namespace:permissionWrite")).thenReturn(false);
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionWrite");
}
dispatcher.invoke(request, response);
@Test
void shouldContainPermissionLinkWhenPermitted() throws UnsupportedEncodingException, URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
assertThat(response.getStatus()).isEqualTo(404);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"permissions\":{\"href\":\"/v2/namespaces/space/permissions\"}");
}
@Test
void shouldReturnPermissions() throws UnsupportedEncodingException, URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"/v2/namespaces/hitchhiker/permissions\"}")
.contains("{\"name\":\"humans\",\"verbs\":[],\"role\":\"READ\",\"groupPermission\":true,\"")
.doesNotContain("create");
}
@Test
void shouldReturnSinglePermission() throws UnsupportedEncodingException, URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"self\":{\"href\":\"/v2/namespaces/hitchhiker/permissions/@humans\"}")
.contains("{\"name\":\"humans\",\"verbs\":[],\"role\":\"READ\",\"groupPermission\":true,\"")
.doesNotContain("update")
.doesNotContain("delete");
}
@Test
void shouldHandleMissingNamespace() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "no_such_namespace/permissions");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void shouldNotCreateNewPermission() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions")
.content("{\"name\":\"dent\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":false}".getBytes())
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(403);
}
@Test
void shouldNotDeletePermission() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.delete("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(403);
}
@Nested
class WithWritePermission {
@BeforeEach
void grantWritePermission() {
lenient().when(subject.isPermitted("namespace:permissionWrite")).thenReturn(true);
lenient().doNothing().when(subject).checkPermission("namespace:permissionWrite");
}
@Test
void shouldContainCreateLink() throws UnsupportedEncodingException, URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"create\":{\"href\":\"/v2/namespaces/space/permissions\"}");
}
@Test
void shouldContainModificationLinks() throws UnsupportedEncodingException, URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"update\":{\"href\":\"/v2/namespaces/hitchhiker/permissions/@humans\"")
.contains("\"delete\":{\"href\":\"/v2/namespaces/hitchhiker/permissions/@humans\"");
}
@Test
void shouldCreateNewPermission() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions")
.content("{\"name\":\"dent\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":false}".getBytes())
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(201);
verify(namespaceManager)
.modify(argThat(
namespace -> {
assertThat(namespace.getPermissions()).hasSize(1);
RepositoryPermission permission = namespace.getPermissions().iterator().next();
assertThat(permission.getName()).isEqualTo("dent");
assertThat(permission.getRole()).isEqualTo("WRITE");
assertThat(permission.getVerbs()).isEmpty();
assertThat(permission.isGroupPermission()).isFalse();
return true;
})
);
assertThat(response.getOutputHeaders().get("Location"))
.containsExactly(URI.create("/v2/namespaces/space/permissions/dent"));
}
@Test
void shouldUpdatePermission() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.put("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans")
.content("{\"name\":\"humans\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":true}".getBytes())
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
verify(namespaceManager)
.modify(argThat(
namespace -> {
assertThat(namespace.getPermissions()).hasSize(1);
RepositoryPermission permission = namespace.getPermissions().iterator().next();
assertThat(permission.getName()).isEqualTo("humans");
assertThat(permission.getRole()).isEqualTo("WRITE");
assertThat(permission.getVerbs()).isEmpty();
assertThat(permission.isGroupPermission()).isTrue();
return true;
})
);
}
@Test
void shouldHandleNotExistingPermissionOnUpdate() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.put("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/humans")
.content("{\"name\":\"humans\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":true}".getBytes())
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(404);
}
@Test
void shouldHandleExistingPermissionOnCreate() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions")
.content("{\"name\":\"humans\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":true}".getBytes())
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(409);
verify(namespaceManager, never()).modify(any());
}
@Test
void shouldDeleteExistingPermission() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.delete("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
verify(namespaceManager)
.modify(argThat(
namespace -> {
assertThat(namespace.getPermissions()).isEmpty();
return true;
})
);
}
@Test
void shouldHandleRedundantDeleteIdempotent() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.delete("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/humans");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
verify(namespaceManager, never()).modify(any());
}
}
}
}

View File

@@ -78,6 +78,7 @@ public class ResourceLinksMock {
lenient().when(resourceLinks.annotate()).thenReturn(new ResourceLinks.AnnotateLinks(pathInfo));
lenient().when(resourceLinks.namespace()).thenReturn(new ResourceLinks.NamespaceLinks(pathInfo));
lenient().when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(pathInfo));
lenient().when(resourceLinks.namespacePermission()).thenReturn(new ResourceLinks.NamespacePermissionLinks(pathInfo));
return resourceLinks;
}