From f63ea546f656b7150bea668f76a7f367eee97a41 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lievremont Date: Fri, 22 May 2015 09:58:17 +0200 Subject: [PATCH] SONAR-6472 New WS to create user groups --- .../platformlevel/PlatformLevel4.java | 1 + .../server/usergroups/ws/CreateAction.java | 134 +++++++++++++++ .../server/usergroups/ws/example-create.json | 8 + .../usergroups/ws/CreateActionTest.java | 157 ++++++++++++++++++ .../usergroups/ws/UserGroupsWsTest.java | 17 +- .../java/org/sonar/core/user/GroupMapper.java | 6 +- .../org/sonar/core/persistence/schema-h2.ddl | 2 +- 7 files changed, 317 insertions(+), 8 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/CreateAction.java create mode 100644 server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-create.json create mode 100644 server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/CreateActionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index 192c9fecd65..0972fecfe08 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -495,6 +495,7 @@ public class PlatformLevel4 extends PlatformLevel { GroupMembershipFinder.class, UserGroupsWs.class, org.sonar.server.usergroups.ws.SearchAction.class, + org.sonar.server.usergroups.ws.CreateAction.class, // permissions PermissionFacade.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/CreateAction.java b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/CreateAction.java new file mode 100644 index 00000000000..edfb2e8aeee --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/usergroups/ws/CreateAction.java @@ -0,0 +1,134 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.usergroups.ws; + +import com.google.common.base.Preconditions; +import java.net.HttpURLConnection; +import javax.annotation.Nullable; +import org.sonar.api.security.DefaultGroups; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService.NewAction; +import org.sonar.api.server.ws.WebService.NewController; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.user.GroupDto; +import org.sonar.server.db.DbClient; +import org.sonar.server.exceptions.ServerException; +import org.sonar.server.user.UserSession; + +import static org.sonar.core.persistence.MyBatis.closeQuietly; + +public class CreateAction implements UserGroupsWsAction { + + private static final String PARAM_DESCRIPTION = "description"; + private static final String PARAM_NAME = "name"; + + // Database column size should be 500 (since migration #353), + // but on some instances, column size is still 255, + // hence the validation is done with 255 + private static final int NAME_MAX_LENGTH = 255; + private static final int DESCRIPTION_MAX_LENGTH = 200; + + private final DbClient dbClient; + private final UserSession userSession; + + public CreateAction(DbClient dbClient, UserSession userSession) { + this.dbClient = dbClient; + this.userSession = userSession; + } + + @Override + public void define(NewController context) { + NewAction action = context.createAction("create") + .setDescription("Create a group.") + .setHandler(this) + .setPost(true) + .setResponseExample(getClass().getResource("example-create.json")) + .setSince("5.2"); + + action.createParam(PARAM_NAME) + .setDescription("Name for the new group. A group name cannot be larger than 255 characters and must be unique. " + + "The value 'anyone' (whatever the case) is reserved and cannot be used.") + .setExampleValue("sonar-users") + .setRequired(true); + + action.createParam(PARAM_DESCRIPTION) + .setDescription("Description for the new group. A group description cannot be larger than 200 characters.") + .setExampleValue("Default group for new users"); + } + + @Override + public void handle(Request request, Response response) throws Exception { + userSession.checkLoggedIn().checkGlobalPermission(GlobalPermissions.SYSTEM_ADMIN); + + String name = request.mandatoryParam(PARAM_NAME); + String description = request.param(PARAM_DESCRIPTION); + + validateName(name); + validateDescription(description); + + GroupDto newGroup = new GroupDto().setName(name).setDescription(description); + DbSession session = dbClient.openSession(false); + try { + checkNameIsUnique(name, session); + newGroup = dbClient.groupDao().insert(session, new GroupDto().setName(name).setDescription(description)); + session.commit(); + } finally { + closeQuietly(session); + } + + response.newJsonWriter().beginObject().name("group").beginObject() + .prop("id", newGroup.getId().toString()) + .prop(PARAM_NAME, newGroup.getName()) + .prop(PARAM_DESCRIPTION, newGroup.getDescription()) + .prop("membersCount", 0) + .endObject().endObject().close(); + } + + private void validateName(String name) { + checkNameLength(name); + checkNameNotAnyone(name); + } + + private void checkNameLength(String name) { + Preconditions.checkArgument(!name.isEmpty(), "Name cannot be empty"); + Preconditions.checkArgument(name.length() <= NAME_MAX_LENGTH, String.format("Name cannot be longer than %d characters", NAME_MAX_LENGTH)); + } + + private void checkNameNotAnyone(String name) { + Preconditions.checkArgument(!DefaultGroups.isAnyone(name), String.format("Name '%s' is reserved (regardless of case)", DefaultGroups.ANYONE)); + } + + private void checkNameIsUnique(String name, DbSession session) { + // There is no database constraint on column groups.name + // because MySQL cannot create a unique index + // on a UTF-8 VARCHAR larger than 255 characters on InnoDB + if (dbClient.groupDao().selectByKey(session, name) != null) { + throw new ServerException(HttpURLConnection.HTTP_CONFLICT, String.format("Name '%s' is already taken", name)); + } + } + + private void validateDescription(@Nullable String description) { + if (description != null) { + Preconditions.checkArgument(description.length() <= DESCRIPTION_MAX_LENGTH, String.format("Description cannot be longer than %d characters", DESCRIPTION_MAX_LENGTH)); + } + } +} diff --git a/server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-create.json b/server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-create.json new file mode 100644 index 00000000000..00f5eb9f6b2 --- /dev/null +++ b/server/sonar-server/src/main/resources/org/sonar/server/usergroups/ws/example-create.json @@ -0,0 +1,8 @@ +{ + "group": { + "id": "42", + "name": "some-product-bu", + "description": "Business Unit for Some Awesome Product", + "membersCount": 0 + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/CreateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/CreateActionTest.java new file mode 100644 index 00000000000..1967cb1ca01 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/CreateActionTest.java @@ -0,0 +1,157 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.usergroups.ws; + +import java.net.HttpURLConnection; +import org.apache.commons.lang.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.core.permission.GlobalPermissions; +import org.sonar.core.persistence.DbSession; +import org.sonar.core.persistence.DbTester; +import org.sonar.core.user.GroupDto; +import org.sonar.server.db.DbClient; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.ServerException; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.user.db.GroupDao; +import org.sonar.server.ws.WsTester; +import org.sonar.test.DbTests; + +@Category(DbTests.class) +public class CreateActionTest { + + @ClassRule + public static final DbTester dbTester = new DbTester(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private WsTester tester; + + private GroupDao groupDao; + + private DbSession session; + + @Before + public void setUp() { + dbTester.truncateTables(); + + groupDao = new GroupDao(System2.INSTANCE); + + DbClient dbClient = new DbClient(dbTester.database(), dbTester.myBatis(), groupDao); + + tester = new WsTester(new UserGroupsWs(new CreateAction(dbClient, userSession))); + + session = dbClient.openSession(false); + } + + @After + public void after() { + session.close(); + } + + @Test + public void create_nominal() throws Exception { + loginAsAdmin(); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", "some-product-bu") + .setParam("description", "Business Unit for Some Awesome Product") + .execute().assertJson("{" + + " \"group\": {" + + " \"name\": \"some-product-bu\"," + + " \"description\": \"Business Unit for Some Awesome Product\"," + + " \"membersCount\": 0" + + " }" + + "}"); + } + + @Test(expected = ForbiddenException.class) + public void require_admin_permission() throws Exception { + userSession.login("not-admin"); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", "some-product-bu") + .setParam("description", "Business Unit for Some Awesome Product") + .execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void name_too_short() throws Exception { + loginAsAdmin(); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", "") + .execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void name_too_long() throws Exception { + loginAsAdmin(); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", StringUtils.repeat("a", 255 + 1)) + .execute(); + } + + @Test(expected = IllegalArgumentException.class) + public void forbidden_name() throws Exception { + loginAsAdmin(); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", "AnYoNe") + .execute(); + } + + @Test + public void non_unique_name() throws Exception { + String groupName = "conflicting-name"; + groupDao.insert(session, new GroupDto() + .setName(groupName)); + session.commit(); + + expectedException.expect(ServerException.class); + expectedException.expectMessage("already taken"); + + loginAsAdmin(); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", groupName) + .execute().assertStatus(HttpURLConnection.HTTP_CONFLICT); + } + + @Test(expected = IllegalArgumentException.class) + public void description_too_long() throws Exception { + loginAsAdmin(); + tester.newPostRequest("api/usergroups", "create") + .setParam("name", "long-group-description-is-looooooooooooong") + .setParam("description", StringUtils.repeat("a", 200 + 1)) + .execute(); + } + + private void loginAsAdmin() { + userSession.login("admin").setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java index ad4ff6c6bfc..e0d9d4e060c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/usergroups/ws/UserGroupsWsTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.sonar.api.server.ws.WebService; import org.sonar.server.db.DbClient; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.user.UserSession; import org.sonar.server.ws.WsTester; import static org.assertj.core.api.Assertions.assertThat; @@ -38,7 +39,9 @@ public class UserGroupsWsTest { @Before public void setUp() { - WsTester tester = new WsTester(new UserGroupsWs(new SearchAction(mock(DbClient.class)))); + WsTester tester = new WsTester(new UserGroupsWs( + new SearchAction(mock(DbClient.class)), + new CreateAction(mock(DbClient.class), mock(UserSession.class)))); controller = tester.controller("api/usergroups"); } @@ -47,15 +50,23 @@ public class UserGroupsWsTest { assertThat(controller).isNotNull(); assertThat(controller.description()).isNotEmpty(); assertThat(controller.since()).isEqualTo("5.2"); - assertThat(controller.actions()).hasSize(1); + assertThat(controller.actions()).hasSize(2); } @Test public void define_search_action() { WebService.Action action = controller.action("search"); assertThat(action).isNotNull(); - assertThat(action.isPost()).isFalse(); assertThat(action.responseExampleAsString()).isNotEmpty(); assertThat(action.params()).hasSize(4); } + + @Test + public void define_create_action() { + WebService.Action action = controller.action("create"); + assertThat(action).isNotNull(); + assertThat(action.isPost()).isTrue(); + assertThat(action.responseExampleAsString()).isNotEmpty(); + assertThat(action.params()).hasSize(2); + } } diff --git a/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java b/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java index 1dadf30f572..24dfd2e6768 100644 --- a/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java +++ b/sonar-core/src/main/java/org/sonar/core/user/GroupMapper.java @@ -20,11 +20,9 @@ package org.sonar.core.user; -import org.apache.ibatis.session.RowBounds; - -import javax.annotation.CheckForNull; - import java.util.List; +import javax.annotation.CheckForNull; +import org.apache.ibatis.session.RowBounds; public interface GroupMapper { diff --git a/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl b/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl index f1b4b16c6c4..035a93ad9f3 100644 --- a/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl +++ b/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl @@ -82,7 +82,7 @@ CREATE TABLE "WIDGETS" ( CREATE TABLE "GROUPS" ( "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "NAME" VARCHAR(255), + "NAME" VARCHAR(500), "DESCRIPTION" VARCHAR(200), "CREATED_AT" TIMESTAMP, "UPDATED_AT" TIMESTAMP -- 2.39.5