From 7a57a3ad0f517f598a5bc4d38620ec3a517425e8 Mon Sep 17 00:00:00 2001 From: Anita Stanisz <106669481+anita-stanisz-sonarsource@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:39:40 +0200 Subject: [PATCH] SONAR-23008 Add AI Code Assurance badge endpoint (#11794) --- .../UserSessionInitializerIT.java | 1 + .../UserSessionInitializer.java | 2 +- .../ws/AbstractProjectBadgesWsAction.java | 71 +++++++++++++++++++ .../sonar/server/badge/ws/MeasureAction.java | 44 ++---------- .../server/badge/ws/ProjectBadgesSupport.java | 20 ++++-- .../badge/ws/ProjectBadgesWsAction.java | 2 +- .../server/badge/ws/QualityGateAction.java | 39 ++-------- .../sonar/server/badge/ws/SvgGenerator.java | 14 ++-- 8 files changed, 108 insertions(+), 85 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/AbstractProjectBadgesWsAction.java diff --git a/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/UserSessionInitializerIT.java b/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/UserSessionInitializerIT.java index b964228838b..af63a2f03ac 100644 --- a/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/UserSessionInitializerIT.java +++ b/server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/UserSessionInitializerIT.java @@ -104,6 +104,7 @@ public class UserSessionInitializerIT { // exclude project_badge url, as they can be auth. by a token as queryparam assertPathIsIgnored("/api/project_badges/measure"); assertPathIsIgnored("/api/project_badges/quality_gate"); + assertPathIsIgnored("/api/project_badges/ai_code_assurance"); // exlude passcode urls assertPathIsIgnoredWithAnonymousAccess("/api/ce/info"); diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java index 3e8a530f530..869be858595 100644 --- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java +++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java @@ -66,7 +66,7 @@ public class UserSessionInitializer { "/api/server/version", "/api/users/identity_providers", "/api/l10n/index", "/api/authentication/login", "/api/authentication/logout", "/api/authentication/validate", - "/api/project_badges/measure", "/api/project_badges/quality_gate", + "/api/project_badges/measure", "/api/project_badges/quality_gate", "/api/project_badges/ai_code_assurance", "/api/settings/login_message"); private static final Set URL_USING_PASSCODE = Set.of( diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/AbstractProjectBadgesWsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/AbstractProjectBadgesWsAction.java new file mode 100644 index 00000000000..88328940e49 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/AbstractProjectBadgesWsAction.java @@ -0,0 +1,71 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.badge.ws; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Optional; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.io.IOUtils.write; +import static org.sonar.server.badge.ws.ETagUtils.RFC1123_DATE; +import static org.sonar.server.badge.ws.ETagUtils.getETag; +import static org.sonarqube.ws.MediaTypes.SVG; + +public abstract class AbstractProjectBadgesWsAction implements ProjectBadgesWsAction { + protected final ProjectBadgesSupport support; + protected final SvgGenerator svgGenerator; + + protected AbstractProjectBadgesWsAction(ProjectBadgesSupport support, SvgGenerator svgGenerator) { + this.support = support; + this.svgGenerator = svgGenerator; + } + + @Override + public void handle(Request request, Response response) throws IOException { + response.setHeader("Cache-Control", "no-cache"); + response.stream().setMediaType(SVG); + try { + support.validateToken(request); + String result = getBadge(request); + String eTag = getETag(result); + Optional requestedETag = request.header("If-None-Match"); + if (requestedETag.filter(eTag::equals).isPresent()) { + response.stream().setStatus(304); + return; + } + response.setHeader("ETag", eTag); + write(result, response.stream().output(), UTF_8); + } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) { + // There is an issue, so do not return any ETag but make this response expire now + SimpleDateFormat sdf = new SimpleDateFormat(RFC1123_DATE, Locale.US); + response.setHeader("Expires", sdf.format(new Date())); + write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8); + } + } + + protected abstract String getBadge(Request request); +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/MeasureAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/MeasureAction.java index baecd8df161..bf604a531db 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/MeasureAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/MeasureAction.java @@ -21,16 +21,11 @@ package org.sonar.server.badge.ws; import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.EnumMap; -import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.NewAction; import org.sonar.db.DbClient; @@ -39,14 +34,10 @@ import org.sonar.db.component.BranchDto; import org.sonar.db.measure.LiveMeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.server.badge.ws.SvgGenerator.Color; -import org.sonar.server.exceptions.ForbiddenException; -import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.measure.Rating; import static com.google.common.base.Preconditions.checkState; import static java.lang.String.format; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.io.IOUtils.write; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; import static org.sonar.api.measures.CoreMetrics.BUGS_KEY; import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY; @@ -63,8 +54,6 @@ import static org.sonar.api.measures.Metric.Level; import static org.sonar.api.measures.Metric.Level.ERROR; import static org.sonar.api.measures.Metric.Level.OK; import static org.sonar.api.measures.Metric.ValueType; -import static org.sonar.server.badge.ws.ETagUtils.RFC1123_DATE; -import static org.sonar.server.badge.ws.ETagUtils.getETag; import static org.sonar.server.badge.ws.SvgFormatter.formatDuration; import static org.sonar.server.badge.ws.SvgFormatter.formatNumeric; import static org.sonar.server.badge.ws.SvgFormatter.formatPercent; @@ -74,9 +63,8 @@ import static org.sonar.server.measure.Rating.C; import static org.sonar.server.measure.Rating.D; import static org.sonar.server.measure.Rating.E; import static org.sonar.server.measure.Rating.valueOf; -import static org.sonarqube.ws.MediaTypes.SVG; -public class MeasureAction implements ProjectBadgesWsAction { +public class MeasureAction extends AbstractProjectBadgesWsAction { private static final String PARAM_METRIC = "metric"; @@ -113,13 +101,10 @@ public class MeasureAction implements ProjectBadgesWsAction { E, Color.RATING_E)); private final DbClient dbClient; - private final ProjectBadgesSupport support; - private final SvgGenerator svgGenerator; public MeasureAction(DbClient dbClient, ProjectBadgesSupport support, SvgGenerator svgGenerator) { + super(support, svgGenerator); this.dbClient = dbClient; - this.support = support; - this.svgGenerator = svgGenerator; } @Override @@ -129,7 +114,8 @@ public class MeasureAction implements ProjectBadgesWsAction { .setDescription("Generate badge for project's measure as an SVG.
" + "Requires 'Browse' permission on the specified project.") .setSince("7.1") - .setChangelog(new Change("10.4", String.format("The following metric keys are now deprecated: %s", String.join(", ", DEPRECATED_METRIC_KEYS)))) + .setChangelog(new Change("10.4", String.format("The following metric keys are now deprecated: %s", String.join(", ", + DEPRECATED_METRIC_KEYS)))) .setResponseExample(Resources.getResource(getClass(), "measure-example.svg")); support.addProjectAndBranchParams(action); action.createParam(PARAM_METRIC) @@ -139,30 +125,14 @@ public class MeasureAction implements ProjectBadgesWsAction { } @Override - public void handle(Request request, Response response) throws Exception { - response.setHeader("Cache-Control", "no-cache"); - response.stream().setMediaType(SVG); - String metricKey = request.mandatoryParam(PARAM_METRIC); + protected String getBadge(Request request) { try (DbSession dbSession = dbClient.openSession(false)) { - support.validateToken(request); + String metricKey = request.mandatoryParam(PARAM_METRIC); BranchDto branch = support.getBranch(dbSession, request); MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey); checkState(metric != null && metric.isEnabled(), "Metric '%s' hasn't been found", metricKey); LiveMeasureDto measure = getMeasure(dbSession, branch, metricKey); - String result = generateSvg(metric, measure); - String eTag = getETag(result); - Optional requestedETag = request.header("If-None-Match"); - if (requestedETag.filter(eTag::equals).isPresent()) { - response.stream().setStatus(304); - return; - } - response.setHeader("ETag", eTag); - write(result, response.stream().output(), UTF_8); - } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) { - // There is an issue, so do not return any ETag but make this response expire now - SimpleDateFormat sdf = new SimpleDateFormat(RFC1123_DATE, Locale.US); - response.setHeader("Expires", sdf.format(new Date())); - write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8); + return generateSvg(metric, measure); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesSupport.java index cec59de51d6..a6f23e7dc91 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesSupport.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesSupport.java @@ -56,14 +56,18 @@ public class ProjectBadgesSupport { } void addProjectAndBranchParams(WebService.NewAction action) { - action.createParam(PARAM_PROJECT) - .setDescription("Project or application key") - .setRequired(true) - .setExampleValue(KEY_PROJECT_EXAMPLE_001); + addProjectAndTokenParams(action); action .createParam(PARAM_BRANCH) .setDescription("Branch key") .setExampleValue(KEY_BRANCH_EXAMPLE_001); + } + + public void addProjectAndTokenParams(WebService.NewAction action) { + action.createParam(PARAM_PROJECT) + .setDescription("Project or application key") + .setRequired(true) + .setExampleValue(KEY_PROJECT_EXAMPLE_001); action .createParam(PARAM_TOKEN) .setDescription("Project badge token") @@ -72,9 +76,8 @@ public class ProjectBadgesSupport { BranchDto getBranch(DbSession dbSession, Request request) { try { - String projectKey = request.mandatoryParam(PARAM_PROJECT); String branchName = request.param(PARAM_BRANCH); - ProjectDto project = componentFinder.getProjectOrApplicationByKey(dbSession, projectKey); + ProjectDto project = getProject(dbSession, request); BranchDto branch = componentFinder.getBranchOrPullRequest(dbSession, project, branchName, null); @@ -88,6 +91,11 @@ public class ProjectBadgesSupport { } } + public ProjectDto getProject(DbSession dbSession, Request request) { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + return componentFinder.getProjectOrApplicationByKey(dbSession, projectKey); + } + private static ProjectBadgesException generateInvalidProjectException() { return new ProjectBadgesException("Project is invalid"); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsAction.java index bbdba2441da..ea6457604ef 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsAction.java @@ -21,6 +21,6 @@ package org.sonar.server.badge.ws; import org.sonar.server.ws.WsAction; -interface ProjectBadgesWsAction extends WsAction { +public interface ProjectBadgesWsAction extends WsAction { // Marker interface } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java index ed71caff49d..a4d03563852 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java @@ -20,39 +20,24 @@ package org.sonar.server.badge.ws; import com.google.common.io.Resources; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Optional; import org.sonar.api.measures.Metric.Level; import org.sonar.api.server.ws.Request; -import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.NewAction; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; import org.sonar.db.measure.LiveMeasureDto; -import org.sonar.server.exceptions.ForbiddenException; -import org.sonar.server.exceptions.NotFoundException; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.io.IOUtils.write; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; -import static org.sonar.server.badge.ws.ETagUtils.RFC1123_DATE; -import static org.sonar.server.badge.ws.ETagUtils.getETag; -import static org.sonarqube.ws.MediaTypes.SVG; -public class QualityGateAction implements ProjectBadgesWsAction { +public class QualityGateAction extends AbstractProjectBadgesWsAction { private final DbClient dbClient; - private final ProjectBadgesSupport support; - private final SvgGenerator svgGenerator; public QualityGateAction(DbClient dbClient, ProjectBadgesSupport support, SvgGenerator svgGenerator) { + super(support, svgGenerator); this.dbClient = dbClient; - this.support = support; - this.svgGenerator = svgGenerator; } @Override @@ -67,27 +52,11 @@ public class QualityGateAction implements ProjectBadgesWsAction { } @Override - public void handle(Request request, Response response) throws Exception { - response.setHeader("Cache-Control", "no-cache"); - response.stream().setMediaType(SVG); + protected String getBadge(Request request) { try (DbSession dbSession = dbClient.openSession(false)) { - support.validateToken(request); BranchDto branch = support.getBranch(dbSession, request); Level qualityGateStatus = getQualityGate(dbSession, branch); - String result = svgGenerator.generateQualityGate(qualityGateStatus); - String eTag = getETag(result); - Optional requestedETag = request.header("If-None-Match"); - if (requestedETag.filter(eTag::equals).isPresent()) { - response.stream().setStatus(304); - return; - } - response.setHeader("ETag", eTag); - write(result, response.stream().output(), UTF_8); - } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) { - // There is an issue, so do not return any ETag but make this response expire now - SimpleDateFormat sdf = new SimpleDateFormat(RFC1123_DATE, Locale.US); - response.setHeader("Expires", sdf.format(new Date())); - write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8); + return svgGenerator.generateQualityGate(qualityGateStatus); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java index 47d241556b1..b5ed8ab09d3 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java @@ -166,6 +166,14 @@ public class SvgGenerator { return strSubstitutor.replace(errorTemplate); } + public static String readTemplate(String template, Class clazz) { + try { + return IOUtils.toString(clazz.getResource(template), UTF_8); + } catch (IOException e) { + throw new IllegalStateException(String.format("Can't read svg template '%s'", template), e); + } + } + private static int computeWidth(String text) { return text.chars() .mapToObj(i -> (char) i) @@ -178,11 +186,7 @@ public class SvgGenerator { } private String readTemplate(String template) { - try { - return IOUtils.toString(getClass().getResource(template), UTF_8); - } catch (IOException e) { - throw new IllegalStateException(String.format("Can't read svg template '%s'", template), e); - } + return readTemplate(template, getClass()); } static class Color { -- 2.39.5