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_OK;
31  import static org.opencastproject.util.RestUtil.getEndpointUrl;
32  import static org.opencastproject.util.UrlSupport.uri;
33  import static org.opencastproject.util.doc.rest.RestParameter.Type.STRING;
34  
35  import org.opencastproject.security.api.JaxbUser;
36  import org.opencastproject.security.api.JaxbUserList;
37  import org.opencastproject.security.api.SecurityService;
38  import org.opencastproject.security.api.UnauthorizedException;
39  import org.opencastproject.security.api.User;
40  import org.opencastproject.security.impl.jpa.JpaOrganization;
41  import org.opencastproject.security.impl.jpa.JpaRole;
42  import org.opencastproject.security.impl.jpa.JpaUser;
43  import org.opencastproject.userdirectory.JpaUserAndRoleProvider;
44  import org.opencastproject.util.NotFoundException;
45  import org.opencastproject.util.UrlSupport;
46  import org.opencastproject.util.data.Tuple;
47  import org.opencastproject.util.doc.rest.RestParameter;
48  import org.opencastproject.util.doc.rest.RestQuery;
49  import org.opencastproject.util.doc.rest.RestResponse;
50  import org.opencastproject.util.doc.rest.RestService;
51  
52  import org.apache.commons.lang3.StringUtils;
53  import org.json.simple.JSONArray;
54  import org.json.simple.JSONValue;
55  import org.osgi.service.component.ComponentContext;
56  import org.osgi.service.component.annotations.Component;
57  import org.osgi.service.component.annotations.Reference;
58  import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import java.io.IOException;
63  import java.util.HashSet;
64  import java.util.Iterator;
65  import java.util.Set;
66  
67  import javax.ws.rs.DELETE;
68  import javax.ws.rs.FormParam;
69  import javax.ws.rs.GET;
70  import javax.ws.rs.POST;
71  import javax.ws.rs.PUT;
72  import javax.ws.rs.Path;
73  import javax.ws.rs.PathParam;
74  import javax.ws.rs.Produces;
75  import javax.ws.rs.QueryParam;
76  import javax.ws.rs.core.MediaType;
77  import javax.ws.rs.core.Response;
78  
79  /**
80   * Provides a sorted set of known users
81   */
82  @Path("/user-utils")
83  @RestService(
84      name = "UsersUtils",
85      title = "User utils",
86      notes = "This service offers the default CRUD Operations for the internal Opencast users.",
87      abstractText = "Provides operations for internal Opencast users")
88  @Component(
89      property = {
90          "service.description=User REST endpoint",
91          "opencast.service.type=org.opencastproject.userdirectory.endpoint.UserEndpoint",
92          "opencast.service.path=/user-utils",
93          "opencast.service.jobproducer=false"
94      },
95      immediate = true,
96      service = { UserEndpoint.class }
97  )
98  @JaxrsResource
99  public class UserEndpoint {
100 
101   /** The logger */
102   private static final Logger logger = LoggerFactory.getLogger(UserEndpoint.class);
103 
104   private JpaUserAndRoleProvider jpaUserAndRoleProvider;
105 
106   private SecurityService securityService;
107 
108   private String endpointBaseUrl;
109 
110   /** OSGi callback. */
111   public void activate(ComponentContext cc) {
112     logger.info("Start users endpoint");
113     final Tuple<String, String> endpointUrl = getEndpointUrl(cc);
114     endpointBaseUrl = UrlSupport.concat(endpointUrl.getA(), endpointUrl.getB());
115   }
116 
117   /**
118    * @param securityService
119    *          the securityService to set
120    */
121   @Reference
122   public void setSecurityService(SecurityService securityService) {
123     this.securityService = securityService;
124   }
125 
126   /**
127    * @param jpaUserAndRoleProvider
128    *          the persistenceProperties to set
129    */
130   @Reference
131   public void setJpaUserAndRoleProvider(JpaUserAndRoleProvider jpaUserAndRoleProvider) {
132     this.jpaUserAndRoleProvider = jpaUserAndRoleProvider;
133   }
134 
135   @GET
136   @Path("users.json")
137   @Produces(MediaType.APPLICATION_JSON)
138   @RestQuery(
139       name = "allusersasjson",
140       description = "Returns a list of users",
141       returnDescription = "Returns a JSON representation of the list of user accounts",
142       restParameters = {
143       @RestParameter(
144         name = "limit",
145         defaultValue = "100",
146         description = "The maximum number of items to return per page.",
147         isRequired = false,
148         type = RestParameter.Type.STRING),
149       @RestParameter(
150         name = "offset",
151         defaultValue = "0",
152         description = "The page number.",
153         isRequired = false,
154         type = RestParameter.Type.STRING)
155       }, responses = {
156       @RestResponse(
157         responseCode = SC_OK,
158         description = "The user accounts.")
159     })
160   public JaxbUserList getUsersAsJson(@QueryParam("limit") int limit, @QueryParam("offset") int offset)
161           throws IOException {
162 
163     // Set the maximum number of items to return to 100 if this limit parameter is not given
164     if (limit < 1) {
165       limit = 100;
166     }
167 
168     JaxbUserList userList = new JaxbUserList();
169     for (Iterator<User> i = jpaUserAndRoleProvider.findUsers("%", offset, limit); i.hasNext();) {
170       userList.add(i.next());
171     }
172     return userList;
173   }
174 
175   @GET
176   @Path("{username}.json")
177   @Produces(MediaType.APPLICATION_JSON)
178   @RestQuery(
179       name = "user",
180       description = "Returns a user",
181       returnDescription = "Returns a JSON representation of a user",
182       pathParameters = {
183       @RestParameter(
184         name = "username",
185         description = "The username.",
186         isRequired = true,
187         type = STRING)
188       }, responses = {
189       @RestResponse(
190         responseCode = SC_OK,
191         description = "The user account."),
192       @RestResponse(
193         responseCode = SC_NOT_FOUND,
194         description = "User not found")
195     })
196   public Response getUserAsJson(@PathParam("username") String username) throws NotFoundException {
197     User user = jpaUserAndRoleProvider.loadUser(username);
198     if (user == null) {
199       logger.debug("Requested user not found: {}", username);
200       return Response.status(SC_NOT_FOUND).build();
201     }
202     return Response.ok(JaxbUser.fromUser(user)).build();
203   }
204 
205   @GET
206   @Path("users/md5.json")
207   @Produces(MediaType.APPLICATION_JSON)
208   @RestQuery(
209       name = "users-with-insecure-hashing",
210       description = "Returns a list of users which passwords are stored using MD5 hashes",
211       returnDescription = "Returns a JSON representation of the list of matching user accounts",
212       responses = {
213       @RestResponse(
214           responseCode = SC_OK,
215           description = "The user accounts.")
216   })
217   public JaxbUserList getUserWithInsecurePasswordHashingAsJson() {
218     JaxbUserList userList = new JaxbUserList();
219     for (User user: jpaUserAndRoleProvider.findInsecurePasswordHashes()) {
220       userList.add(user);
221     }
222     return userList;
223   }
224 
225   @POST
226   @Path("/")
227   @RestQuery(
228       name = "createUser",
229       description = "Create a new  user",
230       returnDescription = "Location of the new ressource",
231       restParameters = {
232       @RestParameter(
233         name = "username",
234         description = "The username.",
235         isRequired = true,
236         type = STRING),
237       @RestParameter(
238         name = "password",
239         description = "The password.",
240         isRequired = true,
241         type = STRING),
242       @RestParameter(
243         name = "name",
244         description = "The name.",
245         isRequired = false,
246         type = STRING),
247       @RestParameter(
248         name = "email",
249         description = "The email.",
250         isRequired = false,
251         type = STRING),
252       @RestParameter(
253         name = "roles",
254         description = "The user roles as a json array, for example: [\"ROLE_USER\", \"ROLE_ADMIN\"]",
255         isRequired = false,
256         type = STRING)
257       }, responses = {
258       @RestResponse(
259         responseCode = SC_BAD_REQUEST,
260         description = "Malformed request syntax."),
261       @RestResponse(
262         responseCode = SC_CREATED,
263         description = "User has been created."),
264       @RestResponse(
265         responseCode = SC_CONFLICT,
266         description = "An user with this username already exist."),
267       @RestResponse(
268         responseCode = SC_FORBIDDEN,
269         description = "Not enough permissions to create a user with the admin role.")
270     })
271   public Response createUser(@FormParam("username") String username, @FormParam("password") String password,
272           @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
273 
274     if (jpaUserAndRoleProvider.loadUser(username) != null) {
275       return Response.status(SC_CONFLICT).build();
276     }
277 
278     try {
279       Set<JpaRole> rolesSet = parseRoles(roles);
280 
281       /* Add new user */
282       logger.debug("Updating user {}", username);
283       JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
284       JpaUser user = new JpaUser(username, password, organization, name, email, jpaUserAndRoleProvider.getName(), true,
285               rolesSet);
286       try {
287         jpaUserAndRoleProvider.addUser(user);
288         return Response.created(uri(endpointBaseUrl, user.getUsername() + ".json")).build();
289       } catch (UnauthorizedException ex) {
290         logger.debug("Create user failed", ex);
291         return Response.status(Response.Status.FORBIDDEN).build();
292       }
293 
294     } catch (IllegalArgumentException e) {
295       logger.debug("Request with malformed ROLE data: {}", roles);
296       return Response.status(SC_BAD_REQUEST).build();
297     }
298   }
299 
300   @PUT
301   @Path("{username}.json")
302   @RestQuery(
303       name = "updateUser",
304       description = "Update an user",
305       returnDescription = "Status ok",
306       restParameters = {
307       @RestParameter(
308         name = "password",
309         description = "The password.",
310         isRequired = true,
311         type = STRING),
312       @RestParameter(
313         name = "name",
314         description = "The name.",
315         isRequired = false,
316         type = STRING),
317       @RestParameter(
318         name = "email",
319         description = "The email.",
320         isRequired = false,
321         type = STRING),
322       @RestParameter(
323         name = "roles",
324         description = "The user roles as a json array, for example: [\"ROLE_USER\", \"ROLE_ADMIN\"]",
325         isRequired = false,
326         type = STRING)
327       }, pathParameters = @RestParameter(
328       name = "username",
329       description = "The username",
330       isRequired = true,
331       type = STRING),
332       responses = {
333       @RestResponse(
334         responseCode = SC_BAD_REQUEST,
335         description = "Malformed request syntax."),
336       @RestResponse(
337         responseCode = SC_FORBIDDEN,
338         description = "Not enough permissions to update a user with the admin role."),
339       @RestResponse(
340         responseCode = SC_OK,
341         description = "User has been updated.")    })
342   public Response setUser(@PathParam("username") String username, @FormParam("password") String password,
343           @FormParam("name") String name, @FormParam("email") String email, @FormParam("roles") String roles) {
344 
345     try {
346       User user = jpaUserAndRoleProvider.loadUser(username);
347       if (user == null) {
348         return createUser(username, password, name, email, roles);
349       }
350 
351       Set<JpaRole> rolesSet = parseRoles(roles);
352 
353       logger.debug("Updating user {}", username);
354       JpaOrganization organization = (JpaOrganization) securityService.getOrganization();
355       jpaUserAndRoleProvider.updateUser(new JpaUser(username, password, organization, name, email,
356                 jpaUserAndRoleProvider.getName(), true, rolesSet));
357       return Response.status(SC_OK).build();
358     } catch (NotFoundException e) {
359       logger.debug("User {} not found.", username);
360       return Response.status(SC_NOT_FOUND).build();
361     } catch (UnauthorizedException e) {
362       logger.debug("Update user failed", e);
363       return Response.status(Response.Status.FORBIDDEN).build();
364     } catch (IllegalArgumentException e) {
365       logger.debug("Request with malformed ROLE data: {}", roles);
366       return Response.status(SC_BAD_REQUEST).build();
367     }
368   }
369 
370   @DELETE
371   @Path("{username}.json")
372   @RestQuery(
373       name = "deleteUser",
374       description = "Delete a new  user",
375       returnDescription = "Status ok",
376       pathParameters = @RestParameter(
377       name = "username",
378       type = STRING,
379       isRequired = true,
380       description = "The username"),
381       responses = {
382       @RestResponse(
383         responseCode = SC_OK,
384         description = "User has been deleted."),
385       @RestResponse(
386         responseCode = SC_FORBIDDEN,
387         description = "Not enough permissions to delete a user with the admin role."),
388       @RestResponse(
389         responseCode = SC_NOT_FOUND,
390         description = "User not found.")
391     })
392   public Response deleteUser(@PathParam("username") String username) {
393     try {
394       jpaUserAndRoleProvider.deleteUser(username, securityService.getOrganization().getId());
395     } catch (NotFoundException e) {
396       logger.debug("User {} not found.", username);
397       return Response.status(SC_NOT_FOUND).build();
398     } catch (UnauthorizedException e) {
399       logger.debug("Error during deletion of user {}", username, e);
400       return Response.status(SC_FORBIDDEN).build();
401     } catch (Exception e) {
402       logger.error("Error during deletion of user {}", username, e);
403       return Response.status(SC_INTERNAL_SERVER_ERROR).build();
404     }
405 
406     logger.debug("User {} removed.", username);
407     return Response.status(SC_OK).build();
408   }
409 
410   /**
411    * Parse JSON roles array.
412    *
413    * @param roles
414    *          String representation of JSON array containing roles
415    */
416   private Set<JpaRole> parseRoles(String roles) throws IllegalArgumentException {
417     JSONArray rolesArray = null;
418     /* Try parsing JSON. Return Bad Request if malformed. */
419     try {
420       rolesArray = (JSONArray) JSONValue.parseWithException(StringUtils.isEmpty(roles) ? "[]" : roles);
421     } catch (Exception e) {
422       throw new IllegalArgumentException("Error parsing JSON array", e);
423     }
424 
425     Set<JpaRole> rolesSet = new HashSet<JpaRole>();
426     /* Add given roles */
427     for (Object role : rolesArray) {
428       try {
429         rolesSet.add(new JpaRole((String) role, (JpaOrganization) securityService.getOrganization()));
430       } catch (ClassCastException e) {
431         throw new IllegalArgumentException("Error parsing array vales as String", e);
432       }
433     }
434 
435     return rolesSet;
436   }
437 
438 }