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 {
add(
UserTokensWs.class,
GenerateAction.class,
+ RevokeAction.class,
UserTokenAuthenticator.class,
TokenGeneratorImpl.class);
}
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;
--- /dev/null
+/*
+ * 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. <br/>"+
+ "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));
+ }
+}
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
--- /dev/null
+/*
+ * 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();
+ }
+}
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
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();
+ }
}
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);
}
import org.apache.ibatis.annotations.Param;
public interface UserTokenMapper {
-
void insert(UserTokenDto userToken);
UserTokenDto selectByTokenHash(String tokenHash);
List<UserTokenDto> selectByLogin(String login);
void deleteByLogin(String login);
+
+ void deleteByLoginAndName(@Param("login") String login, @Param("name") String name);
}
<delete id="deleteByLogin">
DELETE FROM user_tokens WHERE login=#{login}
</delete>
+
+ <delete id="deleteByLoginAndName">
+ DELETE FROM user_tokens WHERE login=#{login} and name=#{name}
+ </delete>
+
</mapper>
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();
--- /dev/null
+/*
+ * 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;
+ }
+}
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 = "/";
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;
}
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";