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.userdirectory.endpoint;
23  
24  import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
25  import static org.apache.http.HttpStatus.SC_CONFLICT;
26  import static org.apache.http.HttpStatus.SC_CREATED;
27  import static org.apache.http.HttpStatus.SC_FORBIDDEN;
28  import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
29  import static org.apache.http.HttpStatus.SC_NOT_FOUND;
30  import static org.apache.http.HttpStatus.SC_NO_CONTENT;
31  import static org.apache.http.HttpStatus.SC_OK;
32  import static org.opencastproject.util.RestUtil.getEndpointUrl;
33  import static org.opencastproject.util.UrlSupport.uri;
34  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
35  
36  import org.opencastproject.security.api.JaxbUser;
37  import org.opencastproject.security.api.JaxbUserList;
38  import org.opencastproject.security.api.SecurityService;
39  import org.opencastproject.security.api.UnauthorizedException;
40  import org.opencastproject.security.api.User;
41  import org.opencastproject.security.impl.jpa.JpaOrganization;
42  import org.opencastproject.security.impl.jpa.JpaRole;
43  import org.opencastproject.security.impl.jpa.JpaUser;
44  import org.opencastproject.userdirectory.JpaUserAndRoleProvider;
45  import org.opencastproject.util.NotFoundException;
46  import org.opencastproject.util.UrlSupport;
47  import org.opencastproject.util.data.Tuple;
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  
53  import com.google.gson.Gson;
54  import com.google.gson.JsonSyntaxException;
55  import com.google.gson.reflect.TypeToken;
56  
57  import org.apache.commons.lang3.StringUtils;
58  import org.osgi.service.component.ComponentContext;
59  import org.osgi.service.component.annotations.Component;
60  import org.osgi.service.component.annotations.Reference;
61  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  import java.io.IOException;
66  import java.lang.reflect.Type;
67  import java.util.ArrayList;
68  import java.util.Collections;
69  import java.util.HashSet;
70  import java.util.Iterator;
71  import java.util.List;
72  import java.util.Objects;
73  import java.util.Set;
74  import java.util.stream.Collectors;
75  
76  import javax.persistence.RollbackException;
77  import javax.ws.rs.Consumes;
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.core.MediaType;
88  import javax.ws.rs.core.Response;
89  
90  /**
91   * Provides a sorted set of known users
92   */
93  @Path("/user-utils")
94  @RestService(
95      name = "UsersUtils",
96      title = "User utils",
97      notes = "This service offers the default CRUD Operations for the internal Opencast users.",
98      abstractText = "Provides operations for internal Opencast users")
99  @Component(
100     property = {
101         "service.description=User REST endpoint",
102         "opencast.service.type=org.opencastproject.userdirectory.endpoint.UserEndpoint",
103         "opencast.service.path=/user-utils",
104         "opencast.service.jobproducer=false"
105     },
106     immediate = true,
107     service = { UserEndpoint.class }
108 )
109 @JaxrsResource
110 public class UserEndpoint {
111 
112   /** The logger */
113   private static final Logger logger = LoggerFactory.getLogger(UserEndpoint.class);
114 
115   private JpaUserAndRoleProvider jpaUserAndRoleProvider;
116 
117   private SecurityService securityService;
118 
119   private String endpointBaseUrl;
120 
121   private static final Gson gson = new Gson();
122 
123   private class UserData {
124     private String username;
125     private String password;
126     private String name;
127     private String email;
128     private Set<String> roles;
129   }
130 
131   private final Type userListType = new TypeToken<List<User>>() { }.getType();
132 
133   /** OSGi callback. */
134   public void activate(ComponentContext cc) {
135     logger.info("Start users endpoint");
136     final Tuple<String, String> endpointUrl = getEndpointUrl(cc);
137     endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
138   }
139 
140   /**
141    * @param securityService
142    *          the securityService to set
143    */
144   @Reference
145   public void setSecurityService(SecurityService securityService) {
146     this.securityService = securityService;
147   }
148 
149   /**
150    * @param jpaUserAndRoleProvider
151    *          the persistenceProperties to set
152    */
153   @Reference
154   public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
155     this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
156   }
157 
158   @GET
159   @Path("users.json")
160   @Produces(MediaType.APPLICATION_JSON)
161   @RestQuery(
162       name = "allusersasjson",
163       description = "Returns a list of users",
164       returnDescription = "Returns a JSON representation of the list of user accounts",
165       restParameters = {
166       @RestParameter(
167         name = "limit",
168         defaultValue = "100",
169         description = "The maximum number of items to return per page.",
170         isRequired = false,
171         type = RestParameter.Type.STRING),
172       @RestParameter(
173         name = "offset",
174         defaultValue = "0",
175         description = "The page number.",
176         isRequired = false,
177         type = RestParameter.Type.STRING)
178       }, responses = {
179       @RestResponse(
180         responseCode = SC_OK,
181         description = "The user accounts.")
182     })
183   public JaxbUserList getUsersAsJson(@QueryParam("limit") int limit, @QueryParam("offset") int offset)
184           throws IOException {
185 
186     // Set the maximum number of items to return to 100 if this limit parameter is not given
187     if (limit < 1) {
188       limit = 100;
189     }
190 
191     JaxbUserList userList = new JaxbUserList();
192     for (Iterator<User> i = jpaUserAndRoleProvider.findUsers("%", offset, limit); i.hasNext();) {
193       userList.add(i.next());
194     }
195     return userList;
196   }
197 
198   @GET
199   @Path("{username}.json")
200   @Produces(MediaType.APPLICATION_JSON)
201   @RestQuery(
202       name = "user",
203       description = "Returns a user",
204       returnDescription = "Returns a JSON representation of a user",
205       pathParameters = {
206       @RestParameter(
207         name = "username",
208         description = "The username.",
209         isRequired = true,
210         type = STRING)
211       }, responses = {
212       @RestResponse(
213         responseCode = SC_OK,
214         description = "The user account."),
215       @RestResponse(
216         responseCode = SC_NOT_FOUND,
217         description = "User not found")
218     })
219   public Response getUserAsJson(@PathParam("username") String username) throws NotFoundException {
220     User user = jpaUserAndRoleProvider.loadUser(username);
221     if (user == null) {
222       logger.debug("Requested user not found: {}", username);
223       return Response.status(SC_NOT_FOUND).build();
224     }
225     return Response.ok(JaxbUser.fromUser(user)).build();
226   }
227 
228   @GET
229   @Path("users/md5.json")
230   @Produces(MediaType.APPLICATION_JSON)
231   @RestQuery(
232       name = "users-with-insecure-hashing",
233       description = "Returns a list of users which passwords are stored using MD5 hashes",
234       returnDescription = "Returns a JSON representation of the list of matching user accounts",
235       responses = {
236       @RestResponse(
237           responseCode = SC_OK,
238           description = "The user accounts.")
239   })
240   public JaxbUserList getUserWithInsecurePasswordHashingAsJson() {
241     JaxbUserList userList = new JaxbUserList();
242     for (User user: jpaUserAndRoleProvider.findInsecurePasswordHashes()) {
243       userList.add(user);
244     }
245     return userList;
246   }
247 
248   @POST
249   @Path("/")
250   @RestQuery(
251       name = "createUser",
252       description = "Create a new  user",
253       returnDescription = "Location of the new resource",
254       restParameters = {
255       @RestParameter(
256         name = "username",
257         description = "The username.",
258         isRequired = true,
259         type = STRING),
260       @RestParameter(
261         name = "password",
262         description = "The password.",
263         isRequired = true,
264         type = STRING),
265       @RestParameter(
266         name = "name",
267         description = "The name.",
268         isRequired = false,
269         type = STRING),
270       @RestParameter(
271         name = "email",
272         description = "The email.",
273         isRequired = false,
274         type = STRING),
275       @RestParameter(
276         name = "roles",
277         description = "The user roles as a json array, for example: [\"ROLE_USER\", \"ROLE_ADMIN\"]",
278         isRequired = false,
279         type = STRING)
280       }, responses = {
281       @RestResponse(
282         responseCode = SC_BAD_REQUEST,
283         description = "Malformed request syntax."),
284       @RestResponse(
285         responseCode = SC_CREATED,
286         description = "User has been created."),
287       @RestResponse(
288         responseCode = SC_CONFLICT,
289         description = "An user with this username already exist."),
290       @RestResponse(
291         responseCode = SC_FORBIDDEN,
292         description = "Not enough permissions to create a user with the admin role.")
293     })
294   public Response createUser(@FormParam("username") String username, @FormParam("password") String password,
295           @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
296 
297     if (jpaUserAndRoleProvider.loadUser(username) != null) {
298       return Response.status(SC_CONFLICT).build();
299     }
300 
301     try {
302       Set<JpaRole> rolesSet = parseRoles(roles);
303 
304       /* Add new user */
305       logger.debug("Updating user {}", username);
306       JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
307       JpaUser user = new JpaUser(username, password, organization, name, email, jpaUserAndRoleProvider.getName(), true,
308               rolesSet);
309       try {
310         jpaUserAndRoleProvider.addUser(user);
311         return Response.created(uri(endpointBaseUrl, user.getUsername() + ".json")).build();
312       } catch (UnauthorizedException ex) {
313         logger.debug("Create user failed", ex);
314         return Response.status(Response.Status.FORBIDDEN).build();
315       }
316 
317     } catch (IllegalArgumentException e) {
318       logger.debug("Request with malformed ROLE data: {}", roles);
319       return Response.status(SC_BAD_REQUEST).build();
320     }
321   }
322 
323   @POST
324   @Path("/users.json")
325   @Consumes(MediaType.APPLICATION_JSON)
326   @RestQuery(
327       name = "createUsers",
328       description = "Create a list of new users",
329       returnDescription = "If the operation succeeded or not",
330       responses = {
331           @RestResponse(
332               responseCode = SC_BAD_REQUEST,
333               description = "Malformed request syntax."),
334           @RestResponse(
335               responseCode = SC_CONFLICT,
336               description = "At least one user already existed."),
337           @RestResponse(
338               responseCode = SC_NO_CONTENT,
339               description = "Users have been created.") })
340   public Response createUsers(String body) throws UnauthorizedException {
341     logger.debug("Provided user JSON: {}", body);
342     try {
343       for (var user: parseUserData(body)) {
344         jpaUserAndRoleProvider.addUser(user);
345         logger.info("Created user {}", user.getUsername());
346       }
347     } catch (RollbackException e) {
348       logger.debug("Error storing user in database", e);
349       return Response.status(Response.Status.CONFLICT).build();
350     } catch (IllegalArgumentException e) {
351       logger.debug("Error parsing the provided JSON body", e);
352       return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
353     }
354     return Response.status(Response.Status.CREATED).build();
355   }
356 
357   @PUT
358   @Path("{username}.json")
359   @RestQuery(
360       name = "updateUser",
361       description = "Update an user",
362       returnDescription = "Status ok",
363       restParameters = {
364       @RestParameter(
365         name = "password",
366         description = "The password.",
367         isRequired = true,
368         type = STRING),
369       @RestParameter(
370         name = "name",
371         description = "The name.",
372         isRequired = false,
373         type = STRING),
374       @RestParameter(
375         name = "email",
376         description = "The email.",
377         isRequired = false,
378         type = STRING),
379       @RestParameter(
380         name = "roles",
381         description = "The user roles as a json array, for example: [\"ROLE_USER\", \"ROLE_ADMIN\"]",
382         isRequired = false,
383         type = STRING)
384       }, pathParameters = @RestParameter(
385       name = "username",
386       description = "The username",
387       isRequired = true,
388       type = STRING),
389       responses = {
390       @RestResponse(
391         responseCode = SC_BAD_REQUEST,
392         description = "Malformed request syntax."),
393       @RestResponse(
394         responseCode = SC_FORBIDDEN,
395         description = "Not enough permissions to update a user with the admin role."),
396       @RestResponse(
397         responseCode = SC_OK,
398         description = "User has been updated.")    })
399   public Response setUser(@PathParam("username") String username, @FormParam("password") String password,
400           @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
401 
402     try {
403       User user = jpaUserAndRoleProvider.loadUser(username);
404       if (user == null) {
405         return createUser(username, password, name, email, roles);
406       }
407 
408       Set<JpaRole> rolesSet = parseRoles(roles);
409 
410       logger.debug("Updating user {}", username);
411       JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
412       jpaUserAndRoleProvider.updateUser(new JpaUser(username, password, organization, name, email,
413                 jpaUserAndRoleProvider.getName(), true, rolesSet));
414       return Response.status(SC_OK).build();
415     } catch (NotFoundException e) {
416       logger.debug("User {} not found.", username);
417       return Response.status(SC_NOT_FOUND).build();
418     } catch (UnauthorizedException e) {
419       logger.debug("Update user failed", e);
420       return Response.status(Response.Status.FORBIDDEN).build();
421     } catch (IllegalArgumentException e) {
422       logger.debug("Request with malformed ROLE data: {}", roles);
423       return Response.status(SC_BAD_REQUEST).build();
424     }
425   }
426 
427   @DELETE
428   @Path("{username}.json")
429   @RestQuery(
430       name = "deleteUser",
431       description = "Delete a new  user",
432       returnDescription = "Status ok",
433       pathParameters = @RestParameter(
434       name = "username",
435       type = STRING,
436       isRequired = true,
437       description = "The username"),
438       responses = {
439       @RestResponse(
440         responseCode = SC_OK,
441         description = "User has been deleted."),
442       @RestResponse(
443         responseCode = SC_FORBIDDEN,
444         description = "Not enough permissions to delete a user with the admin role."),
445       @RestResponse(
446         responseCode = SC_NOT_FOUND,
447         description = "User not found.")
448     })
449   public Response deleteUser(@PathParam("username") String username) {
450     try {
451       jpaUserAndRoleProvider.deleteUser(username, securityService.getOrganization().getId());
452     } catch (NotFoundException e) {
453       logger.debug("User {} not found.", username);
454       return Response.status(SC_NOT_FOUND).build();
455     } catch (UnauthorizedException e) {
456       logger.debug("Error during deletion of user {}", username, e);
457       return Response.status(SC_FORBIDDEN).build();
458     } catch (Exception e) {
459       logger.error("Error during deletion of user {}", username, e);
460       return Response.status(SC_INTERNAL_SERVER_ERROR).build();
461     }
462 
463     logger.debug("User {} removed.", username);
464     return Response.status(SC_OK).build();
465   }
466 
467   /**
468    * Parse JSON roles array.
469    *
470    * @param roles
471    *          String representation of JSON array containing roles
472    */
473   private Set<JpaRole> parseRoles(String roles) throws IllegalArgumentException {
474     /* Try parsing JSON. Return Bad Request if malformed. */
475     final String[] rolesArray;
476     try {
477       rolesArray = gson.fromJson(StringUtils.isEmpty(roles) ? "[]" : roles, String[].class);
478     } catch (JsonSyntaxException e) {
479       throw new IllegalArgumentException("Error parsing JSON array", e);
480     }
481 
482     Set<JpaRole> rolesSet = new HashSet<>();
483     /* Add given roles */
484     for (var role : rolesArray) {
485       rolesSet.add(new JpaRole(role, (JpaOrganization) securityService.getOrganization()));
486     }
487 
488     return rolesSet;
489   }
490 
491   private List<JpaUser> parseUserData(String userJson) {
492     final UserData[] userArray;
493     try {
494       userArray = gson.fromJson(userJson, UserData[].class);
495     } catch (JsonSyntaxException e) {
496       throw new IllegalArgumentException("Error parsing JSON array", e);
497     }
498 
499     if (Objects.isNull(userArray)) {
500       throw new IllegalArgumentException("The JSON may not be empty or `null`");
501     }
502 
503     var users = new ArrayList<JpaUser>(userArray.length);
504     var organization = (JpaOrganization) securityService.getOrganization();
505     var provider = jpaUserAndRoleProvider.getName();
506     for (var u: userArray) {
507       if (Objects.isNull(u.username)) {
508         throw new IllegalArgumentException("Field `username` may not be `null`");
509       }
510       // parse roles
511       Set<JpaRole> roles = Objects.isNull(u.roles)
512           ? Collections.emptySet()
513           : u.roles.stream().map(r -> new JpaRole(r, organization)).collect(Collectors.toSet());
514       users.add(new JpaUser(u.username, u.password, organization, u.name, u.email, provider, true, roles));
515     }
516     return users;
517   }
518 
519 }