From 7c42d81e08599b315076118e961361a96df62323 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 27 Sep 2017 12:28:56 +0200 Subject: [PATCH] SONAR-1330 Return available actions in api/qualityprofiles/search --- .../qualityprofile/QProfileEditGroupsDao.java | 14 ++- .../QProfileEditGroupsMapper.java | 2 + .../qualityprofile/QProfileEditUsersDao.java | 5 + .../QProfileEditUsersMapper.java | 2 + .../QProfileEditGroupsMapper.xml | 9 ++ .../QProfileEditUsersMapper.xml | 9 ++ .../QProfileEditGroupsDaoTest.java | 26 +++++ .../QProfileEditUsersDaoTest.java | 53 ++++++++-- .../qualityprofile/ws/SearchAction.java | 43 ++++++-- .../server/qualityprofile/ws/SearchData.java | 43 +++++--- .../qualityprofile/ws/search-example.json | 8 +- .../qualityprofile/ws/QProfilesWsTest.java | 16 +-- .../qualityprofile/ws/SearchActionTest.java | 88 ++++++++++++++--- .../main/protobuf/ws-qualityprofiles.proto | 9 +- .../QualityProfilesEditTest.java | 99 +++++++++++++------ 15 files changed, 341 insertions(+), 85 deletions(-) diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java index bd023bec7dc..93df5f96361 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsDao.java @@ -19,13 +19,18 @@ */ package org.sonar.db.qualityprofile; +import java.util.Collection; import java.util.List; import org.sonar.api.utils.System2; import org.sonar.db.Dao; +import org.sonar.db.DatabaseUtils; import org.sonar.db.DbSession; import org.sonar.db.Pagination; +import org.sonar.db.organization.OrganizationDto; import org.sonar.db.user.GroupDto; +import static org.sonar.core.util.stream.MoreCollectors.toList; + public class QProfileEditGroupsDao implements Dao { private final System2 system2; @@ -38,14 +43,19 @@ public class QProfileEditGroupsDao implements Dao { return mapper(dbSession).selectByQProfileAndGroup(profile.getKee(), group.getId()) != null; } - public int countByQuery(DbSession dbSession, SearchGroupsQuery query){ + public int countByQuery(DbSession dbSession, SearchGroupsQuery query) { return mapper(dbSession).countByQuery(query); } - public List selectByQuery(DbSession dbSession, SearchGroupsQuery query, Pagination pagination){ + public List selectByQuery(DbSession dbSession, SearchGroupsQuery query, Pagination pagination) { return mapper(dbSession).selectByQuery(query, pagination); } + public List selectQProfileUuidsByOrganizationAndGroups(DbSession dbSession, OrganizationDto organization, Collection groups) { + return DatabaseUtils.executeLargeInputs(groups.stream().map(GroupDto::getId).collect(toList()), g -> + mapper(dbSession).selectQProfileUuidsByOrganizationAndGroups(organization.getUuid(), g)); + } + public void insert(DbSession dbSession, QProfileEditGroupsDto dto) { mapper(dbSession).insert(dto, system2.now()); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java index dde92469d0f..5bdaac413bb 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.java @@ -31,6 +31,8 @@ public interface QProfileEditGroupsMapper { List selectByQuery(@Param("query") SearchGroupsQuery query, @Param("pagination") Pagination pagination); + List selectQProfileUuidsByOrganizationAndGroups(@Param("organizationUuid") String organizationUuid, @Param("groupIds") List groupIds); + void insert(@Param("dto") QProfileEditGroupsDto dto, @Param("now") long now); void delete(@Param("qProfileUuid") String qProfileUuid, @Param("groupId") int groupId); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java index 2cab9a929aa..527ca96d1d6 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersDao.java @@ -24,6 +24,7 @@ import org.sonar.api.utils.System2; import org.sonar.db.Dao; import org.sonar.db.DbSession; import org.sonar.db.Pagination; +import org.sonar.db.organization.OrganizationDto; import org.sonar.db.user.UserDto; public class QProfileEditUsersDao implements Dao { @@ -46,6 +47,10 @@ public class QProfileEditUsersDao implements Dao { return mapper(dbSession).selectByQuery(query, pagination); } + public List selectQProfileUuidsByOrganizationAndUser(DbSession dbSession, OrganizationDto organization, UserDto userDto){ + return mapper(dbSession).selectQProfileUuidsByOrganizationAndUser(organization.getUuid(), userDto.getId()); + } + public void insert(DbSession dbSession, QProfileEditUsersDto dto) { mapper(dbSession).insert(dto, system2.now()); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java index 70694c3307f..07232efe63f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/QProfileEditUsersMapper.java @@ -31,6 +31,8 @@ public interface QProfileEditUsersMapper { List selectByQuery(@Param("query") SearchUsersQuery query, @Param("pagination") Pagination pagination); + List selectQProfileUuidsByOrganizationAndUser(@Param("organizationUuid") String organizationUuid, @Param("userId") int userId); + void insert(@Param("dto") QProfileEditUsersDto dto, @Param("now") long now); void delete(@Param("qProfileUuid") String qProfileUuid, @Param("userId") int userId); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml index ee1a1fc6adb..50c1d355e1a 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditGroupsMapper.xml @@ -73,6 +73,15 @@ + + insert into qprofile_edit_groups( uuid, diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml index 8bf84485bc4..b678ef808d1 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QProfileEditUsersMapper.xml @@ -76,6 +76,15 @@ + + insert into qprofile_edit_users( uuid, diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java index db5204ecb98..4b370cbdc0a 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditGroupsDaoTest.java @@ -19,6 +19,7 @@ */ package org.sonar.db.qualityprofile; +import java.util.Collections; import org.junit.Rule; import org.junit.Test; import org.sonar.api.utils.System2; @@ -28,6 +29,7 @@ import org.sonar.db.Pagination; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.user.GroupDto; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.Assertions.tuple; @@ -196,6 +198,30 @@ public class QProfileEditGroupsDaoTest { .containsExactly(group1.getId(), group2.getId(), group3.getId()); } + @Test + public void selectQProfileUuidsByOrganizationAndGroups() { + OrganizationDto organization = db.organizations().insert(); + OrganizationDto anotherOrganization = db.organizations().insert(); + QProfileDto profile1 = db.qualityProfiles().insert(organization); + QProfileDto profile2 = db.qualityProfiles().insert(organization); + QProfileDto anotherProfile = db.qualityProfiles().insert(anotherOrganization); + GroupDto group1 = db.users().insertGroup(organization, "group1"); + GroupDto group2 = db.users().insertGroup(organization, "group2"); + GroupDto group3 = db.users().insertGroup(organization, "group3"); + db.qualityProfiles().addGroupPermission(profile1, group1); + db.qualityProfiles().addGroupPermission(profile1, group2); + db.qualityProfiles().addGroupPermission(profile2, group2); + db.qualityProfiles().addGroupPermission(anotherProfile, group1); + db.qualityProfiles().addGroupPermission(anotherProfile, group3); + + assertThat(underTest.selectQProfileUuidsByOrganizationAndGroups(db.getSession(), organization, asList(group1, group2))) + .containsExactlyInAnyOrder(profile1.getKee(), profile2.getKee()) + .doesNotContain(anotherProfile.getKee()); + assertThat(underTest.selectQProfileUuidsByOrganizationAndGroups(db.getSession(), organization, asList(group1, group2, group3))) + .containsExactlyInAnyOrder(profile1.getKee(), profile2.getKee()); + assertThat(underTest.selectQProfileUuidsByOrganizationAndGroups(db.getSession(), organization, Collections.emptyList())).isEmpty(); + } + @Test public void insert() { underTest.insert(db.getSession(), new QProfileEditGroupsDto() diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java index e0a90e7cbb4..741f396b858 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QProfileEditUsersDaoTest.java @@ -19,8 +19,10 @@ */ package org.sonar.db.qualityprofile; +import java.sql.SQLException; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.sonar.api.utils.System2; import org.sonar.api.utils.internal.TestSystem2; import org.sonar.db.DbTester; @@ -35,6 +37,7 @@ import static org.sonar.db.qualityprofile.SearchUsersQuery.ANY; import static org.sonar.db.qualityprofile.SearchUsersQuery.IN; import static org.sonar.db.qualityprofile.SearchUsersQuery.OUT; import static org.sonar.db.qualityprofile.SearchUsersQuery.builder; +import static org.sonar.test.ExceptionCauseMatcher.hasType; public class QProfileEditUsersDaoTest { @@ -42,6 +45,8 @@ public class QProfileEditUsersDaoTest { private System2 system2 = new TestSystem2().setNow(NOW); + @Rule + public ExpectedException expectedException = ExpectedException.none(); @Rule public DbTester db = DbTester.create(system2); @@ -82,15 +87,15 @@ public class QProfileEditUsersDaoTest { .isEqualTo(3); assertThat(underTest.countByQuery(db.getSession(), builder() - .setOrganization(organization) - .setProfile(profile) - .setMembership(IN).build())) + .setOrganization(organization) + .setProfile(profile) + .setMembership(IN).build())) .isEqualTo(2); assertThat(underTest.countByQuery(db.getSession(), builder() - .setOrganization(organization) - .setProfile(profile) - .setMembership(OUT).build())) + .setOrganization(organization) + .setProfile(profile) + .setMembership(OUT).build())) .isEqualTo(1); } @@ -217,6 +222,25 @@ public class QProfileEditUsersDaoTest { .containsExactly(user1.getId(), user2.getId(), user3.getId()); } + @Test + public void selectQProfileUuidsByOrganizationAndUser() { + OrganizationDto organization = db.organizations().insert(); + OrganizationDto anotherOrganization = db.organizations().insert(); + QProfileDto profile1 = db.qualityProfiles().insert(organization); + QProfileDto profile2 = db.qualityProfiles().insert(organization); + QProfileDto anotherProfile = db.qualityProfiles().insert(anotherOrganization); + UserDto user1 = db.users().insertUser(u -> u.setName("user1")); + UserDto user2 = db.users().insertUser(u -> u.setName("user2")); + db.qualityProfiles().addUserPermission(profile1, user1); + db.qualityProfiles().addUserPermission(profile2, user1); + db.qualityProfiles().addUserPermission(anotherProfile, user1); + + assertThat(underTest.selectQProfileUuidsByOrganizationAndUser(db.getSession(), organization, user1)) + .containsExactlyInAnyOrder(profile1.getKee(), profile2.getKee()) + .doesNotContain(anotherProfile.getKee()); + assertThat(underTest.selectQProfileUuidsByOrganizationAndUser(db.getSession(), organization, user2)).isEmpty(); + } + @Test public void insert() { underTest.insert(db.getSession(), new QProfileEditUsersDto() @@ -232,6 +256,23 @@ public class QProfileEditUsersDaoTest { entry("createdAt", NOW)); } + @Test + public void fail_to_insert_same_row_twice() { + underTest.insert(db.getSession(), new QProfileEditUsersDto() + .setUuid("UUID-1") + .setUserId(100) + .setQProfileUuid("QPROFILE") + ); + + expectedException.expectCause(hasType(SQLException.class)); + + underTest.insert(db.getSession(), new QProfileEditUsersDto() + .setUuid("UUID-2") + .setUserId(100) + .setQProfileUuid("QPROFILE") + ); + } + @Test public void deleteByQProfileAndUser() { OrganizationDto organization = db.organizations().insert(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchAction.java index 6d4a594d26f..fe6af2db9f5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchAction.java @@ -28,6 +28,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.api.resources.Language; @@ -44,8 +45,10 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.qualityprofile.ActiveRuleCountQuery; import org.sonar.db.qualityprofile.QProfileDto; +import org.sonar.db.user.UserDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.user.UserSession; import org.sonar.server.util.LanguageParamUtils; import org.sonarqube.ws.QualityProfiles.SearchWsResponse; import org.sonarqube.ws.QualityProfiles.SearchWsResponse.QualityProfile; @@ -54,10 +57,13 @@ import org.sonarqube.ws.client.qualityprofile.SearchWsRequest; import static com.google.common.base.Preconditions.checkState; import static java.lang.String.format; +import static java.util.Collections.emptyList; import static java.util.function.Function.identity; import static org.sonar.api.rule.RuleStatus.DEPRECATED; import static org.sonar.api.utils.DateUtils.formatDateTime; import static org.sonar.core.util.Protobuf.setNullable; +import static org.sonar.core.util.stream.MoreCollectors.toList; +import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_PROFILES; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_SEARCH; @@ -72,12 +78,14 @@ public class SearchAction implements QProfileWsAction { .comparing(QProfileDto::getLanguage) .thenComparing(QProfileDto::getName); + private final UserSession userSession; private final Languages languages; private final DbClient dbClient; private final QProfileWsSupport wsSupport; private final ComponentFinder componentFinder; - public SearchAction(Languages languages, DbClient dbClient, QProfileWsSupport wsSupport, ComponentFinder componentFinder) { + public SearchAction(UserSession userSession, Languages languages, DbClient dbClient, QProfileWsSupport wsSupport, ComponentFinder componentFinder) { + this.userSession = userSession; this.languages = languages; this.dbClient = dbClient; this.wsSupport = wsSupport; @@ -151,6 +159,7 @@ public class SearchAction implements QProfileWsAction { ComponentDto project = findProject(dbSession, organization, request); List defaultProfiles = dbClient.qualityProfileDao().selectDefaultProfiles(dbSession, organization, getLanguageKeys()); + List editableProfiles = searchEditableProfiles(dbSession, organization); List profiles = searchProfiles(dbSession, request, organization, defaultProfiles, project); ActiveRuleCountQuery.Builder builder = ActiveRuleCountQuery.builder().setOrganization(organization); @@ -162,7 +171,9 @@ public class SearchAction implements QProfileWsAction { .setActiveDeprecatedRuleCountByProfileKey( dbClient.activeRuleDao().countActiveRulesByQuery(dbSession, builder.setProfiles(profiles).setRuleStatus(DEPRECATED).build())) .setProjectCountByProfileKey(dbClient.qualityProfileDao().countProjectsByOrganizationAndProfiles(dbSession, organization, profiles)) - .setDefaultProfileKeys(defaultProfiles); + .setDefaultProfileKeys(defaultProfiles) + .setEditableProfileKeys(editableProfiles) + .setGlobalQProfileAdmin(userSession.hasPermission(ADMINISTER_QUALITY_PROFILES, organization)); } } @@ -184,8 +195,24 @@ public class SearchAction implements QProfileWsAction { return component; } + private List searchEditableProfiles(DbSession dbSession, OrganizationDto organization) { + if (!userSession.isLoggedIn()) { + return emptyList(); + } + + String login = userSession.getLogin(); + UserDto user = dbClient.userDao().selectActiveUserByLogin(dbSession, login); + checkState(user != null, "User with login '%s' is not found'", login); + + return + Stream.concat( + dbClient.qProfileEditUsersDao().selectQProfileUuidsByOrganizationAndUser(dbSession, organization, user).stream(), + dbClient.qProfileEditGroupsDao().selectQProfileUuidsByOrganizationAndGroups(dbSession, organization, userSession.getGroups()).stream()) + .collect(toList()); + } + private List searchProfiles(DbSession dbSession, SearchWsRequest request, OrganizationDto organization, List defaultProfiles, - @Nullable ComponentDto project) { + @Nullable ComponentDto project) { Collection profiles = selectAllProfiles(dbSession, organization); return profiles.stream() @@ -238,7 +265,7 @@ public class SearchAction implements QProfileWsAction { Map profilesByKey = profiles.stream().collect(Collectors.toMap(QProfileDto::getKee, identity())); SearchWsResponse.Builder response = SearchWsResponse.newBuilder(); - response.setActions(SearchWsResponse.Actions.newBuilder().setCreate(true)); + response.setActions(SearchWsResponse.Actions.newBuilder().setCreate(data.isGlobalQProfileAdmin())); for (QProfileDto profile : profiles) { QualityProfile.Builder profileBuilder = response.addProfilesBuilder(); @@ -263,10 +290,10 @@ public class SearchAction implements QProfileWsAction { profileBuilder.setIsInherited(profile.getParentKee() != null); profileBuilder.setIsBuiltIn(profile.isBuiltIn()); - profileBuilder.setActions(SearchWsResponse.Actions.newBuilder() - .setEdit(true) - .setSetAsDefault(false) - .setCopy(false)); + profileBuilder.setActions(SearchWsResponse.QualityProfile.Actions.newBuilder() + .setEdit(data.isEditable(profile)) + .setSetAsDefault(data.isGlobalQProfileAdmin()) + .setCopy(data.isGlobalQProfileAdmin())); } return response.build(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchData.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchData.java index cf7f39fd759..d07bfcb13d3 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchData.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchData.java @@ -19,6 +19,7 @@ */ package org.sonar.server.qualityprofile.ws; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -30,56 +31,58 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.collect.ImmutableList.copyOf; import static com.google.common.collect.ImmutableMap.copyOf; -public class SearchData { +class SearchData { private OrganizationDto organization; private List profiles; private Map activeRuleCountByProfileKey; private Map activeDeprecatedRuleCountByProfileKey; private Map projectCountByProfileKey; private Set defaultProfileKeys; + private Set editableProfileKeys; + private boolean isGlobalQProfileAdmin; - public SearchData setOrganization(OrganizationDto organization) { + SearchData setOrganization(OrganizationDto organization) { this.organization = organization; return this; } - public OrganizationDto getOrganization() { + OrganizationDto getOrganization() { return organization; } - public List getProfiles() { + List getProfiles() { return profiles; } - public SearchData setProfiles(List profiles) { + SearchData setProfiles(List profiles) { this.profiles = copyOf(profiles); return this; } - public SearchData setActiveRuleCountByProfileKey(Map activeRuleCountByProfileKey) { + SearchData setActiveRuleCountByProfileKey(Map activeRuleCountByProfileKey) { this.activeRuleCountByProfileKey = copyOf(activeRuleCountByProfileKey); return this; } - public SearchData setActiveDeprecatedRuleCountByProfileKey(Map activeDeprecatedRuleCountByProfileKey) { + SearchData setActiveDeprecatedRuleCountByProfileKey(Map activeDeprecatedRuleCountByProfileKey) { this.activeDeprecatedRuleCountByProfileKey = activeDeprecatedRuleCountByProfileKey; return this; } - public SearchData setProjectCountByProfileKey(Map projectCountByProfileKey) { + SearchData setProjectCountByProfileKey(Map projectCountByProfileKey) { this.projectCountByProfileKey = copyOf(projectCountByProfileKey); return this; } - public long getActiveRuleCount(String profileKey) { + long getActiveRuleCount(String profileKey) { return firstNonNull(activeRuleCountByProfileKey.get(profileKey), 0L); } - public long getProjectCount(String profileKey) { + long getProjectCount(String profileKey) { return firstNonNull(projectCountByProfileKey.get(profileKey), 0L); } - public long getActiveDeprecatedRuleCount(String profileKey) { + long getActiveDeprecatedRuleCount(String profileKey) { return firstNonNull(activeDeprecatedRuleCountByProfileKey.get(profileKey), 0L); } @@ -91,4 +94,22 @@ public class SearchData { this.defaultProfileKeys = s.stream().map(QProfileDto::getKee).collect(MoreCollectors.toSet()); return this; } + + boolean isEditable(QProfileDto profile) { + return !profile.isBuiltIn() && (isGlobalQProfileAdmin || editableProfileKeys.contains(profile.getKee())); + } + + SearchData setEditableProfileKeys(List editableProfileKeys) { + this.editableProfileKeys = new HashSet<>(editableProfileKeys); + return this; + } + + boolean isGlobalQProfileAdmin() { + return isGlobalQProfileAdmin; + } + + SearchData setGlobalQProfileAdmin(boolean globalQProfileAdmin) { + isGlobalQProfileAdmin = globalQProfileAdmin; + return this; + } } diff --git a/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search-example.json b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search-example.json index 70698410621..66df3d862bc 100644 --- a/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search-example.json +++ b/server/sonar-server/src/main/resources/org/sonar/server/qualityprofile/ws/search-example.json @@ -13,7 +13,7 @@ "ruleUpdatedAt": "2016-12-22T19:10:03+0100", "lastUsed": "2016-12-01T19:10:03+0100", "actions": { - "edit": true, + "edit": false, "setAsDefault": false, "copy": false } @@ -53,7 +53,7 @@ "ruleUpdatedAt": "2016-12-22T19:10:03+0100", "userUpdatedAt": "2016-06-29T21:57:01+0200", "actions": { - "edit": true, + "edit": false, "setAsDefault": false, "copy": false } @@ -70,13 +70,13 @@ "isDefault": true, "ruleUpdatedAt": "2014-12-22T19:10:03+0100", "actions": { - "edit": true, + "edit": false, "setAsDefault": false, "copy": false } } ], "actions": { - "create": true + "create": false } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfilesWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfilesWsTest.java index 1797e4935a0..0ffe1990556 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfilesWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/QProfilesWsTest.java @@ -42,13 +42,13 @@ import static org.mockito.Mockito.mock; public class QProfilesWsTest { @Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); + public UserSessionRule userSession = UserSessionRule.standalone(); private WebService.Controller controller; private String xoo1Key = "xoo1"; private String xoo2Key = "xoo2"; private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.fromUuid("ORG1"); - private QProfileWsSupport wsSupport = new QProfileWsSupport(mock(DbClient.class), userSessionRule, defaultOrganizationProvider); + private QProfileWsSupport wsSupport = new QProfileWsSupport(mock(DbClient.class), userSession, defaultOrganizationProvider); @Before public void setUp() { @@ -59,18 +59,18 @@ public class QProfilesWsTest { ProfileImporter[] importers = createImporters(languages); controller = new WsTester(new QProfilesWs( - new CreateAction(null, null, null, languages, wsSupport, userSessionRule, null, importers), + new CreateAction(null, null, null, languages, wsSupport, userSession, null, importers), new ImportersAction(importers), - new SearchAction(languages, dbClient, wsSupport, null), + new SearchAction(userSession, languages, dbClient, wsSupport, null), new SetDefaultAction(languages, null, null, wsSupport), - new ProjectsAction(null, userSessionRule, wsSupport), + new ProjectsAction(null, userSession, wsSupport), new ChangelogAction(null, wsSupport, languages, dbClient), - new ChangeParentAction(dbClient, null, languages, wsSupport, userSessionRule), + new ChangeParentAction(dbClient, null, languages, wsSupport, userSession), new CompareAction(null, null, languages), - new DeleteAction(languages, null, null, userSessionRule, wsSupport), + new DeleteAction(languages, null, null, userSession, wsSupport), new ExportersAction(), new InheritanceAction(null, null, languages), - new RenameAction(dbClient, userSessionRule, wsSupport))).controller(QProfilesWs.API_ENDPOINT); + new RenameAction(dbClient, userSession, wsSupport))).controller(QProfilesWs.API_ENDPOINT); } private ProfileImporter[] createImporters(Languages languages) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchActionTest.java index 9616039f8a9..1e53bbf1ef0 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/qualityprofile/ws/SearchActionTest.java @@ -34,9 +34,12 @@ 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.permission.OrganizationPermission; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.db.qualityprofile.QualityProfileDbTester; import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.organization.DefaultOrganizationProvider; @@ -61,8 +64,8 @@ import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_DEFAULTS; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_ORGANIZATION; -import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_QUALITY_PROFILE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_PROJECT_KEY; +import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_QUALITY_PROFILE; public class SearchActionTest { @@ -81,7 +84,7 @@ public class SearchActionTest { private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db); private QProfileWsSupport qProfileWsSupport = new QProfileWsSupport(dbClient, userSession, defaultOrganizationProvider); - private SearchAction underTest = new SearchAction(LANGUAGES, dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null)); + private SearchAction underTest = new SearchAction(userSession, LANGUAGES, dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null)); private WsActionTester ws = new WsActionTester(underTest); @Test @@ -130,7 +133,7 @@ public class SearchActionTest { @Test public void empty_when_no_language_installed() { - WsActionTester ws = new WsActionTester(new SearchAction(new Languages(), dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null))); + WsActionTester ws = new WsActionTester(new SearchAction(userSession, new Languages(), dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null))); db.qualityProfiles().insert(db.getDefaultOrganization()); SearchWsResponse result = call(ws.newRequest()); @@ -307,7 +310,7 @@ public class SearchActionTest { @Test public void empty_when_filtering_on_project_and_no_language_installed() { - WsActionTester ws = new WsActionTester(new SearchAction(new Languages(), dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null))); + WsActionTester ws = new WsActionTester(new SearchAction(userSession, new Languages(), dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null))); db.qualityProfiles().insert(db.getDefaultOrganization()); ComponentDto project = db.components().insertPrivateProject(); QProfileDto profileOnXoo1 = db.qualityProfiles().insert(db.getDefaultOrganization(), q -> q.setLanguage(XOO1.getKey())); @@ -320,6 +323,63 @@ public class SearchActionTest { assertThat(result.getProfilesList()).isEmpty(); } + @Test + public void actions_when_user_is_global_qprofile_administer() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto customProfile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO1.getKey())); + QProfileDto builtInProfile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO1.getKey()).setIsBuiltIn(true)); + UserDto user = db.users().insertUser(); + userSession.logIn(user).addPermission(OrganizationPermission.ADMINISTER_QUALITY_PROFILES, organization); + + SearchWsResponse result = call(ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey())); + + assertThat(result.getProfilesList()).extracting(QualityProfile::getKey, qp -> qp.getActions().getEdit(), qp -> qp.getActions().getCopy(), qp -> qp.getActions().getSetAsDefault()) + .containsExactlyInAnyOrder( + tuple(customProfile.getKee(), true, true, true), + tuple(builtInProfile.getKee(), false, true, true)); + assertThat(result.getActions().getCreate()).isTrue(); + } + + @Test + public void actions_when_user_can_edit_profile() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile1 = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO1.getKey())); + QProfileDto profile2 = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO2.getKey())); + QProfileDto profile3 = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO2.getKey())); + QProfileDto builtInProfile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO2.getKey()).setIsBuiltIn(true)); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(organization); + db.qualityProfiles().addUserPermission(profile1, user); + db.qualityProfiles().addGroupPermission(profile3, group); + userSession.logIn(user).setGroups(group); + + SearchWsResponse result = call(ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey())); + + assertThat(result.getProfilesList()).extracting(QualityProfile::getKey, qp -> qp.getActions().getEdit(), qp -> qp.getActions().getCopy(), qp -> qp.getActions().getSetAsDefault()) + .containsExactlyInAnyOrder( + tuple(profile1.getKee(), true, false, false), + tuple(profile2.getKee(), false, false, false), + tuple(profile3.getKee(), true, false, false), + tuple(builtInProfile.getKee(), false, false, false)); + assertThat(result.getActions().getCreate()).isFalse(); + } + + @Test + public void actions_when_not_logged_in() { + OrganizationDto organization = db.organizations().insert(); + QProfileDto profile = db.qualityProfiles().insert(organization, p -> p.setLanguage(XOO1.getKey())); + userSession.anonymous(); + + SearchWsResponse result = call(ws.newRequest() + .setParam(PARAM_ORGANIZATION, organization.getKey())); + + assertThat(result.getProfilesList()).extracting(QualityProfile::getKey, qp -> qp.getActions().getEdit(), qp -> qp.getActions().getCopy(), qp -> qp.getActions().getSetAsDefault()) + .containsExactlyInAnyOrder(tuple(profile.getKee(), false, false, false)); + assertThat(result.getActions().getCreate()).isFalse(); + } + @Test public void fail_if_project_does_not_exist() { expectedException.expect(NotFoundException.class); @@ -408,18 +468,19 @@ public class SearchActionTest { @Test public void json_example() { + OrganizationDto organization = db.organizations().insertForKey("My Organization"); // languages Language cs = newLanguage("cs", "C#"); Language java = newLanguage("java", "Java"); Language python = newLanguage("py", "Python"); // profiles - QProfileDto sonarWayCs = db.qualityProfiles().insert(db.getDefaultOrganization(), + QProfileDto sonarWayCs = db.qualityProfiles().insert(organization, p -> p.setName("Sonar way").setKee("AU-TpxcA-iU5OvuD2FL3").setIsBuiltIn(true).setLanguage(cs.getKey())); - QProfileDto myCompanyProfile = db.qualityProfiles().insert(db.getDefaultOrganization(), + QProfileDto myCompanyProfile = db.qualityProfiles().insert(organization, p -> p.setName("My Company Profile").setKee("iU5OvuD2FLz").setLanguage(java.getKey())); - QProfileDto myBuProfile = db.qualityProfiles().insert(db.getDefaultOrganization(), + QProfileDto myBuProfile = db.qualityProfiles().insert(organization, p -> p.setName("My BU Profile").setKee("AU-TpxcA-iU5OvuD2FL1").setParentKee(myCompanyProfile.getKee()).setLanguage(java.getKey())); - QProfileDto sonarWayPython = db.qualityProfiles().insert(db.getDefaultOrganization(), + QProfileDto sonarWayPython = db.qualityProfiles().insert(organization, p -> p.setName("Sonar way").setKee("AU-TpxcB-iU5OvuD2FL7").setIsBuiltIn(true).setLanguage(python.getKey())); db.qualityProfiles().setAsDefault(sonarWayCs, myCompanyProfile, sonarWayPython); // rules @@ -440,12 +501,17 @@ public class SearchActionTest { .forEach(rule -> db.qualityProfiles().activateRule(sonarWayCs, rule)); // project range(0, 7) - .mapToObj(i -> db.components().insertPrivateProject()) + .mapToObj(i -> db.components().insertPrivateProject(organization)) .forEach(project -> db.qualityProfiles().associateWithProject(project, myBuProfile)); + // User + UserDto user = db.users().insertUser(); + db.qualityProfiles().addUserPermission(sonarWayCs, user); + db.qualityProfiles().addUserPermission(myBuProfile, user); + userSession.logIn(user); - underTest = new SearchAction(new Languages(cs, java, python), dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null)); + underTest = new SearchAction(userSession, new Languages(cs, java, python), dbClient, qProfileWsSupport, new ComponentFinder(dbClient, null)); ws = new WsActionTester(underTest); - String result = ws.newRequest().execute().getInput(); + String result = ws.newRequest().setParam(PARAM_ORGANIZATION, organization.getKey()).execute().getInput(); assertJson(result).ignoreFields("ruleUpdatedAt", "lastUsed", "userUpdatedAt") .isSimilarTo(ws.getDef().responseExampleAsString()); } diff --git a/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto b/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto index cd3143aa43a..ae1097dd5b5 100644 --- a/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto +++ b/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto @@ -49,13 +49,16 @@ message SearchWsResponse { optional string organization = 15; optional bool isBuiltIn = 16; optional Actions actions = 17; + + message Actions { + optional bool edit = 1; + optional bool setAsDefault = 2; + optional bool copy = 3; + } } message Actions { optional bool create = 1; - optional bool edit = 2; - optional bool setAsDefault = 3; - optional bool copy = 4; } } diff --git a/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java b/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java index 4d4764ef87b..02c71b7a9ad 100644 --- a/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java +++ b/tests/src/test/java/org/sonarqube/tests/qualityProfile/QualityProfilesEditTest.java @@ -27,17 +27,20 @@ import org.sonarqube.tests.Category6Suite; import org.sonarqube.tests.Tester; import org.sonarqube.ws.Common; import org.sonarqube.ws.Organizations.Organization; +import org.sonarqube.ws.QualityProfiles; import org.sonarqube.ws.QualityProfiles.CreateWsResponse; import org.sonarqube.ws.QualityProfiles.SearchGroupsResponse; import org.sonarqube.ws.QualityProfiles.SearchUsersResponse; import org.sonarqube.ws.WsUserGroups; import org.sonarqube.ws.WsUsers; +import org.sonarqube.ws.client.permission.AddUserWsRequest; import org.sonarqube.ws.client.qualityprofile.AddGroupRequest; import org.sonarqube.ws.client.qualityprofile.AddUserRequest; import org.sonarqube.ws.client.qualityprofile.RemoveGroupRequest; import org.sonarqube.ws.client.qualityprofile.RemoveUserRequest; import org.sonarqube.ws.client.qualityprofile.SearchGroupsRequest; import org.sonarqube.ws.client.qualityprofile.SearchUsersRequest; +import org.sonarqube.ws.client.qualityprofile.SearchWsRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; @@ -57,12 +60,7 @@ public class QualityProfilesEditTest { WsUsers.CreateWsResponse.User user1 = tester.users().generateMember(organization, u -> u.setEmail("user1@email.com")); WsUsers.CreateWsResponse.User user2 = tester.users().generateMember(organization, u -> u.setEmail("user2@email.com")); CreateWsResponse.QualityProfile xooProfile = tester.qProfiles().createXooProfile(organization); - tester.qProfiles().service().addUser(AddUserRequest.builder() - .setOrganization(organization.getKey()) - .setQualityProfile(xooProfile.getName()) - .setLanguage(xooProfile.getLanguage()) - .setUserLogin(user1.getLogin()) - .build()); + addUserPermission(organization, user1, xooProfile); SearchUsersResponse users = tester.qProfiles().service().searchUsers(SearchUsersRequest.builder() .setOrganization(organization.getKey()) @@ -98,12 +96,7 @@ public class QualityProfilesEditTest { .isEmpty(); // Add user 1 - tester.qProfiles().service().addUser(AddUserRequest.builder() - .setOrganization(organization.getKey()) - .setQualityProfile(xooProfile.getName()) - .setLanguage(xooProfile.getLanguage()) - .setUserLogin(user1.getLogin()) - .build()); + addUserPermission(organization, user1, xooProfile); assertThat(tester.qProfiles().service().searchUsers(SearchUsersRequest.builder() .setOrganization(organization.getKey()) .setQualityProfile(xooProfile.getName()) @@ -137,18 +130,8 @@ public class QualityProfilesEditTest { WsUserGroups.Group group2 = tester.groups().generate(organization); WsUserGroups.Group group3 = tester.groups().generate(organization); CreateWsResponse.QualityProfile xooProfile = tester.qProfiles().createXooProfile(organization); - tester.qProfiles().service().addGroup(AddGroupRequest.builder() - .setOrganization(organization.getKey()) - .setQualityProfile(xooProfile.getName()) - .setLanguage(xooProfile.getLanguage()) - .setGroup(group1.getName()) - .build()); - tester.qProfiles().service().addGroup(AddGroupRequest.builder() - .setOrganization(organization.getKey()) - .setQualityProfile(xooProfile.getName()) - .setLanguage(xooProfile.getLanguage()) - .setGroup(group2.getName()) - .build()); + addGroupPermission(organization, group1, xooProfile); + addGroupPermission(organization, group2, xooProfile); SearchGroupsResponse groups = tester.qProfiles().service().searchGroups(SearchGroupsRequest.builder() .setOrganization(organization.getKey()) @@ -181,13 +164,8 @@ public class QualityProfilesEditTest { .extracting(Group::getName) .isEmpty(); - // Add user 1 - tester.qProfiles().service().addGroup(AddGroupRequest.builder() - .setOrganization(organization.getKey()) - .setQualityProfile(xooProfile.getName()) - .setLanguage(xooProfile.getLanguage()) - .setGroup(group1.getName()) - .build()); + // Add group 1 + addGroupPermission(organization, group1, xooProfile); assertThat(tester.qProfiles().service().searchGroups(SearchGroupsRequest.builder() .setOrganization(organization.getKey()) .setQualityProfile(xooProfile.getName()) @@ -197,7 +175,7 @@ public class QualityProfilesEditTest { .extracting(Group::getName) .containsExactlyInAnyOrder(group1.getName()); - // Remove user 1 + // Remove group 1 tester.qProfiles().service().removeGroup(RemoveGroupRequest.builder() .setOrganization(organization.getKey()) .setQualityProfile(xooProfile.getName()) @@ -213,4 +191,61 @@ public class QualityProfilesEditTest { .extracting(Group::getName) .isEmpty(); } + + @Test + public void actions_when_user_can_edit_profiles() { + Organization organization = tester.organizations().generate(); + WsUsers.CreateWsResponse.User user = tester.users().generateMember(organization); + CreateWsResponse.QualityProfile xooProfile1 = tester.qProfiles().createXooProfile(organization); + addUserPermission(organization, user, xooProfile1); + CreateWsResponse.QualityProfile xooProfile2 = tester.qProfiles().createXooProfile(organization); + WsUserGroups.Group group = tester.groups().generate(organization); + tester.groups().addMemberToGroups(organization, user.getLogin(), group.getName()); + addGroupPermission(organization, group, xooProfile2); + CreateWsResponse.QualityProfile xooProfile3 = tester.qProfiles().createXooProfile(organization); + + QualityProfiles.SearchWsResponse result = tester.as(user.getLogin()) + .qProfiles().service().search(new SearchWsRequest().setOrganizationKey(organization.getKey())); + assertThat(result.getActions().getCreate()).isFalse(); + assertThat(result.getProfilesList()) + .extracting(QualityProfiles.SearchWsResponse.QualityProfile::getKey, qp -> qp.getActions().getEdit(), qp -> qp.getActions().getCopy(), qp -> qp.getActions().getSetAsDefault()) + .contains( + tuple(xooProfile1.getKey(), true, false, false), + tuple(xooProfile2.getKey(), true, false, false), + tuple(xooProfile3.getKey(), false, false, false)); + } + + @Test + public void actions_when_user_is_global_qprofile_administer() { + Organization organization = tester.organizations().generate(); + WsUsers.CreateWsResponse.User user = tester.users().generateMember(organization); + CreateWsResponse.QualityProfile xooProfile = tester.qProfiles().createXooProfile(organization); + tester.wsClient().permissions().addUser(new AddUserWsRequest().setOrganization(organization.getKey()).setLogin(user.getLogin()).setPermission("profileadmin")); + + QualityProfiles.SearchWsResponse result = tester.as(user.getLogin()) + .qProfiles().service().search(new SearchWsRequest().setOrganizationKey(organization.getKey())); + assertThat(result.getActions().getCreate()).isTrue(); + assertThat(result.getProfilesList()) + .extracting(QualityProfiles.SearchWsResponse.QualityProfile::getKey, qp -> qp.getActions().getEdit(), qp -> qp.getActions().getCopy(), qp -> qp.getActions().getSetAsDefault()) + .contains( + tuple(xooProfile.getKey(), true, true, true)); + } + + private void addUserPermission(Organization organization, WsUsers.CreateWsResponse.User user, CreateWsResponse.QualityProfile qProfile){ + tester.qProfiles().service().addUser(AddUserRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(qProfile.getName()) + .setLanguage(qProfile.getLanguage()) + .setUserLogin(user.getLogin()) + .build()); + } + + private void addGroupPermission(Organization organization, WsUserGroups.Group group, CreateWsResponse.QualityProfile qProfile){ + tester.qProfiles().service().addGroup(AddGroupRequest.builder() + .setOrganization(organization.getKey()) + .setQualityProfile(qProfile.getName()) + .setLanguage(qProfile.getLanguage()) + .setGroup(group.getName()) + .build()); + } } -- 2.39.5