View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.adminui.endpoint;
23  
24  import static java.lang.Math.max;
25  import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
26  import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
27  import static javax.servlet.http.HttpServletResponse.SC_CREATED;
28  import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
29  import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
30  import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
31  import static javax.servlet.http.HttpServletResponse.SC_OK;
32  import static org.opencastproject.index.service.util.JSONUtils.safeString;
33  import static org.opencastproject.index.service.util.RestUtils.okJsonList;
34  import static org.opencastproject.util.doc.rest.RestParameter.Type.INTEGER;
35  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
36  import static org.opencastproject.util.doc.rest.RestParameter.Type.TEXT;
37  
38  import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
39  import org.opencastproject.index.service.api.IndexService;
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.api.User;
45  import org.opencastproject.security.api.UserDirectoryService;
46  import org.opencastproject.security.impl.jpa.JpaGroup;
47  import org.opencastproject.userdirectory.ConflictException;
48  import org.opencastproject.userdirectory.JpaGroupRoleProvider;
49  import org.opencastproject.util.NotFoundException;
50  import org.opencastproject.util.doc.rest.RestParameter;
51  import org.opencastproject.util.doc.rest.RestQuery;
52  import org.opencastproject.util.doc.rest.RestResponse;
53  import org.opencastproject.util.doc.rest.RestService;
54  import org.opencastproject.util.requests.SortCriterion;
55  
56  import com.google.gson.JsonArray;
57  import com.google.gson.JsonObject;
58  
59  import org.apache.commons.lang3.StringUtils;
60  import org.osgi.service.component.ComponentContext;
61  import org.osgi.service.component.annotations.Activate;
62  import org.osgi.service.component.annotations.Component;
63  import org.osgi.service.component.annotations.Reference;
64  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  import java.io.IOException;
69  import java.util.ArrayList;
70  import java.util.HashMap;
71  import java.util.Iterator;
72  import java.util.List;
73  import java.util.Map;
74  import java.util.Objects;
75  import java.util.Optional;
76  import java.util.stream.Collectors;
77  
78  import javax.ws.rs.DELETE;
79  import javax.ws.rs.FormParam;
80  import javax.ws.rs.GET;
81  import javax.ws.rs.POST;
82  import javax.ws.rs.PUT;
83  import javax.ws.rs.Path;
84  import javax.ws.rs.PathParam;
85  import javax.ws.rs.Produces;
86  import javax.ws.rs.QueryParam;
87  import javax.ws.rs.WebApplicationException;
88  import javax.ws.rs.core.MediaType;
89  import javax.ws.rs.core.Response;
90  import javax.ws.rs.core.Response.Status;
91  
92  @Path("/admin-ng/groups")
93  @RestService(
94      name = "groups",
95      title = "Group service",
96      abstractText = "Provides operations for groups",
97      notes = { "This service offers the default groups CRUD operations for the admin interface.",
98                "<strong>Important:</strong> "
99                  + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
100                 + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
101                 + "DO NOT use this for integration of third-party applications.<em>"})
102 @Component(
103     immediate = true,
104     service = GroupsEndpoint.class,
105     property = {
106         "service.description=Admin UI - Groups Endpoint",
107         "opencast.service.type=org.opencastproject.adminui.GroupsEndpoint",
108         "opencast.service.path=/admin-ng/groups",
109     }
110 )
111 @JaxrsResource
112 public class GroupsEndpoint {
113 
114   /** The logging facility */
115   private static final Logger logger = LoggerFactory.getLogger(GroupsEndpoint.class);
116 
117   /** The admin UI search index */
118   private ElasticsearchIndex searchIndex;
119 
120   /** The security service */
121   private SecurityService securityService;
122 
123   /** The user directory service */
124   private UserDirectoryService userDirectoryService;
125 
126   /** The index service */
127   private IndexService indexService;
128 
129   /** The group provider */
130   private JpaGroupRoleProvider jpaGroupRoleProvider;
131 
132   /** OSGi callback for the security service. */
133   @Reference
134   public void setSecurityService(SecurityService securityService) {
135     this.securityService = securityService;
136   }
137 
138   /** OSGi callback for the index service. */
139   @Reference
140   public void setIndexService(IndexService indexService) {
141     this.indexService = indexService;
142   }
143 
144   /** OSGi callback for users services. */
145   @Reference
146   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
147     this.userDirectoryService = userDirectoryService;
148   }
149 
150   /** OSGi callback for the search index. */
151   @Reference
152   public void setSearchIndex(ElasticsearchIndex searchIndex) {
153     this.searchIndex = searchIndex;
154   }
155 
156   /** OSGi callback for the group provider. */
157   @Reference
158   public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
159     this.jpaGroupRoleProvider = jpaGroupRoleProvider;
160   }
161 
162   /** OSGi callback. */
163   @Activate
164   protected void activate(ComponentContext cc) {
165     logger.info("Activate the Admin ui - Groups facade endpoint");
166   }
167 
168   @GET
169   @Produces(MediaType.APPLICATION_JSON)
170   @Path("groups.json")
171   @RestQuery(
172       name = "allgroupsasjson",
173       description = "Returns a list of groups",
174       returnDescription = "List of groups for the current user's organization as JSON.",
175       restParameters = {
176           @RestParameter(name = "filter", isRequired = false, type = STRING,
177               description = "Filter used for the query, formatted like: 'filter1:value1,filter2:value2'"),
178           @RestParameter(name = "sort", isRequired = false, type = STRING,
179               description = "The sort order. May include any of the following: NAME, DESCRIPTION, ROLE. "
180               + "Add '_DESC' to reverse the sort order (e.g. NAME_DESC)."),
181           @RestParameter(name = "limit", isRequired = false, type = INTEGER, defaultValue = "100",
182               description = "The maximum number of items to return per page."),
183           @RestParameter(name = "offset", isRequired = false, type = INTEGER, defaultValue = "0",
184               description = "The page number.")},
185       responses = {
186           @RestResponse(responseCode = SC_OK, description = "The groups.")
187       })
188   public Response getGroups(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
189       @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) throws IOException {
190     Optional<Integer> optLimit = Optional.ofNullable(limit);
191     Optional<Integer> optOffset = Optional.ofNullable(offset);
192 
193     Map<String, String> filters = RestUtils.parseFilter(filter);
194     Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
195     Optional<String> optRoleFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_ROLE_NAME));
196     Optional<String> optTextFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_TEXT_NAME));
197 
198     ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
199 
200     List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter, optRoleFilter,
201         optTextFilter, sortCriteria);
202 
203     // load users
204     List<String> userNames = results.stream().flatMap(item -> item.getMembers().stream())
205             .collect(Collectors.toList());
206     final Map<String, User> users = new HashMap<>(userNames.size());
207     userDirectoryService.loadUsers(userNames).forEachRemaining(user -> users.put(user.getUsername(), user));
208 
209     List<JsonObject> groupsJsonArray = new ArrayList<>();
210     for (JpaGroup group : results) {
211       JsonObject groupJson = new JsonObject();
212       groupJson.addProperty("id", group.getGroupId());
213       groupJson.addProperty("name", safeString(group.getName()));
214       groupJson.addProperty("description", safeString(group.getDescription()));
215       groupJson.addProperty("role", group.getRole());
216       groupJson.add("users",
217           membersToJSON(group.getMembers().stream().map(users::get).filter(Objects::nonNull).iterator()));
218 
219       groupsJsonArray.add(groupJson);
220     }
221 
222     long dbTotal = jpaGroupRoleProvider.countTotalGroups(optNameFilter, optRoleFilter, optTextFilter);
223     long resultsTotal = optOffset.orElse(0) + results.size();
224 
225     // groups could've been added or deleted in the meantime, so...
226     long total;
227     // don't show next page if current page isn't full
228     if (!optLimit.isPresent() || results.size() < optLimit.get()) {
229       total = resultsTotal;
230       // don't show less than the current results
231     } else {
232       total = max(dbTotal, resultsTotal);
233     }
234 
235     return okJsonList(groupsJsonArray, optOffset, optLimit, total);
236   }
237 
238   @DELETE
239   @Path("{id}")
240   @RestQuery(
241       name = "removegrouop",
242       description = "Remove a group",
243       returnDescription = "Returns no content",
244       pathParameters = {
245           @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING)
246       },
247       responses = {
248           @RestResponse(responseCode = SC_OK, description = "Group deleted"),
249           @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to delete the group with "
250               + "admin role."),
251           @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found."),
252           @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "An internal server error occured.")
253       })
254   public Response removeGroup(@PathParam("id") String groupId) throws NotFoundException {
255     try {
256       jpaGroupRoleProvider.removeGroup(groupId);
257       return Response.noContent().build();
258     } catch (NotFoundException e) {
259       return Response.status(SC_NOT_FOUND).build();
260     } catch (UnauthorizedException e) {
261       return Response.status(SC_FORBIDDEN).build();
262     } catch (Exception e) {
263       logger.error("Unable to delete group {}", groupId, e);
264       throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
265     }
266   }
267 
268   @POST
269   @Path("")
270   @RestQuery(
271       name = "createGroup",
272       description = "Add a group",
273       returnDescription = "Returns Created (201) if the group has been created",
274       restParameters = {
275           @RestParameter(name = "name", description = "The group name", isRequired = true, type = STRING),
276           @RestParameter(name = "description", description = "The group description", isRequired = false,
277               type = STRING),
278           @RestParameter(name = "roles", description = "Comma seperated list of roles", isRequired = false,
279               type = TEXT),
280           @RestParameter(name = "users", description = "Comma seperated list of members", isRequired = false,
281               type = TEXT)
282       },
283       responses = {
284           @RestResponse(responseCode = SC_CREATED, description = "Group created"),
285           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Name too long"),
286           @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to create a group with "
287               + "admin role."),
288           @RestResponse(responseCode = SC_CONFLICT, description = "An group with this name already exists.")
289       })
290   public Response createGroup(@FormParam("name") String name, @FormParam("description") String description,
291       @FormParam("roles") String roles, @FormParam("users") String users) {
292     try {
293       jpaGroupRoleProvider.createGroup(name, description, roles, users);
294     } catch (IllegalArgumentException e) {
295       logger.warn("Unable to create group with name {}: {}", name, e.getMessage());
296       return Response.status(Status.BAD_REQUEST).build();
297     } catch (UnauthorizedException e) {
298       return Response.status(SC_FORBIDDEN).build();
299     } catch (ConflictException e) {
300       return Response.status(SC_CONFLICT).build();
301     }
302     return Response.status(Status.CREATED).build();
303   }
304 
305   @PUT
306   @Path("{id}")
307   @RestQuery(
308       name = "updateGroup",
309       description = "Update a group",
310       returnDescription = "Return the status codes",
311       pathParameters = {
312           @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING) },
313       restParameters = {
314           @RestParameter(name = "name", description = "The group name", isRequired = true, type = STRING),
315           @RestParameter(name = "description", description = "The group description", isRequired = false,
316               type = STRING),
317           @RestParameter(name = "roles", description = "Comma seperated list of roles", isRequired = false,
318               type = TEXT),
319           @RestParameter(name = "users", description = "Comma seperated list of members", isRequired = false,
320               type = TEXT)
321       },
322       responses = {
323           @RestResponse(responseCode = SC_OK, description = "Group updated"),
324           @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to update the group with "
325               + "admin role."),
326           @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found"),
327           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Name too long")
328       })
329   public Response updateGroup(@PathParam("id") String groupId, @FormParam("name") String name,
330       @FormParam("description") String description, @FormParam("roles") String roles,
331       @FormParam("users") String users) throws NotFoundException {
332     try {
333       jpaGroupRoleProvider.updateGroup(groupId, name, description, roles, users);
334     } catch (IllegalArgumentException e) {
335       logger.warn("Unable to update group with id {}: {}", groupId, e.getMessage());
336       return Response.status(Status.BAD_REQUEST).build();
337     } catch (UnauthorizedException ex) {
338       return Response.status(SC_FORBIDDEN).build();
339     }
340     return Response.ok().build();
341   }
342 
343   @GET
344   @Produces(MediaType.APPLICATION_JSON)
345   @Path("{id}")
346   @RestQuery(
347       name = "getGroup",
348       description = "Get a single group",
349       returnDescription = "Return the status codes",
350       pathParameters = {
351           @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING)
352       },
353       responses = {
354           @RestResponse(responseCode = SC_OK, description = "Group found and returned as JSON"),
355           @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found")
356       })
357   public Response getGroup(@PathParam("id") String groupId) throws NotFoundException {
358     JpaGroup group = jpaGroupRoleProvider.getGroup(groupId);
359 
360     if (group == null) {
361       throw new NotFoundException("Group " + groupId + " does not exist.");
362     }
363 
364     // Convert roles
365     JsonArray rolesJSON = new JsonArray();
366     for (String role : group.getRoleNames()) {
367       rolesJSON.add(role);
368     }
369 
370     Iterator<User> users = userDirectoryService.loadUsers(group.getMembers());
371     JsonObject jsonGroup = new JsonObject();
372     jsonGroup.addProperty("id", group.getGroupId());
373     jsonGroup.addProperty("name", safeString(group.getName()));
374     jsonGroup.addProperty("description", safeString(group.getDescription()));
375     jsonGroup.addProperty("role", safeString(group.getRole()));
376     jsonGroup.add("roles", rolesJSON);
377     jsonGroup.add("users", membersToJSON(users));
378 
379     return RestUtils.okJson(jsonGroup);
380   }
381 
382   /**
383    * Generate a JSON array based on the given set of members
384    *
385    * @param members
386    *          the members source
387    * @return a JSON array ({@link JsonArray}) with the given members
388    */
389 
390   private JsonArray membersToJSON(Iterator<User> members) {
391     JsonArray membersJSON = new JsonArray();
392 
393     while (members.hasNext()) {
394       User user = members.next();
395       String name = user.getUsername();
396 
397       if (StringUtils.isNotBlank(user.getName())) {
398         name = user.getName();
399       }
400 
401       JsonObject jsonUser = new JsonObject();
402       jsonUser.addProperty("username", user.getUsername());
403       jsonUser.addProperty("name", name);
404 
405       membersJSON.add(jsonUser);
406     }
407 
408     return membersJSON;
409   }
410 }