1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.external.endpoint;
22
23 import static com.entwinemedia.fn.data.json.Jsons.arr;
24 import static com.entwinemedia.fn.data.json.Jsons.f;
25 import static com.entwinemedia.fn.data.json.Jsons.obj;
26 import static com.entwinemedia.fn.data.json.Jsons.v;
27 import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
28 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
29 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
30 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
31 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
32 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
33 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
34 import static javax.servlet.http.HttpServletResponse.SC_OK;
35 import static org.apache.commons.lang3.StringUtils.join;
36 import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
37 import static org.opencastproject.external.common.ApiVersion.VERSION_1_6_0;
38 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
39
40 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
41 import org.opencastproject.external.common.ApiMediaType;
42 import org.opencastproject.external.common.ApiResponseBuilder;
43 import org.opencastproject.external.common.ApiVersion;
44 import org.opencastproject.index.service.resources.list.query.GroupsListQuery;
45 import org.opencastproject.index.service.util.RestUtils;
46 import org.opencastproject.security.api.SecurityService;
47 import org.opencastproject.security.api.UnauthorizedException;
48 import org.opencastproject.security.impl.jpa.JpaGroup;
49 import org.opencastproject.userdirectory.ConflictException;
50 import org.opencastproject.userdirectory.JpaGroupRoleProvider;
51 import org.opencastproject.util.NotFoundException;
52 import org.opencastproject.util.doc.rest.RestParameter;
53 import org.opencastproject.util.doc.rest.RestQuery;
54 import org.opencastproject.util.doc.rest.RestResponse;
55 import org.opencastproject.util.doc.rest.RestService;
56 import org.opencastproject.util.requests.SortCriterion;
57
58 import com.entwinemedia.fn.data.json.Field;
59 import com.entwinemedia.fn.data.json.JValue;
60 import com.entwinemedia.fn.data.json.Jsons;
61
62 import org.apache.commons.collections4.ComparatorUtils;
63 import org.apache.commons.lang3.StringUtils;
64 import org.osgi.service.component.annotations.Activate;
65 import org.osgi.service.component.annotations.Component;
66 import org.osgi.service.component.annotations.Reference;
67 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 import java.util.ArrayList;
72 import java.util.Collections;
73 import java.util.Comparator;
74 import java.util.HashMap;
75 import java.util.HashSet;
76 import java.util.List;
77 import java.util.Map;
78 import java.util.Optional;
79 import java.util.Set;
80 import java.util.stream.Collectors;
81
82 import javax.servlet.http.HttpServletResponse;
83 import javax.ws.rs.DELETE;
84 import javax.ws.rs.FormParam;
85 import javax.ws.rs.GET;
86 import javax.ws.rs.HeaderParam;
87 import javax.ws.rs.POST;
88 import javax.ws.rs.PUT;
89 import javax.ws.rs.Path;
90 import javax.ws.rs.PathParam;
91 import javax.ws.rs.Produces;
92 import javax.ws.rs.QueryParam;
93 import javax.ws.rs.WebApplicationException;
94 import javax.ws.rs.core.Response;
95
96 @Path("/api/groups")
97 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
98 ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
99 ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
100 ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
101 @RestService(name = "externalapigroups", title = "External API Groups Service", notes = {}, abstractText = "Provides resources and operations related to the groups")
102 @Component(
103 immediate = true,
104 service = GroupsEndpoint.class,
105 property = {
106 "service.description=External API - Groups Endpoint",
107 "opencast.service.type=org.opencastproject.external.groups",
108 "opencast.service.path=/api/groups"
109 }
110 )
111 @JaxrsResource
112 public class GroupsEndpoint {
113
114
115 private static final Logger logger = LoggerFactory.getLogger(GroupsEndpoint.class);
116
117
118 private ElasticsearchIndex elasticsearchIndex;
119 private JpaGroupRoleProvider jpaGroupRoleProvider;
120 private SecurityService securityService;
121
122
123 @Reference
124 void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
125 this.elasticsearchIndex = elasticsearchIndex;
126 }
127
128
129 @Reference
130 public void setSecurityService(SecurityService securityService) {
131 this.securityService = securityService;
132 }
133
134
135 @Reference
136 public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
137 this.jpaGroupRoleProvider = jpaGroupRoleProvider;
138 }
139
140
141 @Activate
142 void activate() {
143 logger.info("Activating External API - Groups Endpoint");
144 }
145
146 @GET
147 @Path("")
148 @RestQuery(name = "getgroups", description = "Returns a list of groups.", returnDescription = "", restParameters = {
149 @RestParameter(name = "filter", isRequired = false, description = "A comma seperated list of filters to limit the results with. A filter is the filter's name followed by a colon \":\" and then the value to filter with so it is the form [Filter Name]:[Value to Filter With].", type = STRING),
150 @RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting criteria. In the comma seperated list each type of sorting is specified as a pair such as: <Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or descending order and is mandatory.", isRequired = false, type = STRING),
151 @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.", isRequired = false, type = RestParameter.Type.INTEGER),
152 @RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false, type = RestParameter.Type.INTEGER) }, responses = {
153 @RestResponse(description = "A (potentially empty) list of groups.", responseCode = HttpServletResponse.SC_OK) })
154 public Response getGroups(@HeaderParam("Accept") String acceptHeader, @QueryParam("filter") String filter,
155 @QueryParam("sort") String sort, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) {
156 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
157 Optional<Integer> optLimit = Optional.ofNullable(limit);
158 Optional<Integer> optOffset = Optional.ofNullable(offset);
159
160 if (optLimit.isPresent() && limit <= 0) {
161 optLimit = Optional.empty();
162 }
163 if (optOffset.isPresent() && offset < 0) {
164 optOffset = Optional.empty();
165 }
166
167
168 Map<String, String> filters = new HashMap<>();
169 if (StringUtils.isNotBlank(filter)) {
170 for (String f : filter.split(",")) {
171 String[] filterTuple = f.split(":");
172 if (filterTuple.length < 2) {
173 logger.debug("No value for filter '{}' in filters list: {}", filterTuple[0], filter);
174 continue;
175 }
176
177 filters.put(filterTuple[0].trim(), f.substring(filterTuple[0].length() + 1).trim());
178 }
179 }
180 Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
181
182 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
183
184
185 Set<SortCriterion> deprecatedSortCriteria = new HashSet<>();
186 if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
187 deprecatedSortCriteria = sortCriteria.stream().filter(
188 sortCriterion -> sortCriterion.getFieldName().equals("members")
189 || sortCriterion.getFieldName().equals("roles")).collect(Collectors.toSet());
190 sortCriteria.removeAll(deprecatedSortCriteria);
191 }
192
193 List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter, Optional.empty(),
194 sortCriteria);
195
196
197 if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
198 List<Comparator<JpaGroup>> comparators = new ArrayList<>();
199 for (SortCriterion sortCriterion : deprecatedSortCriteria) {
200 Comparator<JpaGroup> comparator;
201 switch (sortCriterion.getFieldName()) {
202 case "members":
203 comparator = new GroupComparator() {
204 @Override
205 public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
206 return jpaGroup.getMembers();
207 }
208 };
209 break;
210 case "roles":
211 comparator = new GroupComparator() {
212 @Override
213 public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
214 return jpaGroup.getRoleNames();
215 }
216 };
217 break;
218 default:
219 continue;
220 }
221
222 if (sortCriterion.getOrder() == SortCriterion.Order.Descending) {
223 comparator = comparator.reversed();
224 }
225 comparators.add(comparator);
226 }
227 Collections.sort(results, ComparatorUtils.chainedComparator(comparators));
228 }
229
230 List<JValue> groupsJSON = new ArrayList<>();
231 for (JpaGroup group : results) {
232 List<Field> fields = new ArrayList<>();
233 fields.add(f("identifier", v(group.getGroupId())));
234 fields.add(f("organization", v(group.getOrganization().getId())));
235 fields.add(f("role", v(group.getRole())));
236 fields.add(f("name", v(group.getName(), Jsons.BLANK)));
237 fields.add(f("description", v(group.getDescription(), Jsons.BLANK)));
238 fields.add(f("roles", v(join(group.getRoleNames(), ","), Jsons.BLANK)));
239 fields.add(f("members", v(join(group.getMembers(), ","), Jsons.BLANK)));
240 groupsJSON.add(obj(fields));
241 }
242 return ApiResponseBuilder.Json.ok(acceptHeader, arr(groupsJSON));
243 }
244
245
246
247
248 private abstract class GroupComparator implements Comparator<JpaGroup> {
249
250 public abstract Set<String> getGroupAttribute(JpaGroup jpaGroup);
251
252 @Override
253 public int compare(JpaGroup group1, JpaGroup group2) {
254 List<String> members1 = new ArrayList<>(getGroupAttribute(group1));
255 List<String> members2 = new ArrayList<>(getGroupAttribute(group2));
256
257 for (int i = 0; i < members1.size() && i < members2.size(); i++) {
258 String member1 = members1.get(i);
259 String member2 = members2.get(i);
260 int result = member1.compareTo(member2);
261 if (result != 0) {
262 return result;
263 }
264 }
265 return (members1.size() - members2.size());
266 }
267 }
268
269 @GET
270 @Path("{groupId}")
271 @RestQuery(name = "getgroup", description = "Returns a single group.", returnDescription = "", pathParameters = {
272 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, responses = {
273 @RestResponse(description = "The group is returned.", responseCode = HttpServletResponse.SC_OK),
274 @RestResponse(description = "The specified group does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
275 public Response getGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id) {
276 JpaGroup group = jpaGroupRoleProvider.getGroup(id);
277
278 if (group == null) {
279 return ApiResponseBuilder.notFound("Cannot find a group with id '%s'.", id);
280 }
281
282 return ApiResponseBuilder.Json.ok(acceptHeader,
283 obj(
284 f("identifier", v(group.getGroupId())),
285 f("organization", v(group.getOrganization().getId())), f("role", v(group.getRole())),
286 f("name", v(group.getName(), Jsons.BLANK)),
287 f("description", v(group.getDescription(), Jsons.BLANK)),
288 f("roles", v(join(group.getRoleNames(), ","), Jsons.BLANK)),
289 f("members", v(join(group.getMembers(), ","), Jsons.BLANK))
290 )
291 );
292 }
293
294 @DELETE
295 @Path("{groupId}")
296 @RestQuery(name = "deletegroup", description = "Deletes a group.", returnDescription = "", pathParameters = {
297 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, responses = {
298 @RestResponse(description = "The group has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
299 @RestResponse(description = "The specified group does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
300 public Response deleteGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id)
301 throws NotFoundException {
302 try {
303 jpaGroupRoleProvider.removeGroup(id);
304 return Response.noContent().build();
305 } catch (NotFoundException e) {
306 return Response.status(SC_NOT_FOUND).build();
307 } catch (UnauthorizedException e) {
308 return Response.status(SC_FORBIDDEN).build();
309 } catch (Exception e) {
310 logger.error("Unable to delete group {}", id, e);
311 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
312 }
313 }
314
315 @PUT
316 @Path("{groupId}")
317 @RestQuery(name = "updategroup", description = "Updates a group.", returnDescription = "", pathParameters = {
318 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, restParameters = {
319 @RestParameter(name = "name", isRequired = false, description = "Group Name", type = STRING),
320 @RestParameter(name = "description", description = "Group Description", isRequired = false, type = STRING),
321 @RestParameter(name = "roles", description = "Comma-separated list of roles", isRequired = false, type = STRING),
322 @RestParameter(name = "members", description = "Comma-separated list of members", isRequired = false, type = STRING) }, responses = {
323 @RestResponse(description = "The group has been updated.", responseCode = HttpServletResponse.SC_CREATED),
324 @RestResponse(description = "The specified group does not exist.", responseCode = HttpServletResponse.SC_BAD_REQUEST) })
325 public Response updateGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
326 @FormParam("name") String name, @FormParam("description") String description,
327 @FormParam("roles") String roles, @FormParam("members") String members) throws Exception {
328 try {
329 jpaGroupRoleProvider.updateGroup(id, name, description, roles, members);
330 } catch (IllegalArgumentException e) {
331 logger.warn("Unable to update group id {}: {}", id, e.getMessage());
332 return Response.status(SC_BAD_REQUEST).build();
333 } catch (UnauthorizedException ex) {
334 return Response.status(SC_FORBIDDEN).build();
335 }
336 return Response.ok().build();
337 }
338
339 @POST
340 @Path("")
341 @RestQuery(name = "creategroup", description = "Creates a group.", returnDescription = "", restParameters = {
342 @RestParameter(name = "name", isRequired = true, description = "Group Name", type = STRING),
343 @RestParameter(name = "description", description = "Group Description", isRequired = false, type = STRING),
344 @RestParameter(name = "roles", description = "Comma-separated list of roles", isRequired = false, type = STRING),
345 @RestParameter(name = "members", description = "Comma-separated list of members", isRequired = false, type = STRING) }, responses = {
346 @RestResponse(description = "A new group is created.", responseCode = SC_CREATED),
347 @RestResponse(description = "The request is invalid or inconsistent.", responseCode = SC_BAD_REQUEST) })
348 public Response createGroup(@HeaderParam("Accept") String acceptHeader, @FormParam("name") String name,
349 @FormParam("description") String description, @FormParam("roles") String roles,
350 @FormParam("members") String members) {
351 try {
352 jpaGroupRoleProvider.createGroup(name, description, roles, members);
353 } catch (IllegalArgumentException e) {
354 logger.warn("Unable to create group {}: {}", name, e.getMessage());
355 return Response.status(SC_BAD_REQUEST).build();
356 } catch (UnauthorizedException e) {
357 return Response.status(SC_FORBIDDEN).build();
358 } catch (ConflictException e) {
359 return Response.status(SC_CONFLICT).build();
360 }
361 return Response.status(SC_CREATED).build();
362 }
363
364 @POST
365 @Path("{groupId}/members")
366 @RestQuery(name = "addgroupmember", description = "Adds a member to a group.", returnDescription = "", pathParameters = {
367 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING) }, restParameters = {
368 @RestParameter(name = "member", description = "Member Name", isRequired = true, type = STRING) }, responses = {
369 @RestResponse(description = "The member was already member of the group.", responseCode = SC_OK),
370 @RestResponse(description = "The member has been added.", responseCode = SC_NO_CONTENT),
371 @RestResponse(description = "The specified group does not exist.", responseCode = SC_NOT_FOUND) })
372 public Response addGroupMember(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
373 @FormParam("member") String member) {
374 try {
375 if (jpaGroupRoleProvider.addMemberToGroup(id, member)) {
376 return Response.ok().build();
377 } else {
378 return ApiResponseBuilder.Json.ok(acceptHeader, "Member is already member of group.");
379 }
380 } catch (IllegalArgumentException e) {
381 logger.warn("Unable to add member to group id {}.", id, e);
382 return Response.status(SC_BAD_REQUEST).build();
383 } catch (UnauthorizedException ex) {
384 return Response.status(SC_FORBIDDEN).build();
385 } catch (NotFoundException e) {
386 return ApiResponseBuilder.notFound("Cannot find group with id '%s'.", id);
387 } catch (Exception e) {
388 logger.warn("Could not update the group with id {}.",id, e);
389 return ApiResponseBuilder.serverError("Could not update group with id '%s', reason: '%s'",id,getMessage(e));
390 }
391 }
392
393 @DELETE
394 @Path("{groupId}/members/{memberId}")
395 @RestQuery(name = "removegroupmember", description = "Removes a member from a group", returnDescription = "", pathParameters = {
396 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING),
397 @RestParameter(name = "memberId", description = "The member id", isRequired = true, type = STRING) }, responses = {
398 @RestResponse(description = "The member has been removed.", responseCode = HttpServletResponse.SC_NO_CONTENT),
399 @RestResponse(description = "The specified group or member does not exist.", responseCode = HttpServletResponse.SC_NOT_FOUND) })
400 public Response removeGroupMember(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
401 @PathParam("memberId") String memberId) {
402 try {
403 if (jpaGroupRoleProvider.removeMemberFromGroup(id, memberId)) {
404 return Response.ok().build();
405 } else {
406 return ApiResponseBuilder.Json.ok(acceptHeader, "Member is already not member of group.");
407 }
408 } catch (IllegalArgumentException e) {
409 logger.warn("Unable to remove member from group id {}.", id, e);
410 return Response.status(SC_BAD_REQUEST).build();
411 } catch (UnauthorizedException ex) {
412 return Response.status(SC_FORBIDDEN).build();
413 } catch (NotFoundException e) {
414 return ApiResponseBuilder.notFound("Cannot find group with id '%s'.", id);
415 } catch (Exception e) {
416 logger.warn("Could not update the group with id {}.", id, e);
417 return ApiResponseBuilder.serverError("Could not update group with id '%s', reason: '%s'", id, getMessage(e));
418 }
419 }
420 }