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