]> source.dussan.org Git - sonarqube.git/blob
456ae51975c251e498bc29ae8610e8b12b4a1bcd
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 package org.sonar.server.qualitygate.ws;
21
22 import java.util.Arrays;
23 import java.util.Optional;
24 import java.util.stream.Collectors;
25 import javax.annotation.Nullable;
26 import javax.annotation.concurrent.Immutable;
27 import org.sonar.api.measures.CoreMetrics;
28 import org.sonar.api.server.ws.Change;
29 import org.sonar.api.server.ws.Request;
30 import org.sonar.api.server.ws.Response;
31 import org.sonar.api.server.ws.WebService;
32 import org.sonar.api.web.UserRole;
33 import org.sonar.core.util.Uuids;
34 import org.sonar.db.DbClient;
35 import org.sonar.db.DbSession;
36 import org.sonar.db.component.BranchDto;
37 import org.sonar.db.component.SnapshotDto;
38 import org.sonar.db.measure.LiveMeasureDto;
39 import org.sonar.db.measure.MeasureDto;
40 import org.sonar.db.permission.GlobalPermission;
41 import org.sonar.db.project.ProjectDto;
42 import org.sonar.server.component.ComponentFinder;
43 import org.sonar.server.exceptions.BadRequestException;
44 import org.sonar.server.qualitygate.QualityGateCaycChecker;
45 import org.sonar.server.qualitygate.QualityGateCaycStatus;
46 import org.sonar.server.user.UserSession;
47 import org.sonar.server.ws.KeyExamples;
48 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
49
50 import static com.google.common.base.Strings.isNullOrEmpty;
51 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
52 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
53 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_PROJECT_STATUS;
54 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID;
55 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
56 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;
57 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY;
58 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PULL_REQUEST;
59 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
60 import static org.sonar.server.ws.WsUtils.writeProtobuf;
61
62 public class ProjectStatusAction implements QualityGatesWsAction {
63   private static final String QG_STATUSES_ONE_LINE = Arrays.stream(ProjectStatusResponse.Status.values())
64     .map(Enum::toString)
65     .collect(Collectors.joining(", "));
66   private static final String MSG_ONE_PROJECT_PARAMETER_ONLY = String.format("Either '%s', '%s' or '%s' must be provided", PARAM_ANALYSIS_ID, PARAM_PROJECT_ID, PARAM_PROJECT_KEY);
67   private static final String MSG_ONE_BRANCH_PARAMETER_ONLY = String.format("Either '%s' or '%s' can be provided, not both", PARAM_BRANCH, PARAM_PULL_REQUEST);
68
69   private final DbClient dbClient;
70   private final ComponentFinder componentFinder;
71   private final UserSession userSession;
72   private final QualityGateCaycChecker qualityGateCaycChecker;
73
74   public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, QualityGateCaycChecker qualityGateCaycChecker) {
75     this.dbClient = dbClient;
76     this.componentFinder = componentFinder;
77     this.userSession = userSession;
78     this.qualityGateCaycChecker = qualityGateCaycChecker;
79   }
80
81   @Override
82   public void define(WebService.NewController controller) {
83     WebService.NewAction action = controller.createAction(ACTION_PROJECT_STATUS)
84       .setDescription(String.format("Get the quality gate status of a project or a Compute Engine task.<br />" +
85         "%s <br />" +
86         "The different statuses returned are: %s. The %s status is returned when there is no quality gate associated with the analysis.<br />" +
87         "Returns an HTTP code 404 if the analysis associated with the task is not found or does not exist.<br />" +
88         "Requires one of the following permissions:" +
89         "<ul>" +
90         "<li>'Administer System'</li>" +
91         "<li>'Administer' rights on the specified project</li>" +
92         "<li>'Browse' on the specified project</li>" +
93         "<li>'Execute Analysis' on the specified project</li>" +
94         "</ul>", MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
95       .setResponseExample(getClass().getResource("project_status-example.json"))
96       .setSince("5.3")
97       .setHandler(this)
98       .setChangelog(
99         new Change("10.0", "The field 'periods' in the response is removed"),
100         new Change("9.9", "'caycStatus' field is added to the response"),
101         new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"),
102         new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
103         new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
104         new Change("7.6", "The field 'warning' in the response is deprecated"),
105         new Change("6.4", "The field 'ignoredConditions' is added to the response"));
106
107     action.createParam(PARAM_ANALYSIS_ID)
108       .setDescription("Analysis id")
109       .setExampleValue(Uuids.UUID_EXAMPLE_04);
110
111     action.createParam(PARAM_PROJECT_ID)
112       .setSince("5.4")
113       .setDescription("Project UUID. Doesn't work with branches or pull requests")
114       .setExampleValue(Uuids.UUID_EXAMPLE_01);
115
116     action.createParam(PARAM_PROJECT_KEY)
117       .setSince("5.4")
118       .setDescription("Project key")
119       .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
120
121     action.createParam(PARAM_BRANCH)
122       .setSince("7.7")
123       .setDescription("Branch key")
124       .setExampleValue(KeyExamples.KEY_BRANCH_EXAMPLE_001);
125
126     action.createParam(PARAM_PULL_REQUEST)
127       .setSince("7.7")
128       .setDescription("Pull request id")
129       .setExampleValue(KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001);
130   }
131
132   @Override
133   public void handle(Request request, Response response) throws Exception {
134     String analysisId = request.param(PARAM_ANALYSIS_ID);
135     String projectId = request.param(PARAM_PROJECT_ID);
136     String projectKey = request.param(PARAM_PROJECT_KEY);
137     String branchKey = request.param(PARAM_BRANCH);
138     String pullRequestId = request.param(PARAM_PULL_REQUEST);
139     checkRequest(
140       !isNullOrEmpty(analysisId)
141         ^ !isNullOrEmpty(projectId)
142         ^ !isNullOrEmpty(projectKey),
143       MSG_ONE_PROJECT_PARAMETER_ONLY);
144     checkRequest(isNullOrEmpty(branchKey) || isNullOrEmpty(pullRequestId), MSG_ONE_BRANCH_PARAMETER_ONLY);
145
146     try (DbSession dbSession = dbClient.openSession(false)) {
147       ProjectStatusResponse projectStatusResponse = doHandle(dbSession, analysisId, projectId, projectKey, branchKey, pullRequestId);
148       writeProtobuf(projectStatusResponse, request, response);
149     }
150   }
151
152   private ProjectStatusResponse doHandle(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
153     @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
154     ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
155     checkPermission(projectAndSnapshot.project);
156     Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
157     QualityGateCaycStatus caycStatus = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid());
158
159     return ProjectStatusResponse.newBuilder()
160       .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), caycStatus).format())
161       .build();
162   }
163
164   private ProjectAndSnapshot getProjectAndSnapshot(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
165     @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
166     if (!isNullOrEmpty(analysisId)) {
167       return getSnapshotThenProject(dbSession, analysisId);
168     }
169     if (!isNullOrEmpty(projectUuid) ^ !isNullOrEmpty(projectKey)) {
170       return getProjectThenSnapshot(dbSession, projectUuid, projectKey, branchKey, pullRequestId);
171     }
172
173     throw BadRequestException.create(MSG_ONE_PROJECT_PARAMETER_ONLY);
174   }
175
176   private ProjectAndSnapshot getProjectThenSnapshot(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey,
177     @Nullable String branchKey, @Nullable String pullRequestId) {
178     ProjectDto projectDto;
179     BranchDto branchDto;
180
181     if (projectUuid != null) {
182       projectDto = componentFinder.getProjectByUuid(dbSession, projectUuid);
183       branchDto = componentFinder.getMainBranch(dbSession, projectDto);
184     } else {
185       projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
186       branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchKey, pullRequestId);
187     }
188     Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchDto.getUuid());
189     return new ProjectAndSnapshot(projectDto, branchDto, snapshot.orElse(null));
190   }
191
192   private ProjectAndSnapshot getSnapshotThenProject(DbSession dbSession, String analysisUuid) {
193     SnapshotDto snapshotDto = getSnapshot(dbSession, analysisUuid);
194     BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, snapshotDto.getComponentUuid())
195       .orElseThrow(() -> new IllegalStateException(String.format("Branch '%s' not found", snapshotDto.getUuid())));
196     ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid())
197       .orElseThrow(() -> new IllegalStateException(String.format("Project '%s' not found", branchDto.getProjectUuid())));
198
199     return new ProjectAndSnapshot(projectDto, branchDto, snapshotDto);
200   }
201
202   private SnapshotDto getSnapshot(DbSession dbSession, String analysisUuid) {
203     Optional<SnapshotDto> snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid);
204     return checkFoundWithOptional(snapshotDto, "Analysis with id '%s' is not found", analysisUuid);
205   }
206
207   private Optional<String> loadQualityGateDetails(DbSession dbSession, ProjectAndSnapshot projectAndSnapshot, boolean onAnalysis) {
208     if (onAnalysis) {
209       if (!projectAndSnapshot.snapshotDto.isPresent()) {
210         return Optional.empty();
211       }
212       // get the gate status as it was computed during the specified analysis
213       String analysisUuid = projectAndSnapshot.snapshotDto.get().getUuid();
214       return dbClient.measureDao().selectMeasure(dbSession, analysisUuid, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY)
215         .map(MeasureDto::getData);
216     }
217
218     // do not restrict to a specified analysis, use the live measure
219     Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY);
220     return measure.map(LiveMeasureDto::getDataAsString);
221   }
222
223   private void checkPermission(ProjectDto project) {
224     if (!userSession.hasProjectPermission(UserRole.ADMIN, project) &&
225       !userSession.hasProjectPermission(UserRole.USER, project) &&
226       !userSession.hasProjectPermission(UserRole.SCAN, project) &&
227       !userSession.hasPermission(GlobalPermission.SCAN)) {
228       throw insufficientPrivilegesException();
229     }
230   }
231
232   @Immutable
233   private static class ProjectAndSnapshot {
234     private final BranchDto branch;
235     private final Optional<SnapshotDto> snapshotDto;
236     private final ProjectDto project;
237
238     private ProjectAndSnapshot(ProjectDto project, BranchDto branch, @Nullable SnapshotDto snapshotDto) {
239       this.project = project;
240       this.branch = branch;
241       this.snapshotDto = Optional.ofNullable(snapshotDto);
242     }
243   }
244 }