2 * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved.
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
9 package org.opendaylight.aaa.idm.rest;
11 import java.util.Collection;
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;
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;
36 * REST application used to manipulate the H2 database users table. The REST
37 * endpoint is <code>/auth/v1/users</code>.
39 * A wrapper script called <code>idmtool</code> is provided to manipulate AAA data.
41 * @author peter.mellquist@hp.com
42 * @author Ryan Goulding (ryandgoulding@gmail.com)
45 public class UserHandler {
47 private static final Logger LOG = LoggerFactory.getLogger(UserHandler.class);
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
54 private final static String DEFAULT_PWD = "changeme";
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
61 private static final String REDACTED_PASSWORD = "**********";
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.
67 private static final String REDACTED_SALT = "**********";
70 * When creating a user, the description is optional and defaults to an
73 private static final String DEFAULT_DESCRIPTION = "";
76 * When creating a user, the email is optional and defaults to an empty
79 private static final String DEFAULT_EMAIL = "";
82 * Extracts all users. The password and salt fields are redacted for
85 * @return A response containing the users, or internal error if one occurs
88 @Produces("application/json")
89 public Response getUsers() {
90 LOG.info("GET /auth/v1/users (extracts all users)");
93 final Users users = AAAIDMLightModule.getStore().getUsers();
95 // Redact the password and salt for security purposes.
96 final Collection<User> usersList = users.getUsers();
97 for (User user : usersList) {
98 redactUserPasswordInfo(user);
101 return Response.ok(users).build();
102 } catch (IDMStoreException se) {
103 return internalError("getting", se);
108 * Extracts the user represented by <code>id</code>. The password and salt
109 * fields are redacted for security reasons.
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
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);
121 final User user = AAAIDMLightModule.getStore().readUser(id);
124 final String error = "user not found! id: " + id;
125 return new IDMError(404, error, "").response();
128 // Redact the password and salt for security purposes.
129 redactUserPasswordInfo(user);
131 return Response.ok(user).build();
132 } catch (IDMStoreException se) {
133 return internalError("getting", se);
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>:
146 * If a password is not provided, please ensure you change the default
147 * password ASAP for security reasons!
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
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");
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();
169 // The "enabled" field is optional, and defaults to true.
170 if (user.isEnabled() == null) {
171 user.setEnabled(true);
174 // The "name" field is required.
175 final String userName = user.getName();
176 if (userName == null) {
177 return missingRequiredField("name");
179 // The "name" field has a maximum length.
180 if (userName.length() > IdmLightApplication.MAX_FIELD_LEN) {
181 return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN);
184 // The "domain field is required.
185 final String domainId = user.getDomainid();
186 if (domainId == null) {
187 return missingRequiredField("domain");
189 // The "domain" field has a maximum length.
190 if (domainId.length() > IdmLightApplication.MAX_FIELD_LEN) {
191 return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN);
194 // The "description" field is optional and defaults to "".
195 final String userDescription = user.getDescription();
196 if (userDescription == null) {
197 user.setDescription(DEFAULT_DESCRIPTION);
199 // The "description" field has a maximum length.
200 if (userDescription.length() > IdmLightApplication.MAX_FIELD_LEN) {
201 return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN);
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;
210 if (userEmail.length() > IdmLightApplication.MAX_FIELD_LEN) {
211 return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN);
213 // TODO add a check on email format here.
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);
224 // At this point, fields have been properly verified. Create the
226 final User createdUser = AAAIDMLightModule.getStore().writeUser(user);
227 user.setUserid(createdUser.getUserid());
228 } catch (IDMStoreException se) {
229 return internalError("creating", se);
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();
240 * REST endpoint to update a user account.
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
249 @Consumes("application/json")
250 @Produces("application/json")
251 public Response putUser(@Context UriInfo info, User user, @PathParam("id") String id) {
253 LOG.info("PUT /auth/v1/users/{} (Updates a user account)", id);
258 if (checkInputFieldLength(user.getPassword())) {
259 return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN);
262 if (checkInputFieldLength(user.getName())) {
263 return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN);
266 if (checkInputFieldLength(user.getDescription())) {
267 return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN);
270 if (checkInputFieldLength(user.getEmail())) {
271 return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN);
274 if (checkInputFieldLength(user.getDomainid())) {
275 return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN);
278 user = AAAIDMLightModule.getStore().updateUser(user);
280 return new IDMError(404, String.format("User not found for id %s", id), "").response();
283 IdmLightProxy.clearClaimCache();
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);
294 * REST endpoint to delete a user account.
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
302 public Response deleteUser(@Context UriInfo info, @PathParam("id") String id) {
303 LOG.info("DELETE /auth/v1/users/{} (Delete a user account)", id);
306 final User user = AAAIDMLightModule.getStore().deleteUser(id);
309 return new IDMError(404,
310 String.format("Error deleting user. " +
311 "Couldn't find user with id %s", id),
314 } catch (IDMStoreException se) {
315 return internalError("deleting", se);
318 // Successfully deleted the user; report success to the client.
319 IdmLightProxy.clearClaimCache();
320 return Response.status(204).build();
324 * Creates a <code>Response</code> related to an internal server error.
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
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();
338 * Creates a <code>Response</code> related to the user not providing a
341 * @param fieldName the name of the field which is missing
342 * @return A response explaining that the request is missing a field
344 private Response missingRequiredField(final String fieldName) {
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),
353 * Creates a <code>Response</code> related to the user providing a field
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
360 private Response providedFieldTooLong(final String fieldName, final int maxFieldLength) {
362 return new IDMError(400,
363 getProvidedFieldTooLongMessage(fieldName, maxFieldLength),
368 * Creates the client-facing message related to the user providing a field
371 * @param fieldName the name of the field that is too long
372 * @param maxFieldLength the maximum length of <code>fieldName</code>
375 private static String getProvidedFieldTooLongMessage(final String fieldName,
376 final int maxFieldLength) {
378 return String.format("The provided %s field is too long. " +
379 "The max length is %s.", fieldName, maxFieldLength);
383 * Prepares a user account for output by redacting the appropriate fields.
384 * This method side-effects the <code>user</code> parameter.
386 * @param user the user account which will have fields redacted
388 private static void redactUserPasswordInfo(final User user) {
389 user.setPassword(REDACTED_PASSWORD);
390 user.setSalt(REDACTED_SALT);
394 * Validate the input field length
397 * @return true if input field bigger than the MAX_FIELD_LEN
399 private boolean checkInputFieldLength(final String inputField) {
400 return inputField != null && inputField.length() > IdmLightApplication.MAX_FIELD_LEN;