From 2f7bbfad874d63e537cd147f97f878046f9cf244 Mon Sep 17 00:00:00 2001 From: Matteo Mara Date: Thu, 19 Sep 2024 14:59:45 +0200 Subject: [PATCH] SONAR-23064 Return 403 when trying to select/deselect a Quality Gate for AI-flagged project --- .../sonar/db/component/ComponentDbTester.java | 5 ++ .../qualitygate/ws/DeselectActionIT.java | 68 ++++++++++++++----- .../server/qualitygate/ws/SelectActionIT.java | 61 ++++++++++++----- .../server/qualitygate/ws/DeselectAction.java | 21 ++++-- .../server/qualitygate/ws/SelectAction.java | 16 ++++- 5 files changed, 132 insertions(+), 39 deletions(-) diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java index 5a31ff436c5..c4fd31374e0 100644 --- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java +++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/component/ComponentDbTester.java @@ -184,6 +184,11 @@ public class ComponentDbTester { return insertComponentAndBranchAndProject(ComponentTesting.newPrivateProjectDto(), true, branchPopulator, componentPopulator, projectPopulator); } + public ProjectData insertProjectWithAiCode() { + return insertComponentAndBranchAndProject(ComponentTesting.newPrivateProjectDto(), true, defaults(), defaults(), p -> p.setAiCodeAssurance(true)); + } + + public final ComponentDto insertPublicPortfolio() { return insertComponentAndPortfolio(ComponentTesting.newPortfolio().setPrivate(false), false, defaults(), defaults()); } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/DeselectActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/DeselectActionIT.java index 620569c5757..5f33072d2a7 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/DeselectActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/DeselectActionIT.java @@ -20,8 +20,9 @@ package org.sonar.server.qualitygate.ws; import java.util.Optional; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.WebService; import org.sonar.api.web.UserRole; @@ -31,33 +32,47 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ProjectData; import org.sonar.db.project.ProjectDto; import org.sonar.db.qualitygate.QualityGateDto; +import org.sonar.server.ai.code.assurance.AiCodeAssuranceVerifier; import org.sonar.server.component.ComponentFinder; import org.sonar.server.component.TestComponentFinder; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.web.UserRole.ADMIN; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; +import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_GATE_NAME; -public class DeselectActionIT { +class DeselectActionIT { - @Rule + @RegisterExtension public UserSessionRule userSession = UserSessionRule.standalone(); - @Rule + @RegisterExtension public DbTester db = DbTester.create(); private final DbClient dbClient = db.getDbClient(); private final ComponentFinder componentFinder = TestComponentFinder.from(db); - private final DeselectAction underTest = new DeselectAction(dbClient, new QualityGatesWsSupport(db.getDbClient(), userSession, componentFinder)); + private final AiCodeAssuranceVerifier aiCodeAssuranceVerifier = mock(AiCodeAssuranceVerifier.class); + private final DeselectAction underTest = new DeselectAction(dbClient, new QualityGatesWsSupport(db.getDbClient(), userSession, + componentFinder), aiCodeAssuranceVerifier); private final WsActionTester ws = new WsActionTester(underTest); + @BeforeEach + void setUp() { + when(aiCodeAssuranceVerifier.isAiCodeAssured(any())).thenReturn(false); + } + @Test - public void deselect_by_key() { + void deselect_by_key() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); @@ -71,7 +86,7 @@ public class DeselectActionIT { } @Test - public void project_admin() { + void project_admin() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); associateProjectToQualityGate(project, qualityGate); @@ -85,7 +100,7 @@ public class DeselectActionIT { } @Test - public void other_project_should_not_be_updated() { + void other_project_should_not_be_updated() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); @@ -103,7 +118,7 @@ public class DeselectActionIT { } @Test - public void default_is_used() { + void default_is_used() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); @@ -117,7 +132,7 @@ public class DeselectActionIT { } @Test - public void fail_when_no_project_key() { + void fail_when_no_project_key() { userSession.addPermission(ADMINISTER_QUALITY_GATES); assertThatThrownBy(() -> ws.newRequest() @@ -127,7 +142,7 @@ public class DeselectActionIT { } @Test - public void fail_when_anonymous() { + void fail_when_anonymous() { ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); userSession.anonymous(); @@ -138,7 +153,7 @@ public class DeselectActionIT { } @Test - public void fail_when_not_project_admin() { + void fail_when_not_project_admin() { ProjectData project = db.components().insertPrivateProject(); userSession.logIn().addProjectPermission(UserRole.ISSUE_ADMIN, project.getProjectDto()); @@ -149,7 +164,7 @@ public class DeselectActionIT { } @Test - public void fail_when_not_quality_gates_admin() { + void fail_when_not_quality_gates_admin() { userSession.addPermission(ADMINISTER_QUALITY_GATES); ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); @@ -161,15 +176,16 @@ public class DeselectActionIT { } @Test - public void definition() { + void definition() { WebService.Action def = ws.getDef(); assertThat(def.description()).isNotEmpty(); assertThat(def.isPost()).isTrue(); assertThat(def.since()).isEqualTo("4.3"); - assertThat(def.changelog()).extracting(Change::getVersion, Change::getDescription).containsExactly( + assertThat(def.changelog()).extracting(Change::getVersion, Change::getDescription).containsExactlyInAnyOrder( tuple("6.6", "The parameter 'gateId' was removed"), - tuple("8.3", "The parameter 'projectId' was removed")); + tuple("8.3", "The parameter 'projectId' was removed"), + tuple("10.7", "It is not possible anymore to change the Quality Gate of a project flagged as containing AI code.")); assertThat(def.params()) .extracting(WebService.Param::key, WebService.Param::isRequired) @@ -177,6 +193,23 @@ public class DeselectActionIT { tuple("projectKey", true)); } + @Test + void whenAiCodeAssuranceIsSet_failIfEditionIsDeveloperOrHigher() { + QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); + ProjectDto projectDto = db.components().insertProjectWithAiCode().getProjectDto(); + when(aiCodeAssuranceVerifier.isAiCodeAssured(projectDto)).thenReturn(true); + + userSession.logIn().addProjectPermission(ADMIN, projectDto); + + TestRequest request = ws.newRequest() + .setParam(PARAM_GATE_NAME, qualityGate.getName()) + .setParam("projectKey", projectDto.getKey()); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Quality gate cannot be changed for project with AI Code Assurance enabled."); + } + private void associateProjectToQualityGate(ProjectDto project, QualityGateDto qualityGate) { db.qualityGates().associateProjectToQualityGate(project, qualityGate); db.commit(); @@ -196,4 +229,5 @@ public class DeselectActionIT { .isNotEmpty() .hasValue(qualityGate.getUuid()); } + } diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/SelectActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/SelectActionIT.java index 9b193891199..1a2a0843b85 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/SelectActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualitygate/ws/SelectActionIT.java @@ -20,43 +20,55 @@ package org.sonar.server.qualitygate.ws; import java.util.Optional; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ProjectData; import org.sonar.db.project.ProjectDto; import org.sonar.db.qualitygate.QualityGateDto; +import org.sonar.server.ai.code.assurance.AiCodeAssuranceVerifier; import org.sonar.server.component.ComponentFinder; import org.sonar.server.component.TestComponentFinder; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.sonar.api.web.UserRole.ADMIN; import static org.sonar.api.web.UserRole.ISSUE_ADMIN; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_GATE_NAME; -public class SelectActionIT { +class SelectActionIT { - @Rule + @RegisterExtension public UserSessionRule userSession = UserSessionRule.standalone(); - @Rule + @RegisterExtension public DbTester db = DbTester.create(); private final DbClient dbClient = db.getDbClient(); private final ComponentFinder componentFinder = TestComponentFinder.from(db); + private final AiCodeAssuranceVerifier aiCodeAssuranceVerifier = mock(AiCodeAssuranceVerifier.class); private final SelectAction underTest = new SelectAction(dbClient, - new QualityGatesWsSupport(db.getDbClient(), userSession, componentFinder)); + new QualityGatesWsSupport(db.getDbClient(), userSession, componentFinder), aiCodeAssuranceVerifier); private final WsActionTester ws = new WsActionTester(underTest); + @BeforeEach + void setUp() { + when(aiCodeAssuranceVerifier.isAiCodeAssured(any())).thenReturn(false); + } + @Test - public void select_by_key() { + void select_by_key() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); @@ -70,7 +82,7 @@ public class SelectActionIT { } @Test - public void change_quality_gate_for_project() { + void change_quality_gate_for_project() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto initialQualityGate = db.qualityGates().insertQualityGate(); QualityGateDto secondQualityGate = db.qualityGates().insertQualityGate(); @@ -90,7 +102,7 @@ public class SelectActionIT { } @Test - public void select_same_quality_gate_for_project_twice() { + void select_same_quality_gate_for_project_twice() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto initialQualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); @@ -109,7 +121,7 @@ public class SelectActionIT { } @Test - public void project_admin() { + void project_admin() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); userSession.logIn().addProjectPermission(ADMIN, project); @@ -123,7 +135,7 @@ public class SelectActionIT { } @Test - public void gate_administrator_can_associate_a_gate_to_a_project() { + void gate_administrator_can_associate_a_gate_to_a_project() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectDto project = db.components().insertPrivateProject().getProjectDto(); @@ -137,7 +149,7 @@ public class SelectActionIT { } @Test - public void fail_when_no_quality_gate() { + void fail_when_no_quality_gate() { userSession.addPermission(ADMINISTER_QUALITY_GATES); ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); @@ -149,7 +161,7 @@ public class SelectActionIT { } @Test - public void fail_when_no_project_key() { + void fail_when_no_project_key() { userSession.addPermission(ADMINISTER_QUALITY_GATES); QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); @@ -161,7 +173,7 @@ public class SelectActionIT { } @Test - public void fail_when_anonymous() { + void fail_when_anonymous() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); userSession.anonymous(); @@ -174,7 +186,7 @@ public class SelectActionIT { } @Test - public void fail_when_not_project_admin() { + void fail_when_not_project_admin() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ProjectData project = db.components().insertPrivateProject(); userSession.logIn().addProjectPermission(ISSUE_ADMIN, project.getProjectDto()); @@ -187,7 +199,7 @@ public class SelectActionIT { } @Test - public void fail_when_not_quality_gates_admin() { + void fail_when_not_quality_gates_admin() { QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent(); userSession.logIn(); @@ -199,6 +211,23 @@ public class SelectActionIT { .isInstanceOf(ForbiddenException.class); } + @Test + void whenAiCodeAssuranceIsSet_failIfEditionIsDeveloperOrHigher() { + QualityGateDto qualityGate = db.qualityGates().insertQualityGate(); + ProjectDto projectDto = db.components().insertProjectWithAiCode().getProjectDto(); + when(aiCodeAssuranceVerifier.isAiCodeAssured(projectDto)).thenReturn(true); + + userSession.logIn().addProjectPermission(ADMIN, projectDto); + + TestRequest request = ws.newRequest() + .setParam(PARAM_GATE_NAME, qualityGate.getName()) + .setParam("projectKey", projectDto.getKey()); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Quality gate cannot be changed for project with AI Code Assurance enabled."); + } + private void assertSelected(QualityGateDto qualityGate, ProjectDto project) { Optional qGateUuid = db.qualityGates().selectQGateUuidByProjectUuid(project.getUuid()); assertThat(qGateUuid) diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/DeselectAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/DeselectAction.java index 937bf359c92..a9c008f3f17 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/DeselectAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/DeselectAction.java @@ -26,6 +26,8 @@ import org.sonar.api.server.ws.WebService; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.project.ProjectDto; +import org.sonar.server.ai.code.assurance.AiCodeAssuranceVerifier; +import org.sonar.server.exceptions.ForbiddenException; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; @@ -34,10 +36,12 @@ public class DeselectAction implements QualityGatesWsAction { private final DbClient dbClient; private final QualityGatesWsSupport wsSupport; + private final AiCodeAssuranceVerifier aiCodeAssuranceVerifier; - public DeselectAction(DbClient dbClient, QualityGatesWsSupport wsSupport) { + public DeselectAction(DbClient dbClient, QualityGatesWsSupport wsSupport, AiCodeAssuranceVerifier aiCodeAssuranceVerifier) { this.wsSupport = wsSupport; this.dbClient = dbClient; + this.aiCodeAssuranceVerifier = aiCodeAssuranceVerifier; } @Override @@ -52,8 +56,10 @@ public class DeselectAction implements QualityGatesWsAction { .setPost(true) .setSince("4.3") .setHandler(this) - .setChangelog(new Change("6.6", "The parameter 'gateId' was removed"), - new Change("8.3", "The parameter 'projectId' was removed")); + .setChangelog( + new Change("10.7", "It is not possible anymore to change the Quality Gate of a project flagged as containing AI code."), + new Change("8.3", "The parameter 'projectId' was removed"), + new Change("6.6", "The parameter 'gateId' was removed")); action.createParam(PARAM_PROJECT_KEY) .setRequired(true) @@ -72,8 +78,15 @@ public class DeselectAction implements QualityGatesWsAction { } private void dissociateProject(DbSession dbSession, ProjectDto project) { - wsSupport.checkCanAdminProject(project); + checkProjectQGCanChange(project); dbClient.projectQgateAssociationDao().deleteByProjectUuid(dbSession, project.getUuid()); dbSession.commit(); } + + private void checkProjectQGCanChange(ProjectDto project) { + wsSupport.checkCanAdminProject(project); + if (aiCodeAssuranceVerifier.isAiCodeAssured(project)) { + throw new ForbiddenException("Quality gate cannot be changed for project with AI Code Assurance enabled."); + } + } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SelectAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SelectAction.java index f92d4e37344..b6390f3a7bc 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SelectAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/SelectAction.java @@ -27,6 +27,8 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.project.ProjectDto; import org.sonar.db.qualitygate.QualityGateDto; +import org.sonar.server.ai.code.assurance.AiCodeAssuranceVerifier; +import org.sonar.server.exceptions.ForbiddenException; import static org.sonar.server.qualitygate.ws.CreateAction.NAME_MAXIMUM_LENGTH; import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_SELECT; @@ -37,10 +39,12 @@ import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; public class SelectAction implements QualityGatesWsAction { private final DbClient dbClient; private final QualityGatesWsSupport wsSupport; + private final AiCodeAssuranceVerifier aiCodeAssuranceVerifier; - public SelectAction(DbClient dbClient, QualityGatesWsSupport wsSupport) { + public SelectAction(DbClient dbClient, QualityGatesWsSupport wsSupport, AiCodeAssuranceVerifier aiCodeAssuranceVerifier) { this.dbClient = dbClient; this.wsSupport = wsSupport; + this.aiCodeAssuranceVerifier = aiCodeAssuranceVerifier; } @Override @@ -56,6 +60,7 @@ public class SelectAction implements QualityGatesWsAction { .setSince("4.3") .setHandler(this) .setChangelog( + new Change("10.7", "It is not possible anymore to change the Quality Gate of a project flagged as containing AI code."), new Change("10.0", "Parameter 'gateId' is removed. Use 'gateName' instead."), new Change("8.4", "Parameter 'gateName' added"), new Change("8.4", "Parameter 'gateId' is deprecated. Format changes from integer to string. Use 'gateName' instead."), @@ -85,7 +90,7 @@ public class SelectAction implements QualityGatesWsAction { QualityGateDto qualityGate; qualityGate = wsSupport.getByName(dbSession, gateName); ProjectDto project = wsSupport.getProject(dbSession, projectKey); - wsSupport.checkCanAdminProject(project); + checkProjectQGCanChange(project); QualityGateDto currentQualityGate = dbClient.qualityGateDao().selectByProjectUuid(dbSession, project.getUuid()); if (currentQualityGate == null) { @@ -101,4 +106,11 @@ public class SelectAction implements QualityGatesWsAction { } response.noContent(); } + + private void checkProjectQGCanChange(ProjectDto project) { + wsSupport.checkCanAdminProject(project); + if (aiCodeAssuranceVerifier.isAiCodeAssured(project)) { + throw new ForbiddenException("Quality gate cannot be changed for project with AI Code Assurance enabled."); + } + } } -- 2.39.5