]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23008 Add AI Code Assurance badge endpoint (#11794)
authorAnita Stanisz <106669481+anita-stanisz-sonarsource@users.noreply.github.com>
Mon, 23 Sep 2024 07:39:40 +0000 (09:39 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 25 Sep 2024 20:02:53 +0000 (20:02 +0000)
server/sonar-webserver-auth/src/it/java/org/sonar/server/authentication/UserSessionInitializerIT.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/UserSessionInitializer.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/AbstractProjectBadgesWsAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/MeasureAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesSupport.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java

index b964228838bfda3f9c142a44935a6854fdc05041..af63a2f03ac5093c4932a74ebcc7ce907f2800da 100644 (file)
@@ -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");
index 3e8a530f530dfbdec4e3a25f733ae41bd3557de3..869be858595a8c2bcb6ca210b2e25cbb5018d901 100644 (file)
@@ -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<String> 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 (file)
index 0000000..8832894
--- /dev/null
@@ -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<String> 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);
+}
index baecd8df161b1cc55206a977062964b75ac88293..bf604a531db375b8d0710a0f070c0af28e386c6f 100644 (file)
@@ -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.<br/>" +
         "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<String> 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);
     }
   }
 
index cec59de51d679a6d8e20a63d75ff3547e92625c4..a6f23e7dc913adef83a3ef740166615309534044 100644 (file)
@@ -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");
   }
index bbdba2441daabee9b6e5c26b5d71ec117ddda565..ea6457604ef6950030ffa47ae8c37f32b994a865 100644 (file)
@@ -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
 }
index ed71caff49d3be95028bbcf9795333bd9a880813..a4d035638528d0e1a13ca6da3bbfb1625fdd3d48 100644 (file)
 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<String> 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);
     }
   }
 
index 47d241556b1776e8b1d9f3eacf305b5a90fb2038..b5ed8ab09d32621f15bb78d114e67d8be4ed2bb3 100644 (file)
@@ -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 {