From 36f0e6075ebfb3c58c5bc26cfb91460a061201d1 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 12 Nov 2021 10:39:19 +0100 Subject: [PATCH] SONAR-13426 accept tokens on api/project_badges/measure and quality_gate endpoints, including with force auth enabled --- .../UserSessionInitializer.java | 5 ++- .../UserSessionInitializerTest.java | 6 ++- .../sonar/server/badge/ws/MeasureAction.java | 2 +- .../server/badge/ws/ProjectBadgesSupport.java | 42 +++++++++++++++---- .../server/badge/ws/QualityGateAction.java | 3 +- .../server/badge/ws/MeasureActionTest.java | 18 ++------ .../badge/ws/QualityGateActionTest.java | 5 ++- .../java/org/sonar/server/ws/KeyExamples.java | 2 + 8 files changed, 51 insertions(+), 32 deletions(-) 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 6f8cc31b90f..205b669eb99 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 @@ -50,14 +50,15 @@ public class UserSessionInitializer { private static final String ACCESS_LOG_LOGIN = "LOGIN"; // SONAR-6546 these urls should be get from WebService - private static final Set SKIPPED_URLS = ImmutableSet.of( + private static final Set SKIPPED_URLS = Set.of( "/batch/index", "/batch/file", "/maintenance/*", "/setup/*", "/sessions/*", "/oauth2/callback/*", "/api/system/db_migration_status", "/api/system/status", "/api/system/migrate_db", "/api/server/version", "/api/users/identity_providers", "/api/l10n/index", - "/api/authentication/login", "/api/authentication/logout", "/api/authentication/validate"); + "/api/authentication/login", "/api/authentication/logout", "/api/authentication/validate", + "/api/project_badges/measure", "/api/project_badges/quality_gate"); private static final Set URL_USING_PASSCODE = ImmutableSet.of( "/api/ce/info", "/api/ce/pause", "/api/ce/resume", "/api/system/health", "/api/system/analytics", "/api/system/migrate_es"); diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java index 74a87664bdf..df26d723052 100644 --- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java +++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/UserSessionInitializerTest.java @@ -32,7 +32,6 @@ import org.sonar.api.utils.System2; import org.sonar.db.DbTester; import org.sonar.server.authentication.event.AuthenticationEvent; import org.sonar.server.authentication.event.AuthenticationEvent.Method; -import org.sonar.server.authentication.event.AuthenticationEvent.Provider; import org.sonar.server.authentication.event.AuthenticationEvent.Source; import org.sonar.server.authentication.event.AuthenticationException; import org.sonar.server.tester.AnonymousMockUserSession; @@ -42,7 +41,6 @@ import org.sonar.server.user.UserSession; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; @@ -96,6 +94,10 @@ public class UserSessionInitializerTest { assertPathIsIgnored("/api/users/identity_providers"); assertPathIsIgnored("/api/l10n/index"); + // 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"); + // exlude passcode urls assertPathIsIgnoredWithAnonymousAccess("/api/ce/info"); assertPathIsIgnoredWithAnonymousAccess("/api/ce/pause"); 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 b69dc764eb3..e85c633816a 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 @@ -141,6 +141,7 @@ public class MeasureAction implements ProjectBadgesWsAction { response.stream().setMediaType(SVG); String metricKey = request.mandatoryParam(PARAM_METRIC); try (DbSession dbSession = dbClient.openSession(false)) { + support.validateToken(request); 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); @@ -205,5 +206,4 @@ public class MeasureAction implements ProjectBadgesWsAction { return value; } - } 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 95dd958a4d7..981f8a7b947 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 @@ -19,31 +19,34 @@ */ package org.sonar.server.badge.ws; +import javax.annotation.Nullable; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.WebService; +import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.BranchDto; +import org.sonar.db.project.ProjectBadgeTokenDto; import org.sonar.db.project.ProjectDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.user.UserSession; -import static org.sonar.api.web.UserRole.USER; import static org.sonar.db.component.BranchType.BRANCH; import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.server.ws.KeyExamples.PROJECT_BADGE_TOKEN_EXAMPLE; public class ProjectBadgesSupport { private static final String PARAM_PROJECT = "project"; private static final String PARAM_BRANCH = "branch"; + private static final String PARAM_TOKEN = "token"; - private final UserSession userSession; private final ComponentFinder componentFinder; + private final DbClient dbClient; - public ProjectBadgesSupport(UserSession userSession, ComponentFinder componentFinder) { - this.userSession = userSession; + public ProjectBadgesSupport(ComponentFinder componentFinder, DbClient dbClient) { this.componentFinder = componentFinder; + this.dbClient = dbClient; } void addProjectAndBranchParams(WebService.NewAction action) { @@ -55,6 +58,10 @@ public class ProjectBadgesSupport { .createParam(PARAM_BRANCH) .setDescription("Branch key") .setExampleValue(KEY_BRANCH_EXAMPLE_001); + action + .createParam(PARAM_TOKEN) + .setDescription("Project badge token") + .setExampleValue(PROJECT_BADGE_TOKEN_EXAMPLE); } BranchDto getBranch(DbSession dbSession, Request request) { @@ -62,10 +69,6 @@ public class ProjectBadgesSupport { String projectKey = request.mandatoryParam(PARAM_PROJECT); String branchName = request.param(PARAM_BRANCH); ProjectDto project = componentFinder.getProjectOrApplicationByKey(dbSession, projectKey); - userSession.checkProjectPermission(USER, project); - if (project.isPrivate()) { - throw generateInvalidProjectException(); - } BranchDto branch; if (branchName == null) { @@ -87,4 +90,25 @@ public class ProjectBadgesSupport { private static ProjectBadgesException generateInvalidProjectException() { return new ProjectBadgesException("Project is invalid"); } + + public void validateToken(Request request) { + try (DbSession dbSession = dbClient.openSession(false)) { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + ProjectDto projectDto; + try { + projectDto = componentFinder.getProjectOrApplicationByKey(dbSession, projectKey); + } catch (NotFoundException e) { + throw new NotFoundException("Project has not been found"); + } + String token = request.param(PARAM_TOKEN); + if (projectDto.isPrivate() && !isTokenValid(dbSession, projectDto, token)) { + throw generateInvalidProjectException(); + } + } + } + + private boolean isTokenValid(DbSession dbSession, ProjectDto projectDto, @Nullable String token) { + ProjectBadgeTokenDto projectBadgeTokenDto = dbClient.projectBadgeTokenDao().selectTokenByProject(dbSession, projectDto); + return token != null && projectBadgeTokenDto != null && token.equals(projectBadgeTokenDto.getToken()); + } } 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 3e4f5951360..cf20fd55e66 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 @@ -43,7 +43,7 @@ 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 implements ProjectBadgesWsAction { private final DbClient dbClient; private final ProjectBadgesSupport support; @@ -71,6 +71,7 @@ public class QualityGateAction implements ProjectBadgesWsAction { response.setHeader("Cache-Control", "no-cache"); response.stream().setMediaType(SVG); 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); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java index 19bb7c6b2b0..e4e55bf1da3 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java @@ -84,7 +84,7 @@ public class MeasureActionTest { private WsActionTester ws = new WsActionTester( new MeasureAction( db.getDbClient(), - new ProjectBadgesSupport(userSession, new ComponentFinder(db.getDbClient(), null)), + new ProjectBadgesSupport(new ComponentFinder(db.getDbClient(), null), db.getDbClient()), new SvgGenerator(mapSettings.asConfig()))); @Test @@ -346,19 +346,6 @@ public class MeasureActionTest { checkError(response, "Measure has not been found"); } - @Test - public void return_error_if_unauthorized() throws ParseException { - ComponentDto project = db.components().insertPublicProject(); - MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); - - TestResponse response = ws.newRequest() - .setParam("project", project.getKey()) - .setParam("metric", metric.getKey()) - .execute(); - - checkError(response, "Insufficient privileges"); - } - @Test public void fail_on_invalid_quality_gate() { ComponentDto project = db.components().insertPublicProject(); @@ -419,7 +406,8 @@ public class MeasureActionTest { .containsExactlyInAnyOrder( tuple("project", true), tuple("branch", false), - tuple("metric", true)); + tuple("metric", true), + tuple("token", false)); } private void checkSvg(TestResponse response, String expectedLabel, String expectedValue, Color expectedColorValue) { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java index b5b66edd5dc..f139f890bf0 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java @@ -64,7 +64,7 @@ public class QualityGateActionTest { private WsActionTester ws = new WsActionTester( new QualityGateAction(db.getDbClient(), - new ProjectBadgesSupport(userSession, new ComponentFinder(db.getDbClient(), null)), + new ProjectBadgesSupport(new ComponentFinder(db.getDbClient(), null), db.getDbClient()), new SvgGenerator(mapSettings.asConfig()))); @Test @@ -290,7 +290,8 @@ public class QualityGateActionTest { .extracting(Param::key, Param::isRequired) .containsExactlyInAnyOrder( tuple("project", true), - tuple("branch", false)); + tuple("branch", false), + tuple("token", false)); } private MetricDto createQualityGateMetric() { diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java index 498f6e251d8..34eeb435a63 100644 --- a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java +++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java @@ -35,6 +35,8 @@ public class KeyExamples { public static final String NAME_WEBHOOK_EXAMPLE_001 = "My Webhook"; public static final String URL_WEBHOOK_EXAMPLE_001 = "https://www.my-webhook-listener.com/sonar"; + public static final String PROJECT_BADGE_TOKEN_EXAMPLE = "8bb493196edb5896ccb64582499895f187a2ae8f"; + private KeyExamples() { // prevent instantiation } -- 2.39.5