Browse Source

SONAR-10864 Author information only available to members (#552)

tags/7.5
Eric Hartmann 5 years ago
parent
commit
7253fe3904
39 changed files with 876 additions and 205 deletions
  1. 1
    0
      server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl
  2. 47
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java
  3. 1
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java
  4. 55
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java
  5. 1
    1
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java
  6. 6
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql
  7. 5
    5
      server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java
  8. 28
    6
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java
  9. 46
    11
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
  10. 9
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java
  11. 6
    3
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
  12. 12
    4
      server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
  13. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
  14. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java
  15. 4
    4
      server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java
  16. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
  17. 53
    14
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java
  18. 10
    4
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
  19. 64
    37
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
  20. 167
    0
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
  21. 125
    88
      server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
  22. 2
    2
      server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java
  23. 1
    1
      server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java
  24. 1
    1
      server/sonar-server/src/test/java/org/sonar/server/user/TestUserSessionFactory.java
  25. 24
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTest/author_is_hidden.json
  26. 20
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_author_and_no_authors_facet.json
  27. 22
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_authors_facet.json
  28. 37
    0
      server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/with_authors_facet.json
  29. 11
    0
      server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/hide_scmAuthors.json
  30. 12
    0
      server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_scmAuthors.json
  31. 1
    1
      server/sonar-web/src/main/js/apps/explore/ExploreIssues.tsx
  32. 30
    4
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  33. 10
    3
      server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
  34. 3
    2
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  35. 4
    0
      server/sonar-web/src/main/js/apps/issues/utils.ts
  36. 3
    3
      server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
  37. 4
    2
      server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
  38. 12
    3
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx
  39. 33
    0
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap

+ 1
- 0
server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl View File

@@ -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,

+ 47
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java View File

@@ -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());
}
}

+ 1
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java View File

@@ -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)
;
}
}

+ 55
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java View File

@@ -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();
}

}

+ 1
- 1
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java View File

@@ -35,6 +35,6 @@ public class DbVersion73Test {

@Test
public void verify_migration_count() {
verifyMigrationCount(underTest, 12);
verifyMigrationCount(underTest, 13);
}
}

+ 6
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql View File

@@ -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")
);

+ 5
- 5
server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java View File

@@ -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;
}
}

+ 28
- 6
server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java View File

@@ -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<String, ComponentDto> files;

ChangeLogResults(DbSession dbSession, String issueKey) {
IssueDto dbIssue = issueFinder.getByKey(dbSession, issueKey);
this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dbIssue.getKey());
List<String> 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<String> 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<ComponentDto> 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<OrganizationDto> 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<String> getFileUuids(List<FieldDiffs> changes) {

+ 46
- 11
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java View File

@@ -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<String> getLogins(Request request) {

List<String> assigneeLogins = request.paramAsStrings(PARAM_ASSIGNEES);

List<String> 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<String> facets = request.getFacets();

if (facets == null || facets.isEmpty()) {
return options;
}

List<String> requestedFacets = new ArrayList<>(facets.size());
requestedFacets.addAll(facets);

if (isOnSonarCloud) {
Optional<OrganizationDto> 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;
}

+ 9
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java View File

@@ -57,6 +57,7 @@ public class SearchResponseData {
private final ListMultimap<String, String> actionsByIssueKey = ArrayListMultimap.create();
private final ListMultimap<String, Transition> transitionsByIssueKey = ArrayListMultimap.create();
private final Set<String> updatableComments = new HashSet<>();
private final Set<String> 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<String> organizationUuids) {
this.userOrganizationUuids.addAll(organizationUuids);
}

public Set<String> getUserOrganizationUuids() {
return this.userOrganizationUuids;
}

@CheckForNull
public UserDto getUserByUuid(@Nullable String userUuid) {
UserDto userDto = usersByUuid.get(userUuid);

+ 6
- 3
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java View File

@@ -82,8 +82,7 @@ public class SearchResponseFormat {
this.avatarFactory = avatarFactory;
}

public SearchWsResponse formatSearch(Set<SearchAdditionalField> fields, SearchResponseData data,
Paging paging, @Nullable Facets facets) {
public SearchWsResponse formatSearch(Set<SearchAdditionalField> 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);

+ 12
- 4
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java View File

@@ -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<DbFileSources.Line> 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<DbFileSources.Line> lines, JsonWriter json) {
private void writeSource(Iterable<DbFileSources.Line> 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())));
}

+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java View File

@@ -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<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components) {

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java View File

@@ -124,7 +124,7 @@ public final class DoPrivileged {
}

@Override
public boolean hasMembershipImpl(OrganizationDto organization) {
public boolean hasMembershipImpl(OrganizationDto organizationDto) {
return true;
}
}

+ 4
- 4
server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java View File

@@ -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;
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java View File

@@ -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

+ 53
- 14
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java View File

@@ -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())

