diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java index 50867b4f92..b81789c110 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleDto.java @@ -18,6 +18,7 @@ public class RepositoryRoleDto extends HalRepresentation { private String name; @NoBlankStrings @NotEmpty private Collection verbs; + private boolean system; RepositoryRoleDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java index 86b4709fd2..5f77e38fed 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRoleToRepositoryRoleDtoMapper.java @@ -3,9 +3,9 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Links; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.ObjectFactory; import sonia.scm.repository.RepositoryRole; -import sonia.scm.repository.RepositoryRoleManager; import sonia.scm.repository.RepositoryRolePermissions; import javax.inject.Inject; @@ -19,16 +19,17 @@ import static de.otto.edison.hal.Links.linkingTo; @Mapper public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper { - @Inject - private RepositoryRoleManager repositoryRoleManager; - @Inject private ResourceLinks resourceLinks; + @Override + @Mapping(source = "type", target = "system") + public abstract RepositoryRoleDto map(RepositoryRole modelObject); + @ObjectFactory RepositoryRoleDto createDto(RepositoryRole repositoryRole) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.repositoryRole().self(repositoryRole.getName())); - if (RepositoryRolePermissions.modify().isPermitted()) { + if (!isSystemRole(repositoryRole.getType()) && RepositoryRolePermissions.modify().isPermitted()) { linksBuilder.single(link("delete", resourceLinks.repositoryRole().delete(repositoryRole.getName()))); linksBuilder.single(link("update", resourceLinks.repositoryRole().update(repositoryRole.getName()))); } @@ -39,4 +40,7 @@ public abstract class RepositoryRoleToRepositoryRoleDtoMapper extends BaseMapper return new RepositoryRoleDto(linksBuilder.build(), embeddedBuilder.build()); } + boolean isSystemRole(String type) { + return "system".equals(type); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java index bc52d38dd2..2507d4bf25 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryRoleManager.java @@ -33,8 +33,6 @@ package sonia.scm.repository; -import com.github.sdorra.ssp.PermissionActionCheck; -import com.github.sdorra.ssp.PermissionCheck; import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -44,6 +42,7 @@ import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; +import sonia.scm.security.RepositoryPermissionProvider; import sonia.scm.util.Util; import java.util.ArrayList; @@ -51,6 +50,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; @Singleton @EagerSingleton @@ -62,10 +62,11 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager LoggerFactory.getLogger(DefaultRepositoryRoleManager.class); @Inject - public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO) + public DefaultRepositoryRoleManager(RepositoryRoleDAO repositoryRoleDAO, RepositoryPermissionProvider repositoryPermissionProvider) { this.repositoryRoleDAO = repositoryRoleDAO; this.managerDaoAdapter = new ManagerDaoAdapter<>(repositoryRoleDAO); + this.repositoryPermissionProvider = repositoryPermissionProvider; } @Override @@ -131,6 +132,10 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager public RepositoryRole get(String id) { RepositoryRolePermissions.read(); + return findSystemRole(id).orElse(findCustomRole(id)); + } + + private RepositoryRole findCustomRole(String id) { RepositoryRole repositoryRole = repositoryRoleDAO.get(id); if (repositoryRole != null) { @@ -140,6 +145,10 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager } } + private Optional findSystemRole(String id) { + return repositoryPermissionProvider.availableRoles().stream().filter(role -> role.getName().equals(id)).findFirst(); + } + @Override public Collection getAll() { return getAll(repositoryRole -> true, null); @@ -152,7 +161,7 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager if (!RepositoryRolePermissions.read().isPermitted()) { return Collections.emptySet(); } - for (RepositoryRole repositoryRole : repositoryRoleDAO.getAll()) { + for (RepositoryRole repositoryRole : repositoryPermissionProvider.availableRoles()) { repositoryRoles.add(repositoryRole.clone()); } @@ -188,4 +197,5 @@ public class DefaultRepositoryRoleManager extends AbstractRepositoryRoleManager private final RepositoryRoleDAO repositoryRoleDAO; private final ManagerDaoAdapter managerDaoAdapter; + private final RepositoryPermissionProvider repositoryPermissionProvider; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java index 0323df27e4..0489717926 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRoleRootResourceTest.java @@ -28,7 +28,7 @@ import java.net.URISyntaxException; import java.util.Collections; import static java.net.URI.create; -import static java.util.Collections.singletonList; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -46,8 +46,10 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @RunWith(MockitoJUnitRunner.Silent.class) public class RepositoryRoleRootResourceTest { - public static final String EXISTING_ROLE = "existingRole"; - public static final RepositoryRole REPOSITORY_ROLE = new RepositoryRole("existingRole", Collections.singleton("verb"), "xml"); + public static final String CUSTOM_ROLE = "customRole"; + public static final String SYSTEM_ROLE = "systemRole"; + public static final RepositoryRole CUSTOM_REPOSITORY_ROLE = new RepositoryRole(CUSTOM_ROLE, Collections.singleton("verb"), "xml"); + public static final RepositoryRole SYSTEM_REPOSITORY_ROLE = new RepositoryRole(SYSTEM_ROLE, Collections.singleton("admin"), "system"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/")); @Rule @@ -88,8 +90,9 @@ public class RepositoryRoleRootResourceTest { dispatcher = createDispatcher(rootResource); dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get())); - when(repositoryRoleManager.get(EXISTING_ROLE)).thenReturn(REPOSITORY_ROLE); - when(repositoryRoleManager.getPage(any(), any(), anyInt(), anyInt())).thenReturn(new PageResult<>(singletonList(REPOSITORY_ROLE), 1)); + when(repositoryRoleManager.get(CUSTOM_ROLE)).thenReturn(CUSTOM_REPOSITORY_ROLE); + when(repositoryRoleManager.get(SYSTEM_ROLE)).thenReturn(SYSTEM_REPOSITORY_ROLE); + when(repositoryRoleManager.getPage(any(), any(), anyInt(), anyInt())).thenReturn(new PageResult<>(asList(CUSTOM_REPOSITORY_ROLE, SYSTEM_REPOSITORY_ROLE), 2)); } @Test @@ -103,8 +106,8 @@ public class RepositoryRoleRootResourceTest { } @Test - public void shouldGetExistingRole() throws URISyntaxException, UnsupportedEncodingException { - MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole"); + public void shouldGetCustomRole() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -112,17 +115,38 @@ public class RepositoryRoleRootResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()) .contains( - "\"name\":\"existingRole\"", + "\"name\":\"" + CUSTOM_ROLE + "\"", "\"verbs\":[\"verb\"]", - "\"self\":{\"href\":\"/v2/repository-roles/existingRole\"}", - "\"delete\":{\"href\":\"/v2/repository-roles/existingRole\"}" + "\"self\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", + "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" + ); + } + + @Test + public void shouldGetSystemRole() throws URISyntaxException, UnsupportedEncodingException { + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + SYSTEM_ROLE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()) + .contains( + "\"name\":\"" + SYSTEM_ROLE + "\"", + "\"verbs\":[\"admin\"]", + "\"self\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + ) + .doesNotContain( + "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", + "\"update\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}" ); } @Test @SubjectAware(username = "dent") public void shouldNotGetDeleteLinkWithoutPermission() throws URISyntaxException, UnsupportedEncodingException { - MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole"); + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -135,23 +159,23 @@ public class RepositoryRoleRootResourceTest { @Test public void shouldUpdateRole() throws URISyntaxException { MockHttpRequest request = MockHttpRequest - .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole") + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE) .contentType(VndMediaType.REPOSITORY_ROLE) - .content(content("{'name': 'existingRole', 'verbs': ['write', 'push']}")); + .content(content("{'name': '" + CUSTOM_ROLE + "', 'verbs': ['write', 'push']}")); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); verify(repositoryRoleManager).modify(any()); - assertThat(modifyCaptor.getValue().getName()).isEqualTo("existingRole"); + assertThat(modifyCaptor.getValue().getName()).isEqualTo(CUSTOM_ROLE); assertThat(modifyCaptor.getValue().getVerbs()).containsExactly("write", "push"); } @Test public void shouldNotChangeRoleName() throws URISyntaxException { MockHttpRequest request = MockHttpRequest - .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + "existingRole") + .put("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE) .contentType(VndMediaType.REPOSITORY_ROLE) .content(content("{'name': 'changedName', 'verbs': ['write', 'push']}")); MockHttpResponse response = new MockHttpResponse(); @@ -197,14 +221,14 @@ public class RepositoryRoleRootResourceTest { @Test public void shouldDeleteRole() throws URISyntaxException { MockHttpRequest request = MockHttpRequest - .delete("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + EXISTING_ROLE); + .delete("/" + RepositoryRoleRootResource.REPOSITORY_ROLES_PATH_V2 + CUSTOM_ROLE); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT); verify(repositoryRoleManager).delete(any()); - assertThat(deleteCaptor.getValue().getName()).isEqualTo(EXISTING_ROLE); + assertThat(deleteCaptor.getValue().getName()).isEqualTo(CUSTOM_ROLE); } @Test @@ -217,12 +241,17 @@ public class RepositoryRoleRootResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()) .contains( - "\"name\":\"existingRole\"", + "\"name\":\"" + CUSTOM_ROLE + "\"", + "\"name\":\"" + SYSTEM_ROLE + "\"", "\"verbs\":[\"verb\"]", + "\"verbs\":[\"admin\"]", "\"self\":{\"href\":\"/v2/repository-roles", - "\"delete\":{\"href\":\"/v2/repository-roles/existingRole\"}", + "\"delete\":{\"href\":\"/v2/repository-roles/" + CUSTOM_ROLE + "\"}", "\"create\":{\"href\":\"/v2/repository-roles/\"}" - ); + ) + .doesNotContain( + "\"delete\":{\"href\":\"/v2/repository-roles/" + SYSTEM_ROLE + "\"}" + ); } @Test