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(name = "groups", title = "Group service",
94    abstractText = "Provides operations for groups",
95    notes = { "This service offers the default groups CRUD operations for the admin interface.",
96              "<strong>Important:</strong> "
97                + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
98                + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
99                + "DO NOT use this for integration of third-party applications.<em>"})
100 @Component(
101         immediate = true,
102         service = GroupsEndpoint.class,
103         property = {
104                 "service.description=Admin UI - Groups Endpoint",
105                 "opencast.service.type=org.opencastproject.adminui.GroupsEndpoint",
106                 "opencast.service.path=/admin-ng/groups",
107         }
108 )
109 @JaxrsResource
110 public class GroupsEndpoint {
111 
112   /** The logging facility */
113   private static final Logger logger = LoggerFactory.getLogger(GroupsEndpoint.class);
114 
115   /** The admin UI search index */
116   private ElasticsearchIndex searchIndex;
117 
118   /** The security service */
119   private SecurityService securityService;
120 
121   /** The user directory service */
122   private UserDirectoryService userDirectoryService;
123 
124   /** The index service */
125   private IndexService indexService;
126 
127   /** The group provider */
128   private JpaGroupRoleProvider jpaGroupRoleProvider;
129 
130   /** OSGi callback for the security service. */
131   @Reference
132   public void setSecurityService(SecurityService securityService) {
133     this.securityService = securityService;
134   }
135 
136   /** OSGi callback for the index service. */
137   @Reference
138   public void setIndexService(IndexService indexService) {
139     this.indexService = indexService;
140   }
141 
142   /** OSGi callback for users services. */
143   @Reference
144   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
145     this.userDirectoryService = userDirectoryService;
146   }
147 
148   /** OSGi callback for the search index. */
149   @Reference
150   public void setSearchIndex(ElasticsearchIndex searchIndex) {
151     this.searchIndex = searchIndex;
152   }
153 
154   /** OSGi callback for the group provider. */
155   @Reference
156   public void setGroupRoleProvider(JpaGroupRoleProvider jpaGroupRoleProvider) {
157     this.jpaGroupRoleProvider = jpaGroupRoleProvider;
158   }
159 
160   /** OSGi callback. */
161   @Activate
162   protected void activate(ComponentContext cc) {
163     logger.info("Activate the Admin ui - Groups facade endpoint");
164   }
165 
166   @GET
167   @Produces(MediaType.APPLICATION_JSON)
168   @Path("groups.json")
169   @RestQuery(
170     name = "allgroupsasjson",
171     description = "Returns a list of groups",
172     returnDescription = "List of groups for the current user's organization as JSON.",
173     restParameters = {
174       @RestParameter(name = "filter", isRequired = false, type = STRING,
175         description = "Filter used for the query, formatted like: 'filter1:value1,filter2:value2'"),
176       @RestParameter(name = "sort", isRequired = false, type = STRING,
177         description = "The sort order. May include any of the following: NAME, DESCRIPTION, ROLE. "
178         + "Add '_DESC' to reverse the sort order (e.g. NAME_DESC)."),
179       @RestParameter(name = "limit", isRequired = false, type = INTEGER, defaultValue = "100",
180         description = "The maximum number of items to return per page."),
181       @RestParameter(name = "offset", isRequired = false, type = INTEGER, defaultValue = "0",
182         description = "The page number.")},
183     responses = {
184       @RestResponse(responseCode = SC_OK, description = "The groups.")})
185   public Response getGroups(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
186           @QueryParam("offset") Integer offset, @QueryParam("limit") Integer limit) throws IOException {
187     Optional<Integer> optLimit = Optional.ofNullable(limit);
188     Optional<Integer> optOffset = Optional.ofNullable(offset);
189 
190     Map<String, String> filters = RestUtils.parseFilter(filter);
191     Optional<String> optNameFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_NAME_NAME));
192     Optional<String> optRoleFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_ROLE_NAME));
193     Optional<String> optTextFilter = Optional.ofNullable(filters.get(GroupsListQuery.FILTER_TEXT_NAME));
194 
195     ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
196 
197     List<JpaGroup> results = jpaGroupRoleProvider.getGroups(optLimit, optOffset, optNameFilter, optRoleFilter,
198         optTextFilter, sortCriteria);
199 
200     // load users
201     List<String> userNames = results.stream().flatMap(item -> item.getMembers().stream())
202             .collect(Collectors.toList());
203     final Map<String, User> users = new HashMap<>(userNames.size());
204     userDirectoryService.loadUsers(userNames).forEachRemaining(user -> users.put(user.getUsername(), user));
205 
206     List<JsonObject> groupsJsonArray = new ArrayList<>();
207     for (JpaGroup group : results) {
208       JsonObject groupJson = new JsonObject();
209       groupJson.addProperty("id", group.getGroupId());
210       groupJson.addProperty("name", safeString(group.getName()));
211       groupJson.addProperty("description", safeString(group.getDescription()));
212       groupJson.addProperty("role", group.getRole());
213       groupJson.add("users",
214           membersToJSON(group.getMembers().stream().map(users::get).filter(Objects::nonNull).iterator()));
215 
216       groupsJsonArray.add(groupJson);
217     }
218 
219     long dbTotal = jpaGroupRoleProvider.countTotalGroups(optNameFilter, optRoleFilter, optTextFilter);
220     long resultsTotal = optOffset.orElse(0) + results.size();
221 
222     // groups could've been added or deleted in the meantime, so...
223     long total;
224     // don't show next page if current page isn't full
225     if (!optLimit.isPresent() || results.size() < optLimit.get()) {
226       total = resultsTotal;
227       // don't show less than the current results
228     } else {
229       total = max(dbTotal, resultsTotal);
230     }
231 
232     return okJsonList(groupsJsonArray, optOffset, optLimit, total);
233   }
234 
235   @DELETE
236   @Path("{id}")
237   @RestQuery(
238     name = "removegrouop",
239     description = "Remove a group",
240     returnDescription = "Returns no content",
241     pathParameters = {
242       @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING)},
243     responses = {
244       @RestResponse(responseCode = SC_OK, description = "Group deleted"),
245       @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to delete the group with admin role."),
246       @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found."),
247       @RestResponse(responseCode = SC_INTERNAL_SERVER_ERROR, description = "An internal server error occured.")})
248   public Response removeGroup(@PathParam("id") String groupId) throws NotFoundException {
249     try {
250       jpaGroupRoleProvider.removeGroup(groupId);
251       return Response.noContent().build();
252     } catch (NotFoundException e) {
253       return Response.status(SC_NOT_FOUND).build();
254     } catch (UnauthorizedException e) {
255       return Response.status(SC_FORBIDDEN).build();
256     } catch (Exception e) {
257       logger.error("Unable to delete group {}", groupId, e);
258       throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
259     }
260   }
261 
262   @POST
263   @Path("")
264   @RestQuery(
265     name = "createGroup",
266     description = "Add a group",
267     returnDescription = "Returns Created (201) if the group has been created",
268     restParameters = {
269       @RestParameter(name = "name", description = "The group name", isRequired = true, type = STRING),
270       @RestParameter(name = "description", description = "The group description", isRequired = false, type = STRING),
271       @RestParameter(name = "roles", description = "Comma seperated list of roles", isRequired = false, type = TEXT),
272       @RestParameter(name = "users", description = "Comma seperated list of members", isRequired = false, type = TEXT)},
273     responses = {
274       @RestResponse(responseCode = SC_CREATED, description = "Group created"),
275       @RestResponse(responseCode = SC_BAD_REQUEST, description = "Name too long"),
276       @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to create a group with admin role."),
277       @RestResponse(responseCode = SC_CONFLICT, description = "An group with this name already exists.") })
278   public Response createGroup(@FormParam("name") String name, @FormParam("description") String description,
279           @FormParam("roles") String roles, @FormParam("users") String users) {
280     try {
281       jpaGroupRoleProvider.createGroup(name, description, roles, users);
282     } catch (IllegalArgumentException e) {
283       logger.warn("Unable to create group with name {}: {}", name, e.getMessage());
284       return Response.status(Status.BAD_REQUEST).build();
285     } catch (UnauthorizedException e) {
286       return Response.status(SC_FORBIDDEN).build();
287     } catch (ConflictException e) {
288       return Response.status(SC_CONFLICT).build();
289     }
290     return Response.status(Status.CREATED).build();
291   }
292 
293   @PUT
294   @Path("{id}")
295   @RestQuery(
296     name = "updateGroup",
297     description = "Update a group",
298     returnDescription = "Return the status codes",
299     pathParameters = {
300       @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING) },
301     restParameters = {
302       @RestParameter(name = "name", description = "The group name", isRequired = true, type = STRING),
303       @RestParameter(name = "description", description = "The group description", isRequired = false, type = STRING),
304       @RestParameter(name = "roles", description = "Comma seperated list of roles", isRequired = false, type = TEXT),
305       @RestParameter(name = "users", description = "Comma seperated list of members", isRequired = false, type = TEXT)},
306     responses = {
307       @RestResponse(responseCode = SC_OK, description = "Group updated"),
308       @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to update the group with admin role."),
309       @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found"),
310       @RestResponse(responseCode = SC_BAD_REQUEST, description = "Name too long")})
311   public Response updateGroup(@PathParam("id") String groupId, @FormParam("name") String name,
312           @FormParam("description") String description, @FormParam("roles") String roles,
313           @FormParam("users") String users) throws NotFoundException {
314     try {
315       jpaGroupRoleProvider.updateGroup(groupId, name, description, roles, users);
316     } catch (IllegalArgumentException e) {
317       logger.warn("Unable to update group with id {}: {}", groupId, e.getMessage());
318       return Response.status(Status.BAD_REQUEST).build();
319     } catch (UnauthorizedException ex) {
320       return Response.status(SC_FORBIDDEN).build();
321     }
322     return Response.ok().build();
323   }
324 
325   @GET
326   @Produces(MediaType.APPLICATION_JSON)
327   @Path("{id}")
328   @RestQuery(
329     name = "getGroup",
330     description = "Get a single group",
331     returnDescription = "Return the status codes",
332     pathParameters = {
333       @RestParameter(name = "id", description = "The group identifier", isRequired = true, type = STRING)},
334     responses = {
335       @RestResponse(responseCode = SC_OK, description = "Group found and returned as JSON"),
336       @RestResponse(responseCode = SC_NOT_FOUND, description = "Group not found")})
337   public Response getGroup(@PathParam("id") String groupId) throws NotFoundException {
338     JpaGroup group = jpaGroupRoleProvider.getGroup(groupId);
339 
340     if (group == null) {
341       throw new NotFoundException("Group " + groupId + " does not exist.");
342     }
343 
344     // Convert roles
345     JsonArray rolesJSON = new JsonArray();
346     for (String role : group.getRoleNames()) {
347       rolesJSON.add(role);
348     }
349 
350     Iterator<User> users = userDirectoryService.loadUsers(group.getMembers());
351     JsonObject jsonGroup = new JsonObject();
352     jsonGroup.addProperty("id", group.getGroupId());
353     jsonGroup.addProperty("name", safeString(group.getName()));
354     jsonGroup.addProperty("description", safeString(group.getDescription()));
355     jsonGroup.addProperty("role", safeString(group.getRole()));
356     jsonGroup.add("roles", rolesJSON);
357     jsonGroup.add("users", membersToJSON(users));
358 
359     return RestUtils.okJson(jsonGroup);
360   }
361 
362   /**
363    * Generate a JSON array based on the given set of members
364    *
365    * @param members
366    *          the members source
367    * @return a JSON array ({@link JsonArray}) with the given members
368    */
369 
370   private JsonArray membersToJSON(Iterator<User> members) {
371     JsonArray membersJSON = new JsonArray();
372 
373     while (members.hasNext()) {
374       User user = members.next();
375       String name = user.getUsername();
376 
377       if (StringUtils.isNotBlank(user.getName())) {
378         name = user.getName();
379       }
380 
381       JsonObject jsonUser = new JsonObject();
382       jsonUser.addProperty("username", user.getUsername());
383       jsonUser.addProperty("name", name);
384 
385       membersJSON.add(jsonUser);
386     }
387 
388     return membersJSON;
389   }
390 }