+ 10
- 4
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java View File

@@ -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")

+ 64
- 37
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java View File

@@ -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) {

+ 167
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java View File

@@ -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));
}
}

+ 125
- 88
server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java View File

@@ -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<String>() {
@Override
public String answer(InvocationOnMock invocationOnMock) {
return "<p>" + invocationOnMock.getArguments()[0] + "</p>";
}
});
sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator);
componentDao = new ComponentDao();
HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class);
when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer<String>)
invocationOnMock -> "<p>" + invocationOnMock.getArguments()[0] + "</p>");
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() {

+ 2
- 2
server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java View File

@@ -110,8 +110,8 @@ public abstract class AbstractMockUserSession<T extends 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) {

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java View File

@@ -65,7 +65,7 @@ public class AnonymousMockUserSession extends AbstractMockUserSession<AnonymousM
}

@Override
public boolean hasMembershipImpl(OrganizationDto organization) {
public boolean hasMembershipImpl(OrganizationDto organizationDto) {
return false;
}
}

+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/user/TestUserSessionFactory.java View File

@@ -118,7 +118,7 @@ public class TestUserSessionFactory implements UserSessionFactory {
}

@Override
public boolean hasMembershipImpl(OrganizationDto organization) {
public boolean hasMembershipImpl(OrganizationDto organizationDto) {
throw notImplemented();
}


+ 24
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTest/author_is_hidden.json View File

@@ -0,0 +1,24 @@
{
"issues": [
{
"organization": "my-org-2",
"key": "82fd47d4-b650-4037-80bc-7b112bd4eac2",
"rule": "external_xoo:x1",
"severity": "MAJOR",
"component": "FILE_KEY",
"resolution": "FIXED",
"status": "RESOLVED",
"message": "the message",
"effort": "10min",
"line": 42,
"hash": "a227e508d6646b55a086ee11d63b21e9",
"tags": [
"bug",
"owasp"
],
"creationDate": "2014-09-04T01:00:00+0200",
"updateDate": "2017-12-04T00:00:00+0100",
"externalRuleEngine" : "xoo"
}
]
}

+ 20
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_author_and_no_authors_facet.json View File

@@ -0,0 +1,20 @@
{
"total": 2,
"p": 1,
"issues": [
{
"organization": "org-1",
"key": "82fd47d4-b650-4037-80bc-7b1182fd47d4",
"rule": "xoo:x1",
"component": "FK1",
"project": "PK1"
},
{
"organization": "org-1",
"key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
"rule": "xoo:x1",
"component": "FK1",
"project": "PK1"
}
]
}

+ 22
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_authors_facet.json View File

@@ -0,0 +1,22 @@
{
"total": 2,
"p": 1,
"issues": [
{
"organization": "org-1",
"key": "82fd47d4-b650-4037-80bc-7b1182fd47d4",
"rule": "xoo:x1",
"component": "FK1",
"project": "PK1",
"author": "luke@skywalker.name"
},
{
"organization": "org-1",
"key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
"rule": "xoo:x1",
"component": "FK1",
"project": "PK1",
"author": "leia"
}
]
}

+ 37
- 0
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/with_authors_facet.json View File

@@ -0,0 +1,37 @@
{
"total": 2,
"p": 1,
"issues": [
{
"organization": "org-1",
"key": "82fd47d4-b650-4037-80bc-7b1182fd47d4",
"rule": "xoo:x1",
"component": "FK1",
"project": "PK1",
"author": "luke@skywalker.name"
},
{
"organization": "org-1",
"key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
"rule": "xoo:x1",
"component": "FK1",
"project": "PK1",
"author": "leia"
}
],
"facets": [
{
"property": "authors",
"values": [
{
"val": "leia",
"count": 1
},
{
"val": "luke@skywalker.name",
"count": 1
}
]
}
]
}

+ 11
- 0
server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/hide_scmAuthors.json View File

@@ -0,0 +1,11 @@
{
"sources": [
{
"line": 1,
"code": "\u003cp\u003eSOURCE_1\u003c/p\u003e",
"scmRevision": "REVISION_1",
"scmDate": "1974-10-03T03:40:00+0100",
"duplicated": false
}
]
}

+ 12
- 0
server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_scmAuthors.json View File

@@ -0,0 +1,12 @@
{
"sources": [
{
"line": 1,
"code": "\u003cp\u003eSOURCE_1\u003c/p\u003e",
"scmAuthor": "isaac@asimov.com",
"scmRevision": "REVISION_1",
"scmDate": "1974-10-03T03:40:00+0100",
"duplicated": false
}
]
}

+ 1
- 1
server/sonar-web/src/main/js/apps/explore/ExploreIssues.tsx View File

@@ -26,5 +26,5 @@ interface Props {
}

export default function ExploreIssues(props: Props) {
return <AppContainer myIssues={false} {...props} />;
return <AppContainer hideAuthorFacet={true} myIssues={false} {...props} />;
}

+ 30
- 4
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

@@ -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<FetchIssuesPromise>;
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<Props, State> {
requestFacets = false,
requestOrganizations = true
): Promise<FetchIssuesPromise> => {
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<Props, State> {
.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<Props, State> {
}

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 (
<div className="layout-page-filters">
{currentUser.isLoggedIn &&
@@ -876,6 +901,7 @@ export default class App extends React.PureComponent<Props, State> {
<Sidebar
component={component}
facets={this.state.facets}
hideAuthorFacet={hideAuthorFacet}
loading={this.state.loading}
loadingFacets={this.state.loadingFacets}
myIssues={this.state.myIssues}

+ 10
- 3
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx View File

@@ -22,9 +22,13 @@ import { Dispatch } from 'redux';
import { uniq } from 'lodash';
import { searchIssues } from '../../../api/issues';
import { getOrganizations } from '../../../api/organizations';
import { CurrentUser } from '../../../app/types';
import { CurrentUser, Organization } from '../../../app/types';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer';
import {
getCurrentUser,
areThereCustomOrganizations,
getMyOrganizations
} from '../../../store/rootReducer';
import { lazyLoad } from '../../../components/lazyLoad';
import { parseIssueFromResponse } from '../../../helpers/issues';
import { RawQuery } from '../../../helpers/query';
@@ -32,10 +36,12 @@ import { receiveOrganizations } from '../../../store/organizations/duck';

interface StateProps {
currentUser: CurrentUser;
userOrganizations: Organization[];
}

const mapStateToProps = (state: any): StateProps => ({
currentUser: getCurrentUser(state)
currentUser: getCurrentUser(state),
userOrganizations: getMyOrganizations(state)
});

const fetchIssueOrganizations = (organizationKeys: string[]) => (dispatch: Dispatch<any>) => {
@@ -83,6 +89,7 @@ const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps;

interface OwnProps {
location: { pathname: string; query: RawQuery };
hideAuthorFacet?: boolean;
myIssues?: boolean;
}


+ 3
- 2
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx View File

@@ -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<Props> {
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) ||

+ 4
- 0
server/sonar-web/src/main/js/apps/issues/utils.ts View File

@@ -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',

+ 3
- 3
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx View File

@@ -49,7 +49,7 @@ export default class LineSCM extends React.PureComponent<Props> {
const { line, popupOpen, previousLine } = this.props;
const hasPopup = !!line.line;
const cell = isSCMChanged(line, previousLine) && (
<div className="source-line-scm-inner" data-author={line.scmAuthor} />
<div className="source-line-scm-inner" data-author={line.scmAuthor || '…'} />
);
return hasPopup ? (
<td
@@ -76,8 +76,8 @@ export default class LineSCM extends React.PureComponent<Props> {

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;
}

+ 4
- 2
server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx View File

@@ -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 (
<DropdownOverlay placement={PopupPlacement.RightTop}>
<div className="source-viewer-bubble-popup abs-width-400">
<div>{line.scmAuthor}</div>
{hasAuthor && <div>{line.scmAuthor}</div>}
{line.scmDate && (
<div className="spacer-top">
<div className={classNames({ 'spacer-top': hasAuthor })}>
<DateFormatter date={line.scmDate} />
</div>
)}

+ 12
- 3
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx View File

@@ -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(
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} />
);
@@ -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(
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
);
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(
<LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
);

+ 33
- 0
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap View File

@@ -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`] = `
</Toggler>
</td>
`;

exports[`renders ellipsis when no author info 1`] = `
<td
className="source-meta source-line-scm"
data-line-number={3}
onClick={[Function]}
role="button"
tabIndex={0}
>
<Toggler
onRequestClose={[Function]}
open={false}
overlay={
<SCMPopup
line={
Object {
"line": 3,
"scmDate": "2017-01-01",
"scmRevision": "foo",
}
}
/>
}
>
<div
className="source-line-scm-inner"
data-author="…"
/>
</Toggler>
</td>
`;

Loading…
Cancel
Save