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.String.CASE_INSENSITIVE_ORDER;
25  import static org.apache.commons.lang3.StringUtils.trimToEmpty;
26  import static org.apache.commons.lang3.StringUtils.trimToNull;
27  import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
28  import static org.apache.http.HttpStatus.SC_CONFLICT;
29  import static org.apache.http.HttpStatus.SC_CREATED;
30  import static org.apache.http.HttpStatus.SC_FORBIDDEN;
31  import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
32  import static org.apache.http.HttpStatus.SC_NOT_FOUND;
33  import static org.apache.http.HttpStatus.SC_OK;
34  import static org.opencastproject.util.RestUtil.getEndpointUrl;
35  import static org.opencastproject.util.UrlSupport.uri;
36  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
37  
38  import org.opencastproject.adminui.util.TextFilter;
39  import org.opencastproject.index.service.resources.list.query.UsersListQuery;
40  import org.opencastproject.index.service.util.RestUtils;
41  import org.opencastproject.security.api.Organization;
42  import org.opencastproject.security.api.Role;
43  import org.opencastproject.security.api.SecurityService;
44  import org.opencastproject.security.api.UnauthorizedException;
45  import org.opencastproject.security.api.User;
46  import org.opencastproject.security.api.UserDirectoryService;
47  import org.opencastproject.security.impl.jpa.JpaOrganization;
48  import org.opencastproject.security.impl.jpa.JpaRole;
49  import org.opencastproject.security.impl.jpa.JpaUser;
50  import org.opencastproject.userdirectory.JpaUserAndRoleProvider;
51  import org.opencastproject.userdirectory.JpaUserReferenceProvider;
52  import org.opencastproject.util.NotFoundException;
53  import org.opencastproject.util.SmartIterator;
54  import org.opencastproject.util.UrlSupport;
55  import org.opencastproject.util.data.Tuple;
56  import org.opencastproject.util.doc.rest.RestParameter;
57  import org.opencastproject.util.doc.rest.RestQuery;
58  import org.opencastproject.util.doc.rest.RestResponse;
59  import org.opencastproject.util.doc.rest.RestService;
60  import org.opencastproject.util.requests.SortCriterion;
61  import org.opencastproject.util.requests.SortCriterion.Order;
62  import org.opencastproject.workflow.api.WorkflowDatabaseException;
63  import org.opencastproject.workflow.api.WorkflowService;
64  
65  import com.google.gson.Gson;
66  import com.google.gson.JsonSyntaxException;
67  import com.google.gson.reflect.TypeToken;
68  
69  import org.apache.commons.lang3.StringUtils;
70  import org.osgi.service.component.ComponentContext;
71  import org.osgi.service.component.annotations.Activate;
72  import org.osgi.service.component.annotations.Component;
73  import org.osgi.service.component.annotations.Reference;
74  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
75  import org.slf4j.Logger;
76  import org.slf4j.LoggerFactory;
77  
78  import java.io.IOException;
79  import java.lang.reflect.Type;
80  import java.util.ArrayList;
81  import java.util.Comparator;
82  import java.util.HashMap;
83  import java.util.HashSet;
84  import java.util.Iterator;
85  import java.util.List;
86  import java.util.Map;
87  import java.util.Set;
88  import java.util.stream.Collectors;
89  
90  import javax.ws.rs.DELETE;
91  import javax.ws.rs.FormParam;
92  import javax.ws.rs.GET;
93  import javax.ws.rs.POST;
94  import javax.ws.rs.PUT;
95  import javax.ws.rs.Path;
96  import javax.ws.rs.PathParam;
97  import javax.ws.rs.Produces;
98  import javax.ws.rs.QueryParam;
99  import javax.ws.rs.core.MediaType;
100 import javax.ws.rs.core.Response;
101 
102 @Path("/admin-ng/users")
103 @RestService(name = "users", title = "User service",
104   abstractText = "Provides operations for users",
105   notes = { "This service offers the default users CRUD Operations for the admin UI.",
106             "<strong>Important:</strong> "
107               + "<em>This service is for exclusive use by the module admin-ui. Its API might change "
108               + "anytime without prior notice. Any dependencies other than the admin UI will be strictly ignored. "
109               + "DO NOT use this for integration of third-party applications.<em>"})
110 @Component(
111   immediate = true,
112   service = UsersEndpoint.class,
113   property = {
114     "service.description=Admin UI - Users facade Endpoint",
115     "opencast.service.type=org.opencastproject.adminui.endpoint.UsersEndpoint",
116     "opencast.service.path=/admin-ng/users"
117   }
118 )
119 @JaxrsResource
120 public class UsersEndpoint {
121 
122   /** The logging facility */
123   private static final Logger logger = LoggerFactory.getLogger(UsersEndpoint.class);
124 
125   /** The global user directory service */
126   protected UserDirectoryService userDirectoryService;
127 
128   /** The internal role and user provider */
129   private JpaUserAndRoleProvider jpaUserAndRoleProvider;
130 
131   /** The internal user reference provider */
132   private JpaUserReferenceProvider jpaUserReferenceProvider;
133 
134   /** The security service */
135   private SecurityService securityService;
136 
137   /** The workflow service */
138   private WorkflowService workflowService;
139 
140   /** Base url of this endpoint */
141   private String endpointBaseUrl;
142 
143   /** For JSON serialization */
144   private static final Type listType = new TypeToken<ArrayList<JsonRole>>() { }.getType();
145   private static final Gson gson = new Gson();
146 
147   /**
148    * Sets the user directory service
149    *
150    * @param userDirectoryService
151    *          the userDirectoryService to set
152    */
153   @Reference
154   public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
155     this.userDirectoryService = userDirectoryService;
156   }
157 
158   /**
159    * @param securityService
160    *          the securityService to set
161    */
162   @Reference
163   public void setSecurityService(SecurityService securityService) {
164     this.securityService = securityService;
165   }
166 
167   /**
168    * @param jpaUserReferenceProvider
169    *          the user provider to set
170    */
171   @Reference
172   public void setJpaUserReferenceProvider(JpaUserReferenceProvider jpaUserReferenceProvider) {
173     this.jpaUserReferenceProvider = jpaUserReferenceProvider;
174   }
175 
176   /**
177    * @param jpaUserAndRoleProvider
178    *          the user provider to set
179    */
180   @Reference
181   public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
182     this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
183   }
184 
185   /**
186    * @param workflowService
187    *          the user provider to set
188    */
189   @Reference
190   public void setWorkflowService(WorkflowService workflowService) {
191     this.workflowService = workflowService;
192   }
193 
194   /** OSGi callback. */
195   @Activate
196   protected void activate(ComponentContext cc) {
197     logger.info("Activate the Admin ui - Users facade endpoint");
198     final Tuple<String, String> endpointUrl = getEndpointUrl(cc);
199     endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
200   }
201 
202   @GET
203   @Path("users.json")
204   @Produces(MediaType.APPLICATION_JSON)
205   @RestQuery(name = "allusers", description = "Returns a list of users", returnDescription = "Returns a JSON representation of the list of user accounts", restParameters = {
206           @RestParameter(name = "filter", isRequired = false, description = "The filter used for the query. They should be formated like that: 'filter1:value1,filter2:value2'", type = STRING),
207           @RestParameter(name = "sort", isRequired = false, description = "The sort order. May include any of the following: STATUS, NAME OR LAST_UPDATED.  Add '_DESC' to reverse the sort order (e.g. STATUS_DESC).", type = STRING),
208           @RestParameter(defaultValue = "100", description = "The maximum number of items to return per page.", isRequired = false, name = "limit", type = RestParameter.Type.STRING),
209           @RestParameter(defaultValue = "0", description = "The page number.", isRequired = false, name = "offset", type = RestParameter.Type.STRING) }, responses = { @RestResponse(responseCode = SC_OK, description = "The user accounts.") })
210   public Response getUsers(@QueryParam("filter") String filter, @QueryParam("sort") String sort,
211           @QueryParam("limit") int limit, @QueryParam("offset") int offset) throws IOException {
212     if (limit < 1)
213       limit = 100;
214 
215     sort = trimToNull(sort);
216     String filterName = null;
217     String filterRole = null;
218     String filterProvider = null;
219     String filterText = null;
220 
221     Map<String, String> filters = RestUtils.parseFilter(filter);
222     for (String name : filters.keySet()) {
223       String value = filters.get(name);
224       if (UsersListQuery.FILTER_NAME_NAME.equals(name)) {
225         filterName = value;
226       } else if (UsersListQuery.FILTER_ROLE_NAME.equals(name)) {
227         filterRole = value;
228       } else if (UsersListQuery.FILTER_PROVIDER_NAME.equals(name)) {
229         filterProvider = value;
230       } else if ((UsersListQuery.FILTER_TEXT_NAME.equals(name)) && (StringUtils.isNotBlank(value))) {
231         filterText = value;
232       }
233     }
234 
235     // Filter users by filter criteria
236     List<User> filteredUsers = new ArrayList<>();
237     for (Iterator<User> i = userDirectoryService.getUsers(); i.hasNext();) {
238       User user = i.next();
239 
240       // Filter list
241       final String finalFilterRole = filterRole;
242       if (filterName != null && !filterName.equals(user.getName())
243               || (filterRole != null
244         && user.getRoles().stream().noneMatch((r) -> r.getName().equals(finalFilterRole)))
245               || (filterProvider != null
246                   && !filterProvider.equals(user.getProvider()))
247               || (filterText != null
248                   && !TextFilter.match(filterText, user.getUsername(), user.getName(), user.getEmail(), user.getProvider())
249                   && !TextFilter.match(filterText,
250                       user.getRoles().stream().map(Role::getName).collect(Collectors.joining(" "))))) {
251         continue;
252       }
253       filteredUsers.add(user);
254     }
255     int total = filteredUsers.size();
256 
257     // Sort by name, description or role
258     if (sort != null) {
259       final ArrayList<SortCriterion> sortCriteria = RestUtils.parseSortQueryParameter(sort);
260       filteredUsers.sort((user1, user2) -> {
261         for (SortCriterion criterion : sortCriteria) {
262           Order order = criterion.getOrder();
263           switch (criterion.getFieldName()) {
264             case "name":
265               if (order.equals(Order.Descending))
266                 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user2.getName()), trimToEmpty(user1.getName()));
267               return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user1.getName()), trimToEmpty(user2.getName()));
268             case "username":
269               if (order.equals(Order.Descending))
270                 return CASE_INSENSITIVE_ORDER
271                   .compare(trimToEmpty(user2.getUsername()), trimToEmpty(user1.getUsername()));
272               return CASE_INSENSITIVE_ORDER
273                 .compare(trimToEmpty(user1.getUsername()), trimToEmpty(user2.getUsername()));
274             case "email":
275               if (order.equals(Order.Descending))
276                 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user2.getEmail()), trimToEmpty(user1.getEmail()));
277               return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(user1.getEmail()), trimToEmpty(user2.getEmail()));
278             case "roles":
279               String roles1 = user1.getRoles().stream().map(Role::getName).collect(Collectors.joining(","));
280               String roles2 = user1.getRoles().stream().map(Role::getName).collect(Collectors.joining(","));
281               if (order.equals(Order.Descending))
282                 return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(roles2), trimToEmpty(roles1));
283               return CASE_INSENSITIVE_ORDER.compare(trimToEmpty(roles1), trimToEmpty(roles2));
284             case "provider":
285               if (order.equals(Order.Descending))
286                 return CASE_INSENSITIVE_ORDER
287                   .compare(trimToEmpty(user2.getProvider()), trimToEmpty(user1.getProvider()));
288               return CASE_INSENSITIVE_ORDER
289                 .compare(trimToEmpty(user1.getProvider()), trimToEmpty(user2.getProvider()));
290             default:
291               logger.info("Unknown sort type: {}", criterion.getFieldName());
292               return 0;
293           }
294         }
295         return 0;
296       });
297     }
298 
299     // Apply Limit and offset
300     filteredUsers = new SmartIterator<User>(limit, offset).applyLimitAndOffset(filteredUsers);
301 
302     List<Map<String, Object>> usersJSON = new ArrayList<>();
303     for (User user : filteredUsers) {
304       usersJSON.add(generateJsonUser(user));
305     }
306 
307     Map<String, Object> response = Map.of(
308         "results", usersJSON,
309         "count", usersJSON.size(),
310         "offset", offset,
311         "limit", limit,
312         "total", total);
313     return Response.ok(gson.toJson(response)).build();
314   }
315 
316   @POST
317   @Path("/")
318   @RestQuery(name = "createUser", description = "Create a new  user", returnDescription = "The location of the new ressource", restParameters = {
319           @RestParameter(description = "The username.", isRequired = true, name = "username", type = STRING),
320           @RestParameter(description = "The password.", isRequired = true, name = "password", type = STRING),
321           @RestParameter(description = "The name.", isRequired = false, name = "name", type = STRING),
322           @RestParameter(description = "The email.", isRequired = false, name = "email", type = STRING),
323           @RestParameter(name = "roles", type = STRING, isRequired = false, description = "The user roles as a json array, e.g. <br>"
324                   + "[{'name': 'ROLE_ADMIN', 'type': 'INTERNAL'}, {'name': 'ROLE_XY', 'type': 'INTERNAL'}]") },
325           responses = {
326           @RestResponse(responseCode = SC_CREATED, description = "User has been created."),
327           @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to create a user with a admin role."),
328           @RestResponse(responseCode = SC_CONFLICT, description = "An user with this username already exist.")})
329   public Response createUser(@FormParam("username") String username, @FormParam("password") String password,
330           @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
331 
332     if (StringUtils.isBlank(username)) {
333       return Response.status(SC_BAD_REQUEST).entity("Missing username").build();
334     }
335     if (StringUtils.isBlank(password)) {
336       return Response.status(SC_BAD_REQUEST).entity("Missing password").build();
337     }
338 
339     User existingUser = jpaUserAndRoleProvider.loadUser(username);
340     if (existingUser != null) {
341       return Response.status(SC_CONFLICT).build();
342     }
343 
344     JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
345     Set<JpaRole> rolesSet;
346     try {
347       rolesSet = parseJsonRoles(roles);
348     } catch (IllegalArgumentException e) {
349       logger.debug("Received invalid JSON for roles", e);
350       return Response.status(SC_BAD_REQUEST).entity("Invalid JSON for roles").build();
351     }
352 
353     if (rolesSet == null) {
354       rolesSet = new HashSet<>();
355       rolesSet.add(new JpaRole(organization.getAnonymousRole(), organization));
356     }
357 
358     JpaUser user = new JpaUser(username, password, organization, name, email, jpaUserAndRoleProvider.getName(), true,
359             rolesSet);
360     try {
361       jpaUserAndRoleProvider.addUser(user);
362       return Response.created(uri(endpointBaseUrl, user.getUsername() + ".json")).build();
363     } catch (UnauthorizedException e) {
364       return Response.status(SC_FORBIDDEN).build();
365     }
366   }
367 
368   @GET
369   @Path("{username}.json")
370   @RestQuery(name = "getUser", description = "Get an user", returnDescription = "Status ok", pathParameters = @RestParameter(name = "username", type = STRING, isRequired = true, description = "The username"), responses = {
371           @RestResponse(responseCode = SC_OK, description = "User has been found."),
372           @RestResponse(responseCode = SC_NOT_FOUND, description = "User not found.") })
373   public Response getUser(@PathParam("username") String username) {
374 
375     User user = userDirectoryService.loadUser(username);
376     if (user == null) {
377       return Response.status(SC_NOT_FOUND).build();
378     }
379 
380     return Response.ok(gson.toJson(generateJsonUser(user))).build();
381   }
382 
383   @PUT
384   @Path("{username}.json")
385   @RestQuery(name = "updateUser", description = "Update an user", returnDescription = "Status ok", restParameters = {
386           @RestParameter(description = "The password.", isRequired = false, name = "password", type = STRING),
387           @RestParameter(description = "The name.", isRequired = false, name = "name", type = STRING),
388           @RestParameter(description = "The email.", isRequired = false, name = "email", type = STRING),
389           @RestParameter(name = "roles", type = STRING, isRequired = false, description = "The user roles as a json array") }, pathParameters = @RestParameter(name = "username", type = STRING, isRequired = true, description = "The username"), responses = {
390           @RestResponse(responseCode = SC_OK, description = "User has been updated."),
391           @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to update a user with admin role."),
392           @RestResponse(responseCode = SC_BAD_REQUEST, description = "Invalid data provided.")})
393   public Response updateUser(@PathParam("username") String username, @FormParam("password") String password,
394           @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
395 
396     User user = jpaUserAndRoleProvider.loadUser(username);
397     if (user == null) {
398       return createUser(username, password, name, email, roles);
399     }
400 
401     Set<JpaRole> rolesSet;
402     try {
403       rolesSet = parseJsonRoles(roles);
404     } catch (IllegalArgumentException e) {
405       logger.debug("Received invalid JSON for roles", e);
406       return Response.status(SC_BAD_REQUEST).build();
407     }
408 
409     JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
410     if (rolesSet == null) {
411       //  use the previous roles if no new ones are provided
412       rolesSet = new HashSet<>();
413       for (Role role : user.getRoles()) {
414         rolesSet.add(new JpaRole(role.getName(), organization, role.getDescription(), role.getType()));
415       }
416     }
417 
418     try {
419       jpaUserAndRoleProvider.updateUser(new JpaUser(username, password, organization, name, email,
420         jpaUserAndRoleProvider.getName(), true, rolesSet));
421       userDirectoryService.invalidate(username);
422       return Response.status(SC_OK).build();
423     } catch (UnauthorizedException ex) {
424       return Response.status(Response.Status.FORBIDDEN).build();
425     } catch (NotFoundException e) {
426       return Response.serverError().build();
427     }
428   }
429 
430   @DELETE
431   @Path("{username}.json")
432   @RestQuery(name = "deleteUser", description = "Deleter a new  user", returnDescription = "Status ok", pathParameters = @RestParameter(name = "username", type = STRING, isRequired = true, description = "The username"), responses = {
433           @RestResponse(responseCode = SC_OK, description = "User has been deleted."),
434           @RestResponse(responseCode = SC_FORBIDDEN, description = "Not enough permissions to delete a user with admin role."),
435           @RestResponse(responseCode = SC_NOT_FOUND, description = "User not found.") })
436   public Response deleteUser(@PathParam("username") String username) throws NotFoundException {
437     Organization organization = securityService.getOrganization();
438     boolean userReferenceNotFound = false;
439     boolean userNotFound = false;
440 
441     try {
442       if (workflowService.userHasActiveWorkflows(username)) {
443         logger.debug("Workflow still active for user {}:", username);
444         return Response.status(SC_CONFLICT).build();
445       }
446     } catch (WorkflowDatabaseException e) {
447       logger.error("Error during deletion of user {}", username, e);
448       return Response.status(SC_INTERNAL_SERVER_ERROR).build();
449     }
450 
451     try {
452       try {
453         jpaUserReferenceProvider.deleteUser(username, organization.getId());
454       } catch (NotFoundException e) {
455         userReferenceNotFound = true;
456       }
457       try {
458         jpaUserAndRoleProvider.deleteUser(username, organization.getId());
459       } catch (NotFoundException e) {
460         userNotFound = true;
461       }
462 
463       if (userNotFound && userReferenceNotFound) {
464         throw new NotFoundException();
465       }
466 
467       userDirectoryService.invalidate(username);
468     } catch (NotFoundException e) {
469       logger.debug("User {} not found.", username);
470       return Response.status(SC_NOT_FOUND).build();
471     } catch (UnauthorizedException e) {
472       return Response.status(SC_FORBIDDEN).build();
473     } catch (Exception e) {
474       logger.error("Error during deletion of user {}", username, e);
475       return Response.status(SC_INTERNAL_SERVER_ERROR).build();
476     }
477 
478     logger.debug("User {} removed.", username);
479     return Response.status(SC_OK).build();
480   }
481 
482   /**
483    * Parse a JSON roles string.
484    *
485    * @param roles
486    *          Array of roles as JSON strings.
487    * @return Set of roles or null
488    * @throws IllegalArgumentException
489    *          Invalid JSON data
490    */
491   private Set<JpaRole> parseJsonRoles(final String roles) throws IllegalArgumentException {
492     List<JsonRole> rolesList;
493     try {
494       rolesList = gson.fromJson(roles, listType);
495     } catch (JsonSyntaxException e) {
496       throw new IllegalArgumentException(e);
497     }
498     if (rolesList == null) {
499       return null;
500     }
501 
502     JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
503     Set<JpaRole> rolesSet = new HashSet<>();
504     for (JsonRole role: rolesList) {
505       try {
506         rolesSet.add(new JpaRole(role.getName(), organization, null, role.getType()));
507       } catch (NullPointerException e) {
508         throw new IllegalArgumentException(e);
509       }
510     }
511     return rolesSet;
512   }
513 
514   private Map<String, Object> generateJsonUser(User user) {
515     // Prepare the roles
516     Map<String, Object> userData = new HashMap<>();
517     userData.put("username", user.getUsername());
518     userData.put("manageable", user.isManageable());
519     userData.put("name", user.getName());
520     userData.put("email", user.getEmail());
521     userData.put("provider", user.getProvider());
522     userData.put("roles", user.getRoles().stream()
523       .sorted(Comparator.comparing(Role::getName))
524       .map((r) -> new JsonRole(r.getName(), r.getType()))
525       .collect(Collectors.toList()));
526     return userData;
527   }
528 
529   class JsonRole {
530     private String name;
531     private String type;
532 
533     JsonRole(String name, Role.Type type) {
534       this.name = name;
535       this.type = type.toString();
536     }
537 
538     public String getName() {
539       return name;
540     }
541 
542     public Role.Type getType() {
543       return Role.Type.valueOf(type);
544     }
545   }
546 
547 }