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(name = "externalapigroups", title = "External API Groups Service", notes = {}, abstractText = "Provides resources and operations related to the groups")
97 @Component(
98 immediate = true,
99 service = GroupsEndpoint.class,
100 property = {
101 "service.description=External API - Groups Endpoint",
102 "opencast.service.type=org.opencastproject.external.groups",
103 "opencast.service.path=/api/groups"
104 }
105 )
106 @JaxrsResource
107 public class GroupsEndpoint {
108
109
110 private static final Logger logger = LoggerFactory.getLogger(GroupsEndpoint.class);
111
112
113 private ElasticsearchIndex elasticsearchIndex;
114 private JpaGroupRoleProvider jpaGroupRoleProvider;
115 private SecurityService securityService;
116
117
118 @Reference
119 void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
120 this.elasticsearchIndex = elasticsearchIndex;
121 }
122
123
124 @Reference
125 public void setSecurityService(SecurityService securityService) {
126 this.securityService = securityService;
127 }
128
129
130 @Reference
131 public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
132 this.jpaGroupRoleProvider = jpaGroupRoleProvider;
133 }
134
135
136 @Activate
137 void activate() {
138 logger.info("Activating External API - Groups Endpoint");
139 }
140
141 @GET
142 @Path("")
143 @RestQuery(name = "getgroups", description = "Returns a list of groups.", returnDescription = "", restParameters = {
144 @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),
145 @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),
146 @RestParameter(name = "limit", description = "The maximum number of results to return for a single request.", isRequired = false, type = RestParameter.Type.INTEGER),
147 @RestParameter(name = "offset", description = "The index of the first result to return.", isRequired = false, type = RestParameter.Type.INTEGER) }, responses = {
148 @RestResponse(description = "A (potentially empty) list of groups.", responseCode = HttpServletResponse.SC_OK) })
149 public Response getGroups(@HeaderParam("Accept") String acceptHeader, @QueryParam("filter") String filter,
150 @QueryParam("sort") String sort, @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) {
151 final ApiVersion requestedVersion = ApiMediaType.parse(acceptHeader).getVersion();
152 Optional<Integer> optLimit = Optional.ofNullable(limit);
153 Optional<Integer> optOffset = Optional.ofNullable(offset);
154
155 if (optLimit.isPresent() && limit <= 0) {
156 optLimit = Optional.empty();
157 }
158 if (optOffset.isPresent() && offset < 0) {
159 optOffset = Optional.empty();
160 }
161
162
163 Map<String, String> filters = new HashMap<>();
164 if (StringUtils.isNotBlank(filter)) {
165 for (String f : filter.split(",")) {
166 String[] filterTuple = f.split(":");
167 if (filterTuple.length < 2) {
168 logger.debug("No value for filter '{}' in filters list: {}", filterTuple[0], filter);
169 continue;
170 }
171
172 filters.put(filterTuple[0].trim(), f.substring(filterTuple[0].length() + 1).trim());
173 }
174 }
175 Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
176
177 ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
178
179
180 Set<SortCriterion> deprecatedSortCriteria = new HashSet<>();
181 if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
182 deprecatedSortCriteria = sortCriteria.stream().filter(
183 sortCriterion -> sortCriterion.getFieldName().equals("members")
184 || sortCriterion.getFieldName().equals("roles")).collect(Collectors.toSet());
185 sortCriteria.removeAll(deprecatedSortCriteria);
186 }
187
188 List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter,
189 Optional.empty(), Optional.empty(), sortCriteria);
190
191
192 if (requestedVersion.isSmallerThan(VERSION_1_6_0)) {
193 List<Comparator<JpaGroup>> comparators = new ArrayList<>();
194 for (SortCriterion sortCriterion : deprecatedSortCriteria) {
195 Comparator<JpaGroup> comparator;
196 switch (sortCriterion.getFieldName()) {
197 case "members":
198 comparator = new GroupComparator() {
199 @Override
200 public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
201 return jpaGroup.getMembers();
202 }
203 };
204 break;
205 case "roles":
206 comparator = new GroupComparator() {
207 @Override
208 public Set<String> getGroupAttribute(JpaGroup jpaGroup) {
209 return jpaGroup.getRoleNames();
210 }
211 };
212 break;
213 default:
214 continue;
215 }
216
217 if (sortCriterion.getOrder() == SortCriterion.Order.Descending) {
218 comparator = comparator.reversed();
219 }
220 comparators.add(comparator);
221 }
222 Collections.sort(results, ComparatorUtils.chainedComparator(comparators));
223 }
224
225 List<JsonObject> groupsJson = new ArrayList<>();
226 for (JpaGroup group : results) {
227 JsonObject groupJson = new JsonObject();
228
229 groupJson.addProperty("identifier", group.getGroupId());
230 groupJson.addProperty("organization", group.getOrganization().getId());
231 groupJson.addProperty("role", group.getRole());
232 groupJson.addProperty("name", safeString(group.getName()));
233 groupJson.addProperty("description", safeString(group.getDescription()));
234 groupJson.addProperty("roles", group.getRoleNames() != null ? String.join(",", group.getRoleNames()) : "");
235 groupJson.addProperty("members", group.getMembers() != null ? String.join(",", group.getMembers()) : "");
236
237 groupsJson.add(groupJson);
238 }
239 JsonArray responseArray = new JsonArray();
240 groupsJson.forEach(responseArray::add);
241
242 return ApiResponseBuilder.Json.ok(acceptHeader, responseArray);
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 JsonObject json = new JsonObject();
283 json.addProperty("identifier", group.getGroupId());
284 json.addProperty("organization", group.getOrganization().getId());
285 json.addProperty("role", group.getRole());
286 json.addProperty("name", safeString(group.getName()));
287 json.addProperty("description", safeString(group.getDescription()));
288 json.addProperty("roles", safeString(group.getRoleNames()));
289 json.addProperty("members", safeString(group.getMembers()));
290
291 return ApiResponseBuilder.Json.ok(acceptHeader, json);
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 }