From 814f9f9f0f46f64b8d0029f8815ddfaab9423639 Mon Sep 17 00:00:00 2001 From: Lukasz Jarocki Date: Mon, 15 Nov 2021 16:06:52 +0100 Subject: [PATCH] SONAR-13427 Added a new endpoint to renew the project badge token --- .../org/sonar/db/audit/AuditPersister.java | 2 + .../sonar/db/audit/NoOpAuditPersister.java | 5 + .../db/project/ProjectBadgeTokenDao.java | 10 +- .../db/project/ProjectBadgeTokenMapper.java | 2 + .../db/project/ProjectBadgeTokenMapper.xml | 9 ++ .../db/project/ProjectBadgeTokenDaoTest.java | 59 ++++++++- .../server/badge/ws/ProjectBadgesWs.java | 2 +- .../badge/ws/ProjectBadgesWsModule.java | 3 +- .../server/badge/ws/TokenRenewAction.java | 81 ++++++++++++ .../server/badge/ws/TokenRenewActionTest.java | 116 ++++++++++++++++++ 10 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/TokenRenewAction.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/TokenRenewActionTest.java diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/audit/AuditPersister.java b/server/sonar-db-dao/src/main/java/org/sonar/db/audit/AuditPersister.java index ac41ffe6e36..03657a8016f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/audit/AuditPersister.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/audit/AuditPersister.java @@ -74,6 +74,8 @@ public interface AuditPersister { void addProjectBadgeToken(DbSession dbSession, ProjectBadgeTokenNewValue newValue); + void updateProjectBadgeToken(DbSession session, ProjectBadgeTokenNewValue projectBadgeTokenNewValue); + void updateUserToken(DbSession dbSession, UserTokenNewValue newValue); void deleteUserToken(DbSession dbSession, UserTokenNewValue newValue); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/audit/NoOpAuditPersister.java b/server/sonar-db-dao/src/main/java/org/sonar/db/audit/NoOpAuditPersister.java index 1923b0cca13..30c16d31a95 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/audit/NoOpAuditPersister.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/audit/NoOpAuditPersister.java @@ -119,6 +119,11 @@ public class NoOpAuditPersister implements AuditPersister { // no op } + @Override + public void updateProjectBadgeToken(DbSession session, ProjectBadgeTokenNewValue projectBadgeTokenNewValue) { + // no op + } + @Override public void updateUserToken(DbSession dbSession, UserTokenNewValue newValue) { // no op diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenDao.java index 8c9758f1845..9565bf0a0a7 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenDao.java @@ -49,6 +49,15 @@ public class ProjectBadgeTokenDao implements Dao { return projectBadgeTokenDto; } + public void upsert(DbSession session, String token, ProjectDto projectDto, String userUuid, String userLogin) { + if(selectTokenByProject(session, projectDto) == null) { + insert(session, token, projectDto, userUuid, userLogin); + } else { + mapper(session).update(token, projectDto.getUuid(), system2.now()); + auditPersister.updateProjectBadgeToken(session, new ProjectBadgeTokenNewValue(projectDto.getKey(), userUuid, userLogin)); + } + } + private static ProjectBadgeTokenMapper mapper(DbSession session) { return session.getMapper(ProjectBadgeTokenMapper.class); } @@ -56,6 +65,5 @@ public class ProjectBadgeTokenDao implements Dao { @CheckForNull public ProjectBadgeTokenDto selectTokenByProject(DbSession session, ProjectDto projectDto) { return mapper(session).selectTokenByProjectUuid(projectDto.getUuid()); - } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenMapper.java index 3f8cda7408c..55dd6106267 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/project/ProjectBadgeTokenMapper.java @@ -26,6 +26,8 @@ public interface ProjectBadgeTokenMapper { void insert(ProjectBadgeTokenDto projectBadgeTokenDto); + int update(@Param("token") String token, @Param("projectUuid") String projectUuid, @Param("updatedAt") long updatedAt); + @CheckForNull ProjectBadgeTokenDto selectTokenByProjectUuid(@Param("projectUuid") String projectUuid); } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/project/ProjectBadgeTokenMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/project/ProjectBadgeTokenMapper.xml index 59967a17741..739fd28f5ab 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/project/ProjectBadgeTokenMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/project/ProjectBadgeTokenMapper.xml @@ -10,6 +10,15 @@ p.updated_at as updatedAt + + update project_badge_token + set + token = #{token, jdbcType=VARCHAR}, + updated_at = #{updatedAt, jdbcType=BIGINT} + where + project_uuid = #{projectUuid, jdbcType=VARCHAR} + + INSERT INTO project_badge_token ( uuid, diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/project/ProjectBadgeTokenDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/project/ProjectBadgeTokenDaoTest.java index 6f99c2cdb13..1e50c8c337b 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/project/ProjectBadgeTokenDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/project/ProjectBadgeTokenDaoTest.java @@ -51,17 +51,16 @@ public class ProjectBadgeTokenDaoTest { private final ProjectBadgeTokenDao projectBadgeTokenDao = new ProjectBadgeTokenDao(system2, auditPersister, uuidFactory); - @Test public void should_insert_and_select_by_project_uuid() { when(uuidFactory.create()).thenReturn("generated_uuid_1"); ProjectDto projectDto = new ProjectDto().setUuid("project_uuid_1"); ProjectBadgeTokenDto insertedProjectBadgeToken = projectBadgeTokenDao.insert(db.getSession(), "token", projectDto, "userUuid", "userLogin"); - assertProjectBadgeToken(insertedProjectBadgeToken); + assertProjectBadgeToken(insertedProjectBadgeToken, "token"); ProjectBadgeTokenDto selectedProjectBadgeToken = projectBadgeTokenDao.selectTokenByProject(db.getSession(), projectDto); - assertProjectBadgeToken(selectedProjectBadgeToken); + assertProjectBadgeToken(selectedProjectBadgeToken, "token"); } @Test @@ -70,7 +69,7 @@ public class ProjectBadgeTokenDaoTest { ProjectDto projectDto = new ProjectDto().setUuid("project_uuid_1"); ProjectBadgeTokenDto insertedProjectBadgeToken = projectBadgeTokenDao.insert(db.getSession(), "token", projectDto, "user-uuid", "user-login"); - assertProjectBadgeToken(insertedProjectBadgeToken); + assertProjectBadgeToken(insertedProjectBadgeToken, "token"); ArgumentCaptor captor = ArgumentCaptor.forClass(ProjectBadgeTokenNewValue.class); @@ -80,9 +79,57 @@ public class ProjectBadgeTokenDaoTest { Assertions.assertThat(captor.getValue()).hasToString("{\"userUuid\": \"user-uuid\", \"userLogin\": \"user-login\" }"); } - private void assertProjectBadgeToken(@Nullable ProjectBadgeTokenDto projectBadgeTokenDto) { + @Test + public void upsert_existing_token_and_select_by_project_uuid() { + when(uuidFactory.create()).thenReturn("generated_uuid_1"); + ProjectDto projectDto = new ProjectDto().setUuid("project_uuid_1"); + + // first insert + ProjectBadgeTokenDto insertedProjectBadgeToken = projectBadgeTokenDao.insert(db.getSession(), "token", projectDto, "user-uuid", "user-login"); + assertProjectBadgeToken(insertedProjectBadgeToken, "token"); + + // renew + projectBadgeTokenDao.upsert(db.getSession(), "new-token", projectDto, "user-uuid", "user-login"); + ProjectBadgeTokenDto selectedProjectBadgeToken = projectBadgeTokenDao.selectTokenByProject(db.getSession(), projectDto); + assertProjectBadgeToken(selectedProjectBadgeToken, "new-token"); + } + + @Test + public void upsert_non_existing_token_and_select_by_project_uuid() { + when(uuidFactory.create()).thenReturn("generated_uuid_1"); + ProjectDto projectDto = new ProjectDto().setUuid("project_uuid_1"); + + // renew + projectBadgeTokenDao.upsert(db.getSession(), "new-token", projectDto, "user-uuid", "user-login"); + ProjectBadgeTokenDto selectedProjectBadgeToken = projectBadgeTokenDao.selectTokenByProject(db.getSession(), projectDto); + assertProjectBadgeToken(selectedProjectBadgeToken, "new-token"); + } + + + @Test + public void token_upsert_is_log_in_audit() { + when(uuidFactory.create()).thenReturn("generated_uuid_1"); + ProjectDto projectDto = new ProjectDto().setUuid("project_uuid_1"); + + // fist insert + projectBadgeTokenDao.insert(db.getSession(), "token", projectDto, "user-uuid", "user-login"); + ArgumentCaptor captor = ArgumentCaptor.forClass(ProjectBadgeTokenNewValue.class); + verify(auditPersister).addProjectBadgeToken(eq(db.getSession()), captor.capture()); + + // upsert + projectBadgeTokenDao.upsert(db.getSession(), "new-token", projectDto, "user-uuid", "user-login"); + ProjectBadgeTokenDto selectedProjectBadgeToken = projectBadgeTokenDao.selectTokenByProject(db.getSession(), projectDto); + assertProjectBadgeToken(selectedProjectBadgeToken, "new-token"); + + verify(auditPersister).updateProjectBadgeToken(eq(db.getSession()), captor.capture()); + verifyNoMoreInteractions(auditPersister); + + Assertions.assertThat(captor.getValue()).hasToString("{\"userUuid\": \"user-uuid\", \"userLogin\": \"user-login\" }"); + } + + private void assertProjectBadgeToken(@Nullable ProjectBadgeTokenDto projectBadgeTokenDto, String expectedToken) { assertThat(projectBadgeTokenDto).isNotNull(); - assertThat(projectBadgeTokenDto.getToken()).isEqualTo("token"); + assertThat(projectBadgeTokenDto.getToken()).isEqualTo(expectedToken); assertThat(projectBadgeTokenDto.getProjectUuid()).isEqualTo("project_uuid_1"); assertThat(projectBadgeTokenDto.getUuid()).isEqualTo("generated_uuid_1"); assertThat(projectBadgeTokenDto.getCreatedAt()).isEqualTo(1000L); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWs.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWs.java index 6661522c4da..a8edb3d09e1 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWs.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWs.java @@ -32,7 +32,7 @@ public class ProjectBadgesWs implements WebService { @Override public void define(Context context) { - NewController controller = context.createController("api/project_badges"); + NewController controller = context.createController("api/project_badges"); controller.setDescription("Generate badges based on quality gates or measures"); controller.setSince("7.1"); actions.forEach(action -> action.define(controller)); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsModule.java index bb695a08db8..c9cf545d6f7 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsModule.java @@ -31,6 +31,7 @@ public class ProjectBadgesWsModule extends Module { MeasureAction.class, TokenAction.class, SvgGenerator.class, - ProjectBadgesSupport.class); + ProjectBadgesSupport.class, + TokenRenewAction.class); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/TokenRenewAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/TokenRenewAction.java new file mode 100644 index 00000000000..0b915a7a6d7 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/badge/ws/TokenRenewAction.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 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.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.user.UserSession; +import org.sonar.server.usertoken.TokenGenerator; + +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; + +public class TokenRenewAction implements ProjectBadgesWsAction { + + private static final String PROJECT_KEY_PARAM = "project"; + private final DbClient dbClient; + private final TokenGenerator tokenGenerator; + private final UserSession userSession; + + public TokenRenewAction(DbClient dbClient, TokenGenerator tokenGenerator, UserSession userSession) { + this.dbClient = dbClient; + this.tokenGenerator = tokenGenerator; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController controller) { + NewAction action = controller.createAction("renew_token") + .setHandler(this) + .setSince("9.2") + .setPost(true) + .setDescription("Creates new token replacing any existing token for project badge access for private projects.
" + + "This token can be used to authenticate with api/project_badges/quality_gate and api/project_badges/measure endpoints.
" + + "Requires 'Administer' permission on the specified project."); + action.createParam(PROJECT_KEY_PARAM) + .setDescription("Project key") + .setRequired(true) + .setExampleValue(KEY_PROJECT_EXAMPLE_001); + } + + @Override + public void handle(Request request, Response response) throws Exception { + doHandle(request); + response.noContent(); + } + + private void doHandle(Request request) { + try (DbSession dbSession = dbClient.openSession(false)) { + String projectKey = request.mandatoryParam(PROJECT_KEY_PARAM); + + ProjectDto projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey).orElseThrow(() -> new IllegalArgumentException("project not found")); + userSession.checkProjectPermission(UserRole.ADMIN, projectDto); + String newGeneratedToken = tokenGenerator.generate(); + dbClient.projectBadgeTokenDao().upsert(dbSession, newGeneratedToken, projectDto, userSession.getUuid(), userSession.getLogin()); + dbSession.commit(); + } + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/TokenRenewActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/TokenRenewActionTest.java new file mode 100644 index 00000000000..9fa6074acb5 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/badge/ws/TokenRenewActionTest.java @@ -0,0 +1,116 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.project.ProjectBadgeTokenDto; +import org.sonar.db.project.ProjectDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.usertoken.TokenGenerator; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TokenRenewActionTest { + + private final System2 system2 = mock(System2.class); + + @Rule + public DbTester db = DbTester.create(system2); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private final TokenGenerator tokenGenerator = Mockito.mock(TokenGenerator.class); + + private final WsActionTester ws = new WsActionTester( + new TokenRenewAction( + db.getDbClient(), + tokenGenerator, userSession)); + + @Before + public void before(){ + when(system2.now()).thenReturn(1000L); + } + + @Test + public void missing_project_parameter_should_fail() { + TestRequest request = ws.newRequest(); + Assertions.assertThatThrownBy(request::execute) + .hasMessage("The 'project' parameter is missing") + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void missing_project_admin_permission_should_fail() { + ComponentDto project = db.components().insertPrivateProject(); + + TestRequest request = ws.newRequest().setParam("project", project.getKey()); + + Assertions.assertThatThrownBy(request::execute) + .hasMessage("Insufficient privileges") + .isInstanceOf(ForbiddenException.class); + } + + @Test + public void should_add_token_when_no_token_yet_and_return_204() { + ProjectDto project = db.components().insertPrivateProjectDto(); + userSession.logIn().addProjectPermission(UserRole.ADMIN, project); + when(tokenGenerator.generate()).thenReturn("generated_token"); + + TestResponse response = ws.newRequest().setParam("project", project.getKey()).execute(); + + ProjectBadgeTokenDto projectBadgeTokenDto = db.getDbClient().projectBadgeTokenDao().selectTokenByProject(db.getSession(), project); + assertThat(projectBadgeTokenDto).isNotNull(); + assertThat(projectBadgeTokenDto.getToken()).isEqualTo("generated_token"); + response.assertNoContent(); + } + + @Test + public void should_replace_existing_token_when__token_already_present_and_update_update_at() { + ProjectDto project = db.components().insertPrivateProjectDto(); + userSession.logIn().addProjectPermission(UserRole.ADMIN, project); + when(tokenGenerator.generate()).thenReturn("generated_token"); + + ws.newRequest().setParam("project", project.getKey()).execute(); //inserting first token with updated at 1000 + + when(system2.now()).thenReturn(2000L); + ws.newRequest().setParam("project", project.getKey()).execute(); //replacing first token with updated at 2000 + + ProjectBadgeTokenDto projectBadgeTokenDto = db.getDbClient().projectBadgeTokenDao().selectTokenByProject(db.getSession(), project); + assertThat(projectBadgeTokenDto).isNotNull(); + assertThat(projectBadgeTokenDto.getToken()).isEqualTo("generated_token"); + assertThat(projectBadgeTokenDto.getUpdatedAt()).isEqualTo(2000L); + } + +} -- 2.39.5