From 3a1522e5b2a19ee9c62ea9d2cf3dc52241145b96 Mon Sep 17 00:00:00 2001 From: Antoine Vigneau Date: Tue, 26 Sep 2023 16:06:53 +0200 Subject: [PATCH] SONAR-20532 Add POST endpoint to create permission mappings --- .../GithubPermissionsMappingServiceIT.java | 54 ++++++++ .../permissions/GithubPermissionsMapping.java | 2 +- .../GithubPermissionsMappingService.java | 61 +++++++++ .../DefaultGithubPermissionsController.java | 45 ++++++- .../GithubPermissionsController.java | 17 ++- .../model/RestGithubPermissionsMapping.java | 4 +- .../GithubPermissionsMappingPostRequest.java | 35 +++++ .../permissions/request/RestPermissions.java | 50 +++++++ .../RestResponseEntityExceptionHandler.java | 16 +++ ...efaultGithubPermissionsControllerTest.java | 126 +++++++++++++++++- ...estResponseEntityExceptionHandlerTest.java | 72 ++++++++++ 11 files changed, 465 insertions(+), 17 deletions(-) create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/GithubPermissionsMappingPostRequest.java create mode 100644 server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/RestPermissions.java create mode 100644 server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java diff --git a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingServiceIT.java b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingServiceIT.java index 58a20450506..d14ed0c4cfc 100644 --- a/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingServiceIT.java +++ b/server/sonar-webserver-common/src/it/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingServiceIT.java @@ -34,6 +34,7 @@ import org.sonar.db.provisioning.GithubPermissionsMappingDto; import org.sonar.server.common.permission.Operation; import org.sonar.server.exceptions.NotFoundException; +import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; @@ -252,4 +253,57 @@ public class GithubPermissionsMappingServiceIT { new GithubPermissionsMapping(CUSTOM_ROLE_NAME, false, new SonarqubePermissions(true, true, false, false, false, true))); } + @Test + public void createPermissionMapping_whenRoleExists_shouldThrow() { + Map> githubRolesToSqPermissions = Map.of(CUSTOM_ROLE_NAME, Set.of("user", "codeviewer")); + persistGithubPermissionsMapping(githubRolesToSqPermissions); + + GithubPermissionsMapping request = new GithubPermissionsMapping(CUSTOM_ROLE_NAME, false, new SonarqubePermissions(true, true, true, true, true, true)); + assertThatThrownBy(() -> underTest.createPermissionMapping(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(format("Role %s already exists, it can't be created again.", CUSTOM_ROLE_NAME)); + } + + @Test + public void createPermissionMapping_whenRoleNameConflictsWithBaseRole_shouldThrow() { + assertBaseRoleConflict("read"); + assertBaseRoleConflict("Read"); + assertBaseRoleConflict("READ"); + } + + private void assertBaseRoleConflict(String role) { + GithubPermissionsMapping request = new GithubPermissionsMapping(role, false, new SonarqubePermissions(true, true, true, true, true, true)); + assertThatThrownBy(() -> underTest.createPermissionMapping(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(format("Role %s can conflicts with a GitHub base role, please chose another name.", role)); + } + + @Test + public void createPermissionMapping_whenNoPermissions_shouldThrow() { + GithubPermissionsMapping request = new GithubPermissionsMapping(CUSTOM_ROLE_NAME, false, new SonarqubePermissions(false, false, false, false, false, false)); + assertThatThrownBy(() -> underTest.createPermissionMapping(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(format("Role %s has no permission set, please set at least one permission.", CUSTOM_ROLE_NAME)); + } + + @Test + public void createPermissionMapping_whenValidRequests_shouldCreateMapping() { + GithubPermissionsMapping role1 = new GithubPermissionsMapping("role1", false, new SonarqubePermissions(false, false, false, false, false, true)); + GithubPermissionsMapping role2 = new GithubPermissionsMapping("role2", false, new SonarqubePermissions(false, false, false, false, true, true)); + GithubPermissionsMapping role3 = new GithubPermissionsMapping("role3", false, new SonarqubePermissions(false, false, false, true, true, true)); + GithubPermissionsMapping role4 = new GithubPermissionsMapping("role4", false, new SonarqubePermissions(false, false, true, true, true, true)); + GithubPermissionsMapping role5 = new GithubPermissionsMapping("role5", false, new SonarqubePermissions(false, true, true, true, true, true)); + GithubPermissionsMapping role6 = new GithubPermissionsMapping("role6", false, new SonarqubePermissions(true, true, true, true, true, true)); + + underTest.createPermissionMapping(role1); + underTest.createPermissionMapping(role2); + underTest.createPermissionMapping(role3); + underTest.createPermissionMapping(role4); + underTest.createPermissionMapping(role5); + underTest.createPermissionMapping(role6); + + assertThat(underTest.getPermissionsMapping()) + .contains(role1, role2, role3, role4, role5, role6); + } + } diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMapping.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMapping.java index 449ee3333e4..fc327e9d38b 100644 --- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMapping.java +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMapping.java @@ -19,5 +19,5 @@ */ package org.sonar.server.common.github.permissions; -public record GithubPermissionsMapping(String roleName, boolean isBaseRole, SonarqubePermissions permissions) { +public record GithubPermissionsMapping(String githubRole, boolean isBaseRole, SonarqubePermissions permissions) { } diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingService.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingService.java index 68a4b884ce7..b0578106b33 100644 --- a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingService.java +++ b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/github/permissions/GithubPermissionsMappingService.java @@ -20,8 +20,11 @@ package org.sonar.server.common.github.permissions; import com.google.common.collect.Sets; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import org.sonar.api.web.UserRole; @@ -32,6 +35,7 @@ import org.sonar.db.provisioning.GithubPermissionsMappingDao; import org.sonar.db.provisioning.GithubPermissionsMappingDto; import org.sonar.server.exceptions.NotFoundException; +import static java.lang.String.format; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toSet; import static org.sonar.api.utils.Preconditions.checkArgument; @@ -159,4 +163,61 @@ public class GithubPermissionsMappingService { .forEach(builderConsumer -> builderConsumer.accept(builder)); return builder.build(); } + + public GithubPermissionsMapping createPermissionMapping(GithubPermissionsMapping request) { + try (DbSession dbSession = dbClient.openSession(false)) { + validateCreatePermissionMappingRequest(request, dbSession); + toGithubPermissionsMappingDtos(request).forEach(dto -> githubPermissionsMappingDao.insert(dbSession, dto)); + dbSession.commit(); + return getPermissionsMappingForGithubRole(request.githubRole()); + } + } + + private void validateCreatePermissionMappingRequest(GithubPermissionsMapping request, DbSession dbSession) { + if (!getPermissionsMappingForGithubRole(dbSession, request.githubRole()).isEmpty()) { + throw new IllegalArgumentException(format("Role %s already exists, it can't be created again.", request.githubRole())); + } + if (conflictBaseRole(request.githubRole())) { + throw new IllegalArgumentException(format("Role %s can conflicts with a GitHub base role, please chose another name.", request.githubRole())); + } + if (noPermissionsGranted(request.permissions())) { + throw new IllegalArgumentException(format("Role %s has no permission set, please set at least one permission.", request.githubRole())); + } + } + + private static boolean conflictBaseRole(String githubRole) { + return GITHUB_BASE_ROLES.stream() + .anyMatch(baseRole -> githubRole.toLowerCase(Locale.ROOT).equals(baseRole)); + } + + private static boolean noPermissionsGranted(SonarqubePermissions permissions) { + return !permissions.user() && + !permissions.codeViewer() && + !permissions.issueAdmin() && + !permissions.securityHotspotAdmin() && + !permissions.admin() && + !permissions.scan(); + } + + private Set toGithubPermissionsMappingDtos(GithubPermissionsMapping request) { + SonarqubePermissions permissions = request.permissions(); + Set githubPermissionsMappingDtos = new HashSet<>(); + + toGithubPermissionsMappingDto(permissions.user(), UserRole.USER, request.githubRole()).ifPresent(githubPermissionsMappingDtos::add); + toGithubPermissionsMappingDto(permissions.codeViewer(), UserRole.CODEVIEWER, request.githubRole()).ifPresent(githubPermissionsMappingDtos::add); + toGithubPermissionsMappingDto(permissions.issueAdmin(), UserRole.ISSUE_ADMIN, request.githubRole()).ifPresent(githubPermissionsMappingDtos::add); + toGithubPermissionsMappingDto(permissions.securityHotspotAdmin(), UserRole.SECURITYHOTSPOT_ADMIN, request.githubRole()).ifPresent(githubPermissionsMappingDtos::add); + toGithubPermissionsMappingDto(permissions.admin(), UserRole.ADMIN, request.githubRole()).ifPresent(githubPermissionsMappingDtos::add); + toGithubPermissionsMappingDto(permissions.scan(), UserRole.SCAN, request.githubRole()).ifPresent(githubPermissionsMappingDtos::add); + + return githubPermissionsMappingDtos; + } + + private Optional toGithubPermissionsMappingDto(boolean granted, String permission, String githubRole) { + if (granted) { + return Optional.of(new GithubPermissionsMappingDto(uuidFactory.create(), githubRole, permission)); + } + return Optional.empty(); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsController.java index c6148669832..eb3fcea29db 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsController.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsController.java @@ -22,18 +22,18 @@ package org.sonar.server.v2.api.github.permissions.controller; import java.util.HashSet; import java.util.List; import java.util.Set; -import javax.validation.Valid; import org.sonar.server.common.github.permissions.GithubPermissionsMapping; import org.sonar.server.common.github.permissions.GithubPermissionsMappingService; import org.sonar.server.common.github.permissions.PermissionMappingChange; +import org.sonar.server.common.github.permissions.SonarqubePermissions; import org.sonar.server.common.permission.Operation; import org.sonar.server.user.UserSession; import org.sonar.server.v2.api.github.permissions.model.RestGithubPermissionsMapping; import org.sonar.server.v2.api.github.permissions.request.GithubPermissionMappingUpdateRequest; +import org.sonar.server.v2.api.github.permissions.request.GithubPermissionsMappingPostRequest; import org.sonar.server.v2.api.github.permissions.request.PermissionMappingUpdate; +import org.sonar.server.v2.api.github.permissions.request.RestPermissions; import org.sonar.server.v2.api.github.permissions.response.GithubPermissionsMappingRestResponse; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; import static org.sonar.api.web.UserRole.ADMIN; import static org.sonar.api.web.UserRole.CODEVIEWER; @@ -60,7 +60,7 @@ public class DefaultGithubPermissionsController implements GithubPermissionsCont } @Override - public RestGithubPermissionsMapping updateMapping(@PathVariable("githubRole") String githubRole, @Valid @RequestBody GithubPermissionMappingUpdateRequest request) { + public RestGithubPermissionsMapping updateMapping(String githubRole, GithubPermissionMappingUpdateRequest request) { userSession.checkIsSystemAdministrator(); PermissionMappingUpdate update = request.permissions(); Set changes = new HashSet<>(); @@ -94,12 +94,43 @@ public class DefaultGithubPermissionsController implements GithubPermissionsCont .toList(); } + @Override + public RestGithubPermissionsMapping createMapping(GithubPermissionsMappingPostRequest request) { + userSession.checkIsSystemAdministrator(); + GithubPermissionsMapping githubPermissionsMapping = new GithubPermissionsMapping(request.githubRole(), false, toSonarqubePermissions(request.permissions())); + return toRestGithubPermissionMapping(githubPermissionsMappingService.createPermissionMapping(githubPermissionsMapping)); + } + + private static SonarqubePermissions toSonarqubePermissions(RestPermissions restPermissions) { + SonarqubePermissions.Builder sonarqubePermissionsBuilder = SonarqubePermissions.Builder.builder(); + + sonarqubePermissionsBuilder.user(restPermissions.user()); + sonarqubePermissionsBuilder.codeViewer(restPermissions.codeViewer()); + sonarqubePermissionsBuilder.issueAdmin(restPermissions.issueAdmin()); + sonarqubePermissionsBuilder.securityHotspotAdmin(restPermissions.securityHotspotAdmin()); + sonarqubePermissionsBuilder.admin(restPermissions.admin()); + sonarqubePermissionsBuilder.scan(restPermissions.scan()); + + return sonarqubePermissionsBuilder.build(); + } + private static RestGithubPermissionsMapping toRestGithubPermissionMapping(GithubPermissionsMapping githubPermissionsMapping) { return new RestGithubPermissionsMapping( - githubPermissionsMapping.roleName(), - githubPermissionsMapping.roleName(), + githubPermissionsMapping.githubRole(), + githubPermissionsMapping.githubRole(), githubPermissionsMapping.isBaseRole(), - githubPermissionsMapping.permissions()); + toRestPermissions(githubPermissionsMapping.permissions())); + } + + private static RestPermissions toRestPermissions(SonarqubePermissions permissions) { + return new RestPermissions( + permissions.user(), + permissions.codeViewer(), + permissions.issueAdmin(), + permissions.securityHotspotAdmin(), + permissions.admin(), + permissions.scan() + ); } } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/GithubPermissionsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/GithubPermissionsController.java index fddb261ab60..3c7083fb08a 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/GithubPermissionsController.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/controller/GithubPermissionsController.java @@ -24,37 +24,44 @@ import javax.validation.Valid; import org.sonar.server.v2.WebApiEndpoints; import org.sonar.server.v2.api.github.permissions.model.RestGithubPermissionsMapping; import org.sonar.server.v2.api.github.permissions.request.GithubPermissionMappingUpdateRequest; +import org.sonar.server.v2.api.github.permissions.request.GithubPermissionsMappingPostRequest; import org.sonar.server.v2.api.github.permissions.response.GithubPermissionsMappingRestResponse; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @RequestMapping(WebApiEndpoints.GITHUB_PERMISSIONS_ENDPOINT) @RestController public interface GithubPermissionsController { - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @GetMapping(produces = APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) @Operation(summary = "Fetch the GitHub permissions mapping", description = "Requires Administer System permission.") GithubPermissionsMappingRestResponse fetchAll(); - @PatchMapping(path = "/{githubRole}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = MediaType.APPLICATION_JSON_VALUE) + @PatchMapping(path = "/{githubRole}", consumes = JSON_MERGE_PATCH_CONTENT_TYPE, produces = APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) - @Operation(summary = "Update a single Github permission mapping", description = "Update a single Github permission mapping") + @Operation(summary = "Update a single GitHub permission mapping", description = "Requires Administer System permission.") RestGithubPermissionsMapping updateMapping(@PathVariable("githubRole") String githubRole, @Valid @RequestBody GithubPermissionMappingUpdateRequest request); @DeleteMapping(path = "/{githubRole}") @ResponseStatus(HttpStatus.NO_CONTENT) - @Operation(summary = "Delete a single Github permission mapping", description = "Delete a single Github permission mapping") + @Operation(summary = "Delete a single GitHub permission mapping", description = "Requires Administer System permission.") void deleteMapping(@PathVariable("githubRole") String githubRole); + @PostMapping(consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Create a permission mapping for a GitHub custom role", description = "Requires Administer System permission.") + RestGithubPermissionsMapping createMapping(@Valid @RequestBody GithubPermissionsMappingPostRequest request); + } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/model/RestGithubPermissionsMapping.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/model/RestGithubPermissionsMapping.java index 89a028d2e82..1952c7c9d19 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/model/RestGithubPermissionsMapping.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/model/RestGithubPermissionsMapping.java @@ -19,7 +19,7 @@ */ package org.sonar.server.v2.api.github.permissions.model; -import org.sonar.server.common.github.permissions.SonarqubePermissions; +import org.sonar.server.v2.api.github.permissions.request.RestPermissions; -public record RestGithubPermissionsMapping(String id, String roleName, boolean isBaseRole, SonarqubePermissions permissions) { +public record RestGithubPermissionsMapping(String id, String githubRole, boolean isBaseRole, RestPermissions permissions) { } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/GithubPermissionsMappingPostRequest.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/GithubPermissionsMappingPostRequest.java new file mode 100644 index 00000000000..8ff8e54c3cd --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/GithubPermissionsMappingPostRequest.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.v2.api.github.permissions.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +public record GithubPermissionsMappingPostRequest( + @NotNull + @Schema(description = "Custom role name on GitHub (case-sensitive)") + String githubRole, + + @NotNull + @Valid + @Schema(description = "Permissions") + RestPermissions permissions) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/RestPermissions.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/RestPermissions.java new file mode 100644 index 00000000000..15ed5286ee6 --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/github/permissions/request/RestPermissions.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.v2.api.github.permissions.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.NotNull; + +public record RestPermissions( + @NotNull + @Schema(description = "Browse") + Boolean user, + + @NotNull + @Schema(description = "See Source Code") + Boolean codeViewer, + + @NotNull + @Schema(description = "Administer Issues") + Boolean issueAdmin, + + @NotNull + @Schema(description = "Administer Security Hotspots") + Boolean securityHotspotAdmin, + + @NotNull + @Schema(description = "Administer") + Boolean admin, + + @NotNull + @Schema(description = "Execute Analysis") + Boolean scan +) { +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java index dd8d133e37d..18ff785ecc6 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandler.java @@ -19,6 +19,7 @@ */ package org.sonar.server.v2.common; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import java.util.Optional; import java.util.stream.Collectors; import org.sonar.server.exceptions.BadRequestException; @@ -29,6 +30,7 @@ import org.sonar.server.exceptions.UnauthorizedException; import org.sonar.server.v2.api.model.RestError; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -78,4 +80,18 @@ public class RestResponseEntityExceptionHandler { return new ResponseEntity<>(new RestError(notFoundException.getMessage()), HttpStatus.NOT_FOUND); } + @ExceptionHandler({HttpMessageNotReadableException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + protected ResponseEntity handleHttpMessageNotReadableException(HttpMessageNotReadableException httpMessageNotReadableException) { + String exceptionMessage = getExceptionMessage(httpMessageNotReadableException); + return new ResponseEntity<>(new RestError(exceptionMessage), HttpStatus.BAD_REQUEST); + } + + private static String getExceptionMessage(HttpMessageNotReadableException httpMessageNotReadableException) { + if (httpMessageNotReadableException.getRootCause() instanceof InvalidFormatException invalidFormatException) { + return invalidFormatException.getOriginalMessage(); + } + return Optional.ofNullable(httpMessageNotReadableException.getMessage()).orElse(""); + } + } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsControllerTest.java index dd0282d5079..bb4b9f63965 100644 --- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsControllerTest.java +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/github/permissions/controller/DefaultGithubPermissionsControllerTest.java @@ -35,11 +35,14 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.v2.api.ControllerTester; import org.sonar.server.v2.api.github.permissions.model.RestGithubPermissionsMapping; +import org.sonar.server.v2.api.github.permissions.request.GithubPermissionsMappingPostRequest; +import org.sonar.server.v2.api.github.permissions.request.RestPermissions; import org.sonar.server.v2.api.github.permissions.response.GithubPermissionsMappingRestResponse; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -47,9 +50,11 @@ import static org.mockito.Mockito.when; import static org.sonar.server.common.github.permissions.GithubPermissionsMappingService.READ_GITHUB_ROLE; import static org.sonar.server.v2.WebApiEndpoints.GITHUB_PERMISSIONS_ENDPOINT; import static org.sonar.server.v2.WebApiEndpoints.JSON_MERGE_PATCH_CONTENT_TYPE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -57,6 +62,8 @@ public class DefaultGithubPermissionsControllerTest { public static final String GITHUB_ROLE = "role1"; private static final Gson gson = new GsonBuilder().create(); + public static final GithubPermissionsMappingPostRequest GITHUB_PERMISSIONS_MAPPING_POST_REQUEST = + new GithubPermissionsMappingPostRequest(GITHUB_ROLE, new RestPermissions(true, true, true, true, true, true)); @Rule public UserSessionRule userSession = UserSessionRule.standalone(); @@ -97,7 +104,22 @@ public class DefaultGithubPermissionsControllerTest { } private static RestGithubPermissionsMapping toRestGithubPermissionMapping(GithubPermissionsMapping permissionMapping) { - return new RestGithubPermissionsMapping(permissionMapping.roleName(), permissionMapping.roleName(), permissionMapping.isBaseRole(), permissionMapping.permissions()); + return new RestGithubPermissionsMapping( + permissionMapping.githubRole(), + permissionMapping.githubRole(), + permissionMapping.isBaseRole(), + toRestPermissions(permissionMapping.permissions())); + } + + private static RestPermissions toRestPermissions(SonarqubePermissions permissions) { + return new RestPermissions( + permissions.user(), + permissions.codeViewer(), + permissions.issueAdmin(), + permissions.securityHotspotAdmin(), + permissions.admin(), + permissions.scan() + ); } @Test @@ -144,7 +166,7 @@ public class DefaultGithubPermissionsControllerTest { RestGithubPermissionsMapping response = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestGithubPermissionsMapping.class); RestGithubPermissionsMapping expectedResponse = new RestGithubPermissionsMapping(GITHUB_ROLE, GITHUB_ROLE, false, - new SonarqubePermissions(true, false, false, true, true, false)); + new RestPermissions(true, false, false, true, true, false)); assertThat(response).isEqualTo(expectedResponse); ArgumentCaptor> permissionMappingChangesCaptor = ArgumentCaptor.forClass(Set.class); @@ -199,4 +221,104 @@ public class DefaultGithubPermissionsControllerTest { verify(githubPermissionsMappingService).deletePermissionMappings(GITHUB_ROLE); } + @Test + public void createMapping_whenNotAdmin_shouldReturnForbidden() throws Exception { + userSession.logIn().setNonSystemAdministrator(); + mockMvc + .perform( + post(GITHUB_PERMISSIONS_ENDPOINT) + .contentType(APPLICATION_JSON_VALUE) + .content(gson.toJson(GITHUB_PERMISSIONS_MAPPING_POST_REQUEST))) + .andExpectAll( + status().isForbidden(), + content().json("{\"message\":\"Insufficient privileges\"}")); + } + + @Test + public void createMapping_whenRoleExists_shouldReturnBadRequest() throws Exception { + userSession.logIn().setSystemAdministrator(); + + when(githubPermissionsMappingService.createPermissionMapping(any())) + .thenThrow(new IllegalArgumentException("Exception message")); + + mockMvc + .perform( + post(GITHUB_PERMISSIONS_ENDPOINT) + .contentType(APPLICATION_JSON_VALUE) + .content(gson.toJson(GITHUB_PERMISSIONS_MAPPING_POST_REQUEST))) + .andExpectAll( + status().isBadRequest(), + content().json("{\"message\":\"Exception message\"}")); + } + + @Test + public void createMapping_whenMissingPermission_shouldReturnBadRequest() throws Exception { + userSession.logIn().setSystemAdministrator(); + + mockMvc + .perform( + post(GITHUB_PERMISSIONS_ENDPOINT) + .contentType(APPLICATION_JSON_VALUE) + .content(""" + { + "githubRole": "customRole", + "permissions": { + "user": false, + "codeViewer": false, + "issueAdmin": false, + "securityHotspotAdmin": false, + "admin": false + } + } + """)) + .andExpectAll( + status().isBadRequest(), + content().json("{\"message\":\"Value {} for field permissions.scan was rejected. Error: must not be null.\"}")); + } + + @Test + public void createMapping_whenWrongType_shouldReturnBadRequest() throws Exception { + userSession.logIn().setSystemAdministrator(); + + mockMvc + .perform( + post(GITHUB_PERMISSIONS_ENDPOINT) + .contentType(APPLICATION_JSON_VALUE) + .content(""" + { + "githubRole": "customRole", + "permissions": { + "user": false, + "codeViewer": false, + "issueAdmin": true, + "securityHotspotAdmin": false, + "admin": true, + "scan": "notABooleanType" + } + } + """)) + .andExpect(status().isBadRequest()); + } + @Test + public void createMapping_whenValidRequest_shouldReturnMapping() throws Exception { + userSession.logIn().setSystemAdministrator(); + + GithubPermissionsMapping githubPermissionsMapping = new GithubPermissionsMapping(GITHUB_ROLE, false, new SonarqubePermissions(true, true, true, true, true, true)); + when(githubPermissionsMappingService.createPermissionMapping(githubPermissionsMapping)).thenReturn(githubPermissionsMapping); + + MvcResult mvcResult = mockMvc + .perform( + post(GITHUB_PERMISSIONS_ENDPOINT) + .contentType(APPLICATION_JSON_VALUE) + .content(gson.toJson(GITHUB_PERMISSIONS_MAPPING_POST_REQUEST))) + .andExpect(status().isOk()) + .andReturn(); + + RestGithubPermissionsMapping response = gson.fromJson(mvcResult.getResponse().getContentAsString(), RestGithubPermissionsMapping.class); + + RestGithubPermissionsMapping expectedResponse = new RestGithubPermissionsMapping(GITHUB_ROLE, GITHUB_ROLE, false, new RestPermissions(true, true, true, true, true, true)); + assertThat(response).isEqualTo(expectedResponse); + + } + } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java new file mode 100644 index 00000000000..b64519d72ad --- /dev/null +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/common/RestResponseEntityExceptionHandlerTest.java @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.v2.common; + +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import org.junit.Test; +import org.sonar.server.v2.api.model.RestError; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RestResponseEntityExceptionHandlerTest { + + private RestResponseEntityExceptionHandler underTest = new RestResponseEntityExceptionHandler(); + + @Test + public void handleHttpMessageNotReadableException_whenCauseIsNotInvalidFormatException_shouldUseMessage() { + + HttpMessageNotReadableException exception = new HttpMessageNotReadableException("Message not readable", new Exception()); + + ResponseEntity responseEntity = underTest.handleHttpMessageNotReadableException(exception); + + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(responseEntity.getBody().message()).isEqualTo("Message not readable; nested exception is java.lang.Exception"); + } + + @Test + public void handleHttpMessageNotReadableException_whenCauseIsNotInvalidFormatExceptionAndMessageIsNull_shouldUseEmptyStringAsMessage() { + + HttpMessageNotReadableException exception = new HttpMessageNotReadableException(null, (Exception) null); + + ResponseEntity responseEntity = underTest.handleHttpMessageNotReadableException(exception); + + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(responseEntity.getBody().message()).isEmpty(); + } + + @Test + public void handleHttpMessageNotReadableException_whenCauseIsInvalidFormatException_shouldUseMessageFromCause() { + + InvalidFormatException cause = mock(InvalidFormatException.class); + when(cause.getOriginalMessage()).thenReturn("Cause message"); + + HttpMessageNotReadableException exception = new HttpMessageNotReadableException("Message not readable", cause); + + ResponseEntity responseEntity = underTest.handleHttpMessageNotReadableException(exception); + + assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(responseEntity.getBody().message()).isEqualTo("Cause message"); + } +} -- 2.39.5