From 8000731ab709c0983b96ca126b186a65ece85b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 18 Jan 2019 12:06:37 +0100 Subject: [PATCH] Add REST resource for group permissions --- .../resources/GlobalPermissionResource.java | 10 +-- .../v2/resources/GroupPermissionResource.java | 79 +++++++++++++++++++ .../scm/api/v2/resources/GroupResource.java | 10 ++- .../v2/resources/GroupToGroupDtoMapper.java | 4 + .../scm/api/v2/resources/ResourceLinks.java | 20 +++++ .../v2/resources/UserPermissionResource.java | 4 +- .../scm/security/PermissionAssigner.java | 36 ++++++++- .../v2/resources/GroupRootResourceTest.java | 53 ++++++++++++- .../api/v2/resources/ResourceLinksMock.java | 1 + 9 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java index ac73865910..f9cd015f45 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2.resources; +import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; -import sonia.scm.security.SecuritySystem; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -13,18 +13,18 @@ import javax.ws.rs.core.Response; @Path("v2/permissions") public class GlobalPermissionResource { - private SecuritySystem securitySystem; + private PermissionAssigner permissionAssigner; @Inject - public GlobalPermissionResource(SecuritySystem securitySystem) { - this.securitySystem = securitySystem; + public GlobalPermissionResource(PermissionAssigner permissionAssigner) { + this.permissionAssigner = permissionAssigner; } @GET @Produces(VndMediaType.PERMISSION_COLLECTION) @Path("") public Response getAll() { - String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); + String[] permissions = permissionAssigner.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); return Response.ok(new PermissionListDto(permissions)).build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java new file mode 100644 index 0000000000..888e527eee --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -0,0 +1,79 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +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.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +public class GroupPermissionResource { + + private final PermissionAssigner permissionAssigner; + private final PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + + @Inject + public GroupPermissionResource(PermissionAssigner permissionAssigner, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper) { + this.permissionAssigner = permissionAssigner; + this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper; + } + + /** + * Returns permissions for a group. + * + * @param id the id/name of the group + */ + @GET + @Path("") + @Produces(VndMediaType.PERMISSION_COLLECTION) + @TypeHint(PermissionListDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the group"), + @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response getPermissions(@PathParam("id") String id) { + Collection permissions = permissionAssigner.readPermissionsForGroup(id); + return Response.ok(permissionCollectionToDtoMapper.map(permissions, id)).build(); + } + + /** + * Sets permissions for a group. Overwrites all existing permissions. + * + * @param id id of the group to be modified + * @param newPermissions New list of permissions for the group + */ + @PUT + @Path("") + @Consumes(VndMediaType.PERMISSION_COLLECTION) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current group does not have the correct privilege"), + @ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) { + Collection permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + permissionAssigner.setPermissionsForGroup(id, permissionDescriptors); + return Response.noContent().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 6d0b921d02..cfc1916fc1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -8,7 +8,6 @@ import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.inject.Named; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -24,13 +23,15 @@ public class GroupResource { private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupDtoToGroupMapper dtoToGroupMapper; private final IdResourceManagerAdapter adapter; + private final GroupPermissionResource groupPermissionResource; @Inject public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper, - GroupDtoToGroupMapper groupDtoToGroupMapper) { + GroupDtoToGroupMapper groupDtoToGroupMapper, GroupPermissionResource groupPermissionResource) { this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.dtoToGroupMapper = groupDtoToGroupMapper; this.adapter = new IdResourceManagerAdapter<>(manager, Group.class); + this.groupPermissionResource = groupPermissionResource; } /** @@ -100,4 +101,9 @@ public class GroupResource { public Response update(@PathParam("id") String name, @Valid GroupDto group) { return adapter.update(name, existing -> dtoToGroupMapper.map(group)); } + + @Path("permissions") + public GroupPermissionResource permissions() { + return groupPermissionResource; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java index 9a25e711cd..bf866af350 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java @@ -6,6 +6,7 @@ import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import sonia.scm.group.Group; import sonia.scm.group.GroupPermissions; +import sonia.scm.security.PermissionPermissions; import javax.inject.Inject; import java.util.List; @@ -31,6 +32,9 @@ public abstract class GroupToGroupDtoMapper extends BaseMapper if (GroupPermissions.modify(group).isPermitted()) { linksBuilder.single(link("update", resourceLinks.group().update(target.getName()))); } + if (PermissionPermissions.read().isPermitted()) { + linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(target.getName()))); + } appendLinks(new EdisonLinkAppender(linksBuilder), group); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1bc5f584a9..c62a34f093 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -116,6 +116,26 @@ class ResourceLinks { } } + GroupPermissionLinks groupPermissions() { + return new GroupPermissionLinks(scmPathInfoStore.get()); + } + + static class GroupPermissionLinks { + private final LinkBuilder groupPermissionLinkBuilder; + + GroupPermissionLinks(ScmPathInfo pathInfo) { + this.groupPermissionLinkBuilder = new LinkBuilder(pathInfo, GroupRootResource.class, GroupResource.class, GroupPermissionResource.class); + } + + public String permissions(String name) { + return groupPermissionLinkBuilder.method("getGroupResource").parameters(name).method("permissions").parameters().method("getPermissions").parameters().href(); + } + + public String overwritePermissions(String name) { + return groupPermissionLinkBuilder.method("getGroupResource").parameters(name).method("permissions").parameters().method("overwritePermissions").parameters().href(); + } + } + MeLinks me() { return new MeLinks(scmPathInfoStore.get(), this.user()); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java index 9bd5396aa6..35988b7fb2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserPermissionResource.java @@ -37,8 +37,8 @@ public class UserPermissionResource { */ @GET @Path("") - @Produces(VndMediaType.USER) - @TypeHint(UserDto.class) + @Produces(VndMediaType.PERMISSION_COLLECTION) + @TypeHint(PermissionListDto.class) @StatusCodes({ @ResponseCode(code = 200, condition = "success"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java index 54a82607ab..2821460e55 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -3,6 +3,8 @@ package sonia.scm.security; import javax.inject.Inject; import java.util.Collection; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; public class PermissionAssigner { @@ -19,18 +21,46 @@ public class PermissionAssigner { } public Collection readPermissionsForUser(String id) { - return securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).collect(Collectors.toSet()); + return readPermissions(filterForUser(id)); + } + + public Collection readPermissionsForGroup(String id) { + return readPermissions(filterForGroup(id)); + } + + private Predicate filterForUser(String id) { + return p -> !p.isGroupPermission() && p.getName().equals(id); + } + + private Predicate filterForGroup(String id) { + return p -> p.isGroupPermission() && p.getName().equals(id); + } + + private Set readPermissions(Predicate predicate) { + return securitySystem.getPermissions(predicate) + .stream() + .map(AssignedPermission::getPermission) + .collect(Collectors.toSet()); } public void setPermissionsForUser(String id, Collection permissions) { - Collection existingPermissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)); + Collection existingPermissions = securitySystem.getPermissions(filterForUser(id)); + adaptPermissions(id, false, permissions, existingPermissions); + } + + public void setPermissionsForGroup(String id, Collection permissions) { + Collection existingPermissions = securitySystem.getPermissions(filterForGroup(id)); + adaptPermissions(id, true, permissions, existingPermissions); + } + + private void adaptPermissions(String id, boolean groupPermission, Collection permissions, Collection existingPermissions) { List toRemove = existingPermissions.stream() .filter(p -> !permissions.contains(p.getPermission())) .collect(Collectors.toList()); toRemove.forEach(securitySystem::deletePermission); permissions.stream() - .map(p -> new AssignedPermission(id, false, p)) + .map(p -> new AssignedPermission(id, groupPermission, p)) .filter(p -> !existingPermissions.contains(p)) .forEach(securitySystem::addPermission); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index f28cf49d03..646e9d0839 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -18,6 +18,8 @@ import sonia.scm.api.rest.JSONContextResolver; import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; +import sonia.scm.security.PermissionAssigner; +import sonia.scm.security.PermissionDescriptor; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; @@ -25,6 +27,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Collection; import java.util.Collections; import static java.util.Collections.singletonList; @@ -54,10 +57,15 @@ public class GroupRootResourceTest { @Mock private GroupManager groupManager; + @Mock + private PermissionAssigner permissionAssigner; @InjectMocks private GroupDtoToGroupMapperImpl dtoToGroupMapper; @InjectMocks private GroupToGroupDtoMapperImpl groupToDtoMapper; + @InjectMocks + private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; + private ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); @@ -73,7 +81,8 @@ public class GroupRootResourceTest { GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks); - GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper); + GroupPermissionResource groupPermissionResource = new GroupPermissionResource(permissionAssigner, permissionCollectionToDtoMapper); + GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper, groupPermissionResource); GroupRootResource groupRootResource = new GroupRootResource(Providers.of(groupCollectionResource), Providers.of(groupResource)); dispatcher = createDispatcher(groupRootResource); @@ -307,6 +316,48 @@ public class GroupRootResourceTest { assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}")); } + @Test + public void shouldGetPermissionLink() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertTrue(response.getContentAsString().contains("\"permissions\":{")); + } + + @Test + public void shouldGetPermissions() throws URISyntaxException { + when(permissionAssigner.readPermissionsForGroup("admin")).thenReturn(singletonList(new PermissionDescriptor("something:*"))); + MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertTrue(response.getContentAsString().contains("\"permissions\":[\"something:*\"]")); + } + + @Test + public void shouldSetPermissions() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest + .put("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions") + .contentType(VndMediaType.PERMISSION_COLLECTION) + .content("{\"permissions\":[\"other:*\"]}".getBytes()); + MockHttpResponse response = new MockHttpResponse(); + ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); + doNothing().when(permissionAssigner).setPermissionsForGroup(eq("admin"), captor.capture()); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + assertEquals("other:*", captor.getValue().iterator().next().getValue()); + } + private Group createDummyGroup() { Group group = new Group(); group.setName("admin"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 268652833a..655d00fc10 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -20,6 +20,7 @@ public class ResourceLinksMock { when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo)); when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); + when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo)); when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo));