]> source.dussan.org Git - sonarqube.git/blob
da2660620f9983ed9e1a919be0339b2edaa38da4
[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.user.UserSession;
46 import org.sonar.server.ws.KeyExamples;
47 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
48
49 import static com.google.common.base.Strings.isNullOrEmpty;
50 import static org.sonar.server.exceptions.BadRequestException.checkRequest;
51 import static org.sonar.server.exceptions.NotFoundException.checkFoundWithOptional;
52 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.ACTION_PROJECT_STATUS;
53 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_ANALYSIS_ID;
54 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_BRANCH;
55 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_ID;
56 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PROJECT_KEY;
57 import static org.sonar.server.qualitygate.ws.QualityGatesWsParameters.PARAM_PULL_REQUEST;
58 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
59 import static org.sonar.server.ws.WsUtils.writeProtobuf;
60
61 public class ProjectStatusAction implements QualityGatesWsAction {
62   private static final String QG_STATUSES_ONE_LINE = Arrays.stream(ProjectStatusResponse.Status.values())
63     .map(Enum::toString)
64     .collect(Collectors.joining(", "));
65   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);
66   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);
67
68   private final DbClient dbClient;
69   private final ComponentFinder componentFinder;
70   private final UserSession userSession;
71   private final QualityGateCaycChecker qualityGateCaycChecker;
72
73   public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, QualityGateCaycChecker qualityGateCaycChecker) {
74     this.dbClient = dbClient;
75     this.componentFinder = componentFinder;
76     this.userSession = userSession;
77     this.qualityGateCaycChecker = qualityGateCaycChecker;
78   }
79
80   @Override
81   public void define(WebService.NewController controller) {
82     WebService.NewAction action = controller.createAction(ACTION_PROJECT_STATUS)
83       .setDescription(String.format("Get the quality gate status of a project or a Compute Engine task.<br />" +
84         "%s <br />" +
85         "The different statuses returned are: %s. The %s status is returned when there is no quality gate associated with the analysis.<br />" +
86         "Returns an HTTP code 404 if the analysis associated with the task is not found or does not exist.<br />" +
87         "Requires one of the following permissions:" +
88         "<ul>" +
89         "<li>'Administer System'</li>" +
90         "<li>'Administer' rights on the specified project</li>" +
91         "<li>'Browse' on the specified project</li>" +
92         "<li>'Execute Analysis' on the specified project</li>" +
93         "</ul>", MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
94       .setResponseExample(getClass().getResource("project_status-example.json"))
95       .setSince("5.3")
96       .setHandler(this)
97       .setChangelog(
98         new Change("9.9", "'isCaycCompliant' field is added to the response"),
99         new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"),
100         new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
101         new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
102         new Change("7.6", "The field 'warning' in the response is deprecated"),
103         new Change("6.4", "The field 'ignoredConditions' is added to the response"));
104
105     action.createParam(PARAM_ANALYSIS_ID)
106       .setDescription("Analysis id")
107       .setExampleValue(Uuids.UUID_EXAMPLE_04);
108
109     action.createParam(PARAM_PROJECT_ID)
110       .setSince("5.4")
111       .setDescription("Project UUID. Doesn't work with branches or pull requests")
112       .setExampleValue(Uuids.UUID_EXAMPLE_01);
113
114     action.createParam(PARAM_PROJECT_KEY)
115       .setSince("5.4")
116       .setDescription("Project key")
117       .setExampleValue(KeyExamples.KEY_PROJECT_EXAMPLE_001);
118
119     action.createParam(PARAM_BRANCH)
120       .setSince("7.7")
121       .setDescription("Branch key")
122       .setExampleValue(KeyExamples.KEY_BRANCH_EXAMPLE_001);
123
124     action.createParam(PARAM_PULL_REQUEST)
125       .setSince("7.7")
126       .setDescription("Pull request id")
127       .setExampleValue(KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001);
128   }
129
130   @Override
131   public void handle(Request request, Response response) throws Exception {
132     String analysisId = request.param(PARAM_ANALYSIS_ID);
133     String projectId = request.param(PARAM_PROJECT_ID);
134     String projectKey = request.param(PARAM_PROJECT_KEY);
135     String branchKey = request.param(PARAM_BRANCH);
136     String pullRequestId = request.param(PARAM_PULL_REQUEST);
137     checkRequest(
138       !isNullOrEmpty(analysisId)
139         ^ !isNullOrEmpty(projectId)
140         ^ !isNullOrEmpty(projectKey),
141       MSG_ONE_PROJECT_PARAMETER_ONLY);
142     checkRequest(isNullOrEmpty(branchKey) || isNullOrEmpty(pullRequestId), MSG_ONE_BRANCH_PARAMETER_ONLY);
143
144     try (DbSession dbSession = dbClient.openSession(false)) {
145       ProjectStatusResponse projectStatusResponse = doHandle(dbSession, analysisId, projectId, projectKey, branchKey, pullRequestId);
146       writeProtobuf(projectStatusResponse, request, response);
147     }
148   }
149
150   private ProjectStatusResponse doHandle(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
151     @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
152     ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
153     checkPermission(projectAndSnapshot.project);
154     Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
155     var isCaycCompliant = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid());
156
157     return ProjectStatusResponse.newBuilder()
158       .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), isCaycCompliant).format())
159       .build();
160   }
161
162   private ProjectAndSnapshot getProjectAndSnapshot(DbSession dbSession, @Nullable String analysisId, @Nullable String projectUuid,
163     @Nullable String projectKey, @Nullable String branchKey, @Nullable String pullRequestId) {
164     if (!isNullOrEmpty(analysisId)) {
165       return getSnapshotThenProject(dbSession, analysisId);
166     }
167     if (!isNullOrEmpty(projectUuid) ^ !isNullOrEmpty(projectKey)) {
168       return getProjectThenSnapshot(dbSession, projectUuid, projectKey, branchKey, pullRequestId);
169     }
170
171     throw BadRequestException.create(MSG_ONE_PROJECT_PARAMETER_ONLY);
172   }
173
174   private ProjectAndSnapshot getProjectThenSnapshot(DbSession dbSession, @Nullable String projectUuid, @Nullable String projectKey,
175     @Nullable String branchKey, @Nullable String pullRequestId) {
176     ProjectDto projectDto;
177     BranchDto branchDto;
178
179     if (projectUuid != null) {
180       projectDto = componentFinder.getProjectByUuid(dbSession, projectUuid);
181       branchDto = componentFinder.getMainBranch(dbSession, projectDto);
182     } else {
183       projectDto = componentFinder.getProjectByKey(dbSession, projectKey);
184       branchDto = componentFinder.getBranchOrPullRequest(dbSession, projectDto, branchKey, pullRequestId);
185     }
186     Optional<SnapshotDto> snapshot = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchDto.getUuid());
187     return new ProjectAndSnapshot(projectDto, branchDto, snapshot.orElse(null));
188   }
189
190   private ProjectAndSnapshot getSnapshotThenProject(DbSession dbSession, String analysisUuid) {
191     SnapshotDto snapshotDto = getSnapshot(dbSession, analysisUuid);
192     BranchDto branchDto = dbClient.branchDao().selectByUuid(dbSession, snapshotDto.getComponentUuid())
193       .orElseThrow(() -> new IllegalStateException(String.format("Branch '%s' not found", snapshotDto.getUuid())));
194     ProjectDto projectDto = dbClient.projectDao().selectByUuid(dbSession, branchDto.getProjectUuid())
195       .orElseThrow(() -> new IllegalStateException(String.format("Project '%s' not found", branchDto.getProjectUuid())));
196
197     return new ProjectAndSnapshot(projectDto, branchDto, snapshotDto);
198   }
199
200   private SnapshotDto getSnapshot(DbSession dbSession, String analysisUuid) {
201     Optional<SnapshotDto> snapshotDto = dbClient.snapshotDao().selectByUuid(dbSession, analysisUuid);
202     return checkFoundWithOptional(snapshotDto, "Analysis with id '%s' is not found", analysisUuid);
203   }
204
205   private Optional<String> loadQualityGateDetails(DbSession dbSession, ProjectAndSnapshot projectAndSnapshot, boolean onAnalysis) {
206     if (onAnalysis) {
207       if (!projectAndSnapshot.snapshotDto.isPresent()) {
208         return Optional.empty();
209       }
210       // get the gate status as it was computed during the specified analysis
211       String analysisUuid = projectAndSnapshot.snapshotDto.get().getUuid();
212       return dbClient.measureDao().selectMeasure(dbSession, analysisUuid, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY)
213         .map(MeasureDto::getData);
214     }
215
216     // do not restrict to a specified analysis, use the live measure
217     Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, projectAndSnapshot.branch.getUuid(), CoreMetrics.QUALITY_GATE_DETAILS_KEY);
218     return measure.map(LiveMeasureDto::getDataAsString);
219   }
220
221   private void checkPermission(ProjectDto project) {
222     if (!userSession.hasProjectPermission(UserRole.ADMIN, project) &&
223       !userSession.hasProjectPermission(UserRole.USER, project) &&
224       !userSession.hasProjectPermission(UserRole.SCAN, project) &&
225       !userSession.hasPermission(GlobalPermission.SCAN)) {
226       throw insufficientPrivilegesException();
227     }
228   }
229
230   @Immutable
231   private static class ProjectAndSnapshot {
232     private final BranchDto branch;
233     private final Optional<SnapshotDto> snapshotDto;
234     private final ProjectDto project;
235
236     private ProjectAndSnapshot(ProjectDto project, BranchDto branch, @Nullable SnapshotDto snapshotDto) {
237       this.project = project;
238       this.branch = branch;
239       this.snapshotDto = Optional.ofNullable(snapshotDto);
240     }
241   }
242 }