diff options
9 files changed, 361 insertions, 19 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/root/ws/RootWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/root/ws/RootWsModule.java index 2b5edc86b7a..4e49416f721 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/root/ws/RootWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/root/ws/RootWsModule.java @@ -26,6 +26,7 @@ public class RootWsModule extends Module { protected void configureModule() { add(RootWs.class, SetRootWsAction.class, - UnsetRootWsAction.class); + UnsetRootWsAction.class, + SearchAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/root/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/root/ws/SearchAction.java new file mode 100644 index 00000000000..35b4c02cf65 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/root/ws/SearchAction.java @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.root.ws; + +import java.util.List; +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.user.UserQuery; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.user.UserDto; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.WsRoot; + +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class SearchAction implements RootWsAction { + private final UserSession userSession; + private final DbClient dbClient; + + public SearchAction(UserSession userSession, DbClient dbClient) { + this.userSession = userSession; + this.dbClient = dbClient; + } + + @Override + public void define(WebService.NewController controller) { + controller.createAction("search") + .setInternal(true) + .setPost(false) + .setDescription("Search for root user.<br/>" + + "Requires to be root.") + .setSince("6.2") + .setHandler(this); + } + + @Override + public void handle(Request request, Response response) throws Exception { + userSession.checkIsRoot(); + + try (DbSession dbSession = dbClient.openSession(false)) { + List<UserDto> userDtos = dbClient.userDao().selectUsers( + dbSession, + UserQuery.builder() + .mustBeRoot() + .build()); + + writeResponse(request, response, userDtos); + } + } + + private static void writeResponse(Request request, Response response, List<UserDto> dtos) { + WsRoot.SearchWsResponse.Builder responseBuilder = WsRoot.SearchWsResponse.newBuilder(); + WsRoot.Root.Builder rootBuilder = WsRoot.Root.newBuilder(); + dtos.forEach(dto -> responseBuilder.addRoots(toRoot(rootBuilder, dto))); + writeProtobuf(responseBuilder.build(), request, response); + } + + private static WsRoot.Root toRoot(WsRoot.Root.Builder builder, UserDto dto) { + builder.clear(); + if (dto.getLogin() != null) { + builder.setLogin(dto.getLogin()); + } + if (dto.getName() != null) { + builder.setName(dto.getName()); + } + if (dto.getEmail() != null) { + builder.setEmail(dto.getEmail()); + } + return builder.build(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/root/ws/RootWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/root/ws/RootWsModuleTest.java new file mode 100644 index 00000000000..23337775764 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/root/ws/RootWsModuleTest.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.root.ws; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RootWsModuleTest { + private static final int ADAPTERS_IN_EMPTY_CONTAINER = 2; + + private RootWsModule underTest = new RootWsModule(); + + @Test + public void verify_number_of_components_added_by_module() { + ComponentContainer container = new ComponentContainer(); + + underTest.configure(container); + + assertThat(container.getPicoContainer().getComponentAdapters()).hasSize(ADAPTERS_IN_EMPTY_CONTAINER + 4); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/root/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/root/ws/SearchActionTest.java new file mode 100644 index 00000000000..b3332b42f08 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/root/ws/SearchActionTest.java @@ -0,0 +1,146 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.root.ws; + +import java.io.IOException; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.System2; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.user.UserDao; +import org.sonar.db.user.UserDto; +import org.sonar.db.user.UserTesting; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.MediaTypes; +import org.sonarqube.ws.WsRoot; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SearchActionTest { + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private UserDao userDao = dbTester.getDbClient().userDao(); + private DbSession dbSession = dbTester.getSession(); + private SearchAction underTest = new SearchAction(userSessionRule, dbTester.getDbClient()); + private WsActionTester wsTester = new WsActionTester(underTest); + + @Test + public void verify_definition() { + WebService.Action action = wsTester.getDef(); + assertThat(action.key()).isEqualTo("search"); + assertThat(action.isInternal()).isTrue(); + assertThat(action.isPost()).isFalse(); + assertThat(action.since()).isEqualTo("6.2"); + assertThat(action.description()).isEqualTo("Search for root user.<br/>" + + "Requires to be root."); + assertThat(action.responseExample()).isNull(); + assertThat(action.deprecatedKey()).isNull(); + assertThat(action.deprecatedSince()).isNull(); + assertThat(action.handler()).isSameAs(underTest); + assertThat(action.params()).isEmpty(); + } + + @Test + public void execute_fails_with_ForbiddenException_when_user_is_not_logged_in() { + expectInsufficientPrivilegesForbiddenException(); + + executeRequest(); + } + + @Test + public void execute_fails_with_ForbiddenException_when_user_is_not_root() { + userSessionRule.login(); + + expectInsufficientPrivilegesForbiddenException(); + + executeRequest(); + } + + @Test + public void execute_returns_empty_list_of_root_when_DB_is_empty() { + makeAuthenticatedUserRoot(); + + assertThat(executeRequest()).isEmpty(); + } + + @Test + public void execute_does_not_fail_when_root_user_has_neither_email_nor_name() { + makeAuthenticatedUserRoot(); + UserDto rootDto = userDao.insert(dbSession, UserTesting.newUserDto().setName(null).setEmail(null)); + userDao.setRoot(dbSession, rootDto.getLogin(), true); + dbSession.commit(); + + List<WsRoot.Root> roots = executeRequest(); + assertThat(roots).hasSize(1); + WsRoot.Root root = roots.iterator().next(); + assertThat(root.getLogin()).isEqualTo(rootDto.getLogin()); + assertThat(root.hasName()).isFalse(); + assertThat(root.hasEmail()).isFalse(); + } + + @Test + public void execute_returns_root_users_sorted_by_name() { + makeAuthenticatedUserRoot(); + userDao.insert(dbSession, UserTesting.newUserDto().setName("ddd")); + UserDto root1 = userDao.insert(dbSession, UserTesting.newUserDto().setName("ccc")); + userDao.setRoot(dbSession, root1.getLogin(), true); + UserDto root2 = userDao.insert(dbSession, UserTesting.newUserDto().setName("bbb")); + userDao.setRoot(dbSession, root2.getLogin(), true); + userDao.insert(dbSession, UserTesting.newUserDto().setName("aaa")); + dbSession.commit(); + + assertThat(executeRequest()) + .extracting(WsRoot.Root::getName) + .containsExactly("bbb", "ccc"); + } + + private UserSessionRule makeAuthenticatedUserRoot() { + return userSessionRule.login().setRoot(); + } + + private List<WsRoot.Root> executeRequest() { + TestRequest request = wsTester.newRequest() + .setMediaType(MediaTypes.PROTOBUF); + try { + return WsRoot.SearchWsResponse.parseFrom(request.execute().getInputStream()).getRootsList(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private void expectInsufficientPrivilegesForbiddenException() { + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + } + +} diff --git a/sonar-db/src/main/java/org/sonar/db/user/UserDao.java b/sonar-db/src/main/java/org/sonar/db/user/UserDao.java index 5e1fe961e72..c4f1dc95e9e 100644 --- a/sonar-db/src/main/java/org/sonar/db/user/UserDao.java +++ b/sonar-db/src/main/java/org/sonar/db/user/UserDao.java @@ -51,11 +51,8 @@ public class UserDao implements Dao { } public UserDto selectUserById(long userId) { - DbSession session = mybatis.openSession(false); - try { + try (DbSession session = mybatis.openSession(false)) { return selectUserById(session, userId); - } finally { - MyBatis.closeQuietly(session); } } @@ -80,11 +77,8 @@ public class UserDao implements Dao { */ @CheckForNull public UserDto selectActiveUserByLogin(String login) { - DbSession session = mybatis.openSession(false); - try { + try (DbSession session = mybatis.openSession(false)) { return selectActiveUserByLogin(session, login); - } finally { - MyBatis.closeQuietly(session); } } @@ -107,11 +101,8 @@ public class UserDao implements Dao { */ @Deprecated public List<UserDto> selectByLogins(Collection<String> logins) { - DbSession session = mybatis.openSession(false); - try { + try (DbSession session = mybatis.openSession(false)) { return selectByLogins(session, logins); - } finally { - MyBatis.closeQuietly(session); } } @@ -127,17 +118,13 @@ public class UserDao implements Dao { } public List<UserDto> selectUsers(UserQuery query) { - DbSession session = mybatis.openSession(false); - try { + try (DbSession session = mybatis.openSession(false)) { return selectUsers(session, query); - } finally { - MyBatis.closeQuietly(session); } } public List<UserDto> selectUsers(DbSession dbSession, UserQuery query) { - UserMapper mapper = getMapper(dbSession); - return mapper.selectUsers(query); + return getMapper(dbSession).selectUsers(query); } public long countRootUsersButLogin(DbSession dbSession, String login) { diff --git a/sonar-db/src/main/resources/org/sonar/db/user/UserMapper.xml b/sonar-db/src/main/resources/org/sonar/db/user/UserMapper.xml index f40d1e36fcd..8456c837539 100644 --- a/sonar-db/src/main/resources/org/sonar/db/user/UserMapper.xml +++ b/sonar-db/src/main/resources/org/sonar/db/user/UserMapper.xml @@ -88,6 +88,12 @@ <if test="searchText != null"> AND (u.login LIKE #{searchTextSql} ESCAPE '/' OR u.name LIKE #{searchTextSql} ESCAPE '/') </if> + <if test="mustBeRoot != null and mustBeRoot==true"> + AND u.is_root = ${_true} + </if> + <if test="mustBeRoot != null and mustBeRoot==false"> + AND u.is_root = ${_false} + </if> </where> ORDER BY u.name </select> diff --git a/sonar-db/src/test/java/org/sonar/db/user/UserDaoTest.java b/sonar-db/src/test/java/org/sonar/db/user/UserDaoTest.java index f1d7de6201f..c8cc20bb731 100644 --- a/sonar-db/src/test/java/org/sonar/db/user/UserDaoTest.java +++ b/sonar-db/src/test/java/org/sonar/db/user/UserDaoTest.java @@ -193,6 +193,24 @@ public class UserDaoTest { } @Test + public void selectUsers_returns_both_only_root_or_only_non_root_depending_on_mustBeRoot_and_mustNotBeRoot_calls_on_query() { + UserDto user1 = insertUser(true); + UserDto root1 = insertRootUser(newUserDto()); + UserDto user2 = insertUser(true); + UserDto root2 = insertRootUser(newUserDto()); + + assertThat(underTest.selectUsers(UserQuery.builder().build())) + .extracting(UserDto::getLogin) + .containsOnly(user1.getLogin(), user2.getLogin(), root1.getLogin(), root2.getLogin()); + assertThat(underTest.selectUsers(UserQuery.builder().mustBeRoot().build())) + .extracting(UserDto::getLogin) + .containsOnly(root1.getLogin(), root2.getLogin()); + assertThat(underTest.selectUsers(UserQuery.builder().mustNotBeRoot().build())) + .extracting(UserDto::getLogin) + .containsOnly(user1.getLogin(), user2.getLogin()); + } + + @Test public void countRootUsersButLogin_returns_0_when_there_is_no_user_at_all() { assertThat(underTest.countRootUsersButLogin(session, "bla")).isEqualTo(0); } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/user/UserQuery.java b/sonar-plugin-api/src/main/java/org/sonar/api/user/UserQuery.java index 76a3c0fef59..136e16a1f39 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/user/UserQuery.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/user/UserQuery.java @@ -36,6 +36,7 @@ public class UserQuery { private final Collection<String> logins; private final boolean includeDeactivated; private final String searchText; + private final Boolean mustBeRoot; // for internal use in MyBatis final String searchTextSql; @@ -44,6 +45,7 @@ public class UserQuery { this.logins = builder.logins; this.includeDeactivated = builder.includeDeactivated; this.searchText = builder.searchText; + this.mustBeRoot = builder.mustBeRoot; this.searchTextSql = searchTextToSql(searchText); } @@ -75,6 +77,11 @@ public class UserQuery { return searchText; } + @CheckForNull + public Boolean mustBeRoot() { + return mustBeRoot; + } + public static Builder builder() { return new Builder(); } @@ -83,6 +90,7 @@ public class UserQuery { private boolean includeDeactivated = false; private Collection<String> logins; private String searchText; + private Boolean mustBeRoot; private Builder() { } @@ -108,6 +116,16 @@ public class UserQuery { return this; } + public Builder mustBeRoot() { + this.mustBeRoot = true; + return this; + } + + public Builder mustNotBeRoot() { + this.mustBeRoot = false; + return this; + } + public UserQuery build() { if (logins != null && logins.size() >= 1000) { throw new IllegalArgumentException("Max number of logins is 1000. Got " + logins.size()); diff --git a/sonar-ws/src/main/protobuf/ws-root.proto b/sonar-ws/src/main/protobuf/ws-root.proto new file mode 100644 index 00000000000..032ee5ab24b --- /dev/null +++ b/sonar-ws/src/main/protobuf/ws-root.proto @@ -0,0 +1,36 @@ +// SonarQube, open source software quality management tool. +// Copyright (C) 2008-2015 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. + +syntax = "proto2"; + +package sonarqube.ws.root; + +option java_package = "org.sonarqube.ws"; +option java_outer_classname = "WsRoot"; +option optimize_for = SPEED; + +// WS api/root/search +message SearchWsResponse { + repeated Root roots = 1; +} + +message Root { + optional string login = 1; + optional string name = 2; + optional string email = 3; +} |