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

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