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 javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
24 import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
25 import static javax.servlet.http.HttpServletResponse.SC_CREATED;
26 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
27 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
28 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
29 import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
30 import static javax.servlet.http.HttpServletResponse.SC_OK;
31 import static org.apache.commons.lang3.exception.ExceptionUtils.getMessage;
32 import static org.opencastproject.external.common.ApiVersion.VERSION_1_6_0;
33 import static org.opencastproject.index.service.util.JSONUtils.safeString;
34 import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
35
36 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
37 import org.opencastproject.external.common.ApiMediaType;
38 import org.opencastproject.external.common.ApiResponseBuilder;
39 import org.opencastproject.external.common.ApiVersion;
40 import org.opencastproject.index.service.resources.list.query.GroupsListQuery;
41 import org.opencastproject.index.service.util.RestUtils;
42 import org.opencastproject.security.api.SecurityService;
43 import org.opencastproject.security.api.UnauthorizedException;
44 import org.opencastproject.security.impl.jpa.JpaGroup;
45 import org.opencastproject.userdirectory.ConflictException;
46 import org.opencastproject.userdirectory.JpaGroupRoleProvider;
47 import org.opencastproject.util.NotFoundException;
48 import org.opencastproject.util.doc.rest.RestParameter;
49 import org.opencastproject.util.doc.rest.RestQuery;
50 import org.opencastproject.util.doc.rest.RestResponse;
51 import org.opencastproject.util.doc.rest.RestService;
52 import org.opencastproject.util.requests.SortCriterion;
53
54 import com.google.gson.JsonArray;
55 import com.google.gson.JsonObject;
56
57 import org.apache.commons.collections4.ComparatorUtils;
58 import org.apache.commons.lang3.StringUtils;
59 import org.osgi.service.component.annotations.Activate;
60 import org.osgi.service.component.annotations.Component;
61 import org.osgi.service.component.annotations.Reference;
62 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.Comparator;
69 import java.util.HashMap;
70 import java.util.HashSet;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.Optional;
74 import java.util.Set;
75 import java.util.stream.Collectors;
76
77 import javax.servlet.http.HttpServletResponse;
78 import javax.ws.rs.DELETE;
79 import javax.ws.rs.FormParam;
80 import javax.ws.rs.GET;
81 import javax.ws.rs.HeaderParam;
82 import javax.ws.rs.POST;
83 import javax.ws.rs.PUT;
84 import javax.ws.rs.Path;
85 import javax.ws.rs.PathParam;
86 import javax.ws.rs.Produces;
87 import javax.ws.rs.QueryParam;
88 import javax.ws.rs.WebApplicationException;
89 import javax.ws.rs.core.Response;
90
91 @Path("/api/groups")
92 @Produces({ ApiMediaType.JSON, ApiMediaType.VERSION_1_0_0, ApiMediaType.VERSION_1_1_0, ApiMediaType.VERSION_1_2_0,
93 ApiMediaType.VERSION_1_3_0, ApiMediaType.VERSION_1_4_0, ApiMediaType.VERSION_1_5_0,
94 ApiMediaType.VERSION_1_6_0, ApiMediaType.VERSION_1_7_0, ApiMediaType.VERSION_1_8_0,
95 ApiMediaType.VERSION_1_9_0, ApiMediaType.VERSION_1_10_0, ApiMediaType.VERSION_1_11_0 })
96 @RestService(
97 name = "externalapigroups",
98 title = "External API Groups Service",
99 notes = {},
100 abstractText = "Provides resources and operations related to the groups"
101 )
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(
149 name = "getgroups",
150 description = "Returns a list of groups.",
151 returnDescription = "",
152 restParameters = {
153 @RestParameter(name = "filter", isRequired = false, description = "A comma seperated list of filters to "
154 + "limit the results with. A filter is the filter's name followed by a colon \":\" and then the value to "
155 + "filter with so it is the form [Filter Name]:[Value to Filter With].", type = STRING),
156 @RestParameter(name = "sort", description = "Sort the results based upon a list of comma seperated sorting "
157 + "criteria. In the comma seperated list each type of sorting is specified as a pair such as: "
158 + "<Sort Name>:ASC or <Sort Name>:DESC. Adding the suffix ASC or DESC sets the order as ascending or "
159 + "descending order and is mandatory.", isRequired = false, type = STRING),
160 @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.",
161 isRequired = false, type = RestParameter.Type.INTEGER),
162 @RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false,
163 type = RestParameter.Type.INTEGER)
164 },
165 responses = {
166 @RestResponse(description = "A (potentially empty) list of groups.", responseCode = HttpServletResponse.SC_OK)
167 })
168 public Response getGroups(@HeaderParam("Accept") String acceptHeader, @QueryParam("filter") String filter,
169 @QueryParam("sort") String sort, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) {
170 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
171 Optional<Integer> optLimit = Optional.ofNullable(limit);
172 Optional<Integer> optOffset = Optional.ofNullable(offset);
173
174 if (optLimit.isPresent() && limit <= 0) {
175 optLimit = Optional.empty();
176 }
177 if (optOffset.isPresent() && offset < 0) {
178 optOffset = Optional.empty();
179 }
180
181
182 Map<String, String> filters = new HashMap<>();
183 if (StringUtils.isNotBlank(filter)) {
184 for (String f : filter.split(",")) {
185 String[] filterTuple = f.split(":");
186 if (filterTuple.length < 2) {
187 logger.debug("No value for filter '{}' in filters list: {}", filterTuple[0], filter);
188 continue;
189 }
190
191 filters.put(filterTuple[0].trim(), f.substring(filterTuple[0].length() + 1).trim());
192 }
193 }
194 Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
195
196 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
197
198
199 Set<SortCriterion> deprecatedSortCriteria = new HashSet<>();
200 if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
201 deprecatedSortCriteria = sortCriteria.stream().filter(
202 sortCriterion -> sortCriterion.getFieldName().equals("members")
203 || sortCriterion.getFieldName().equals("roles")).collect(Collectors.toSet());
204 sortCriteria.removeAll(deprecatedSortCriteria);
205 }
206
207 List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter,
208 Optional.empty(), Optional.empty(), sortCriteria);
209
210
211 if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
212 List<Comparator<JpaGroup>> comparators = new ArrayList<>();
213 for (SortCriterion sortCriterion : deprecatedSortCriteria) {
214 Comparator<JpaGroup> comparator;
215 switch (sortCriterion.getFieldName()) {
216 case "members":
217 comparator = new GroupComparator() {
218 @Override
219 public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
220 return jpaGroup.getMembers();
221 }
222 };
223 break;
224 case "roles":
225 comparator = new GroupComparator() {
226 @Override
227 public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
228 return jpaGroup.getRoleNames();
229 }
230 };
231 break;
232 default:
233 continue;
234 }
235
236 if (sortCriterion.getOrder() == SortCriterion.Order.Descending) {
237 comparator = comparator.reversed();
238 }
239 comparators.add(comparator);
240 }
241 Collections.sort(results, ComparatorUtils.chainedComparator(comparators));
242 }
243
244 List<JsonObject> groupsJson = new ArrayList<>();
245 for (JpaGroup group : results) {
246 JsonObject groupJson = new JsonObject();
247
248 groupJson.addProperty("identifier", group.getGroupId());
249 groupJson.addProperty("organization", group.getOrganization().getId());
250 groupJson.addProperty("role", group.getRole());
251 groupJson.addProperty("name", safeString(group.getName()));
252 groupJson.addProperty("description", safeString(group.getDescription()));
253 groupJson.addProperty("roles", group.getRoleNames() != null ? String.join(",", group.getRoleNames()) : "");
254 groupJson.addProperty("members", group.getMembers() != null ? String.join(",", group.getMembers()) : "");
255
256 groupsJson.add(groupJson);
257 }
258 JsonArray responseArray = new JsonArray();
259 groupsJson.forEach(responseArray::add);
260
261 return ApiResponseBuilder.Json.ok(acceptHeader, responseArray);
262 }
263
264
265
266
267 private abstract class GroupComparator implements Comparator<JpaGroup> {
268
269 public abstract Set<String> getGroupAttribute(JpaGroup jpaGroup);
270
271 @Override
272 public int compare(JpaGroup group1, JpaGroup group2) {
273 List<String> members1 = new ArrayList<>(getGroupAttribute(group1));
274 List<String> members2 = new ArrayList<>(getGroupAttribute(group2));
275
276 for (int i = 0; i < members1.size() && i < members2.size(); i++) {
277 String member1 = members1.get(i);
278 String member2 = members2.get(i);
279 int result = member1.compareTo(member2);
280 if (result != 0) {
281 return result;
282 }
283 }
284 return (members1.size() - members2.size());
285 }
286 }
287
288 @GET
289 @Path("{groupId}")
290 @RestQuery(
291 name = "getgroup",
292 description = "Returns a single group.",
293 returnDescription = "",
294 pathParameters = {
295 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING)
296 },
297 responses = {
298 @RestResponse(description = "The group is returned.", responseCode = HttpServletResponse.SC_OK),
299 @RestResponse(description = "The specified group does not exist.",
300 responseCode = HttpServletResponse.SC_NOT_FOUND)
301 })
302 public Response getGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id) {
303 JpaGroup group = jpaGroupRoleProvider.getGroup(id);
304
305 if (group == null) {
306 return ApiResponseBuilder.notFound("Cannot find a group with id '%s'.", id);
307 }
308
309 JsonObject json = new JsonObject();
310 json.addProperty("identifier", group.getGroupId());
311 json.addProperty("organization", group.getOrganization().getId());
312 json.addProperty("role", group.getRole());
313 json.addProperty("name", safeString(group.getName()));
314 json.addProperty("description", safeString(group.getDescription()));
315 json.addProperty("roles", safeString(group.getRoleNames()));
316 json.addProperty("members", safeString(group.getMembers()));
317
318 return ApiResponseBuilder.Json.ok(acceptHeader, json);
319 }
320
321 @DELETE
322 @Path("{groupId}")
323 @RestQuery(
324 name = "deletegroup",
325 description = "Deletes a group.",
326 returnDescription = "",
327 pathParameters = {
328 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING)
329 },
330 responses = {
331 @RestResponse(description = "The group has been deleted.", responseCode = HttpServletResponse.SC_NO_CONTENT),
332 @RestResponse(description = "The specified group does not exist.",
333 responseCode = HttpServletResponse.SC_NOT_FOUND)
334 })
335 public Response deleteGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id)
336 throws NotFoundException {
337 try {
338 jpaGroupRoleProvider.removeGroup(id);
339 return Response.noContent().build();
340 } catch (NotFoundException e) {
341 return Response.status(SC_NOT_FOUND).build();
342 } catch (UnauthorizedException e) {
343 return Response.status(SC_FORBIDDEN).build();
344 } catch (Exception e) {
345 logger.error("Unable to delete group {}", id, e);
346 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
347 }
348 }
349
350 @PUT
351 @Path("{groupId}")
352 @RestQuery(
353 name = "updategroup",
354 description = "Updates a group.",
355 returnDescription = "",
356 pathParameters = {
357 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING)
358 },
359 restParameters = {
360 @RestParameter(name = "name", isRequired = false, description = "Group Name", type = STRING),
361 @RestParameter(name = "description", description = "Group Description", isRequired = false, type = STRING),
362 @RestParameter(name = "roles", description = "Comma-separated list of roles", isRequired = false,
363 type = STRING),
364 @RestParameter(name = "members", description = "Comma-separated list of members", isRequired = false,
365 type = STRING)
366 },
367 responses = {
368 @RestResponse(description = "The group has been updated.", responseCode = HttpServletResponse.SC_CREATED),
369 @RestResponse(description = "The specified group does not exist.",
370 responseCode = HttpServletResponse.SC_BAD_REQUEST)
371 })
372 public Response updateGroup(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
373 @FormParam("name") String name, @FormParam("description") String description,
374 @FormParam("roles") String roles, @FormParam("members") String members) throws Exception {
375 try {
376 jpaGroupRoleProvider.updateGroup(id, name, description, roles, members);
377 } catch (IllegalArgumentException e) {
378 logger.warn("Unable to update group id {}: {}", id, e.getMessage());
379 return Response.status(SC_BAD_REQUEST).build();
380 } catch (UnauthorizedException ex) {
381 return Response.status(SC_FORBIDDEN).build();
382 }
383 return Response.ok().build();
384 }
385
386 @POST
387 @Path("")
388 @RestQuery(
389 name = "creategroup",
390 description = "Creates a group.",
391 returnDescription = "",
392 restParameters = {
393 @RestParameter(name = "name", isRequired = true, description = "Group Name", type = STRING),
394 @RestParameter(name = "description", description = "Group Description", isRequired = false, type = STRING),
395 @RestParameter(name = "roles", description = "Comma-separated list of roles", isRequired = false,
396 type = STRING),
397 @RestParameter(name = "members", description = "Comma-separated list of members", isRequired = false,
398 type = STRING)
399 },
400 responses = {
401 @RestResponse(description = "A new group is created.", responseCode = SC_CREATED),
402 @RestResponse(description = "The request is invalid or inconsistent.", responseCode = SC_BAD_REQUEST)
403 })
404 public Response createGroup(@HeaderParam("Accept") String acceptHeader, @FormParam("name") String name,
405 @FormParam("description") String description, @FormParam("roles") String roles,
406 @FormParam("members") String members) {
407 try {
408 jpaGroupRoleProvider.createGroup(name, description, roles, members);
409 } catch (IllegalArgumentException e) {
410 logger.warn("Unable to create group {}: {}", name, e.getMessage());
411 return Response.status(SC_BAD_REQUEST).build();
412 } catch (UnauthorizedException e) {
413 return Response.status(SC_FORBIDDEN).build();
414 } catch (ConflictException e) {
415 return Response.status(SC_CONFLICT).build();
416 }
417 return Response.status(SC_CREATED).build();
418 }
419
420 @POST
421 @Path("{groupId}/members")
422 @RestQuery(
423 name = "addgroupmember",
424 description = "Adds a member to a group.",
425 returnDescription = "",
426 pathParameters = {
427 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING)
428 },
429 restParameters = {
430 @RestParameter(name = "member", description = "Member Name", isRequired = true, type = STRING)
431 },
432 responses = {
433 @RestResponse(description = "The member was already member of the group.", responseCode = SC_OK),
434 @RestResponse(description = "The member has been added.", responseCode = SC_NO_CONTENT),
435 @RestResponse(description = "The specified group does not exist.", responseCode = SC_NOT_FOUND)
436 })
437 public Response addGroupMember(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
438 @FormParam("member") String member) {
439 try {
440 if (jpaGroupRoleProvider.addMemberToGroup(id, member)) {
441 return Response.ok().build();
442 } else {
443 return ApiResponseBuilder.Json.ok(acceptHeader, "Member is already member of group.");
444 }
445 } catch (IllegalArgumentException e) {
446 logger.warn("Unable to add member to group id {}.", id, e);
447 return Response.status(SC_BAD_REQUEST).build();
448 } catch (UnauthorizedException ex) {
449 return Response.status(SC_FORBIDDEN).build();
450 } catch (NotFoundException e) {
451 return ApiResponseBuilder.notFound("Cannot find group with id '%s'.", id);
452 } catch (Exception e) {
453 logger.warn("Could not update the group with id {}.",id, e);
454 return ApiResponseBuilder.serverError("Could not update group with id '%s', reason: '%s'",id,getMessage(e));
455 }
456 }
457
458 @DELETE
459 @Path("{groupId}/members/{memberId}")
460 @RestQuery(
461 name = "removegroupmember",
462 description = "Removes a member from a group",
463 returnDescription = "",
464 pathParameters = {
465 @RestParameter(name = "groupId", description = "The group id", isRequired = true, type = STRING),
466 @RestParameter(name = "memberId", description = "The member id", isRequired = true, type = STRING)
467 },
468 responses = {
469 @RestResponse(description = "The member has been removed.", responseCode = HttpServletResponse.SC_NO_CONTENT),
470 @RestResponse(description = "The specified group or member does not exist.",
471 responseCode = HttpServletResponse.SC_NOT_FOUND)
472 })
473 public Response removeGroupMember(@HeaderParam("Accept") String acceptHeader, @PathParam("groupId") String id,
474 @PathParam("memberId") String memberId) {
475 try {
476 if (jpaGroupRoleProvider.removeMemberFromGroup(id, memberId)) {
477 return Response.ok().build();
478 } else {
479 return ApiResponseBuilder.Json.ok(acceptHeader, "Member is already not member of group.");
480 }
481 } catch (IllegalArgumentException e) {
482 logger.warn("Unable to remove member from group id {}.", id, e);
483 return Response.status(SC_BAD_REQUEST).build();
484 } catch (UnauthorizedException ex) {
485 return Response.status(SC_FORBIDDEN).build();
486 } catch (NotFoundException e) {
487 return ApiResponseBuilder.notFound("Cannot find group with id '%s'.", id);
488 } catch (Exception e) {
489 logger.warn("Could not update the group with id {}.", id, e);
490 return ApiResponseBuilder.serverError("Could not update group with id '%s', reason: '%s'", id, getMessage(e));
491 }
492 }
493 }