From 7253fe3904de24bbf5829c992c3592e018791951 Mon Sep 17 00:00:00 2001 From: Eric Hartmann Date: Wed, 8 Aug 2018 06:29:27 +0100 Subject: [PATCH] SONAR-10864 Author information only available to members (#552) --- .../org/sonar/db/version/schema-h2.ddl | 1 + .../v73/AddIndexOnOrganizationMembers.java | 47 ++++ .../db/migration/version/v73/DbVersion73.java | 1 + .../AddIndexOnOrganizationMembersTest.java | 55 +++++ .../version/v73/DbVersion73Test.java | 2 +- .../organization_members.sql | 6 + .../authentication/SafeModeUserSession.java | 10 +- .../server/issue/ws/ChangelogAction.java | 34 ++- .../sonar/server/issue/ws/SearchAction.java | 57 ++++- .../server/issue/ws/SearchResponseData.java | 9 + .../server/issue/ws/SearchResponseFormat.java | 9 +- .../sonar/server/source/ws/LinesAction.java | 16 +- .../server/user/AbstractUserSession.java | 6 +- .../org/sonar/server/user/DoPrivileged.java | 2 +- .../sonar/server/user/ServerUserSession.java | 8 +- .../server/user/ThreadLocalUserSession.java | 4 +- .../server/issue/ws/ChangelogActionTest.java | 67 ++++-- .../issue/ws/SearchActionComponentsTest.java | 14 +- .../server/issue/ws/SearchActionTest.java | 101 ++++++--- .../ws/SearchActionTestOnSonarCloud.java | 167 ++++++++++++++ .../server/source/ws/LinesActionTest.java | 213 ++++++++++-------- .../tester/AbstractMockUserSession.java | 4 +- .../tester/AnonymousMockUserSession.java | 2 +- .../server/user/TestUserSessionFactory.java | 2 +- .../ws/SearchActionTest/author_is_hidden.json | 24 ++ .../no_author_and_no_authors_facet.json | 20 ++ .../no_authors_facet.json | 22 ++ .../with_authors_facet.json | 37 +++ .../ws/LinesActionTest/hide_scmAuthors.json | 11 + .../ws/LinesActionTest/show_scmAuthors.json | 12 + .../main/js/apps/explore/ExploreIssues.tsx | 2 +- .../main/js/apps/issues/components/App.tsx | 34 ++- .../apps/issues/components/AppContainer.tsx | 13 +- .../main/js/apps/issues/sidebar/Sidebar.tsx | 5 +- .../src/main/js/apps/issues/utils.ts | 4 + .../SourceViewer/components/LineSCM.tsx | 6 +- .../SourceViewer/components/SCMPopup.tsx | 6 +- .../components/__tests__/LineSCM-test.tsx | 15 +- .../__snapshots__/LineSCM-test.tsx.snap | 33 +++ 39 files changed, 876 insertions(+), 205 deletions(-) create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql create mode 100644 server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTest/author_is_hidden.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_author_and_no_authors_facet.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_authors_facet.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/with_authors_facet.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/hide_scmAuthors.json create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_scmAuthors.json diff --git a/server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl b/server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl index b27bcb01179..e62fc5698f8 100644 --- a/server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl +++ b/server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl @@ -25,6 +25,7 @@ CREATE TABLE "ORGANIZATION_MEMBERS" ( CONSTRAINT "PK_ORGANIZATION_MEMBERS" PRIMARY KEY ("ORGANIZATION_UUID", "USER_ID") ); +CREATE INDEX "IX_ORG_MEMBERS_ON_USER_ID" ON "ORGANIZATION_MEMBERS" ("USER_ID"); CREATE TABLE "GROUPS_USERS" ( "USER_ID" INTEGER, diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java new file mode 100644 index 00000000000..d20c760b433 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.platform.db.migration.version.v73; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.SupportsBlueGreen; +import org.sonar.server.platform.db.migration.def.IntegerColumnDef; +import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +@SupportsBlueGreen +public class AddIndexOnOrganizationMembers extends DdlChange { + public AddIndexOnOrganizationMembers(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + context.execute( + new CreateIndexBuilder(getDialect()) + .setTable("organization_members") + .setName("ix_org_members_on_user_id") + .addColumn(IntegerColumnDef.newIntegerColumnDefBuilder() + .setColumnName("user_id") + .setIsNullable(false) + .build()) + .build()); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java index dc159604ae3..6611ef0b301 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java @@ -39,6 +39,7 @@ public class DbVersion73 implements DbVersion { .add(2209, "Fix missing quality profiles on organizations", FixMissingQualityProfilesOnOrganizations.class) .add(2210, "Add 'securityhotspotadmin' permission to templates characteristics already having 'issueadmin'", PopulateHotspotAdminPermissionOnTemplatesCharacteristics.class) .add(2211, "Set SUBSCRIPTION not nullable in ORGANIZATIONS", SetSubscriptionOnOrganizationsNotNullable.class) + .add(2212, "Add index on ORGANIZATION_MEMBERS", AddIndexOnOrganizationMembers.class) ; } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java new file mode 100644 index 00000000000..e3ba7483891 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.platform.db.migration.version.v73; + +import java.sql.SQLException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.db.CoreDbTester; + +public class AddIndexOnOrganizationMembersTest { + private static final String TABLE_LOADED_TEMPLATES = "organization_members"; + + @Rule + public CoreDbTester db = CoreDbTester.createForSchema(AddIndexOnOrganizationMembersTest.class, "organization_members.sql"); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private AddIndexOnOrganizationMembers underTest = new AddIndexOnOrganizationMembers(db.database()); + + @Test + public void execute_adds_index_ix_loaded_templates_type() throws SQLException { + underTest.execute(); + + db.assertIndex(TABLE_LOADED_TEMPLATES, "ix_org_members_on_user_id", "user_id"); + } + + @Test + public void execute_is_not_reentrant() throws SQLException { + underTest.execute(); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Fail to execute"); + + underTest.execute(); + } + +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java index 056e97d85cf..fbd6a8fd21e 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java @@ -35,6 +35,6 @@ public class DbVersion73Test { @Test public void verify_migration_count() { - verifyMigrationCount(underTest, 12); + verifyMigrationCount(underTest, 13); } } diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql new file mode 100644 index 00000000000..b14c1697212 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql @@ -0,0 +1,6 @@ +CREATE TABLE "ORGANIZATION_MEMBERS" ( + "ORGANIZATION_UUID" VARCHAR(40) NOT NULL, + "USER_ID" INTEGER NOT NULL, + + CONSTRAINT "PK_ORGANIZATION_MEMBERS" PRIMARY KEY ("ORGANIZATION_UUID", "USER_ID") +); diff --git a/server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java index 91330f3917e..3660f91d871 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java @@ -47,6 +47,11 @@ public class SafeModeUserSession extends AbstractUserSession { return false; } + @Override + protected boolean hasMembershipImpl(OrganizationDto organizationDto) { + return false; + } + @CheckForNull @Override public String getLogin() { @@ -90,9 +95,4 @@ public class SafeModeUserSession extends AbstractUserSession { public boolean isSystemAdministrator() { return false; } - - @Override - public boolean hasMembershipImpl(OrganizationDto organization) { - return false; - } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java index e44be5866d7..655bed2a98d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java @@ -19,6 +19,9 @@ */ package org.sonar.server.issue.ws; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; import java.util.List; import java.util.Map; @@ -38,11 +41,14 @@ 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.organization.OrganizationDto; import org.sonar.db.user.UserDto; import org.sonar.server.issue.IssueFinder; +import org.sonar.server.user.UserSession; import org.sonarqube.ws.Issues.ChangelogWsResponse; import org.sonarqube.ws.Issues.ChangelogWsResponse.Changelog; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Strings.emptyToNull; import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.core.util.Protobuf.setNullable; @@ -60,11 +66,13 @@ public class ChangelogAction implements IssuesWsAction { private final DbClient dbClient; private final IssueFinder issueFinder; private final AvatarResolver avatarFactory; + private final UserSession userSession; - public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, AvatarResolver avatarFactory) { + public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, AvatarResolver avatarFactory, UserSession userSession) { this.dbClient = dbClient; this.issueFinder = issueFinder; this.avatarFactory = avatarFactory; + this.userSession = userSession; } @Override @@ -153,11 +161,25 @@ public class ChangelogAction implements IssuesWsAction { private final Map files; ChangeLogResults(DbSession dbSession, String issueKey) { - IssueDto dbIssue = issueFinder.getByKey(dbSession, issueKey); - this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dbIssue.getKey()); - List userUuids = changes.stream().filter(change -> change.userUuid() != null).map(FieldDiffs::userUuid).collect(MoreCollectors.toList()); - this.users = dbClient.userDao().selectByUuids(dbSession, userUuids).stream().collect(MoreCollectors.uniqueIndex(UserDto::getUuid)); - this.files = dbClient.componentDao().selectByUuids(dbSession, getFileUuids(changes)).stream().collect(MoreCollectors.uniqueIndex(ComponentDto::uuid, Function.identity())); + IssueDto issue = issueFinder.getByKey(dbSession, issueKey); + if (isMember(dbSession, issue)) { + this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, issue.getKey()); + List userUuids = changes.stream().filter(change -> change.userUuid() != null).map(FieldDiffs::userUuid).collect(MoreCollectors.toList()); + this.users = dbClient.userDao().selectByUuids(dbSession, userUuids).stream().collect(MoreCollectors.uniqueIndex(UserDto::getUuid)); + this.files = dbClient.componentDao().selectByUuids(dbSession, getFileUuids(changes)).stream().collect(MoreCollectors.uniqueIndex(ComponentDto::uuid, Function.identity())); + } else { + changes = ImmutableList.of(); + users = ImmutableMap.of(); + files = ImmutableMap.of(); + } + } + + private boolean isMember(DbSession dbSession, IssueDto issue) { + Optional project = dbClient.componentDao().selectByUuid(dbSession, issue.getProjectUuid()); + checkState(project.isPresent(), "Cannot find the project with uuid %s from issue.id %s", issue.getProjectUuid(), issue.getId()); + java.util.Optional organization = dbClient.organizationDao().selectByUuid(dbSession, project.get().getOrganizationUuid()); + checkState(organization.isPresent(), "Cannot find the organization with uuid %s from issue.id %s", project.get().getOrganizationUuid(), issue.getId()); + return userSession.hasMembership(organization.get()); } private Set getFileUuids(List changes) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java index 0b03a534e52..818a26b7706 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java @@ -30,12 +30,14 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.search.SearchHit; +import org.sonar.api.config.Configuration; import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; @@ -52,14 +54,15 @@ import org.sonar.api.utils.log.Loggers; import org.sonar.core.util.stream.MoreCollectors; import org.sonar.db.DbClient; import org.sonar.db.DbSession; +import org.sonar.db.organization.OrganizationDto; import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.db.user.UserDto; import org.sonar.server.es.Facets; import org.sonar.server.es.SearchOptions; -import org.sonar.server.issue.index.IssueQuery; -import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.SearchRequest; import org.sonar.server.issue.index.IssueIndex; +import org.sonar.server.issue.index.IssueQuery; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.user.UserSession; import org.sonarqube.ws.Issues.SearchWsResponse; @@ -74,12 +77,13 @@ import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; import static org.sonar.api.utils.Paging.forPageIndex; import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED; import static org.sonar.server.es.SearchOptions.MAX_LIMIT; import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_INSECURE_INTERACTION; import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_POROUS_DEFENSES; import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_RISKY_RESOURCE; -import static org.sonar.server.issue.index.IssueQuery.SORT_BY_ASSIGNEE; import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; +import static org.sonar.server.issue.index.IssueQuery.SORT_BY_ASSIGNEE; import static org.sonar.server.issue.index.IssueQueryFactory.UNKNOWN; import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; @@ -149,10 +153,11 @@ public class SearchAction implements IssuesWsAction { private final SearchResponseFormat searchResponseFormat; private final System2 system2; private final DbClient dbClient; + private final boolean isOnSonarCloud; - public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory, - SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, System2 system2, - DbClient dbClient) { + public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory, SearchResponseLoader searchResponseLoader, + SearchResponseFormat searchResponseFormat, Configuration config, System2 system2, DbClient dbClient) { + this.isOnSonarCloud = config.getBoolean(SONARCLOUD_ENABLED.getKey()).orElse(false); this.userSession = userSession; this.issueIndex = issueIndex; this.issueQueryFactory = issueQueryFactory; @@ -376,10 +381,9 @@ public class SearchAction implements IssuesWsAction { } private List getLogins(Request request) { - List assigneeLogins = request.paramAsStrings(PARAM_ASSIGNEES); - List onlyLogins = new ArrayList<>(); + for (String login : ofNullable(assigneeLogins).orElse(emptyList())) { if (LOGIN_MYSELF.equals(login)) { if (userSession.getLogin() == null) { @@ -433,13 +437,18 @@ public class SearchAction implements IssuesWsAction { .filter(FACETS_REQUIRING_PROJECT_OR_ORGANIZATION::contains) .collect(toSet()); checkArgument(facetsRequiringProjectOrOrganizationParameter.isEmpty() || - (!query.projectUuids().isEmpty()) || query.organizationUuid() != null, "Facet(s) '%s' require to also filter by project or organization", + (!query.projectUuids().isEmpty()) || query.organizationUuid() != null, "Facet(s) '%s' require to also filter by project or organization", COMA_JOINER.join(facetsRequiringProjectOrOrganizationParameter)); } SearchResponseData preloadedData = new SearchResponseData(emptyList()); preloadedData.setRules(ImmutableList.copyOf(query.rules())); SearchResponseData data = searchResponseLoader.load(preloadedData, collector, facets); + if (userSession.isLoggedIn()) { + try (DbSession dbSession = dbClient.openSession(false)) { + data.setUserOrganizationUuids(dbClient.organizationMemberDao().selectOrganizationUuidsByUser(dbSession, userSession.getUserId())); + } + } // format response // Filter and reorder facets according to the requested ordered names. @@ -536,10 +545,36 @@ public class SearchAction implements IssuesWsAction { } } - private static SearchOptions createSearchOptionsFromRequest(SearchRequest request) { + private SearchOptions createSearchOptionsFromRequest(SearchRequest request) { SearchOptions options = new SearchOptions(); options.setPage(request.getPage(), request.getPageSize()); - options.addFacets(request.getFacets()); + + List facets = request.getFacets(); + + if (facets == null || facets.isEmpty()) { + return options; + } + + List requestedFacets = new ArrayList<>(facets.size()); + requestedFacets.addAll(facets); + + if (isOnSonarCloud) { + Optional organizationDto = Optional.empty(); + String organizationKey = request.getOrganization(); + if (organizationKey != null) { + try (DbSession dbSession = dbClient.openSession(false)) { + organizationDto = dbClient.organizationDao().selectByKey(dbSession, organizationKey); + } + } + + if (!organizationDto.isPresent() || !userSession.hasMembership(organizationDto.get())) { + // In order to display the authors facet, the organization parameter must be set and the user + // must be member of this organization + requestedFacets.remove(PARAM_AUTHORS); + } + } + + options.addFacets(requestedFacets); return options; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java index e39cbff71ef..4c8a59d0063 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java @@ -57,6 +57,7 @@ public class SearchResponseData { private final ListMultimap actionsByIssueKey = ArrayListMultimap.create(); private final ListMultimap transitionsByIssueKey = ArrayListMultimap.create(); private final Set updatableComments = new HashSet<>(); + private final Set userOrganizationUuids = new HashSet<>(); public SearchResponseData(IssueDto issue) { checkNotNull(issue); @@ -173,6 +174,14 @@ public class SearchResponseData { this.organizationKeysByUuid.put(organizationDto.getUuid(), organizationDto.getKey()); } + public void setUserOrganizationUuids(Set organizationUuids) { + this.userOrganizationUuids.addAll(organizationUuids); + } + + public Set getUserOrganizationUuids() { + return this.userOrganizationUuids; + } + @CheckForNull public UserDto getUserByUuid(@Nullable String userUuid) { UserDto userDto = usersByUuid.get(userUuid); diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index 2bf22c04931..5e1e656f669 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -82,8 +82,7 @@ public class SearchResponseFormat { this.avatarFactory = avatarFactory; } - public SearchWsResponse formatSearch(Set fields, SearchResponseData data, - Paging paging, @Nullable Facets facets) { + public SearchWsResponse formatSearch(Set fields, SearchResponseData data, Paging paging, @Nullable Facets facets) { SearchWsResponse.Builder response = SearchWsResponse.newBuilder(); formatPaging(paging, response); @@ -195,7 +194,11 @@ public class SearchResponseFormat { setNullable(dto.getLine(), issueBuilder::setLine); setNullable(emptyToNull(dto.getChecksum()), issueBuilder::setHash); completeIssueLocations(dto, issueBuilder, data); - issueBuilder.setAuthor(nullToEmpty(dto.getAuthorLogin())); + + // Filter author only if user is member of the organization + if (data.getUserOrganizationUuids().contains(component.getOrganizationUuid())) { + issueBuilder.setAuthor(nullToEmpty(dto.getAuthorLogin())); + } setNullable(dto.getIssueCreationDate(), issueBuilder::setCreationDate, DateUtils::formatDateTime); setNullable(dto.getIssueUpdateDate(), issueBuilder::setUpdateDate, DateUtils::formatDateTime); setNullable(dto.getIssueCloseDate(), issueBuilder::setCloseDate, DateUtils::formatDateTime); diff --git a/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java b/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java index 263f4c7539e..bd62f627c4c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java @@ -33,6 +33,7 @@ import org.sonar.api.web.UserRole; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; import org.sonar.db.protobuf.DbFileSources; import org.sonar.server.component.ComponentFinder; import org.sonar.server.source.HtmlSourceDecorator; @@ -138,19 +139,24 @@ public class LinesAction implements SourcesWsAction { try (DbSession dbSession = dbClient.openSession(false)) { ComponentDto file = loadComponent(dbSession, request); userSession.checkComponentPermission(UserRole.CODEVIEWER, file); - int from = request.mandatoryParamAsInt(PARAM_FROM); int to = MoreObjects.firstNonNull(request.paramAsInt(PARAM_TO), Integer.MAX_VALUE); Iterable lines = checkFoundWithOptional(sourceService.getLines(dbSession, file.uuid(), from, to), "No source found for file '%s'", file.getDbKey()); try (JsonWriter json = response.newJsonWriter()) { json.beginObject(); - writeSource(lines, json); + writeSource(lines, json, isMemberOfOrganization(dbSession, file)); json.endObject(); } } } + private boolean isMemberOfOrganization(DbSession dbSession, ComponentDto file) { + OrganizationDto organizationDto = dbClient.organizationDao().selectByUuid(dbSession, file.getOrganizationUuid()) + .orElseThrow(() -> new IllegalStateException(String.format("Organization with uuid '%s' not found", file.getOrganizationUuid()))); + return !userSession.hasMembership(organizationDto); + } + private ComponentDto loadComponent(DbSession dbSession, Request wsRequest) { String componentKey = wsRequest.param(PARAM_KEY); String componentId = wsRequest.param(PARAM_UUID); @@ -166,14 +172,16 @@ public class LinesAction implements SourcesWsAction { return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest); } - private void writeSource(Iterable lines, JsonWriter json) { + private void writeSource(Iterable lines, JsonWriter json, boolean filterScmAuthors) { json.name("sources").beginArray(); for (DbFileSources.Line line : lines) { json.beginObject() .prop("line", line.getLine()) .prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.getSource(), line.getHighlighting(), line.getSymbols())) - .prop("scmAuthor", line.getScmAuthor()) .prop("scmRevision", line.getScmRevision()); + if (!filterScmAuthors) { + json.prop("scmAuthor", line.getScmAuthor()); + } if (line.hasScmDate()) { json.prop("scmDate", DateUtils.formatDateTime(new Date(line.getScmDate()))); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java index b73253e059b..1bc20879f51 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java @@ -75,11 +75,11 @@ public abstract class AbstractUserSession implements UserSession { protected abstract boolean hasProjectUuidPermission(String permission, String projectUuid); @Override - public final boolean hasMembership(OrganizationDto organization) { - return isRoot() || hasMembershipImpl(organization); + public final boolean hasMembership(OrganizationDto organizationDto) { + return isRoot() || hasMembershipImpl(organizationDto); } - protected abstract boolean hasMembershipImpl(OrganizationDto organization); + protected abstract boolean hasMembershipImpl(OrganizationDto organizationDto); @Override public final List keepAuthorizedComponents(String permission, Collection components) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java b/server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java index 4f1ccc5b97a..434d3731e85 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java @@ -124,7 +124,7 @@ public final class DoPrivileged { } @Override - public boolean hasMembershipImpl(OrganizationDto organization) { + public boolean hasMembershipImpl(OrganizationDto organizationDto) { return true; } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java index 504b2929032..e366d1713da 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java @@ -228,18 +228,18 @@ public class ServerUserSession extends AbstractUserSession { } @Override - public boolean hasMembershipImpl(OrganizationDto organization) { - return isMember(organization); + public boolean hasMembershipImpl(OrganizationDto organizationDto) { + return isMember(organizationDto.getUuid()); } - private boolean isMember(OrganizationDto organization) { + private boolean isMember(String organizationUuid) { if (!isLoggedIn()) { return false; } if (isRoot()) { return true; } - String organizationUuid = organization.getUuid(); + if (organizationMembership.contains(organizationUuid)) { return true; } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java index 79287e99fad..8b7fb1b6ee2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java @@ -166,8 +166,8 @@ public class ThreadLocalUserSession implements UserSession { } @Override - public boolean hasMembership(OrganizationDto organization) { - return get().hasMembership(organization); + public boolean hasMembership(OrganizationDto organizationDto) { + return get().hasMembership(organizationDto); } @Override diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java index f4463790ac3..f8077aaf54b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java @@ -32,6 +32,7 @@ import org.sonar.core.issue.FieldDiffs; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; +import org.sonar.db.organization.OrganizationDto; import org.sonar.db.rule.RuleDto; import org.sonar.db.user.UserDto; import org.sonar.db.user.UserTesting; @@ -67,7 +68,7 @@ public class ChangelogActionTest { private ComponentDto project; private ComponentDto file; - private WsActionTester tester = new WsActionTester(new ChangelogAction(db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new AvatarResolverImpl())); + private WsActionTester tester = new WsActionTester(new ChangelogAction(db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new AvatarResolverImpl(), userSession)); @Before public void setUp() throws Exception { @@ -79,7 +80,9 @@ public class ChangelogActionTest { public void return_changelog() { UserDto user = insertUser(); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -92,14 +95,30 @@ public class ChangelogActionTest { assertThat(result.getChangelogList().get(0).getDiffsList()).extracting(Diff::getKey, Diff::getOldValue, Diff::getNewValue).containsOnly(tuple("severity", "MAJOR", "BLOCKER")); } + @Test + public void return_empty_changelog_when_not_member() { + UserDto user = insertUser(); + IssueDto issueDto = db.issues().insertIssue(newIssue()); + userSession.logIn("john") + .addProjectPermission(USER, project, file); + db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())); + + ChangelogWsResponse result = call(issueDto.getKey()); + + assertThat(result.getChangelogList()).hasSize(0); + } + @Test public void changelog_of_file_move_contains_file_names() { RuleDto rule = db.rules().insertRule(newRuleDto()); - ComponentDto project = db.components().insertPrivateProject(db.organizations().insert()); + OrganizationDto org = db.organizations().insert(); + ComponentDto project = db.components().insertPrivateProject(org); ComponentDto file1 = db.components().insertComponent(newFileDto(project)); ComponentDto file2 = db.components().insertComponent(newFileDto(project)); IssueDto issueDto = db.issues().insertIssue(newDto(rule, file2, project)); - userSession.logIn("john").addProjectPermission(USER, project, file1, file2); + userSession.logIn("john") + .addMembership(org) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setDiff("file", file1.uuid(), file2.uuid()).setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -114,7 +133,9 @@ public class ChangelogActionTest { @Test public void changelog_of_file_move_is_empty_when_files_does_not_exists() { IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setDiff("file", "UNKNOWN_1", "UNKNOWN_2").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -128,7 +149,9 @@ public class ChangelogActionTest { public void return_changelog_on_user_without_email() { UserDto user = db.users().insertUser(UserTesting.newUserDto("john", "John", null)); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -142,7 +165,9 @@ public class ChangelogActionTest { @Test public void return_changelog_not_having_user() { IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(null).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -157,7 +182,9 @@ public class ChangelogActionTest { @Test public void return_changelog_on_none_existing_user() { IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid("UNKNOWN").setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -173,7 +200,9 @@ public class ChangelogActionTest { public void return_multiple_diffs() { UserDto user = insertUser(); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()) .setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()) .setDiff("status", "RESOLVED", "CLOSED").setCreationDate(new Date())); @@ -189,7 +218,9 @@ public class ChangelogActionTest { public void return_changelog_when_no_old_value() { UserDto user = insertUser(); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", null, "BLOCKER").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -202,7 +233,9 @@ public class ChangelogActionTest { public void return_changelog_when_no_new_value() { UserDto user = insertUser(); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", null).setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -215,7 +248,9 @@ public class ChangelogActionTest { public void return_many_changelog() { UserDto user = insertUser(); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()), new FieldDiffs().setDiff("status", "RESOLVED", "CLOSED").setCreationDate(new Date())); @@ -229,7 +264,9 @@ public class ChangelogActionTest { public void replace_technical_debt_key_by_effort() { UserDto user = insertUser(); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("technicalDebt", "10", "20").setCreationDate(new Date())); ChangelogWsResponse result = call(issueDto.getKey()); @@ -261,7 +298,9 @@ public class ChangelogActionTest { public void test_example() { UserDto user = db.users().insertUser(newUserDto("john.smith", "John Smith", "john@smith.com")); IssueDto issueDto = db.issues().insertIssue(newIssue()); - userSession.logIn("john").addProjectPermission(USER, project, file); + userSession.logIn("john") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(USER, project, file); db.issues().insertFieldDiffs(issueDto, new FieldDiffs() .setUserUuid(user.getUuid()) .setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()) diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java index 3f88cfabadd..4f0576729bd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java @@ -25,6 +25,7 @@ import java.util.Date; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.sonar.api.config.internal.MapSettings; import org.sonar.api.resources.Languages; import org.sonar.api.resources.Qualifiers; import org.sonar.api.rule.RuleKey; @@ -37,13 +38,14 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueDto; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.user.UserDto; import org.sonar.server.es.EsTester; import org.sonar.server.issue.IssueFieldsSetter; -import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.TransitionService; 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.issue.index.IssueQueryFactory; import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.permission.index.PermissionIndexerTester; @@ -105,8 +107,8 @@ public class SearchActionComponentsTest { private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl()); private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer); - private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, System2.INSTANCE, - dbClient)); + private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, + new MapSettings().asConfig(), System2.INSTANCE, dbClient)); @Test public void search_all_issues_when_no_parameter() { @@ -644,7 +646,7 @@ public class SearchActionComponentsTest { } @Test - public void search_by_author() throws Exception { + public void search_by_author() { ComponentDto project = db.components().insertPublicProject(p -> p.setDbKey("PK1")); ComponentDto file = db.components().insertComponent(newFileDto(project, null, "F1").setDbKey("FK1")); RuleDefinitionDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.of("xoo", "x1"))); @@ -653,6 +655,10 @@ public class SearchActionComponentsTest { allowAnyoneOnProjects(project); indexIssues(); + UserDto user = db.users().insertUser(); + db.organizations().addMember(db.getDefaultOrganization(), user); + userSession.logIn(user).addMembership(db.getDefaultOrganization()); + ws.newRequest() .setParam(IssuesWsParameters.PARAM_AUTHORS, "leia") .setParam(WebService.Param.FACETS, "authors") diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java index dc5b66f1721..e4a45d4eb94 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java @@ -27,6 +27,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.sonar.api.config.internal.MapSettings; import org.sonar.api.resources.Languages; import org.sonar.api.rule.RuleStatus; import org.sonar.api.rules.RuleType; @@ -98,28 +99,28 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES; public class SearchActionTest { - @Rule - public UserSessionRule userSessionRule = standalone(); - @Rule - public DbTester db = DbTester.create(); - @Rule - public EsTester es = EsTester.create(); - @Rule - public ExpectedException expectedException = none(); - - private DbClient dbClient = db.getDbClient(); - private DbSession session = db.getSession(); - 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 IssueQueryFactory issueQueryFactory = new IssueQueryFactory(dbClient, Clock.systemUTC(), userSessionRule); - private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); - private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter); - private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSessionRule, dbClient, new TransitionService(userSessionRule, issueWorkflow)); - private Languages languages = new Languages(); - private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl()); - private WsActionTester ws = new WsActionTester(new SearchAction(userSessionRule, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, System2.INSTANCE, - dbClient)); - private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer); + @Rule + public UserSessionRule userSession = standalone(); + @Rule + public DbTester db = DbTester.create(); + @Rule + public EsTester es = EsTester.create(); + @Rule + public ExpectedException expectedException = none(); + + private DbClient dbClient = db.getDbClient(); + private DbSession session = db.getSession(); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); + private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(dbClient, Clock.systemUTC(), userSession); + private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); + private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter); + private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow)); + private Languages languages = new Languages(); + private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl()); + private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, + new MapSettings().asConfig(), System2.INSTANCE, dbClient)); + private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer); private OrganizationDto defaultOrganization; private OrganizationDto otherOrganization1; @@ -248,6 +249,7 @@ public class SearchActionTest { @Test public void response_contains_all_fields_except_additional_fields() { UserDto simon = db.users().insertUser(u -> u.setLogin("simon").setName("Simon").setEmail("simon@email.com")); + db.organizations().addMember(otherOrganization2, simon); ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setDbKey("PROJECT_KEY")); indexPermissions(); @@ -269,9 +271,34 @@ public class SearchActionTest { db.issues().insertIssue(issue); indexIssues(); + userSession.logIn(simon); ws.newRequest().execute().assertJson(this.getClass(), "response_contains_all_fields_except_additional_fields.json"); } + @Test + public void hide_author_if_not_member_of_organization() { + ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setDbKey("PROJECT_KEY")); + indexPermissions(); + ComponentDto file = insertComponent(newFileDto(project, null, "FILE_ID").setDbKey("FILE_KEY")); + IssueDto issue = newDto(newExternalRule(), file, project) + .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2") + .setEffort(10L) + .setLine(42) + .setChecksum("a227e508d6646b55a086ee11d63b21e9") + .setMessage("the message") + .setStatus(STATUS_RESOLVED) + .setResolution(RESOLUTION_FIXED) + .setSeverity("MAJOR") + .setAuthorLogin("John") + .setTags(asList("bug", "owasp")) + .setIssueCreationDate(DateUtils.parseDateTime("2014-09-04T00:00:00+0100")) + .setIssueUpdateDate(DateUtils.parseDateTime("2017-12-04T00:00:00+0100")); + db.issues().insertIssue(issue); + indexIssues(); + + ws.newRequest().execute().assertJson(this.getClass(), "author_is_hidden.json"); + } + @Test public void issue_with_cross_file_locations() { UserDto simon = db.users().insertUser(); @@ -369,7 +396,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); ws.newRequest() .setParam("additionalFields", "comments,users") .execute() @@ -405,7 +432,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); TestResponse result = ws.newRequest().setParam(PARAM_HIDE_COMMENTS, "true").execute(); result.assertJson(this.getClass(), "issue_with_comment_hidden.json"); assertThat(result.getInput()).doesNotContain(fabrice.getLogin()); @@ -426,7 +453,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn("john"); + userSession.logIn("john"); ws.newRequest() .setParam("additionalFields", "_all").execute() .assertJson(this.getClass(), "load_additional_fields.json"); @@ -450,7 +477,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn("john") + userSession.logIn("john") .addProjectPermission(ISSUE_ADMIN, project); // granted by Anyone ws.newRequest() .setParam("additionalFields", "_all").execute() @@ -468,7 +495,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn("john") + userSession.logIn("john") .addProjectPermission(ISSUE_ADMIN, project); // granted by Anyone indexPermissions(); @@ -552,7 +579,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn("john"); + userSession.logIn("john"); ws.newRequest() .setParam("resolved", "false") .setParam(PARAM_COMPONENT_KEYS, project.getKey()) @@ -579,7 +606,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); ws.newRequest() .setParam("resolved", "false") .setParam(PARAM_COMPONENT_KEYS, project.getKey()) @@ -607,7 +634,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); ws.newRequest() .setParam(PARAM_COMPONENT_KEYS, project.getKey()) .setParam("resolved", "false") @@ -623,7 +650,7 @@ public class SearchActionTest { // login looks like an invalid regexp UserDto user = db.users().insertUser(u -> u.setLogin("foo[").setName("foo").setEmail("foo@email.com")); - userSessionRule.logIn(user); + userSession.logIn(user); // should not fail ws.newRequest() @@ -670,7 +697,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); ws.newRequest() .setParam("resolved", "false") @@ -718,7 +745,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); Issues.SearchWsResponse response = ws.newRequest() .setParam("resolved", "false") @@ -732,7 +759,7 @@ public class SearchActionTest { @Test public void filter_by_assigned_to_me_unauthenticated() { UserDto poy = db.users().insertUser(u -> u.setLogin("poy").setName("poypoy").setEmail("poypoy@email.com")); - userSessionRule.logIn(poy); + userSession.logIn(poy); // TODO : check test title w julien @@ -803,7 +830,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn(john); + userSession.logIn(john); ws.newRequest() .setParam("resolved", "false") .setParam("assignees", "alice") @@ -925,7 +952,7 @@ public class SearchActionTest { session.commit(); indexIssues(); - userSessionRule.logIn("john"); + userSession.logIn("john"); ws.newRequest() .setParam("resolved", "false") .setParam(WebService.Param.FACETS, "severities") @@ -979,7 +1006,7 @@ public class SearchActionTest { .setResourceId(project.getId()) .setRole(permission)); session.commit(); - userSessionRule.logIn().addProjectPermission(permission, project); + userSession.logIn().addProjectPermission(permission, project); } private ComponentDto insertComponent(ComponentDto component) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java new file mode 100644 index 00000000000..5c38990c443 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java @@ -0,0 +1,167 @@ +package org.sonar.server.issue.ws;/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import java.time.Clock; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.resources.Languages; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.Durations; +import org.sonar.api.utils.System2; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.issue.IssueFieldsSetter; +import org.sonar.server.issue.TransitionService; +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.issue.index.IssueQueryFactory; +import org.sonar.server.issue.workflow.FunctionExecutor; +import org.sonar.server.issue.workflow.IssueWorkflow; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.WsActionTester; +import org.sonar.server.ws.WsResponseCommonFormat; +import org.sonar.test.JsonAssert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.server.ws.WebService.Param.FACETS; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ORGANIZATION; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_UUIDS; + +public class SearchActionTestOnSonarCloud { + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + @Rule + public EsTester es = EsTester.create(); + + private MapSettings mapSettings = new MapSettings() + .setProperty("sonar.sonarcloud.enabled", true); + + private DbClient dbClient = db.getDbClient(); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); + //private ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client()); + private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(dbClient, Clock.systemUTC(), userSession); + private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); + private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter); + private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow)); + private Languages languages = new Languages(); + private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl()); + private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer); + + private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, + mapSettings.asConfig(), System2.INSTANCE, dbClient)); + + private OrganizationDto organization; + private UserDto user; + private ComponentDto project; + + @Before + public void setup() { + organization = db.organizations().insert(o -> o.setKey("org-1")); + user = db.users().insertUser(); + + project = db.components().insertPublicProject(organization, p -> p.setDbKey("PK1")); + ComponentDto file = db.components().insertComponent(newFileDto(project, null, "F1").setDbKey("FK1")); + RuleDefinitionDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.of("xoo", "x1"))); + db.issues().insert(rule, project, file, i -> i.setAuthorLogin("leia").setKee("2bd4eac2-b650-4037-80bc-7b112bd4eac2")); + db.issues().insert(rule, project, file, i -> i.setAuthorLogin("luke@skywalker.name").setKee("82fd47d4-b650-4037-80bc-7b1182fd47d4")); + db.commit(); + allowAnyoneOnProjects(project); + indexIssues(); + } + + @Test + public void authors_facet_is_hidden_if_organization_is_not_set() { + db.organizations().addMember(organization, user); + userSession + .logIn(user) + .addMembership(organization); + + String input = ws.newRequest() + .setParam(PARAM_PROJECT_UUIDS, project.uuid()) + .setParam(FACETS, "authors") + .execute() + .getInput(); + + JsonAssert.assertJson(input).isSimilarTo(this.getClass().getResource(this.getClass().getSimpleName() + "/no_authors_facet.json")); + + JsonElement gson = new JsonParser().parse(input); + assertThat(gson.getAsJsonObject().get("facets")).isNull(); + } + + @Test + public void authors_facet_is_hidden_if_user_is_not_a_member_of_the_organization() { + userSession + .logIn(user); + + String input = ws.newRequest() + .setParam(PARAM_PROJECT_UUIDS, project.uuid()) + .setParam(FACETS, "authors") + .execute() + .getInput(); + + JsonAssert.assertJson(input).isSimilarTo(this.getClass().getResource(this.getClass().getSimpleName() + "/no_author_and_no_authors_facet.json")); + + JsonElement gson = new JsonParser().parse(input); + assertThat(gson.getAsJsonObject().get("facets")).isNull(); + + } + + @Test + public void authors_facet_is_shown_if_organization_is_set_and_user_is_member_of_the_organization() { + db.organizations().addMember(organization, user); + + userSession + .logIn(user) + .addMembership(organization); + + ws.newRequest() + .setParam(PARAM_PROJECT_UUIDS, project.uuid()) + .setParam(FACETS, "authors") + .setParam(PARAM_ORGANIZATION, organization.getKey()) + .execute() + .assertJson(this.getClass(), "with_authors_facet.json"); + } + + private void indexIssues() { + issueIndexer.indexOnStartup(null); + } + + private void allowAnyoneOnProjects(ComponentDto... projects) { + userSession.registerComponents(projects); + Arrays.stream(projects).forEach(p -> permissionIndexer.allowOnlyAnyone(p)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java index 25cfea083de..70f0811c185 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java @@ -19,12 +19,10 @@ */ package org.sonar.server.source.ws; -import java.io.IOException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.sonar.api.utils.System2; import org.sonar.api.web.UserRole; @@ -32,8 +30,10 @@ import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDao; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; import org.sonar.db.protobuf.DbFileSources; import org.sonar.db.source.FileSourceDto; +import org.sonar.db.user.UserDto; import org.sonar.server.component.TestComponentFinder; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; @@ -42,6 +42,7 @@ import org.sonar.server.source.SourceService; import org.sonar.server.source.index.FileSourceTesting; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsTester; +import org.sonar.server.ws.WsTester.TestRequest; import static java.lang.String.format; import static org.mockito.ArgumentMatchers.anyString; @@ -52,74 +53,60 @@ import static org.sonar.db.component.ComponentTesting.newFileDto; public class LinesActionTest { - private static final String PROJECT_UUID = "abcd"; - private static final String FILE_UUID = "efgh"; - private static final String FILE_KEY = "Foo.java"; - @Rule public ExpectedException expectedException = ExpectedException.none(); - @Rule public DbTester db = DbTester.create(System2.INSTANCE); - @Rule public UserSessionRule userSession = UserSessionRule.standalone(); - private SourceService sourceService; - private HtmlSourceDecorator htmlSourceDecorator; - private ComponentDao componentDao; - - private ComponentDto project; - private ComponentDto file; - + private ComponentDao componentDao = new ComponentDao(); + private ComponentDto privateProject; + private OrganizationDto organization; private WsTester wsTester; @Before public void setUp() { - htmlSourceDecorator = mock(HtmlSourceDecorator.class); - when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then(new Answer() { - @Override - public String answer(InvocationOnMock invocationOnMock) { - return "

" + invocationOnMock.getArguments()[0] + "

"; - } - }); - sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator); - componentDao = new ComponentDao(); + HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class); + when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer) + invocationOnMock -> "

" + invocationOnMock.getArguments()[0] + "

"); + SourceService sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator); wsTester = new WsTester(new SourcesWs( new LinesAction(TestComponentFinder.from(db), db.getDbClient(), sourceService, htmlSourceDecorator, userSession))); - project = ComponentTesting.newPrivateProjectDto(db.organizations().insert(), PROJECT_UUID); - file = newFileDto(project, null, FILE_UUID).setDbKey(FILE_KEY); + organization = db.organizations().insert(); + privateProject = ComponentTesting.newPrivateProjectDto(organization); } @Test public void show_source() throws Exception { - insertFileWithData(FileSourceTesting.newFakeData(3).build()); - setUserWithValidPermission(); + ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(3).build(), privateProject); + setUserWithValidPermission(file); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", FILE_UUID); + TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", file.uuid()); request.execute().assertJson(getClass(), "show_source.json"); } @Test public void fail_to_show_source_if_no_source_found() throws Exception { - setUserWithValidPermission(); - insertFile(); + ComponentDto file = insertFile(privateProject); + setUserWithValidPermission(file); expectedException.expect(NotFoundException.class); - wsTester.newGetRequest("api/sources", "lines").setParam("uuid", FILE_UUID).execute(); + wsTester.newGetRequest("api/sources", "lines").setParam("uuid", file.uuid()).execute(); } @Test public void show_paginated_lines() throws Exception { - setUserWithValidPermission(); - insertFileWithData(FileSourceTesting.newFakeData(3).build()); + ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(3).build(), privateProject); + setUserWithValidPermission(file); - WsTester.TestRequest request = wsTester + wsTester .newGetRequest("api/sources", "lines") - .setParam("uuid", FILE_UUID) + .setParam("uuid", file.uuid()) .setParam("from", "3") - .setParam("to", "3"); - request.execute().assertJson(getClass(), "show_paginated_lines.json"); + .setParam("to", "3") + .execute() + .assertJson(getClass(), "show_paginated_lines.json"); } @Test @@ -133,13 +120,16 @@ public class LinesActionTest { .setFileUuid(file.uuid()) .setSourceData(FileSourceTesting.newFakeData(3).build())); db.commit(); - userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines") - .setParam("key", file.getKey()) - .setParam("branch", file.getBranch()); + userSession.logIn("login") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(UserRole.CODEVIEWER, project, file); - request.execute().assertJson(getClass(), "show_source.json"); + wsTester.newGetRequest("api/sources", "lines") + .setParam("key", file.getKey()) + .setParam("branch", file.getBranch()) + .execute() + .assertJson(getClass(), "show_source.json"); } @Test @@ -153,13 +143,16 @@ public class LinesActionTest { .setFileUuid(file.uuid()) .setSourceData(FileSourceTesting.newFakeData(3).build())); db.commit(); - userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines") - .setParam("key", file.getKey()) - .setParam("pullRequest", file.getPullRequest()); + userSession.logIn("login") + .addMembership(db.getDefaultOrganization()) + .addProjectPermission(UserRole.CODEVIEWER, project, file); - request.execute().assertJson(getClass(), "show_source.json"); + wsTester.newGetRequest("api/sources", "lines") + .setParam("key", file.getKey()) + .setParam("pullRequest", file.getPullRequest()) + .execute() + .assertJson(getClass(), "show_source.json"); } @Test @@ -167,7 +160,7 @@ public class LinesActionTest { expectedException.expect(IllegalArgumentException.class); expectedException.expectMessage("Either 'uuid' or 'key' must be provided"); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines"); + TestRequest request = wsTester.newGetRequest("api/sources", "lines"); request.execute(); } @@ -176,7 +169,7 @@ public class LinesActionTest { expectedException.expect(NotFoundException.class); expectedException.expectMessage("Component key 'Foo.java' not found"); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", FILE_KEY); + TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", "Foo.java"); request.execute(); } @@ -185,50 +178,50 @@ public class LinesActionTest { expectedException.expect(NotFoundException.class); expectedException.expectMessage("Component id 'ABCD' not found"); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", "ABCD"); + TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", "ABCD"); request.execute(); } @Test public void fail_when_file_is_removed() throws Exception { - ComponentDto file = newFileDto(project).setDbKey("file-key").setEnabled(false); - db.components().insertComponents(project, file); - setUserWithValidPermission(); + ComponentDto file = newFileDto(privateProject).setDbKey("file-key").setEnabled(false); + db.components().insertComponents(privateProject, file); + setUserWithValidPermission(file); expectedException.expect(NotFoundException.class); expectedException.expectMessage("Component key 'file-key' not found"); - WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", "file-key"); + TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", "file-key"); request.execute(); } @Test(expected = ForbiddenException.class) public void check_permission() throws Exception { - insertFileWithData(FileSourceTesting.newFakeData(1).build()); + ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(1).build(), privateProject); userSession.logIn("login"); wsTester.newGetRequest("api/sources", "lines") - .setParam("uuid", FILE_UUID) + .setParam("uuid", file.uuid()) .execute(); } @Test public void display_deprecated_fields() throws Exception { - insertFileWithData(FileSourceTesting.newFakeData(1).build()); - setUserWithValidPermission(); + ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(1).build(), privateProject); + setUserWithValidPermission(file); - WsTester.TestRequest request = wsTester + wsTester .newGetRequest("api/sources", "lines") - .setParam("uuid", FILE_UUID); - - request.execute().assertJson(getClass(), "display_deprecated_fields.json"); + .setParam("uuid", file.uuid()) + .execute() + .assertJson(getClass(), "display_deprecated_fields.json"); } @Test public void use_deprecated_overall_coverage_fields_if_exists() throws Exception { DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder(); - insertFileWithData(dataBuilder.addLines(newLineBuilder() + ComponentDto file = insertFileWithData(dataBuilder.addLines(newLineBuilder() .setDeprecatedOverallLineHits(1) .setDeprecatedOverallConditions(2) .setDeprecatedOverallCoveredConditions(3) @@ -237,31 +230,31 @@ public class LinesActionTest { .setDeprecatedUtCoveredConditions(3) .setDeprecatedItLineHits(1) .setDeprecatedItConditions(2) - .setDeprecatedItCoveredConditions(3)).build()); - setUserWithValidPermission(); + .setDeprecatedItCoveredConditions(3)).build(), privateProject); + setUserWithValidPermission(file); - WsTester.TestRequest request = wsTester + wsTester .newGetRequest("api/sources", "lines") - .setParam("uuid", FILE_UUID); - - request.execute().assertJson(getClass(), "convert_deprecated_data.json"); + .setParam("uuid", file.uuid()) + .execute() + .assertJson(getClass(), "convert_deprecated_data.json"); } @Test public void use_deprecated_ut_coverage_fields_if_exists() throws Exception { DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder(); - insertFileWithData(dataBuilder.addLines(newLineBuilder() + ComponentDto file = insertFileWithData(dataBuilder.addLines(newLineBuilder() .setDeprecatedUtLineHits(1) .setDeprecatedUtConditions(2) .setDeprecatedUtCoveredConditions(3) .setDeprecatedItLineHits(1) .setDeprecatedItConditions(2) - .setDeprecatedItCoveredConditions(3)).build()); - setUserWithValidPermission(); + .setDeprecatedItCoveredConditions(3)).build(), privateProject); + setUserWithValidPermission(file); - WsTester.TestRequest request = wsTester + TestRequest request = wsTester .newGetRequest("api/sources", "lines") - .setParam("uuid", FILE_UUID); + .setParam("uuid", file.uuid()); request.execute().assertJson(getClass(), "convert_deprecated_data.json"); } @@ -269,15 +262,15 @@ public class LinesActionTest { @Test public void use_deprecated_it_coverage_fields_if_exists() throws Exception { DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder(); - insertFileWithData(dataBuilder.addLines(newLineBuilder() + ComponentDto file = insertFileWithData(dataBuilder.addLines(newLineBuilder() .setDeprecatedItLineHits(1) .setDeprecatedItConditions(2) - .setDeprecatedItCoveredConditions(3)).build()); - setUserWithValidPermission(); + .setDeprecatedItCoveredConditions(3)).build(), privateProject); + setUserWithValidPermission(file); - WsTester.TestRequest request = wsTester + TestRequest request = wsTester .newGetRequest("api/sources", "lines") - .setParam("uuid", FILE_UUID); + .setParam("uuid", file.uuid()); request.execute().assertJson(getClass(), "convert_deprecated_data.json"); } @@ -342,22 +335,66 @@ public class LinesActionTest { .execute(); } - private void insertFileWithData(DbFileSources.Data fileData) throws IOException { - insertFile(); + @Test + public void hide_scmAuthors_if_not_member_of_organization() throws Exception { + OrganizationDto org = db.organizations().insert(); + ComponentDto publicProject = db.components().insertPublicProject(org); + userSession.registerComponents(publicProject); + + DbFileSources.Data data = DbFileSources.Data.newBuilder() + .addLines(newLineBuilder().setScmAuthor("isaac@asimov.com")) + .build(); + + ComponentDto file = insertFileWithData(data, publicProject); + + wsTester.newGetRequest("api/sources", "lines") + .setParam("uuid", file.uuid()) + .execute() + .assertJson(getClass(), "hide_scmAuthors.json"); + } + + @Test + public void show_scmAuthors_if_member_of_organization() throws Exception { + OrganizationDto org = db.organizations().insert(); + ComponentDto publicProject = db.components().insertPublicProject(org); + UserDto user = db.users().insertUser(); + userSession.logIn(user) + .registerComponents(publicProject) + .addMembership(org); + + DbFileSources.Data data = DbFileSources.Data.newBuilder() + .addLines(newLineBuilder().setScmAuthor("isaac@asimov.com")) + .build(); + + ComponentDto file = insertFileWithData(data, publicProject); + + wsTester.newGetRequest("api/sources", "lines") + .setParam("uuid", file.uuid()) + .execute() + .assertJson(getClass(), "show_scmAuthors.json"); + } + + private ComponentDto insertFileWithData(DbFileSources.Data fileData, ComponentDto project) { + ComponentDto file = insertFile(project); db.getDbClient().fileSourceDao().insert(db.getSession(), new FileSourceDto() - .setProjectUuid(PROJECT_UUID) - .setFileUuid(FILE_UUID) + .setProjectUuid(project.projectUuid()) + .setFileUuid(file.uuid()) .setSourceData(fileData)); db.commit(); + return file; } - private void setUserWithValidPermission() { - userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file); + private void setUserWithValidPermission(ComponentDto file) { + userSession.logIn("login") + .addProjectPermission(UserRole.CODEVIEWER, privateProject, file) + .addMembership(organization); } - private void insertFile() { - componentDao.insert(db.getSession(), project, file); + private ComponentDto insertFile(ComponentDto project) { + ComponentDto file = newFileDto(project); + componentDao.insert(db.getSession(), file); db.getSession().commit(); + return file; } private DbFileSources.Line.Builder newLineBuilder() { diff --git a/server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java b/server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java index 942f2336cef..785ad3c8963 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java +++ b/server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java @@ -110,8 +110,8 @@ public abstract class AbstractMockUserSession } @Override - protected boolean hasMembershipImpl(OrganizationDto organization) { - return organizationMembership.contains(organization.getUuid()); + protected boolean hasMembershipImpl(OrganizationDto organizationDto) { + return organizationMembership.contains(organizationDto.getUuid()); } public void addOrganizationMembership(OrganizationDto organization) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java b/server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java index 98af9348a66..e080df152cc 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java +++ b/server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java @@ -65,7 +65,7 @@ public class AnonymousMockUserSession extends AbstractMockUserSession; + return ; } diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index dd2288d8346..e4486728c14 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -52,7 +52,14 @@ import { serializeQuery, STANDARDS } from '../utils'; -import { Component, CurrentUser, Issue, Paging, BranchLike } from '../../../app/types'; +import { + Component, + CurrentUser, + Issue, + Paging, + BranchLike, + Organization +} from '../../../app/types'; import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication'; import Dropdown from '../../../components/controls/Dropdown'; import ListFooter from '../../../components/controls/ListFooter'; @@ -92,10 +99,12 @@ interface Props { component?: Component; currentUser: CurrentUser; fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise; + hideAuthorFacet?: boolean; location: { pathname: string; query: RawQuery }; myIssues?: boolean; onBranchesChange: () => void; organization?: { key: string }; + userOrganizations: Organization[]; } export interface State { @@ -407,7 +416,7 @@ export default class App extends React.PureComponent { requestFacets = false, requestOrganizations = true ): Promise => { - const { component, organization } = this.props; + const { component } = this.props; const { myIssues, openFacets, query } = this.state; const facets = requestFacets @@ -418,13 +427,17 @@ export default class App extends React.PureComponent { .join(',') : undefined; + const organizationKey = + (component && component.organization) || + (this.props.organization && this.props.organization.key); + const parameters = { ...getBranchLikeQuery(this.props.branchLike), componentKeys: component && component.key, s: 'FILE_LINE', ...serializeQuery(query), ps: '100', - organization: organization && organization.key, + organization: organizationKey, facets, ...additional }; @@ -860,9 +873,21 @@ export default class App extends React.PureComponent { } renderFacets() { - const { component, currentUser } = this.props; + const { component, currentUser, userOrganizations } = this.props; const { query } = this.state; + const organizationKey = + (component && component.organization) || + (this.props.organization && this.props.organization.key); + + const userOrganization = + !isSonarCloud() || + userOrganizations.find(o => { + return o.key === organizationKey; + }); + const hideAuthorFacet = + this.props.hideAuthorFacet || (isSonarCloud() && this.props.myIssues) || !userOrganization; + return (
{currentUser.isLoggedIn && @@ -876,6 +901,7 @@ export default class App extends React.PureComponent { ({ - currentUser: getCurrentUser(state) + currentUser: getCurrentUser(state), + userOrganizations: getMyOrganizations(state) }); const fetchIssueOrganizations = (organizationKeys: string[]) => (dispatch: Dispatch) => { @@ -83,6 +89,7 @@ const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps; interface OwnProps { location: { pathname: string; query: RawQuery }; + hideAuthorFacet?: boolean; myIssues?: boolean; } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index ace5779f36b..d888dbbb507 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -46,6 +46,7 @@ import { Component } from '../../../app/types'; export interface Props { component: Component | undefined; facets: { [facet: string]: Facet }; + hideAuthorFacet?: boolean; loading?: boolean; loadingFacets: { [key: string]: boolean }; myIssues: boolean; @@ -62,14 +63,14 @@ export interface Props { export default class Sidebar extends React.PureComponent { render() { - const { component, facets, openFacets, query } = this.props; + const { component, facets, hideAuthorFacet, openFacets, query } = this.props; const displayProjectsFacet = !component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier); const displayModulesFacet = component !== undefined && component.qualifier !== 'DIR'; const displayDirectoriesFacet = component !== undefined && component.qualifier !== 'DIR'; const displayFilesFacet = component !== undefined; - const displayAuthorFacet = !component || component.qualifier !== 'DEV'; + const displayAuthorFacet = !hideAuthorFacet && (!component || component.qualifier !== 'DEV'); const organizationKey = (component && component.organization) || diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index b9d98b78d9a..fbbbf4dafae 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -158,6 +158,10 @@ export function mapFacet(facet: string) { } export function parseFacets(facets: RawFacet[]) { + if (!facets) { + return {}; + } + // for readability purpose const propertyMapping: { [x: string]: string } = { fileUuids: 'files', diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx index 83c326a12ea..394c321b12d 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx @@ -49,7 +49,7 @@ export default class LineSCM extends React.PureComponent { const { line, popupOpen, previousLine } = this.props; const hasPopup = !!line.line; const cell = isSCMChanged(line, previousLine) && ( -
+
); return hasPopup ? ( { function isSCMChanged(s: SourceLine, p: SourceLine | undefined) { let changed = true; - if (p != null && s.scmAuthor != null && p.scmAuthor != null) { - changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate; + if (p != null && s.scmRevision != null && p.scmRevision != null) { + changed = s.scmRevision !== p.scmRevision || s.scmDate !== p.scmDate; } return changed; } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx index fa1b766c12a..52f78a7382b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import { SourceLine } from '../../../app/types'; import { DropdownOverlay } from '../../controls/Dropdown'; import DateFormatter from '../../intl/DateFormatter'; @@ -28,12 +29,13 @@ interface Props { } export default function SCMPopup({ line }: Props) { + const hasAuthor = line.scmAuthor !== ''; return (
-
{line.scmAuthor}
+ {hasAuthor &&
{line.scmAuthor}
} {line.scmDate && ( -
+
)} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx index 29e82eb3d61..343f9350aad 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx @@ -32,7 +32,7 @@ it('render scm details', () => { }); it('render scm details for the first line', () => { - const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; const wrapper = shallow( ); @@ -40,8 +40,17 @@ it('render scm details for the first line', () => { }); it('does not render scm details', () => { - const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; - const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; + const previousLine = { line: 2, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('renders ellipsis when no author info', () => { + const line = { line: 3, scmRevision: 'foo', scmDate: '2017-01-01' }; + const previousLine = { line: 2, scmRevision: 'bar', scmDate: '2017-01-01' }; const wrapper = shallow( ); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap index dac002460eb..b72bebfac30 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap @@ -18,6 +18,7 @@ exports[`does not render scm details 1`] = ` "line": 3, "scmAuthor": "foo", "scmDate": "2017-01-01", + "scmRevision": "foo", } } /> @@ -75,6 +76,7 @@ exports[`render scm details for the first line 1`] = ` "line": 3, "scmAuthor": "foo", "scmDate": "2017-01-01", + "scmRevision": "foo", } } /> @@ -87,3 +89,34 @@ exports[`render scm details for the first line 1`] = ` `; + +exports[`renders ellipsis when no author info 1`] = ` + + + } + > +
+ + +`; -- 2.39.5