]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9181 WS bulk_apply_template accepts the parameters as projects/search
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Fri, 8 Sep 2017 09:44:30 +0000 (11:44 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Sep 2017 09:28:29 +0000 (11:28 +0200)
server/sonar-server/src/main/java/org/sonar/server/permission/ws/template/BulkApplyTemplateAction.java
server/sonar-server/src/test/java/org/sonar/server/permission/ws/template/BulkApplyTemplateActionTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/permission/BulkApplyTemplateWsRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/permission/PermissionsService.java
sonar-ws/src/test/java/org/sonarqube/ws/client/permission/PermissionsServiceTest.java
tests/src/test/java/org/sonarqube/tests/authorisation/PermissionTemplateTest.java

index 5d17a3eb34a10530791abb57607786a3397d93d8..f3e75e9ae848143eefa65b5afe4233b1847d459d 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.server.permission.ws.template;
 
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import org.sonar.api.i18n.I18n;
 import org.sonar.api.resources.Qualifiers;
@@ -35,20 +37,28 @@ import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.server.permission.PermissionTemplateService;
 import org.sonar.server.permission.ws.PermissionWsSupport;
 import org.sonar.server.permission.ws.PermissionsWsAction;
+import org.sonar.server.project.Visibility;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.client.permission.BulkApplyTemplateWsRequest;
 
+import static org.sonar.api.utils.DateUtils.parseDateOrDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.server.permission.PermissionPrivilegeChecker.checkGlobalAdmin;
 import static org.sonar.server.permission.ws.PermissionsWsParametersBuilder.createTemplateParameters;
 import static org.sonar.server.permission.ws.template.WsTemplateRef.newTemplateRef;
-import static org.sonar.server.ws.WsParameterBuilder.createRootQualifiersParameter;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_002;
 import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
+import static org.sonar.server.ws.WsParameterBuilder.createRootQualifiersParameter;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_ORGANIZATION;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_QUALIFIER;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
 public class BulkApplyTemplateAction implements PermissionsWsAction {
 
@@ -91,6 +101,32 @@ public class BulkApplyTemplateAction implements PermissionsWsAction {
       .setDeprecatedKey(PARAM_QUALIFIER, "6.6");
 
     createTemplateParameters(action);
+
+    action
+      .createParam(PARAM_PROJECTS)
+      .setDescription("Comma-separated list of project keys")
+      .setSince("6.6")
+      .setExampleValue(String.join(",", KEY_PROJECT_EXAMPLE_001, KEY_PROJECT_EXAMPLE_002));
+
+    action.createParam(PARAM_VISIBILITY)
+      .setDescription("Filter the projects that should be visible to everyone (%s), or only specific user/groups (%s).<br/>" +
+        "If no visibility is specified, the default project visibility of the organization will be used.",
+        Visibility.PUBLIC.getLabel(), Visibility.PRIVATE.getLabel())
+      .setRequired(false)
+      .setInternal(true)
+      .setSince("6.6")
+      .setPossibleValues(Visibility.getLabels());
+
+    action.createParam(PARAM_ANALYZED_BEFORE)
+      .setDescription("Filter the projects for which last analysis is older than the given date (exclusive).<br> " +
+        "Format: date or datetime ISO formats.")
+      .setSince("6.6");
+
+    action.createParam(PARAM_ON_PROVISIONED_ONLY)
+      .setDescription("Filter the projects that are provisioned")
+      .setBooleanPossibleValues()
+      .setDefaultValue("false")
+      .setSince("6.6");
   }
 
   @Override
@@ -118,15 +154,29 @@ public class BulkApplyTemplateAction implements PermissionsWsAction {
       .setTemplateId(request.param(PARAM_TEMPLATE_ID))
       .setTemplateName(request.param(PARAM_TEMPLATE_NAME))
       .setQualifiers(request.mandatoryParamAsStrings(PARAM_QUALIFIERS))
-      .setQuery(request.param(Param.TEXT_QUERY));
+      .setQuery(request.param(Param.TEXT_QUERY))
+      .setVisibility(request.param(PARAM_VISIBILITY))
+      .setOnProvisionedOnly(request.mandatoryParamAsBoolean(PARAM_ON_PROVISIONED_ONLY))
+      .setAnalyzedBefore(request.param(PARAM_ANALYZED_BEFORE))
+      .setProjects(request.paramAsStrings(PARAM_PROJECTS));
   }
 
   private static ComponentQuery buildDbQuery(BulkApplyTemplateWsRequest request) {
-    ComponentQuery.Builder dbQuery = ComponentQuery.builder()
-      .setNameOrKeyQuery(request.getQuery());
-    setNullable(request.getQualifiers(), l -> dbQuery.setQualifiers(l.toArray(new String[0])));
-
-    return dbQuery.build();
+    Collection<String> qualifiers = request.getQualifiers();
+    ComponentQuery.Builder query = ComponentQuery.builder()
+      .setQualifiers(qualifiers.toArray(new String[qualifiers.size()]));
+
+    setNullable(request.getQuery(), q -> {
+      query.setNameOrKeyQuery(q);
+      query.setPartialMatchOnKey(true);
+      return query;
+    });
+    setNullable(request.getVisibility(), v -> query.setPrivate(Visibility.isPrivate(v)));
+    setNullable(request.getAnalyzedBefore(), d -> query.setAnalyzedBefore(parseDateOrDateTime(d).getTime()));
+    setNullable(request.isOnProvisionedOnly(), query::setOnProvisionedOnly);
+    setNullable(request.getProjects(), keys -> query.setComponentKeys(new HashSet<>(keys)));
+
+    return query.build();
   }
 
 }
index ac6fffb08d55d6f6ca234b5e1c70be509668e84f..0d04a677a5b2c5692e5596db355116283cec4bf9 100644 (file)
@@ -41,12 +41,18 @@ import org.sonar.server.permission.PermissionTemplateService;
 import org.sonar.server.permission.ws.BasePermissionWsTest;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.utils.DateUtils.parseDate;
 import static org.sonar.db.component.ComponentTesting.newApplication;
 import static org.sonar.db.component.ComponentTesting.newView;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_ORGANIZATION;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
 public class BulkApplyTemplateActionTest extends BasePermissionWsTest<BulkApplyTemplateAction> {
 
@@ -103,17 +109,14 @@ public class BulkApplyTemplateActionTest extends BasePermissionWsTest<BulkApplyT
 
     ComponentDto privateProject = db.components().insertPrivateProject(organization);
     ComponentDto publicProject = db.components().insertPublicProject(organization);
-    ComponentDto view = db.components().insertView(organization);
     loginAsAdmin(organization);
 
     newRequest()
       .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
-      .setParam(PARAM_QUALIFIERS, String.join(",", Qualifiers.PROJECT, Qualifiers.VIEW))
       .execute();
 
     assertTemplate1AppliedToPrivateProject(privateProject);
     assertTemplate1AppliedToPublicProject(publicProject);
-    assertTemplate1AppliedToPublicProject(view);
   }
 
   @Test
@@ -171,14 +174,13 @@ public class BulkApplyTemplateActionTest extends BasePermissionWsTest<BulkApplyT
     db.components().insertProjectAndSnapshot(publicProjectFoundByKey);
     ComponentDto publicProjectFoundByName = ComponentTesting.newPublicProjectDto(organization).setName("name-sonar-name");
     db.components().insertProjectAndSnapshot(publicProjectFoundByName);
-    // match must be exact on key
-    ComponentDto projectUntouched = ComponentTesting.newPublicProjectDto(organization).setDbKey("new-sonar").setName("project-name");
+    ComponentDto projectUntouched = ComponentTesting.newPublicProjectDto(organization).setDbKey("new-sona").setName("project-name");
     db.components().insertProjectAndSnapshot(projectUntouched);
     loginAsAdmin(organization);
 
     newRequest()
       .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
-      .setParam(Param.TEXT_QUERY, "sonar")
+      .setParam(Param.TEXT_QUERY, "SONAR")
       .execute();
 
     assertTemplate1AppliedToPublicProject(publicProjectFoundByKey);
@@ -188,18 +190,18 @@ public class BulkApplyTemplateActionTest extends BasePermissionWsTest<BulkApplyT
 
   @Test
   public void apply_template_by_query_on_name_and_key() throws Exception {
-    ComponentDto privateProjectFoundByKey = ComponentTesting.newPrivateProjectDto(organization).setDbKey("sonar");
+    // partial match on key
+    ComponentDto privateProjectFoundByKey = ComponentTesting.newPrivateProjectDto(organization).setDbKey("sonarqube");
     db.components().insertProjectAndSnapshot(privateProjectFoundByKey);
     ComponentDto privateProjectFoundByName = ComponentTesting.newPrivateProjectDto(organization).setName("name-sonar-name");
     db.components().insertProjectAndSnapshot(privateProjectFoundByName);
-    // match must be exact on key
-    ComponentDto projectUntouched = ComponentTesting.newPublicProjectDto(organization).setDbKey("new-sonar").setName("project-name");
+    ComponentDto projectUntouched = ComponentTesting.newPublicProjectDto(organization).setDbKey("new-sona").setName("project-name");
     db.components().insertProjectAndSnapshot(projectUntouched);
     loginAsAdmin(organization);
 
     newRequest()
       .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
-      .setParam(Param.TEXT_QUERY, "sonar")
+      .setParam(Param.TEXT_QUERY, "SONAR")
       .execute();
 
     assertTemplate1AppliedToPrivateProject(privateProjectFoundByKey);
@@ -207,6 +209,78 @@ public class BulkApplyTemplateActionTest extends BasePermissionWsTest<BulkApplyT
     assertNoPermissionOnProject(projectUntouched);
   }
 
+  @Test
+  public void apply_template_by_project_keys() throws Exception {
+    ComponentDto project1 = db.components().insertPrivateProject(organization);
+    ComponentDto project2 = db.components().insertPrivateProject(organization);
+    ComponentDto untouchedProject = db.components().insertPrivateProject(organization);
+    loginAsAdmin(organization);
+
+    newRequest()
+      .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
+      .setParam(PARAM_PROJECTS, String.join(",", project1.getKey(), project2.getKey()))
+      .execute();
+
+    assertTemplate1AppliedToPrivateProject(project1);
+    assertTemplate1AppliedToPrivateProject(project2);
+    assertNoPermissionOnProject(untouchedProject);
+  }
+
+  @Test
+  public void apply_template_by_provisioned_only() throws Exception {
+    ComponentDto provisionedProject1 = db.components().insertPrivateProject(organization);
+    ComponentDto provisionedProject2 = db.components().insertPrivateProject(organization);
+    ComponentDto analyzedProject = db.components().insertPrivateProject(organization);
+    db.components().insertSnapshot(newAnalysis(analyzedProject));
+    loginAsAdmin(organization);
+
+    newRequest()
+      .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
+      .setParam(PARAM_ON_PROVISIONED_ONLY, "true")
+      .execute();
+
+    assertTemplate1AppliedToPrivateProject(provisionedProject1);
+    assertTemplate1AppliedToPrivateProject(provisionedProject2);
+    assertNoPermissionOnProject(analyzedProject);
+  }
+
+  @Test
+  public void apply_template_by_analyzed_before() throws Exception {
+    ComponentDto oldProject1 = db.components().insertPrivateProject(organization);
+    ComponentDto oldProject2 = db.components().insertPrivateProject(organization);
+    ComponentDto recentProject = db.components().insertPrivateProject(organization);
+    db.components().insertSnapshot(oldProject1, a -> a.setCreatedAt(parseDate("2015-02-03").getTime()));
+    db.components().insertSnapshot(oldProject2, a -> a.setCreatedAt(parseDate("2016-12-11").getTime()));
+    db.components().insertSnapshot(recentProject, a -> a.setCreatedAt(System.currentTimeMillis()));
+    loginAsAdmin(organization);
+
+    newRequest()
+      .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
+      .setParam(PARAM_ANALYZED_BEFORE, "2017-09-07")
+      .execute();
+
+    assertTemplate1AppliedToPrivateProject(oldProject1);
+    assertTemplate1AppliedToPrivateProject(oldProject2);
+    assertNoPermissionOnProject(recentProject);
+  }
+
+  @Test
+  public void apply_template_by_visibility() throws Exception {
+    ComponentDto privateProject1 = db.components().insertPrivateProject(organization);
+    ComponentDto privateProject2 = db.components().insertPrivateProject(organization);
+    ComponentDto publicProject = db.components().insertPublicProject(organization);
+    loginAsAdmin(organization);
+
+    newRequest()
+      .setParam(PARAM_TEMPLATE_ID, template1.getUuid())
+      .setParam(PARAM_VISIBILITY, "private")
+      .execute();
+
+    assertTemplate1AppliedToPrivateProject(privateProject1);
+    assertTemplate1AppliedToPrivateProject(privateProject2);
+    assertNoPermissionOnProject(publicProject);
+  }
+
   @Test
   public void fail_if_no_template_parameter() throws Exception {
     loginAsAdmin(db.getDefaultOrganization());
index 77d7eff64b223376b1027256c22301c08ce113ce..bb17eb37572f3beb3ba0fc8c97520b5cd1e0fb03 100644 (file)
@@ -22,7 +22,9 @@ package org.sonarqube.ws.client.permission;
 import java.util.Collection;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
+import org.sonar.api.resources.Qualifiers;
 
+import static java.util.Collections.singleton;
 import static java.util.Objects.requireNonNull;
 
 public class BulkApplyTemplateWsRequest {
@@ -30,7 +32,11 @@ public class BulkApplyTemplateWsRequest {
   private String organization;
   private String templateName;
   private String query;
-  private Collection<String> qualifiers;
+  private Collection<String> qualifiers = singleton(Qualifiers.PROJECT);
+  private String visibility;
+  private String analyzedBefore;
+  private boolean onProvisionedOnly = false;
+  private Collection<String> projects;
 
   @CheckForNull
   public String getTemplateId() {
@@ -72,7 +78,6 @@ public class BulkApplyTemplateWsRequest {
     return this;
   }
 
-  @CheckForNull
   public Collection<String> getQualifiers() {
     return qualifiers;
   }
@@ -81,4 +86,43 @@ public class BulkApplyTemplateWsRequest {
     this.qualifiers = requireNonNull(qualifiers);
     return this;
   }
+
+  @CheckForNull
+  public String getVisibility() {
+    return visibility;
+  }
+
+  public BulkApplyTemplateWsRequest setVisibility(@Nullable String visibility) {
+    this.visibility = visibility;
+    return this;
+  }
+
+  @CheckForNull
+  public String getAnalyzedBefore() {
+    return analyzedBefore;
+  }
+
+  public BulkApplyTemplateWsRequest setAnalyzedBefore(@Nullable String analyzedBefore) {
+    this.analyzedBefore = analyzedBefore;
+    return this;
+  }
+
+  public boolean isOnProvisionedOnly() {
+    return onProvisionedOnly;
+  }
+
+  public BulkApplyTemplateWsRequest setOnProvisionedOnly(boolean onProvisionedOnly) {
+    this.onProvisionedOnly = onProvisionedOnly;
+    return this;
+  }
+
+  @CheckForNull
+  public Collection<String> getProjects() {
+    return projects;
+  }
+
+  public BulkApplyTemplateWsRequest setProjects(@Nullable Collection<String> projects) {
+    this.projects = projects;
+    return this;
+  }
 }
index 97802e65da74b2601b5570f8068827914ac77126..ba58f34a27493b8a012e8bafde7c3ef750f3eec2 100644 (file)
@@ -30,6 +30,7 @@ import org.sonarqube.ws.client.BaseService;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.PostRequest;
 import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.client.project.ProjectsWsParameters;
 
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_DESCRIPTION;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_GROUP_ID;
@@ -45,7 +46,6 @@ import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_Q
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_USER_LOGIN;
-import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
 
 public class PermissionsService extends BaseService {
 
@@ -124,7 +124,11 @@ public class PermissionsService extends BaseService {
       .setParam(PARAM_TEMPLATE_ID, request.getTemplateId())
       .setParam(PARAM_TEMPLATE_NAME, request.getTemplateName())
       .setParam("q", request.getQuery())
-      .setParam(PARAM_QUALIFIERS, inlineMultipleParamValue(request.getQualifiers())));
+      .setParam(ProjectsWsParameters.PARAM_QUALIFIERS, inlineMultipleParamValue(request.getQualifiers()))
+      .setParam(ProjectsWsParameters.PARAM_VISIBILITY, request.getVisibility())
+      .setParam(ProjectsWsParameters.PARAM_ANALYZED_BEFORE, request.getAnalyzedBefore())
+      .setParam(ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY, request.isOnProvisionedOnly())
+      .setParam(ProjectsWsParameters.PARAM_PROJECTS, inlineMultipleParamValue(request.getProjects())));
   }
 
   public CreateTemplateWsResponse createTemplate(CreateTemplateWsRequest request) {
index b83868acfa56727dfb0ad06ad5036c8f870ad055..fa45cf550c613dd961312059b75af0c2f3c5229e 100644 (file)
@@ -44,7 +44,11 @@ import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_Q
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_ID;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_TEMPLATE_NAME;
 import static org.sonarqube.ws.client.permission.PermissionsWsParameters.PARAM_USER_LOGIN;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ON_PROVISIONED_ONLY;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECTS;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
 public class PermissionsServiceTest {
   private static final String ORGANIZATION_VALUE = "organization value";
@@ -220,7 +224,11 @@ public class PermissionsServiceTest {
       .setTemplateId(TEMPLATE_ID_VALUE)
       .setTemplateName(TEMPLATE_NAME_VALUE)
       .setQualifiers(Arrays.asList("TRK", "VW"))
-      .setQuery(QUERY_VALUE));
+      .setQuery(QUERY_VALUE)
+      .setVisibility("private")
+      .setAnalyzedBefore("2017-04-01")
+      .setOnProvisionedOnly(true)
+      .setProjects(Arrays.asList("P1", "P2")));
 
     assertThat(serviceTester.getPostParser()).isNull();
     PostRequest postRequest = serviceTester.getPostRequest();
@@ -231,6 +239,10 @@ public class PermissionsServiceTest {
       .hasParam(PARAM_TEMPLATE_NAME, TEMPLATE_NAME_VALUE)
       .hasParam("q", QUERY_VALUE)
       .hasParam(PARAM_QUALIFIERS, "TRK,VW")
+      .hasParam(PARAM_VISIBILITY, "private")
+      .hasParam(PARAM_ANALYZED_BEFORE, "2017-04-01")
+      .hasParam(PARAM_ON_PROVISIONED_ONLY, "true")
+      .hasParam(PARAM_PROJECTS, "P1,P2")
       .andNoOtherParam();
   }
 
index 99d1542756e080231a1ab245609f607ea3e20d9f..366632b06446ea2e55605970e0b80dd10f0ed801 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonarqube.tests.authorisation;
 
 import com.sonar.orchestrator.Orchestrator;
+import java.util.Arrays;
 import java.util.Optional;
 import org.junit.After;
 import org.junit.ClassRule;
@@ -32,16 +33,19 @@ import org.sonarqube.tests.Category6Suite;
 import org.sonarqube.tests.Tester;
 import org.sonarqube.ws.Organizations.Organization;
 import org.sonarqube.ws.WsPermissions;
+import org.sonarqube.ws.WsPermissions.CreateTemplateWsResponse;
 import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
-import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.WsUsers.CreateWsResponse;
 import org.sonarqube.ws.client.WsClient;
 import org.sonarqube.ws.client.component.SearchProjectsRequest;
 import org.sonarqube.ws.client.permission.AddUserToTemplateWsRequest;
 import org.sonarqube.ws.client.permission.ApplyTemplateWsRequest;
+import org.sonarqube.ws.client.permission.BulkApplyTemplateWsRequest;
 import org.sonarqube.ws.client.permission.CreateTemplateWsRequest;
 import org.sonarqube.ws.client.permission.PermissionsService;
 import org.sonarqube.ws.client.permission.UsersWsRequest;
 
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class PermissionTemplateTest {
@@ -62,9 +66,9 @@ public class PermissionTemplateTest {
   @Test
   public void apply_permission_template_on_project() {
     Organization organization = tester.organizations().generate();
-    Project project = tester.projects().generate(organization, p -> p.setVisibility("private"));
-    WsUsers.CreateWsResponse.User user = tester.users().generateMember(organization);
-    WsUsers.CreateWsResponse.User anotherUser = tester.users().generateMember(organization);
+    Project project = createPrivateProject(organization);
+    CreateWsResponse.User user = tester.users().generateMember(organization);
+    CreateWsResponse.User anotherUser = tester.users().generateMember(organization);
 
     assertThatUserDoesNotHavePermission(user, organization, project);
     assertThatUserDoesNotHavePermission(anotherUser, organization, project);
@@ -80,12 +84,40 @@ public class PermissionTemplateTest {
     assertThat(userHasAccessToIndexedProject(anotherUser, organization, project)).isFalse();
   }
 
+  @Test
+  public void bulk_apply_template_on_projects() {
+    Organization organization = tester.organizations().generate();
+    CreateWsResponse.User user = tester.users().generateMember(organization);
+    CreateWsResponse.User anotherUser = tester.users().generateMember(organization);
+    WsPermissions.PermissionTemplate template = createTemplate(organization).getPermissionTemplate();
+    tester.wsClient().permissions().addUserToTemplate(new AddUserToTemplateWsRequest()
+      .setOrganization(organization.getKey())
+      .setTemplateId(template.getId())
+      .setLogin(user.getLogin())
+      .setPermission("user"));
+    Project project1 = createPrivateProject(organization);
+    Project project2 = createPrivateProject(organization);
+    Project untouchedProject = createPrivateProject(organization);
+
+    tester.wsClient().permissions().bulkApplyTemplate(new BulkApplyTemplateWsRequest()
+      .setOrganization(organization.getKey())
+      .setTemplateId(template.getId())
+      .setProjects(Arrays.asList(project1.getKey(), project2.getKey())));
+
+    assertThatUserDoesNotHavePermission(anotherUser, organization, untouchedProject);
+    assertThatUserDoesNotHavePermission(anotherUser, organization, project1);
+    assertThatUserDoesNotHavePermission(anotherUser, organization, project2);
+    assertThatUserHasPermission(user, organization, project1);
+    assertThatUserHasPermission(user, organization, project2);
+    assertThatUserDoesNotHavePermission(user, organization, untouchedProject);
+  }
+
   @Test
   public void indexing_errors_are_recovered_when_applying_permission_template_on_project() throws Exception {
     Organization organization = tester.organizations().generate();
-    Project project = tester.projects().generate(organization, p -> p.setVisibility("private"));
-    WsUsers.CreateWsResponse.User user = tester.users().generateMember(organization);
-    WsUsers.CreateWsResponse.User anotherUser = tester.users().generateMember(organization);
+    Project project = createPrivateProject(organization);
+    CreateWsResponse.User user = tester.users().generateMember(organization);
+    CreateWsResponse.User anotherUser = tester.users().generateMember(organization);
 
     lockWritesOnProjectIndices();
 
@@ -122,7 +154,7 @@ public class PermissionTemplateTest {
    * Gives the read access only to the specified user. All other users and groups
    * loose their ability to see the project.
    */
-  private void createAndApplyTemplate(Organization organization, Project project, WsUsers.CreateWsResponse.User user) {
+  private void createAndApplyTemplate(Organization organization, Project project, CreateWsResponse.User user) {
     String templateName = "For user";
     PermissionsService service = tester.wsClient().permissions();
     service.createTemplate(new CreateTemplateWsRequest()
@@ -140,15 +172,25 @@ public class PermissionTemplateTest {
       .setTemplateName(templateName));
   }
 
-  private void assertThatUserHasPermission(WsUsers.CreateWsResponse.User user, Organization organization, Project project) {
-    assertThat(hasAdminPermission(user, organization, project)).isTrue();
+  private CreateTemplateWsResponse createTemplate(Organization organization) {
+    return tester.wsClient().permissions().createTemplate(new CreateTemplateWsRequest()
+      .setOrganization(organization.getKey())
+      .setName(randomAlphabetic(20)));
+  }
+
+  private Project createPrivateProject(Organization organization) {
+    return tester.projects().generate(organization, p -> p.setVisibility("private"));
+  }
+
+  private void assertThatUserHasPermission(CreateWsResponse.User user, Organization organization, Project project) {
+    assertThat(hasBrowsePermission(user, organization, project)).isTrue();
   }
 
-  private void assertThatUserDoesNotHavePermission(WsUsers.CreateWsResponse.User user, Organization organization, Project project) {
-    assertThat(hasAdminPermission(user, organization, project)).isFalse();
+  private void assertThatUserDoesNotHavePermission(CreateWsResponse.User user, Organization organization, Project project) {
+    assertThat(hasBrowsePermission(user, organization, project)).isFalse();
   }
 
-  private boolean userHasAccessToIndexedProject(WsUsers.CreateWsResponse.User user, Organization organization, Project project) {
+  private boolean userHasAccessToIndexedProject(CreateWsResponse.User user, Organization organization, Project project) {
     SearchProjectsRequest request = SearchProjectsRequest.builder().setOrganization(organization.getKey()).build();
     WsClient userSession = tester.as(user.getLogin()).wsClient();
     return userSession.components().searchProjects(request)
@@ -156,7 +198,7 @@ public class PermissionTemplateTest {
       .anyMatch(c -> c.getKey().equals(project.getKey()));
   }
 
-  private boolean hasAdminPermission(WsUsers.CreateWsResponse.User user, Organization organization, Project project) {
+  private boolean hasBrowsePermission(CreateWsResponse.User user, Organization organization, Project project) {
     UsersWsRequest request = new UsersWsRequest()
       .setOrganization(organization.getKey())
       .setProjectKey(project.getKey())