diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestResponseFactory.java b/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestResponseFactory.java index 07e6f3463d6a4c7e9624668d1d0f275856a6e7e2..b457c6ec72096a65905a19d039c219264680c492 100644 --- a/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestResponseFactory.java +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestResponseFactory.java @@ -60,6 +60,7 @@ import org.activiti.rest.api.history.HistoricProcessInstanceResponse; import org.activiti.rest.api.history.HistoricTaskInstanceResponse; import org.activiti.rest.api.history.HistoricVariableInstanceResponse; import org.activiti.rest.api.identity.GroupResponse; +import org.activiti.rest.api.identity.MembershipResponse; import org.activiti.rest.api.identity.UserInfoResponse; import org.activiti.rest.api.identity.UserResponse; import org.activiti.rest.api.legacy.identity.LegacyRestIdentityLink; @@ -582,6 +583,13 @@ public class RestResponseFactory { return response; } + public MembershipResponse createMembershipResponse(SecuredResource securedResource, String userId, String groupId) { + MembershipResponse response = new MembershipResponse(); + response.setGroupId(groupId); + response.setUserId(userId); + response.setUrl(securedResource.createFullResourceUrl(RestUrls.URL_GROUP_MEMBERSHIP, groupId, userId)); + return response; + } /** * Called once when the converters need to be initialized. Override of custom conversion diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestUrls.java b/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestUrls.java index 7a68f8a474a82b4a5760c32ca49dcb9a0984b7c7..d3ecbe001d0dabb3f8156373f72ebeb755542009 100644 --- a/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestUrls.java +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/api/RestUrls.java @@ -63,6 +63,7 @@ public final class RestUrls { public static final String SEGMENT_GROUPS = "groups"; public static final String SEGMENT_PICTURE = "picture"; public static final String SEGMENT_INFO = "info"; + public static final String SEGMENT_MEMBERS = "members"; /** * URL template for the deployment collection: repository/deployments @@ -378,6 +379,16 @@ public final class RestUrls { */ public static final String[] URL_GROUP = {SEGMENT_IDENTITY_RESOURCES, SEGMENT_GROUPS, "{0}"}; + /** + * URL template for the membership-collection of a group: identity/groups/{0:groupId}/members + */ + public static final String[] URL_GROUP_MEMBERSHIP_COLLECTION = {SEGMENT_IDENTITY_RESOURCES, SEGMENT_GROUPS, "{0}", SEGMENT_MEMBERS}; + + /** + * URL template for the membership-collection of a single group membership: identity/groups/{0:groupId}/members/{1:userId} + */ + public static final String[] URL_GROUP_MEMBERSHIP = {SEGMENT_IDENTITY_RESOURCES, SEGMENT_GROUPS, "{0}", SEGMENT_MEMBERS, "{1}"}; + /** * Creates an url based on the passed fragments and replaces any placeholders with the given arguments. The * placeholders are folowing the {@link MessageFormat} convention diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/GroupMembershipCollectionResource.java b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/GroupMembershipCollectionResource.java new file mode 100644 index 0000000000000000000000000000000000000000..5febff6c541b7e0f5e6669a5e59e5e148a5a38db --- /dev/null +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/GroupMembershipCollectionResource.java @@ -0,0 +1,52 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.activiti.rest.api.identity; + +import org.activiti.engine.ActivitiIllegalArgumentException; +import org.activiti.engine.identity.Group; +import org.activiti.rest.api.ActivitiUtil; +import org.activiti.rest.application.ActivitiRestServicesApplication; +import org.restlet.data.Status; +import org.restlet.resource.Post; +import org.restlet.resource.ResourceException; + +/** + * @author Frederik Heremans + */ +public class GroupMembershipCollectionResource extends BaseGroupResource { + + @Post + public MembershipResponse createMembership(MembershipRequest memberShip) { + Group group = getGroupFromRequest(); + + if(memberShip.getUserId() == null) { + throw new ActivitiIllegalArgumentException("UserId cannot be null."); + } + + // Check if user is member of group since API doesn't return typed exception + if(ActivitiUtil.getIdentityService().createUserQuery() + .memberOfGroup(group.getId()) + .userId(memberShip.getUserId()) + .count() > 0) { + throw new ResourceException(Status.CLIENT_ERROR_CONFLICT.getCode(), "User '" + memberShip.getUserId() + + "' is already part of group '" + group.getId() + "'.", null, null); + } + + ActivitiUtil.getIdentityService().createMembership(memberShip.getUserId(), group.getId()); + setStatus(Status.SUCCESS_CREATED); + + return getApplication(ActivitiRestServicesApplication.class).getRestResponseFactory() + .createMembershipResponse(this, memberShip.getUserId(), group.getId()); + } +} diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/GroupMembershipResource.java b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/GroupMembershipResource.java new file mode 100644 index 0000000000000000000000000000000000000000..cc1fc61d0a17ec468ea3a0da83aeffa4894d15e1 --- /dev/null +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/GroupMembershipResource.java @@ -0,0 +1,49 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.activiti.rest.api.identity; + +import org.activiti.engine.ActivitiIllegalArgumentException; +import org.activiti.engine.identity.Group; +import org.activiti.rest.api.ActivitiUtil; +import org.restlet.data.Status; +import org.restlet.resource.Delete; +import org.restlet.resource.ResourceException; + +/** + * @author Frederik Heremans + */ +public class GroupMembershipResource extends BaseGroupResource { + + @Delete + public void deleteMembership() { + Group group = getGroupFromRequest(); + + String userId = getAttribute("userId"); + if(userId == null) { + throw new ActivitiIllegalArgumentException("UserId cannot be null."); + } + + // Check if user is not a member of group since API doesn't return typed exception + if(ActivitiUtil.getIdentityService().createUserQuery() + .memberOfGroup(group.getId()) + .userId(userId) + .count() != 1) { + throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND.getCode(), "User '" + userId + + "' is not part of group '" + group.getId() + "'.", null, null); + } + + ActivitiUtil.getIdentityService().deleteMembership(userId, group.getId()); + setStatus(Status.SUCCESS_NO_CONTENT); + } +} diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/MembershipRequest.java b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/MembershipRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..a6ca053900a60614aba6cfd35f6693b94cd22922 --- /dev/null +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/MembershipRequest.java @@ -0,0 +1,30 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.activiti.rest.api.identity; + + +/** + * @author Frederik Heremans + */ +public class MembershipRequest { + + protected String userId; + + public void setUserId(String userId) { + this.userId = userId; + } + public String getUserId() { + return userId; + } +} diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/MembershipResponse.java b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/MembershipResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..1603c85338e97b9bc0a72b2a541da8a8ba71b3cf --- /dev/null +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/api/identity/MembershipResponse.java @@ -0,0 +1,37 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.activiti.rest.api.identity; + + +/** + * @author Frederik Heremans + */ +public class MembershipResponse extends MembershipRequest { + + protected String url; + protected String groupId; + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + public String getGroupId() { + return groupId; + } + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } +} diff --git a/modules/activiti-rest/src/main/java/org/activiti/rest/application/RestServicesInit.java b/modules/activiti-rest/src/main/java/org/activiti/rest/application/RestServicesInit.java index 163308ccb0e5a2338e90ab767d6ce4d3dae757c5..01da98e414502cb7d9669190d124ad80687a19c9 100644 --- a/modules/activiti-rest/src/main/java/org/activiti/rest/application/RestServicesInit.java +++ b/modules/activiti-rest/src/main/java/org/activiti/rest/application/RestServicesInit.java @@ -16,6 +16,8 @@ import org.activiti.rest.api.history.HistoricTaskInstanceResource; import org.activiti.rest.api.history.HistoricVariableInstanceCollectionResource; import org.activiti.rest.api.history.HistoricVariableInstanceQueryResource; import org.activiti.rest.api.identity.GroupCollectionResource; +import org.activiti.rest.api.identity.GroupMembershipCollectionResource; +import org.activiti.rest.api.identity.GroupMembershipResource; import org.activiti.rest.api.identity.GroupResource; import org.activiti.rest.api.identity.UserCollectionResource; import org.activiti.rest.api.identity.UserInfoCollectionResource; @@ -179,6 +181,8 @@ public class RestServicesInit { router.attach("/identity/users/{userId}/info", UserInfoCollectionResource.class); router.attach("/identity/groups", GroupCollectionResource.class); router.attach("/identity/groups/{groupId}", GroupResource.class); + router.attach("/identity/groups/{groupId}/members", GroupMembershipCollectionResource.class); + router.attach("/identity/groups/{groupId}/members/{userId}", GroupMembershipResource.class); router.attach("/query/tasks", TaskQueryResource.class); diff --git a/modules/activiti-rest/src/test/java/org/activiti/rest/api/identity/GroupMembershipResourceTest.java b/modules/activiti-rest/src/test/java/org/activiti/rest/api/identity/GroupMembershipResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2e24fef69e60d6703b9cbec71c0a288226929721 --- /dev/null +++ b/modules/activiti-rest/src/test/java/org/activiti/rest/api/identity/GroupMembershipResourceTest.java @@ -0,0 +1,246 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.activiti.rest.api.identity; + +import org.activiti.engine.identity.Group; +import org.activiti.engine.identity.User; +import org.activiti.rest.BaseRestTestCase; +import org.activiti.rest.api.RestUrls; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.ObjectNode; +import org.restlet.data.Status; +import org.restlet.representation.Representation; +import org.restlet.resource.ClientResource; +import org.restlet.resource.ResourceException; + + +/** + * @author Frederik Heremans + */ +public class GroupMembershipResourceTest extends BaseRestTestCase { + + public void testCreatemembership() throws Exception { + try { + Group testGroup = identityService.newGroup("testgroup"); + testGroup.setName("Test group"); + testGroup.setType("Test type"); + identityService.saveGroup(testGroup); + + User testUser = identityService.newUser("testuser"); + identityService.saveUser(testUser); + + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_GROUP_MEMBERSHIP_COLLECTION, "testgroup")); + + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("userId", "testuser"); + + Representation response = client.post(requestNode); + assertEquals(Status.SUCCESS_CREATED, client.getResponse().getStatus()); + + JsonNode responseNode = objectMapper.readTree(response.getStream()); + assertNotNull(responseNode); + assertEquals("testuser", responseNode.get("userId").getTextValue()); + assertEquals("testgroup", responseNode.get("groupId").getTextValue()); + assertTrue(responseNode.get("url").getTextValue().endsWith(RestUrls.createRelativeResourceUrl( + RestUrls.URL_GROUP_MEMBERSHIP, testGroup.getId(), testUser.getId()))); + + Group createdGroup = identityService.createGroupQuery().groupId("testgroup").singleResult(); + assertNotNull(createdGroup); + assertEquals("Test group", createdGroup.getName()); + assertEquals("Test type", createdGroup.getType()); + + assertNotNull(identityService.createUserQuery().memberOfGroup("testgroup").singleResult()); + assertEquals("testuser", identityService.createUserQuery().memberOfGroup("testgroup").singleResult().getId()); + } finally { + try { + identityService.deleteGroup("testgroup"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + + try { + identityService.deleteUser("testuser"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + } + } + + public void testCreateMembershipAlreadyExisting() throws Exception { + try { + Group testGroup = identityService.newGroup("testgroup"); + testGroup.setName("Test group"); + testGroup.setType("Test type"); + identityService.saveGroup(testGroup); + + User testUser = identityService.newUser("testuser"); + identityService.saveUser(testUser); + + identityService.createMembership("testuser", "testgroup"); + + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_GROUP_MEMBERSHIP_COLLECTION, "testgroup")); + + ObjectNode requestNode = objectMapper.createObjectNode(); + requestNode.put("userId", "testuser"); + + try { + client.post(requestNode); + fail("Exception expected"); + } catch(ResourceException expected) { + assertEquals(Status.CLIENT_ERROR_CONFLICT, expected.getStatus()); + assertEquals("User 'testuser' is already part of group 'testgroup'.", expected.getStatus().getDescription()); + } + } finally { + try { + identityService.deleteGroup("testgroup"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + + try { + identityService.deleteUser("testuser"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + } + } + + public void testDeleteMembership() throws Exception { + try { + Group testGroup = identityService.newGroup("testgroup"); + testGroup.setName("Test group"); + testGroup.setType("Test type"); + identityService.saveGroup(testGroup); + + User testUser = identityService.newUser("testuser"); + identityService.saveUser(testUser); + + identityService.createMembership("testuser", "testgroup"); + + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl( + RestUrls.URL_GROUP_MEMBERSHIP, "testgroup", "testuser")); + + Representation response = client.delete(); + assertEquals(Status.SUCCESS_NO_CONTENT, client.getResponse().getStatus()); + assertEquals(0, response.getSize()); + + // Check if membership is actually deleted + assertNull(identityService.createUserQuery().memberOfGroup("testgroup").singleResult()); + } finally { + try { + identityService.deleteGroup("testgroup"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + + try { + identityService.deleteUser("testuser"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + } + } + + /** + * Test delete membership that is no member in the group. + */ + public void testDeleteMembershipNoMember() throws Exception { + try { + Group testGroup = identityService.newGroup("testgroup"); + testGroup.setName("Test group"); + testGroup.setType("Test type"); + identityService.saveGroup(testGroup); + + User testUser = identityService.newUser("testuser"); + identityService.saveUser(testUser); + + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl( + RestUrls.URL_GROUP_MEMBERSHIP, "testgroup", "testuser")); + + try { + client.delete(); + fail("Exception expected"); + } catch(ResourceException expected) { + assertEquals(Status.CLIENT_ERROR_NOT_FOUND, expected.getStatus()); + assertEquals("User 'testuser' is not part of group 'testgroup'.", expected.getStatus().getDescription()); + } + + } finally { + try { + identityService.deleteGroup("testgroup"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + + try { + identityService.deleteUser("testuser"); + } catch(Throwable ignore) { + // Ignore, since the group may not have been created in the test + // or already deleted + } + } + } + + /** + * Test deleting member from an unexisting group. + */ + public void testDeleteMemberfromUnexistingGroup() throws Exception { + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl( + RestUrls.URL_GROUP_MEMBERSHIP, "unexisting", "kermit")); + try { + client.delete(); + fail("Exception expected"); + } catch(ResourceException expected) { + assertEquals(Status.CLIENT_ERROR_NOT_FOUND, expected.getStatus()); + assertEquals("Could not find a group with id 'unexisting'.", expected.getStatus().getDescription()); + } + } + + /** + * Test adding member to an unexisting group. + */ + public void testAddMemberToUnexistingGroup() throws Exception { + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_GROUP_MEMBERSHIP_COLLECTION, "unexisting")); + + try { + client.post(objectMapper.createObjectNode()); + fail("Exception expected"); + } catch(ResourceException expected) { + assertEquals(Status.CLIENT_ERROR_NOT_FOUND, expected.getStatus()); + assertEquals("Could not find a group with id 'unexisting'.", expected.getStatus().getDescription()); + } + } + + /** + * Test adding member to a group, without specifying userId + */ + public void testAddMemberNoUserId() throws Exception { + ClientResource client = getAuthenticatedClient(RestUrls.createRelativeResourceUrl(RestUrls.URL_GROUP_MEMBERSHIP_COLLECTION, "admin")); + + try { + client.post(objectMapper.createObjectNode()); + fail("Exception expected"); + } catch(ResourceException expected) { + assertEquals(Status.CLIENT_ERROR_BAD_REQUEST, expected.getStatus()); + assertEquals("UserId cannot be null.", expected.getStatus().getDescription()); + } + } +} diff --git a/userguide/src/en/chapters/ch14-REST.xml b/userguide/src/en/chapters/ch14-REST.xml index c0020234bbd1a6bdb9b52b0314054795163da44e..c9643a15b03553b2848820d8d4dcc599bbc0bfa3 100644 --- a/userguide/src/en/chapters/ch14-REST.xml +++ b/userguide/src/en/chapters/ch14-REST.xml @@ -5479,6 +5479,153 @@ Only the attachment name is required to create a new attachment. +
+ Get members in a group + There is no GET allowed on identity/groups/members. Use the identity/users?memberOfGroup=sales URL to get all users that are part of a particular group. +
+
+ Add a member to a group + + POST identity/groups/{groupId}/members + + + + URL parameters + + + + Parameter + Required + Value + Description + + + + + groupId + Yes + String + The id of the group to add a member to. + + + +
+
+ + Body JSON: + +{ + "userId":"kermit" +} + + + + Response codes + + + + Response code + Description + + + + + 201 + Indicates the group was found and the member has been added. + + + 404 + Indicates the userId was not included in the request body. + + + 404 + Indicates the requested group was not found. + + + 409 + Indicates the requested user is already a member of the group. + + + +
+
+ + Response Body: + +{ + "userId":"kermit", + "groupId":"sales", + "url":"http://localhost:8182/identity/groups/sales/members/kermit" +} + +
+
+ Delete a member from a group + + DELETE identity/groups/{groupId}/members/{userId} + + + + URL parameters + + + + Parameter + Required + Value + Description + + + + + groupId + Yes + String + The id of the group to remove a member from. + + + userId + Yes + String + The id of the user to remove. + + + +
+
+ + + Response codes + + + + Response code + Description + + + + + 204 + Indicates the group was found and the member has been deleted. The response body is left empty intentionally. + + + 404 + Indicates the requested group was not found or that the user is not a member of the group. The status description contains additional information about the error. + + + +
+
+ + Response Body: + +{ + "userId":"kermit", + "groupId":"sales", + "url":"http://localhost:8182/identity/groups/sales/members/kermit" +} + +