From 93e81b2c3c3a4ebc644833cf4b364af800e808de Mon Sep 17 00:00:00 2001 From: Teryk Bellahsene Date: Wed, 25 Nov 2015 09:41:19 +0100 Subject: [PATCH] SONAR-7042 WS user_tokens/revoke revoke an access token --- .../server/usertoken/UserTokenModule.java | 2 + .../server/usertoken/ws/GenerateAction.java | 2 +- .../server/usertoken/ws/RevokeAction.java | 88 +++++++++++++ .../usertoken/ws/GenerateActionTest.java | 2 +- .../server/usertoken/ws/RevokeActionTest.java | 123 ++++++++++++++++++ .../server/usertoken/ws/UserTokensWsTest.java | 14 +- .../java/org/sonar/db/user/UserTokenDao.java | 4 + .../org/sonar/db/user/UserTokenMapper.java | 3 +- .../org/sonar/db/user/UserTokenMapper.xml | 5 + .../org/sonar/db/user/UserTokenDaoTest.java | 14 ++ .../ws/client/usertoken/RevokeWsRequest.java | 44 +++++++ .../client/usertoken/UserTokensWsClient.java | 10 +- .../usertoken/UserTokensWsParameters.java | 1 + 13 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/RevokeAction.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/RevokeActionTest.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/RevokeWsRequest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenModule.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenModule.java index 2fd1753fe2c..5583e636ddd 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenModule.java @@ -22,6 +22,7 @@ package org.sonar.server.usertoken; import org.sonar.core.platform.Module; import org.sonar.server.usertoken.ws.GenerateAction; +import org.sonar.server.usertoken.ws.RevokeAction; import org.sonar.server.usertoken.ws.UserTokensWs; public class UserTokenModule extends Module { @@ -30,6 +31,7 @@ public class UserTokenModule extends Module { add( UserTokensWs.class, GenerateAction.class, + RevokeAction.class, UserTokenAuthenticator.class, TokenGeneratorImpl.class); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java index 7ed59918eda..300f729faa2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/GenerateAction.java @@ -50,7 +50,7 @@ public class GenerateAction implements UserTokensWsAction { private final System2 system; private final TokenGenerator tokenGenerator; - public GenerateAction(UserSession userSession, DbClient dbClient, System2 system, TokenGenerator tokenGenerator) { + public GenerateAction(DbClient dbClient, UserSession userSession, System2 system, TokenGenerator tokenGenerator) { this.userSession = userSession; this.dbClient = dbClient; this.system = system; diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/RevokeAction.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/RevokeAction.java new file mode 100644 index 00000000000..32dece255cb --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usertoken/ws/RevokeAction.java @@ -0,0 +1,88 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.usertoken.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.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.client.usertoken.RevokeWsRequest; + +import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.ACTION_REVOKE; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_LOGIN; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_NAME; + +public class RevokeAction implements UserTokensWsAction { + private final DbClient dbClient; + private final UserSession userSession; + + public RevokeAction(DbClient dbClient, UserSession userSession) { + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION_REVOKE) + .setDescription("Revoke a user access token.
"+ + "It requires administration permissions.") + .setSince("5.3") + .setPost(true) + .setHandler(this); + + action.createParam(PARAM_LOGIN) + .setRequired(true) + .setDescription("User login") + .setExampleValue("g.hopper"); + + action.createParam(PARAM_NAME) + .setRequired(true) + .setDescription("Token name") + .setExampleValue("Project scan on Travis"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + doHandle(toRevokeWsRequest(request)); + response.noContent(); + } + + private void doHandle(RevokeWsRequest request) { + userSession.checkLoggedIn().checkGlobalPermission(SYSTEM_ADMIN); + + DbSession dbSession = dbClient.openSession(false); + try { + dbClient.userTokenDao().deleteByLoginAndName(dbSession, request.getLogin(), request.getName()); + dbSession.commit(); + } finally { + dbClient.closeSession(dbSession); + } + } + + private static RevokeWsRequest toRevokeWsRequest(Request request) { + return new RevokeWsRequest() + .setLogin(request.mandatoryParam(PARAM_LOGIN)) + .setName(request.mandatoryParam(PARAM_NAME)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java index a4190e479ef..e0b0e11ca9b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java @@ -79,7 +79,7 @@ public class GenerateActionTest { userDb.insertUser(newUserDto().setLogin(ADA_LOVELACE)); ws = new WsActionTester( - new GenerateAction(userSession, db.getDbClient(), System2.INSTANCE, tokenGenerator)); + new GenerateAction(db.getDbClient(), userSession, System2.INSTANCE, tokenGenerator)); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/RevokeActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/RevokeActionTest.java new file mode 100644 index 00000000000..e9e0995338c --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/RevokeActionTest.java @@ -0,0 +1,123 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.usertoken.ws; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.user.UserTokenDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; +import org.sonar.test.DbTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.user.UserTokenTesting.newUserToken; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_LOGIN; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_NAME; + +@Category(DbTests.class) +public class RevokeActionTest { + static final String GRACE_HOPPER = "grace.hopper"; + static final String ADA_LOVELACE = "ada.lovelace"; + static final String TOKEN_NAME = "token-name"; + + + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + DbClient dbClient = db.getDbClient(); + DbSession dbSession = db.getSession(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + WsActionTester ws; + + @Before + public void setUp() { + userSession + .login() + .setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + + ws = new WsActionTester( + new RevokeAction(dbClient, userSession)); + } + + @Test + public void delete_token_in_db() { + insertUserToken(newUserToken().setLogin(GRACE_HOPPER).setName("token-to-delete")); + insertUserToken(newUserToken().setLogin(GRACE_HOPPER).setName("token-to-keep-1")); + insertUserToken(newUserToken().setLogin(GRACE_HOPPER).setName("token-to-keep-2")); + insertUserToken(newUserToken().setLogin(ADA_LOVELACE).setName("token-to-delete")); + + String response = newRequest(GRACE_HOPPER, "token-to-delete"); + + assertThat(response).isEmpty(); + assertThat(dbClient.userTokenDao().selectByLogin(dbSession, GRACE_HOPPER)).extracting("name").containsOnly("token-to-keep-1", "token-to-keep-2"); + assertThat(dbClient.userTokenDao().selectByLogin(dbSession, ADA_LOVELACE)).extracting("name").containsOnly("token-to-delete"); + } + + @Test + public void does_not_fail_when_incorrect_login_or_name() { + insertUserToken(newUserToken().setLogin(GRACE_HOPPER).setName(TOKEN_NAME)); + + newRequest(ADA_LOVELACE, "another-token-name"); + } + + @Test + public void fail_if_not_logged_in() { + userSession.anonymous(); + insertUserToken(newUserToken().setLogin(GRACE_HOPPER).setName(TOKEN_NAME)); + expectedException.expect(UnauthorizedException.class); + + newRequest(GRACE_HOPPER, TOKEN_NAME); + } + + @Test + public void fail_if_insufficient_privileges() { + userSession.login().setGlobalPermissions(GlobalPermissions.SCAN_EXECUTION); + insertUserToken(newUserToken().setLogin(GRACE_HOPPER).setName(TOKEN_NAME)); + expectedException.expect(ForbiddenException.class); + + newRequest(GRACE_HOPPER, TOKEN_NAME); + } + + private String newRequest(String login, String name) { + return ws.newRequest() + .setParam(PARAM_LOGIN, login) + .setParam(PARAM_NAME, name) + .execute().getInput(); + } + + private void insertUserToken(UserTokenDto userToken) { + dbClient.userTokenDao().insert(dbSession, userToken); + dbSession.commit(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/UserTokensWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/UserTokensWsTest.java index 4f3ad84603e..b4b3b561762 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/UserTokensWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/UserTokensWsTest.java @@ -45,7 +45,8 @@ public class UserTokensWsTest { TokenGenerator tokenGenerator = mock(TokenGenerator.class); ws = new WsTester(new UserTokensWs( - new GenerateAction(userSession, dbClient, system, tokenGenerator))); + new GenerateAction(dbClient, userSession, system, tokenGenerator), + new RevokeAction(dbClient, userSession))); } @Test @@ -59,4 +60,15 @@ public class UserTokensWsTest { assertThat(action.param("login").isRequired()).isTrue(); assertThat(action.param("name").isRequired()).isTrue(); } + + @Test + public void revoke_action() { + WebService.Action action = ws.action(CONTROLLER_KEY, "revoke"); + + assertThat(action).isNotNull(); + assertThat(action.since()).isEqualTo("5.3"); + assertThat(action.isPost()).isTrue(); + assertThat(action.param("login").isRequired()).isTrue(); + assertThat(action.param("name").isRequired()).isTrue(); + } } diff --git a/sonar-db/src/main/java/org/sonar/db/user/UserTokenDao.java b/sonar-db/src/main/java/org/sonar/db/user/UserTokenDao.java index 27cfbcea2cd..d9aa3221ba9 100644 --- a/sonar-db/src/main/java/org/sonar/db/user/UserTokenDao.java +++ b/sonar-db/src/main/java/org/sonar/db/user/UserTokenDao.java @@ -58,6 +58,10 @@ public class UserTokenDao implements Dao { mapper(dbSession).deleteByLogin(login); } + public void deleteByLoginAndName(DbSession dbSession, String login, String name) { + mapper(dbSession).deleteByLoginAndName(login, name); + } + private static UserTokenMapper mapper(DbSession dbSession) { return dbSession.getMapper(UserTokenMapper.class); } diff --git a/sonar-db/src/main/java/org/sonar/db/user/UserTokenMapper.java b/sonar-db/src/main/java/org/sonar/db/user/UserTokenMapper.java index 42e3d95dd17..fd72e0c2dc8 100644 --- a/sonar-db/src/main/java/org/sonar/db/user/UserTokenMapper.java +++ b/sonar-db/src/main/java/org/sonar/db/user/UserTokenMapper.java @@ -24,7 +24,6 @@ import java.util.List; import org.apache.ibatis.annotations.Param; public interface UserTokenMapper { - void insert(UserTokenDto userToken); UserTokenDto selectByTokenHash(String tokenHash); @@ -34,4 +33,6 @@ public interface UserTokenMapper { List selectByLogin(String login); void deleteByLogin(String login); + + void deleteByLoginAndName(@Param("login") String login, @Param("name") String name); } diff --git a/sonar-db/src/main/resources/org/sonar/db/user/UserTokenMapper.xml b/sonar-db/src/main/resources/org/sonar/db/user/UserTokenMapper.xml index b1bcf5d0709..1b74df52c2a 100644 --- a/sonar-db/src/main/resources/org/sonar/db/user/UserTokenMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/user/UserTokenMapper.xml @@ -39,4 +39,9 @@ DELETE FROM user_tokens WHERE login=#{login} + + + DELETE FROM user_tokens WHERE login=#{login} and name=#{name} + + diff --git a/sonar-db/src/test/java/org/sonar/db/user/UserTokenDaoTest.java b/sonar-db/src/test/java/org/sonar/db/user/UserTokenDaoTest.java index be50fa659fa..a636eb8871d 100644 --- a/sonar-db/src/test/java/org/sonar/db/user/UserTokenDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/user/UserTokenDaoTest.java @@ -116,6 +116,20 @@ public class UserTokenDaoTest { assertThat(underTest.selectByLogin(dbSession, "login-to-keep")).hasSize(1); } + @Test + public void delete_token_by_login_and_name() { + insertToken(newUserToken().setLogin("login").setName("name")); + insertToken(newUserToken().setLogin("login").setName("another-name")); + insertToken(newUserToken().setLogin("another-login").setName("name")); + + underTest.deleteByLoginAndName(dbSession, "login", "name"); + db.commit(); + + Assertions.assertThat(underTest.selectByLoginAndName(dbSession, "login", "name")).isAbsent(); + Assertions.assertThat(underTest.selectByLoginAndName(dbSession, "login", "another-name")).isPresent(); + Assertions.assertThat(underTest.selectByLoginAndName(dbSession, "another-login", "name")).isPresent(); + } + private void insertToken(UserTokenDto userToken) { underTest.insert(dbSession, userToken); dbSession.commit(); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/RevokeWsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/RevokeWsRequest.java new file mode 100644 index 00000000000..8b2e8fdf204 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/RevokeWsRequest.java @@ -0,0 +1,44 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.sonarqube.ws.client.usertoken; + +public class RevokeWsRequest { + private String login; + private String name; + + public String getLogin() { + return login; + } + + public RevokeWsRequest setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public RevokeWsRequest setName(String name) { + this.name = name; + return this; + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsClient.java index 21e076f3594..3c956477f4b 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsClient.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsClient.java @@ -25,9 +25,10 @@ import org.sonarqube.ws.client.WsClient; import static org.sonarqube.ws.client.WsRequest.newPostRequest; import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.ACTION_GENERATE; -import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.USER_TOKENS_ENDPOINT; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.ACTION_REVOKE; import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_LOGIN; import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_NAME; +import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.USER_TOKENS_ENDPOINT; public class UserTokensWsClient { private static final String SLASH = "/"; @@ -45,6 +46,13 @@ public class UserTokensWsClient { GenerateWsResponse.parser()); } + public void revoke(RevokeWsRequest request) { + wsClient.execute( + newPostRequest(action(ACTION_REVOKE)) + .setParam(PARAM_LOGIN, request.getLogin()) + .setParam(PARAM_NAME, request.getName())); + } + private static String action(String action) { return USER_TOKENS_ENDPOINT + SLASH + action; } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsParameters.java index 856fd6cdffd..37ddfa0099b 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/usertoken/UserTokensWsParameters.java @@ -23,6 +23,7 @@ package org.sonarqube.ws.client.usertoken; public class UserTokensWsParameters { public static final String USER_TOKENS_ENDPOINT = "api/user_tokens"; public static final String ACTION_GENERATE = "generate"; + public static final String ACTION_REVOKE = "revoke"; public static final String PARAM_LOGIN = "login"; public static final String PARAM_NAME = "name"; -- 2.39.5