diff options
Diffstat (limited to 'server')
9 files changed, 1018 insertions, 0 deletions
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWs.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWs.java new file mode 100644 index 00000000000..8ed50bf69b6 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWs.java @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import org.sonar.api.server.ws.WebService; + +public class HotspotsWs implements WebService { + + private final HotspotsWsAction[] actions; + + public HotspotsWs(HotspotsWsAction... actions) { + this.actions = actions; + } + + @Override + public void define(Context context) { + NewController controller = context.createController("api/hotspots"); + controller.setDescription("Read and update Security Hotspots."); + controller.setSince("8.1"); + for (HotspotsWsAction action : actions) { + action.define(controller); + } + controller.done(); + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsAction.java new file mode 100644 index 00000000000..f5527b430c4 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsAction.java @@ -0,0 +1,26 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import org.sonar.server.ws.WsAction; + +public interface HotspotsWsAction extends WsAction { + // marker interface +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java new file mode 100644 index 00000000000..e04dd6d3cce --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/HotspotsWsModule.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import org.sonar.core.platform.Module; + +public class HotspotsWsModule extends Module { + @Override + protected void configureModule() { + add( + SearchAction.class, + HotspotsWs.class + ); + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java new file mode 100644 index 00000000000..a2dd1046c2f --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/SearchAction.java @@ -0,0 +1,297 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.search.SearchHit; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Languages; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +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.web.UserRole; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.index.IssueIndex; +import org.sonar.server.issue.index.IssueQuery; +import org.sonar.server.organization.DefaultOrganizationProvider; +import org.sonar.server.user.UserSession; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Hotspots; + +import static com.google.common.base.Strings.nullToEmpty; +import static java.util.Optional.ofNullable; +import static org.sonar.api.server.ws.WebService.Param.PAGE; +import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.server.ws.WsUtils.writeProtobuf; + +public class SearchAction implements HotspotsWsAction { + private static final String PARAM_PROJECT_KEY = "projectKey"; + + private final DbClient dbClient; + private final UserSession userSession; + private final IssueIndex issueIndex; + private final DefaultOrganizationProvider defaultOrganizationProvider; + private final Languages languages; + + public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex, DefaultOrganizationProvider defaultOrganizationProvider, Languages languages) { + this.dbClient = dbClient; + this.userSession = userSession; + this.issueIndex = issueIndex; + this.defaultOrganizationProvider = defaultOrganizationProvider; + this.languages = languages; + } + + @Override + public void define(WebService.NewController controller) { + + WebService.NewAction action = controller + .createAction("search") + .setHandler(this) + .setDescription("Search for Security Hotpots.") + .setSince("8.1") + .setInternal(true); + + action.addPagingParams(100); + action.createParam(PARAM_PROJECT_KEY) + .setDescription("Key of the project") + .setExampleValue(KEY_PROJECT_EXAMPLE_001) + .setRequired(true); + // FIXME add response example and test it + // action.setResponseExample() + } + + @Override + public void handle(Request request, Response response) throws Exception { + try (DbSession dbSession = dbClient.openSession(false)) { + String projectKey = request.mandatoryParam(PARAM_PROJECT_KEY); + ComponentDto project = dbClient.componentDao().selectByKey(dbSession, projectKey) + .filter(t -> Scopes.PROJECT.equals(t.scope()) && Qualifiers.PROJECT.equals(t.qualifier())) + .filter(ComponentDto::isEnabled) + .filter(t -> t.getMainBranchProjectUuid() == null) + .orElseThrow(() -> new NotFoundException(String.format("Project '%s' not found", projectKey))); + userSession.checkComponentPermission(UserRole.USER, project); + + List<IssueDto> orderedIssues = searchHotspots(request, dbSession, project); + SearchResponseData searchResponseData = new SearchResponseData(orderedIssues); + loadComponents(dbSession, searchResponseData); + loadRules(dbSession, searchResponseData); + writeProtobuf(formatResponse(searchResponseData), request, response); + } + } + + private List<IssueDto> searchHotspots(Request request, DbSession dbSession, ComponentDto project) { + List<String> issueKeys = searchHotspots(request, project); + + List<IssueDto> unorderedIssues = dbClient.issueDao().selectByKeys(dbSession, issueKeys); + Map<String, IssueDto> unorderedIssuesMap = unorderedIssues + .stream() + .collect(uniqueIndex(IssueDto::getKey, unorderedIssues.size())); + + return issueKeys.stream() + .map(unorderedIssuesMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private List<String> searchHotspots(Request request, ComponentDto project) { + IssueQuery.Builder builder = IssueQuery.builder() + .projectUuids(Collections.singletonList(project.uuid())) + .organizationUuid(project.getOrganizationUuid()) + .types(Collections.singleton(RuleType.SECURITY_HOTSPOT.name())) + .resolved(false); + IssueQuery query = builder.build(); + SearchOptions searchOptions = new SearchOptions() + .setPage(request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE)); + SearchResponse result = issueIndex.search(query, searchOptions); + return Arrays.stream(result.getHits().getHits()) + .map(SearchHit::getId) + .collect(MoreCollectors.toList(result.getHits().getHits().length)); + } + + private void loadComponents(DbSession dbSession, SearchResponseData searchResponseData) { + Set<String> componentKeys = searchResponseData.getOrderedHotspots() + .stream() + .flatMap(hotspot -> Stream.of(hotspot.getComponentKey(), hotspot.getProjectKey())) + .collect(Collectors.toSet()); + if (!componentKeys.isEmpty()) { + searchResponseData.addComponents(dbClient.componentDao().selectByDbKeys(dbSession, componentKeys)); + } + } + + private void loadRules(DbSession dbSession, SearchResponseData searchResponseData) { + Set<RuleKey> ruleKeys = searchResponseData.getOrderedHotspots() + .stream() + .map(IssueDto::getRuleKey) + .collect(Collectors.toSet()); + if (!ruleKeys.isEmpty()) { + searchResponseData.addRules(dbClient.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys)); + } + } + + private Hotspots.SearchWsResponse formatResponse(SearchResponseData searchResponseData) { + Hotspots.SearchWsResponse.Builder responseBuilder = Hotspots.SearchWsResponse.newBuilder(); + if (!searchResponseData.isEmpty()) { + formatHotspots(searchResponseData, responseBuilder); + formatComponents(searchResponseData, responseBuilder); + formatRules(searchResponseData, responseBuilder); + } + return responseBuilder.build(); + } + + private static void formatHotspots(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) { + List<IssueDto> orderedHotspots = searchResponseData.getOrderedHotspots(); + if (orderedHotspots.isEmpty()) { + return; + } + + Hotspots.Hotspot.Builder builder = Hotspots.Hotspot.newBuilder(); + for (IssueDto hotspot : orderedHotspots) { + builder + .clear() + .setKey(hotspot.getKey()) + .setComponent(hotspot.getComponentKey()) + .setProject(hotspot.getProjectKey()) + .setRule(hotspot.getRuleKey().toString()); + ofNullable(hotspot.getStatus()).ifPresent(builder::setStatus); +// FIXME resolution field will be added later +// ofNullable(hotspot.getResolution()).ifPresent(builder::setResolution); + ofNullable(hotspot.getLine()).ifPresent(builder::setLine); + builder.setMessage(nullToEmpty(hotspot.getMessage())); + ofNullable(hotspot.getAssigneeUuid()).ifPresent(builder::setAssignee); + // FIXME Filter author only if user is member of the organization (as done in issues/search WS) +// if (data.getUserOrganizationUuids().contains(component.getOrganizationUuid())) { + builder.setAuthor(nullToEmpty(hotspot.getAuthorLogin())); +// } + builder.setCreationDate(formatDateTime(hotspot.getIssueCreationDate())); + builder.setUpdateDate(formatDateTime(hotspot.getIssueUpdateDate())); + + responseBuilder.addHotspots(builder.build()); + } + } + + private void formatComponents(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) { + Set<ComponentDto> components = searchResponseData.getComponents(); + if (components.isEmpty()) { + return; + } + + Hotspots.Component.Builder builder = Hotspots.Component.newBuilder(); + for (ComponentDto component : components) { + builder + .clear() + .setOrganization(defaultOrganizationProvider.get().getKey()) + .setKey(component.getKey()) + .setQualifier(component.qualifier()) + .setName(component.name()) + .setLongName(component.longName()); + ofNullable(component.path()).ifPresent(builder::setPath); + + responseBuilder.addComponents(builder.build()); + } + } + + private void formatRules(SearchResponseData searchResponseData, Hotspots.SearchWsResponse.Builder responseBuilder) { + Set<RuleDefinitionDto> rules = searchResponseData.getRules(); + if (rules.isEmpty()) { + return; + } + + Common.Rules.Builder rulesBuilder = Common.Rules.newBuilder(); + Common.Rule.Builder ruleBuilder = Common.Rule.newBuilder(); + for (RuleDefinitionDto rule : rules) { + ruleBuilder + .clear() + .setKey(rule.getKey().toString()) + .setName(nullToEmpty(rule.getName())) + .setStatus(Common.RuleStatus.valueOf(rule.getStatus().name())); + + String language = rule.getLanguage(); + if (language == null) { + ruleBuilder.setLang(""); + } else { + ruleBuilder.setLang(language); + Language lang = languages.get(language); + if (lang != null) { + ruleBuilder.setLangName(lang.getName()); + } + } + rulesBuilder.addRules(ruleBuilder.build()); + } + responseBuilder.setRules(rulesBuilder.build()); + } + + private static final class SearchResponseData { + private final List<IssueDto> orderedHotspots; + private final Set<ComponentDto> components = new HashSet<>(); + private final Set<RuleDefinitionDto> rules = new HashSet<>(); + + private SearchResponseData(List<IssueDto> orderedHotspots) { + this.orderedHotspots = orderedHotspots; + } + + boolean isEmpty() { + return orderedHotspots.isEmpty(); + } + + List<IssueDto> getOrderedHotspots() { + return orderedHotspots; + } + + void addComponents(Collection<ComponentDto> components) { + this.components.addAll(components); + } + + Set<ComponentDto> getComponents() { + return components; + } + + void addRules(Collection<RuleDefinitionDto> rules) { + this.rules.addAll(rules); + } + + Set<RuleDefinitionDto> getRules() { + return rules; + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/package-info.java new file mode 100644 index 00000000000..ecfd1a189b8 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/hotspot/ws/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.hotspot.ws; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java new file mode 100644 index 00000000000..197753a60a4 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsModuleTest.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class HotspotsWsModuleTest { + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new HotspotsWsModule().configure(container); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 2); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsTest.java new file mode 100644 index 00000000000..f0e7390920e --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/HotspotsWsTest.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import java.util.Arrays; +import java.util.Random; +import java.util.stream.IntStream; +import org.junit.Test; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + +public class HotspotsWsTest { + + @Test + public void define_controller() { + String[] actionKeys = IntStream.range(0, 1 + new Random().nextInt(12)) + .mapToObj(i -> i + randomAlphanumeric(10)) + .toArray(String[]::new); + HotspotsWsAction[] actions = Arrays.stream(actionKeys) + .map(actionKey -> new HotspotsWsAction() { + @Override + public void define(WebService.NewController context) { + context.createAction(actionKey).setHandler(this); + } + + @Override + public void handle(Request request, Response response) { + + } + }) + .toArray(HotspotsWsAction[]::new); + WebService.Context context = new WebService.Context(); + new HotspotsWs(actions).define(context); + WebService.Controller controller = context.controller("api/hotspots"); + + assertThat(controller).isNotNull(); + assertThat(controller.description()).isNotEmpty(); + assertThat(controller.since()).isEqualTo("8.1"); + assertThat(controller.actions()).extracting(WebService.Action::key).containsOnly(actionKeys); + } + +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java new file mode 100644 index 00000000000..8273a49afee --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/SearchActionTest.java @@ -0,0 +1,494 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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.hotspot.ws; + +import java.util.Arrays; +import java.util.Map; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.resources.AbstractLanguage; +import org.sonar.api.resources.Languages; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.rule.RuleTesting; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.StartupIndexer; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.issue.index.IssueIndex; +import org.sonar.server.issue.index.IssueIndexer; +import org.sonar.server.issue.index.IssueIteratorFactory; +import org.sonar.server.organization.TestDefaultOrganizationProvider; +import org.sonar.server.permission.index.PermissionIndexer; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; +import org.sonarqube.ws.Common; +import org.sonarqube.ws.Common.RuleStatus; +import org.sonarqube.ws.Hotspots; +import org.sonarqube.ws.Hotspots.Component; +import org.sonarqube.ws.Hotspots.SearchWsResponse; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; +import static org.sonar.api.utils.DateUtils.formatDateTime; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.db.component.ComponentTesting.newDirectory; +import static org.sonar.db.component.ComponentTesting.newFileDto; + +public class SearchActionTest { + private static final Random RANDOM = new Random(); + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public EsTester es = EsTester.create(); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + + private DbClient dbClient = dbTester.getDbClient(); + private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester); + + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); + private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer); + + private Languages languages = mock(Languages.class); + private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex, defaultOrganizationProvider, languages); + private WsActionTester actionTester = new WsActionTester(underTest); + + @Test + public void ws_is_internal() { + assertThat(actionTester.getDef().isInternal()).isTrue(); + } + + @Test + public void fails_with_IAE_if_parameter_projectKey_is_missing() { + TestRequest request = actionTester.newRequest(); + + assertThatThrownBy(request::execute) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("The 'projectKey' parameter is missing"); + } + + @Test + public void fails_with_NotFoundException_if_project_does_not_exist() { + String key = randomAlphabetic(12); + TestRequest request = actionTester.newRequest() + .setParam("projectKey", key); + + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage("Project '%s' not found", key); + } + + @Test + public void fails_with_NotFoundException_if_project_is_not_a_project() { + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto directory = dbTester.components().insertComponent(ComponentTesting.newDirectory(project, "foo")); + ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); + ComponentDto portfolio = dbTester.components().insertPrivatePortfolio(dbTester.getDefaultOrganization()); + ComponentDto application = dbTester.components().insertPrivateApplication(dbTester.getDefaultOrganization()); + TestRequest request = actionTester.newRequest(); + + for (ComponentDto component : Arrays.asList(directory, file, portfolio, application)) { + request.setParam("projectKey", component.getKey()); + + assertThatThrownBy(request::execute) + .isInstanceOf(NotFoundException.class) + .hasMessage("Project '%s' not found", component.getKey()); + } + } + + @Test + public void fails_with_ForbiddenException_if_project_is_private_and_not_allowed() { + ComponentDto project = dbTester.components().insertPrivateProject(); + userSessionRule.registerComponents(project); + TestRequest request = actionTester.newRequest() + .setParam("projectKey", project.getKey()); + + assertThatThrownBy(request::execute) + .isInstanceOf(ForbiddenException.class) + .hasMessage("Insufficient privileges"); + } + + @Test + public void succeeds_on_public_project() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).isEmpty(); + assertThat(response.getComponentsList()).isEmpty(); + assertThat(response.getRules().getRulesList()).isEmpty(); + } + + @Test + public void succeeds_on_private_project_with_permission() { + ComponentDto project = dbTester.components().insertPrivateProject(); + userSessionRule.registerComponents(project); + userSessionRule.logIn().addProjectPermission(UserRole.USER, project); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).isEmpty(); + assertThat(response.getComponentsList()).isEmpty(); + assertThat(response.getRules().getRulesList()).isEmpty(); + } + + @Test + public void returns_no_hotspot_component_nor_rule_when_project_has_no_hotspot() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + Arrays.stream(RuleType.values()) + .filter(t -> t != SECURITY_HOTSPOT) + .forEach(ruleType -> { + RuleDefinitionDto rule = newRule(ruleType); + dbTester.issues().insert(rule, project, file, t -> t.setType(ruleType)); + }); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).isEmpty(); + assertThat(response.getComponentsList()).isEmpty(); + assertThat(response.getRules().getRulesList()).isEmpty(); + } + + @Test + public void returns_hotspot_component_and_rule_when_project_has_hotspots() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + ComponentDto fileWithHotspot = dbTester.components().insertComponent(newFileDto(project)); + Arrays.stream(RuleType.values()) + .filter(t -> t != SECURITY_HOTSPOT) + .forEach(ruleType -> { + RuleDefinitionDto rule = newRule(ruleType); + dbTester.issues().insert(rule, project, file, t -> t.setType(ruleType)); + }); + IssueDto[] hotspots = IntStream.range(0, 1 + RANDOM.nextInt(10)) + .mapToObj(i -> { + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + return dbTester.issues().insert(rule, project, fileWithHotspot, t -> t.setType(SECURITY_HOTSPOT)); + }) + .toArray(IssueDto[]::new); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()) + .extracting(Hotspots.Hotspot::getKey) + .containsOnly(Arrays.stream(hotspots).map(IssueDto::getKey).toArray(String[]::new)); + assertThat(response.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project.getKey(), fileWithHotspot.getKey()); + assertThat(response.getRules().getRulesList()) + .extracting(Common.Rule::getKey) + .containsOnly(Arrays.stream(hotspots).map(t -> t.getRuleKey().toString()).toArray(String[]::new)); + } + + @Test + public void returns_hotspots_of_specified_project() { + ComponentDto project1 = dbTester.components().insertPublicProject(); + ComponentDto project2 = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project1, project2); + indexPermissions(); + ComponentDto file1 = dbTester.components().insertComponent(newFileDto(project1)); + ComponentDto file2 = dbTester.components().insertComponent(newFileDto(project2)); + IssueDto[] hotspots2 = IntStream.range(0, 1 + RANDOM.nextInt(10)) + .mapToObj(i -> { + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + dbTester.issues().insert(rule, project1, file1, t -> t.setType(SECURITY_HOTSPOT)); + return dbTester.issues().insert(rule, project2, file2, t -> t.setType(SECURITY_HOTSPOT)); + }) + .toArray(IssueDto[]::new); + indexIssues(); + + SearchWsResponse responseProject1 = actionTester.newRequest() + .setParam("projectKey", project1.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(responseProject1.getHotspotsList()) + .extracting(Hotspots.Hotspot::getKey) + .doesNotContainAnyElementsOf(Arrays.stream(hotspots2).map(IssueDto::getKey).collect(Collectors.toList())); + assertThat(responseProject1.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project1.getKey(), file1.getKey()); + assertThat(responseProject1.getRules().getRulesList()) + .extracting(Common.Rule::getKey) + .containsOnly(Arrays.stream(hotspots2).map(t -> t.getRuleKey().toString()).toArray(String[]::new)); + + SearchWsResponse responseProject2 = actionTester.newRequest() + .setParam("projectKey", project2.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(responseProject2.getHotspotsList()) + .extracting(Hotspots.Hotspot::getKey) + .containsOnly(Arrays.stream(hotspots2).map(IssueDto::getKey).toArray(String[]::new)); + assertThat(responseProject2.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project2.getKey(), file2.getKey()); + assertThat(responseProject2.getRules().getRulesList()) + .extracting(Common.Rule::getKey) + .containsOnly(Arrays.stream(hotspots2).map(t -> t.getRuleKey().toString()).toArray(String[]::new)); + } + + @Test + public void returns_only_unresolved_hotspots() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + IssueDto unresolvedHotspot = dbTester.issues().insert(rule, project, file, + t -> t.setType(SECURITY_HOTSPOT).setResolution(null)); + // unrealistic case since a resolution must be set, but shows a limit of current implementation + IssueDto badlyClosedHotspot = dbTester.issues().insert(rule, project, file, + t -> t.setType(SECURITY_HOTSPOT).setStatus(Issue.STATUS_CLOSED).setResolution(null)); + IssueDto resolvedHotspot = dbTester.issues().insert(rule, project, file, + t -> t.setType(SECURITY_HOTSPOT).setResolution(randomAlphabetic(5))); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()) + .extracting(Hotspots.Hotspot::getKey) + .containsOnly(unresolvedHotspot.getKey(), badlyClosedHotspot.getKey()); + } + + @Test + public void returns_fields_of_hotspot() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + IssueDto hotspot = dbTester.issues().insert(rule, project, file, + t -> t.setType(SECURITY_HOTSPOT) + .setStatus(randomAlphabetic(11)) + .setLine(RANDOM.nextInt(230)) + .setMessage(randomAlphabetic(10)) + .setAssigneeUuid(randomAlphabetic(9)) + .setAuthorLogin(randomAlphabetic(8))); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).hasSize(1); + Hotspots.Hotspot actual = response.getHotspots(0); + assertThat(actual.getComponent()).isEqualTo(file.getKey()); + assertThat(actual.getProject()).isEqualTo(project.getKey()); + assertThat(actual.getRule()).isEqualTo(hotspot.getRuleKey().toString()); + assertThat(actual.getStatus()).isEqualTo(hotspot.getStatus()); + // FIXME resolution field will be added later + // assertThat(actual.getResolution()).isEqualTo(hotspot.getResolution()); + assertThat(actual.getLine()).isEqualTo(hotspot.getLine()); + assertThat(actual.getMessage()).isEqualTo(hotspot.getMessage()); + assertThat(actual.getAssignee()).isEqualTo(hotspot.getAssigneeUuid()); + assertThat(actual.getAuthor()).isEqualTo(hotspot.getAuthorLogin()); + assertThat(actual.getCreationDate()).isEqualTo(formatDateTime(hotspot.getIssueCreationDate())); + assertThat(actual.getUpdateDate()).isEqualTo(formatDateTime(hotspot.getIssueUpdateDate())); + } + + @Test + public void does_not_fail_when_hotspot_has_none_of_the_nullable_fields() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + dbTester.issues().insert(rule, project, file, + t -> t.setType(SECURITY_HOTSPOT) + .setStatus(null) + // FIXME resolution field will be added later + // .setResolution(null) + .setLine(null) + .setMessage(null) + .setAssigneeUuid(null) + .setAuthorLogin(null)); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()) + .hasSize(1); + Hotspots.Hotspot actual = response.getHotspots(0); + assertThat(actual.hasStatus()).isFalse(); + // FIXME resolution field will be added later + // assertThat(actual.hasResolution()).isFalse(); + assertThat(actual.hasLine()).isFalse(); + assertThat(actual.getMessage()).isEmpty(); + assertThat(actual.hasAssignee()).isFalse(); + assertThat(actual.getAuthor()).isEmpty(); + } + + @Test + public void returns_details_of_components() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto directory = dbTester.components().insertComponent(newDirectory(project, "donut/acme")); + ComponentDto directory2 = dbTester.components().insertComponent(newDirectory(project, "foo/bar")); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + ComponentDto file2 = dbTester.components().insertComponent(newFileDto(project)); + RuleDefinitionDto rule = newRule(SECURITY_HOTSPOT); + IssueDto fileHotspot = dbTester.issues().insert(rule, project, file, t -> t.setType(SECURITY_HOTSPOT)); + IssueDto dirHotspot = dbTester.issues().insert(rule, project, directory, t -> t.setType(SECURITY_HOTSPOT)); + IssueDto projectHotspot = dbTester.issues().insert(rule, project, project, t -> t.setType(SECURITY_HOTSPOT)); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()) + .extracting(Hotspots.Hotspot::getKey) + .containsOnly(fileHotspot.getKey(), dirHotspot.getKey(), projectHotspot.getKey()); + assertThat(response.getComponentsList()).hasSize(3); + assertThat(response.getComponentsList()) + .extracting(Component::getOrganization) + .containsOnly(defaultOrganizationProvider.get().getKey()); + assertThat(response.getComponentsList()) + .extracting(Component::getKey) + .containsOnly(project.getKey(), directory.getKey(), file.getKey()); + Map<String, Component> componentByKey = response.getComponentsList().stream().collect(uniqueIndex(Component::getKey)); + Component actualProject = componentByKey.get(project.getKey()); + assertThat(actualProject.getQualifier()).isEqualTo(project.qualifier()); + assertThat(actualProject.getName()).isEqualTo(project.name()); + assertThat(actualProject.getLongName()).isEqualTo(project.longName()); + assertThat(actualProject.hasPath()).isFalse(); + Component actualDirectory = componentByKey.get(directory.getKey()); + assertThat(actualDirectory.getQualifier()).isEqualTo(directory.qualifier()); + assertThat(actualDirectory.getName()).isEqualTo(directory.name()); + assertThat(actualDirectory.getLongName()).isEqualTo(directory.longName()); + assertThat(actualDirectory.getPath()).isEqualTo(directory.path()); + Component actualFile = componentByKey.get(file.getKey()); + assertThat(actualFile.getQualifier()).isEqualTo(file.qualifier()); + assertThat(actualFile.getName()).isEqualTo(file.name()); + assertThat(actualFile.getLongName()).isEqualTo(file.longName()); + assertThat(actualFile.getPath()).isEqualTo(file.path()); + } + + @Test + public void returns_details_of_rule_with_language_name_when_available() { + ComponentDto project = dbTester.components().insertPublicProject(); + userSessionRule.registerComponents(project); + indexPermissions(); + ComponentDto file = dbTester.components().insertComponent(newFileDto(project)); + String language1 = randomAlphabetic(3); + String language2 = randomAlphabetic(2); + RuleDefinitionDto rule1a = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language1)); + RuleDefinitionDto rule1b = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language1)); + RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, t -> t.setLanguage(language2)); + when(languages.get(language1)).thenReturn(new AbstractLanguage(language1, language1 + "_name") { + @Override + public String[] getFileSuffixes() { + return new String[0]; + } + }); + IssueDto hotspot1a = dbTester.issues().insert(rule1a, project, file, t -> t.setType(SECURITY_HOTSPOT)); + IssueDto hotspot1b = dbTester.issues().insert(rule1b, project, file, t -> t.setType(SECURITY_HOTSPOT)); + IssueDto hotspot2 = dbTester.issues().insert(rule2, project, file, t -> t.setType(SECURITY_HOTSPOT)); + indexIssues(); + + SearchWsResponse response = actionTester.newRequest() + .setParam("projectKey", project.getKey()) + .executeProtobuf(SearchWsResponse.class); + + assertThat(response.getHotspotsList()).hasSize(3); + assertThat(response.getRules().getRulesList()).hasSize(3); + Map<RuleKey, Common.Rule> rulesByKey = response.getRules().getRulesList() + .stream() + .collect(uniqueIndex(t -> RuleKey.parse(t.getKey()))); + Common.Rule actualRule1a = rulesByKey.get(hotspot1a.getRuleKey()); + assertThat(actualRule1a.getName()).isEqualTo(rule1a.getName()); + assertThat(actualRule1a.getLang()).isEqualTo(rule1a.getLanguage()); + assertThat(actualRule1a.getLangName()).isEqualTo(rule1a.getLanguage() + "_name"); + assertThat(actualRule1a.getStatus()).isEqualTo(RuleStatus.valueOf(rule1a.getStatus().name())); + Common.Rule actualRule1b = rulesByKey.get(hotspot1b.getRuleKey()); + assertThat(actualRule1b.getName()).isEqualTo(rule1b.getName()); + assertThat(actualRule1b.getLang()).isEqualTo(rule1b.getLanguage()); + assertThat(actualRule1b.getLangName()).isEqualTo(rule1b.getLanguage() + "_name"); + assertThat(actualRule1b.getStatus()).isEqualTo(RuleStatus.valueOf(rule1b.getStatus().name())); + Common.Rule actualRule2 = rulesByKey.get(hotspot2.getRuleKey()); + assertThat(actualRule2.getName()).isEqualTo(rule2.getName()); + assertThat(actualRule2.getLang()).isEqualTo(rule2.getLanguage()); + assertThat(actualRule2.hasLangName()).isFalse(); + assertThat(actualRule2.getStatus()).isEqualTo(RuleStatus.valueOf(rule2.getStatus().name())); + } + + private void indexPermissions() { + permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes()); + } + + private void indexIssues() { + issueIndexer.indexOnStartup(issueIndexer.getIndexTypes()); + } + + private RuleDefinitionDto newRule(RuleType ruleType) { + return newRule(ruleType, t -> { + }); + } + + private RuleDefinitionDto newRule(RuleType ruleType, Consumer<RuleDefinitionDto> populate) { + RuleDefinitionDto ruleDefinition = RuleTesting.newRule() + .setType(ruleType); + populate.accept(ruleDefinition); + dbTester.rules().insert(ruleDefinition); + return ruleDefinition; + } +} diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index c52a7cb1ac6..e53a13750b0 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -74,6 +74,7 @@ import org.sonar.server.extension.CoreExtensionStopper; import org.sonar.server.favorite.FavoriteModule; import org.sonar.server.favorite.ws.FavoriteWsModule; import org.sonar.server.health.NodeHealthModule; +import org.sonar.server.hotspot.ws.HotspotsWsModule; import org.sonar.server.issue.AddTagsAction; import org.sonar.server.issue.AssignAction; import org.sonar.server.issue.CommentAction; @@ -429,6 +430,9 @@ public class PlatformLevel4 extends PlatformLevel { RemoveTagsAction.class, IssueChangePostProcessorImpl.class, + // hotspots + HotspotsWsModule.class, + // source SourceWsModule.class, |