02fc7b60f88913572582ecb0328daae95ed2d7e1
[aaa.git] / aaa-idmlight / src / main / java / org / opendaylight / aaa / idm / rest / UserHandler.java
1 /*
2  * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8
9 package org.opendaylight.aaa.idm.rest;
10
11 import java.util.Collection;
12
13 import javax.ws.rs.Consumes;
14 import javax.ws.rs.DELETE;
15 import javax.ws.rs.GET;
16 import javax.ws.rs.POST;
17 import javax.ws.rs.PUT;
18 import javax.ws.rs.Path;
19 import javax.ws.rs.PathParam;
20 import javax.ws.rs.Produces;
21 import javax.ws.rs.core.Context;
22 import javax.ws.rs.core.Response;
23 import javax.ws.rs.core.UriInfo;
24
25 import org.opendaylight.aaa.api.IDMStoreException;
26 import org.opendaylight.aaa.api.model.IDMError;
27 import org.opendaylight.aaa.api.model.User;
28 import org.opendaylight.aaa.api.model.Users;
29 import org.opendaylight.aaa.idm.IdmLightApplication;
30 import org.opendaylight.aaa.idm.IdmLightProxy;
31 import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 /**
36  * REST application used to manipulate the H2 database users table. The REST
37  * endpoint is <code>/auth/v1/users</code>.
38  *
39  * A wrapper script called <code>idmtool</code> is provided to manipulate AAA data.
40  *
41  * @author peter.mellquist@hp.com
42  * @author Ryan Goulding (ryandgoulding@gmail.com)
43  */
44 @Path("/v1/users")
45 public class UserHandler {
46
47     private static final Logger LOG = LoggerFactory.getLogger(UserHandler.class);
48
49     /**
50      * If a user is created through the <code>/auth/v1/users</code> rest
51      * endpoint without a password, the default password is assigned to the
52      * user.
53      */
54     private final static String DEFAULT_PWD = "changeme";
55
56     /**
57      * When an HTTP GET is performed on <code>/auth/v1/users</code>, the
58      * password field is replaced with <code>REDACTED_PASSWORD</code> for
59      * security reasons.
60      */
61     private static final String REDACTED_PASSWORD = "**********";
62
63     /**
64      * When an HTTP GET is performed on <code>/auth/v1/users</code>, the salt
65      * field is replaced with <code>REDACTED_SALT</code> for security reasons.
66      */
67     private static final String REDACTED_SALT = "**********";
68
69     /**
70      * When creating a user, the description is optional and defaults to an
71      * empty string.
72      */
73     private static final String DEFAULT_DESCRIPTION = "";
74
75     /**
76      * When creating a user, the email is optional and defaults to an empty
77      * string.
78      */
79     private static final String DEFAULT_EMAIL = "";
80
81     /**
82      * Extracts all users. The password and salt fields are redacted for
83      * security reasons.
84      *
85      * @return A response containing the users, or internal error if one occurs
86      */
87     @GET
88     @Produces("application/json")
89     public Response getUsers() {
90         LOG.info("GET /auth/v1/users  (extracts all users)");
91
92         try {
93             final Users users = AAAIDMLightModule.getStore().getUsers();
94
95             // Redact the password and salt for security purposes.
96             final Collection<User> usersList = users.getUsers();
97             for (User user : usersList) {
98                 redactUserPasswordInfo(user);
99             }
100
101             return Response.ok(users).build();
102         } catch (IDMStoreException se) {
103             return internalError("getting", se);
104         }
105     }
106
107     /**
108      * Extracts the user represented by <code>id</code>. The password and salt
109      * fields are redacted for security reasons.
110      *
111      * @param id the unique id of representing the user account
112      * @return A response with the user information, or internal error if one occurs
113      */
114     @GET
115     @Path("/{id}")
116     @Produces("application/json")
117     public Response getUser(@PathParam("id") String id) {
118         LOG.info("GET auth/v1/users/ {}  (extract user with specified id)", id);
119
120         try {
121             final User user = AAAIDMLightModule.getStore().readUser(id);
122
123             if (user == null) {
124                 final String error = "user not found! id: " + id;
125                 return new IDMError(404, error, "").response();
126             }
127
128             // Redact the password and salt for security purposes.
129             redactUserPasswordInfo(user);
130
131             return Response.ok(user).build();
132         } catch (IDMStoreException se) {
133             return internalError("getting", se);
134         }
135     }
136
137     /**
138      * REST endpoint to create a user. Name and domain are required attributes,
139      * and all other fields (description, email, password, enabled) are
140      * optional. Optional fields default in the following manner:
141      * <code>description</code>: An empty string (<code>""</code>).
142      * <code>email</code>: An empty string (<code>""</code>).
143      * <code>password</code>: <code>changeme</code> <code>enabled</code>:
144      * <code>true</code>
145      *
146      * If a password is not provided, please ensure you change the default
147      * password ASAP for security reasons!
148      *
149      * @param info passed from Jersey
150      * @param user the user defined in the JSON payload
151      * @return A response stating success or failure of user creation
152      */
153     @POST
154     @Consumes("application/json")
155     @Produces("application/json")
156     public Response createUser(@Context UriInfo info, User user) {
157         LOG.info("POST /auth/v1/users  (create a user with the specified payload");
158
159         // Bug 8382:  user id is an implementation detail and isn't specifiable
160         if (user.getUserid() != null) {
161             final String errorMessage =
162                     "do not specify userId, it will be assigned automatically for you";
163             LOG.debug(errorMessage);
164             final IDMError idmError = new IDMError();
165             idmError.setMessage(errorMessage);
166             return Response.status(400).entity(idmError).build();
167         }
168
169         // The "enabled" field is optional, and defaults to true.
170         if (user.isEnabled() == null) {
171             user.setEnabled(true);
172         }
173
174         // The "name" field is required.
175         final String userName = user.getName();
176         if (userName == null) {
177             return missingRequiredField("name");
178         }
179         // The "name" field has a maximum length.
180         if (userName.length() > IdmLightApplication.MAX_FIELD_LEN) {
181             return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN);
182         }
183
184         // The "domain field is required.
185         final String domainId = user.getDomainid();
186         if (domainId == null) {
187             return missingRequiredField("domain");
188         }
189         // The "domain" field has a maximum length.
190         if (domainId.length() > IdmLightApplication.MAX_FIELD_LEN) {
191             return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN);
192         }
193
194         // The "description" field is optional and defaults to "".
195         final String userDescription = user.getDescription();
196         if (userDescription == null) {
197             user.setDescription(DEFAULT_DESCRIPTION);
198         }
199         // The "description" field has a maximum length.
200         if (userDescription.length() > IdmLightApplication.MAX_FIELD_LEN) {
201             return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN);
202         }
203
204         // The "email" field is optional and defaults to "".
205         String userEmail = user.getEmail();
206         if (userEmail == null) {
207             user.setEmail(DEFAULT_EMAIL);
208             userEmail = DEFAULT_EMAIL;
209         }
210         if (userEmail.length() > IdmLightApplication.MAX_FIELD_LEN) {
211             return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN);
212         }
213         // TODO add a check on email format here.
214
215         // The "password" field is optional and defautls to "changeme".
216         final String userPassword = user.getPassword();
217         if (userPassword == null) {
218             user.setPassword(DEFAULT_PWD);
219         } else if (userPassword.length() > IdmLightApplication.MAX_FIELD_LEN) {
220             return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN);
221         }
222
223         try {
224             // At this point, fields have been properly verified. Create the
225             // user account
226             final User createdUser = AAAIDMLightModule.getStore().writeUser(user);
227             user.setUserid(createdUser.getUserid());
228         } catch (IDMStoreException se) {
229             return internalError("creating", se);
230         }
231
232         // Redact the password and salt for security reasons.
233         redactUserPasswordInfo(user);
234         // TODO report back to the client a warning message to change the
235         // default password if none was specified.
236         return Response.status(201).entity(user).build();
237     }
238
239     /**
240      * REST endpoint to update a user account.
241      *
242      * @param info passed from Jersey
243      * @param user the user defined in the JSON payload
244      * @param id the unique id for the user that will be updated
245      * @return A response stating success or failure of the user update
246      */
247     @PUT
248     @Path("/{id}")
249     @Consumes("application/json")
250     @Produces("application/json")
251     public Response putUser(@Context UriInfo info, User user, @PathParam("id") String id) {
252
253         LOG.info("PUT /auth/v1/users/{}  (Updates a user account)", id);
254
255         try {
256             user.setUserid(id);
257
258             if (checkInputFieldLength(user.getPassword())) {
259                 return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN);
260             }
261
262             if (checkInputFieldLength(user.getName())) {
263                 return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN);
264             }
265
266             if (checkInputFieldLength(user.getDescription())) {
267                 return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN);
268             }
269
270             if (checkInputFieldLength(user.getEmail())) {
271                 return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN);
272             }
273
274             if (checkInputFieldLength(user.getDomainid())) {
275                 return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN);
276             }
277
278             user = AAAIDMLightModule.getStore().updateUser(user);
279             if (user == null) {
280                 return new IDMError(404, String.format("User not found for id %s", id), "").response();
281             }
282
283             IdmLightProxy.clearClaimCache();
284
285             // Redact the password and salt for security reasons.
286             redactUserPasswordInfo(user);
287             return Response.status(200).entity(user).build();
288         } catch (IDMStoreException se) {
289             return internalError("updating", se);
290         }
291     }
292
293     /**
294      * REST endpoint to delete a user account.
295      *
296      * @param info passed from Jersey
297      * @param id the unique id of the user which is being deleted
298      * @return A response stating success or failure of user deletion
299      */
300     @DELETE
301     @Path("/{id}")
302     public Response deleteUser(@Context UriInfo info, @PathParam("id") String id) {
303         LOG.info("DELETE /auth/v1/users/{}  (Delete a user account)", id);
304
305         try {
306             final User user = AAAIDMLightModule.getStore().deleteUser(id);
307
308             if (user == null) {
309                 return new IDMError(404,
310                         String.format("Error deleting user.  " +
311                                       "Couldn't find user with id %s", id),
312                                       "").response();
313             }
314         } catch (IDMStoreException se) {
315             return internalError("deleting", se);
316         }
317
318         // Successfully deleted the user; report success to the client.
319         IdmLightProxy.clearClaimCache();
320         return Response.status(204).build();
321     }
322
323     /**
324      * Creates a <code>Response</code> related to an internal server error.
325      *
326      * @param verbal such as "creating", "deleting", "updating"
327      * @param e The exception, which is propagated in the response
328      * @return A response containing internal error with specific reasoning
329      */
330     private Response internalError(final String verbal, final Exception e) {
331         LOG.error("There was an internal error {} the user", verbal, e);
332         return new IDMError(500,
333                 String.format("There was an internal error %s the user", verbal),
334                 e.getMessage()).response();
335     }
336
337     /**
338      * Creates a <code>Response</code> related to the user not providing a
339      * required field.
340      *
341      * @param fieldName the name of the field which is missing
342      * @return A response explaining that the request is missing a field
343      */
344     private Response missingRequiredField(final String fieldName) {
345
346         return new IDMError(400,
347                 String.format("%s is required to create the user account.  " +
348                               "Please provide a %s in your payload.", fieldName, fieldName),
349                               "").response();
350     }
351
352     /**
353      * Creates a <code>Response</code> related to the user providing a field
354      * that is too long.
355      *
356      * @param fieldName the name of the field that is too long
357      * @param maxFieldLength the maximum length of <code>fieldName</code>
358      * @return A response containing the bad field and the maximum field length
359      */
360     private Response providedFieldTooLong(final String fieldName, final int maxFieldLength) {
361
362         return new IDMError(400,
363                 getProvidedFieldTooLongMessage(fieldName, maxFieldLength),
364                 "").response();
365     }
366
367     /**
368      * Creates the client-facing message related to the user providing a field
369      * that is too long.
370      *
371      * @param fieldName the name of the field that is too long
372      * @param maxFieldLength the maximum length of <code>fieldName</code>
373      * @return
374      */
375     private static String getProvidedFieldTooLongMessage(final String fieldName,
376             final int maxFieldLength) {
377
378         return String.format("The provided %s field is too long.  " +
379                              "The max length is %s.", fieldName, maxFieldLength);
380     }
381
382     /**
383      * Prepares a user account for output by redacting the appropriate fields.
384      * This method side-effects the <code>user</code> parameter.
385      *
386      * @param user the user account which will have fields redacted
387      */
388     private static void redactUserPasswordInfo(final User user) {
389         user.setPassword(REDACTED_PASSWORD);
390         user.setSalt(REDACTED_SALT);
391     }
392
393     /**
394      * Validate the input field length
395      *
396      * @param inputField
397      * @return true if input field bigger than the MAX_FIELD_LEN
398      */
399     private boolean checkInputFieldLength(final String inputField) {
400         return inputField != null && inputField.length() > IdmLightApplication.MAX_FIELD_LEN;
401     }
402 }