diff options
Diffstat (limited to 'upstream/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java')
-rw-r--r-- | upstream/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/upstream/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java b/upstream/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java new file mode 100644 index 00000000..1649faa2 --- /dev/null +++ b/upstream/odl-aaa-moon/aaa/aaa-idmlight/src/main/java/org/opendaylight/aaa/idm/rest/UserHandler.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.idm.rest; + +import java.util.Collection; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.opendaylight.aaa.api.IDMStoreException; +import org.opendaylight.aaa.api.model.IDMError; +import org.opendaylight.aaa.api.model.User; +import org.opendaylight.aaa.api.model.Users; +import org.opendaylight.aaa.idm.IdmLightApplication; +import org.opendaylight.aaa.idm.IdmLightProxy; +import org.opendaylight.yang.gen.v1.config.aaa.authn.idmlight.rev151204.AAAIDMLightModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * REST application used to manipulate the H2 database users table. The REST + * endpoint is <code>/auth/v1/users</code>. + * + * The following provides examples of how curl commands and payloads to utilize + * the users REST endpoint: + * + * <b>Get All Users</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/users</code> + * + * <b>Get A Specific User</b> + * <code>curl -u admin:admin http://{HOST}:{PORT}/auth/v1/users/{id}</code> + * + * <b>Create A User</b> + * <code>curl -u admin:admin -X POST -H "Content-type: application/json" --data-binary {@literal @}user.json http://{HOST}:{PORT}/auth/v1/users</code> + * An example of user.json file is: + * <code>{ + * "name": "admin2", + * "password", "admin2", + * "domain": "sdn" + * }</code> + * + * <b>Update A User</b> + * <code>curl -u admin:admin -X PUT -H "Content-type: application/json" --data-binary {@literal @}user.json http://{HOST}:{PORT}/auth/v1/users/{id}</code> + * An example of user.json file is: + * <code>{ + * "name": "admin2", + * "password", "admin2", + * "domain": "sdn", + * "description", "Simple description." + * }</code> + * + * <b>Delete A User</b> + * <code>curl -u admin:admin -X DELETE http://{HOST}:{PORT}/auth/v1/users/{id}</code> + * + * @author peter.mellquist@hp.com + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +@Path("/v1/users") +public class UserHandler { + + private static final Logger LOG = LoggerFactory.getLogger(UserHandler.class); + + /** + * If a user is created through the <code>/auth/v1/users</code> rest + * endpoint without a password, the default password is assigned to the + * user. + */ + private final static String DEFAULT_PWD = "changeme"; + + /** + * When an HTTP GET is performed on <code>/auth/v1/users</code>, the + * password field is replaced with <code>REDACTED_PASSWORD</code> for + * security reasons. + */ + private static final String REDACTED_PASSWORD = "**********"; + + /** + * When an HTTP GET is performed on <code>/auth/v1/users</code>, the salt + * field is replaced with <code>REDACTED_SALT</code> for security reasons. + */ + private static final String REDACTED_SALT = "**********"; + + /** + * When creating a user, the description is optional and defaults to an + * empty string. + */ + private static final String DEFAULT_DESCRIPTION = ""; + + /** + * When creating a user, the email is optional and defaults to an empty + * string. + */ + private static final String DEFAULT_EMAIL = ""; + + /** + * Extracts all users. The password and salt fields are redacted for + * security reasons. + * + * @return A response containing the users, or internal error if one occurs + */ + @GET + @Produces("application/json") + public Response getUsers() { + LOG.info("GET /auth/v1/users (extracts all users)"); + + try { + final Users users = AAAIDMLightModule.getStore().getUsers(); + + // Redact the password and salt for security purposes. + final Collection<User> usersList = users.getUsers(); + for (User user : usersList) { + redactUserPasswordInfo(user); + } + + return Response.ok(users).build(); + } catch (IDMStoreException se) { + return internalError("getting", se); + } + } + + /** + * Extracts the user represented by <code>id</code>. The password and salt + * fields are redacted for security reasons. + * + * @param id the unique id of representing the user account + * @return A response with the user information, or internal error if one occurs + */ + @GET + @Path("/{id}") + @Produces("application/json") + public Response getUser(@PathParam("id") String id) { + LOG.info("GET auth/v1/users/ {} (extract user with specified id)", id); + + try { + final User user = AAAIDMLightModule.getStore().readUser(id); + + if (user == null) { + final String error = "user not found! id: " + id; + return new IDMError(404, error, "").response(); + } + + // Redact the password and salt for security purposes. + redactUserPasswordInfo(user); + + return Response.ok(user).build(); + } catch (IDMStoreException se) { + return internalError("getting", se); + } + } + + /** + * REST endpoint to create a user. Name and domain are required attributes, + * and all other fields (description, email, password, enabled) are + * optional. Optional fields default in the following manner: + * <code>description</code>: An empty string (<code>""</code>). + * <code>email</code>: An empty string (<code>""</code>). + * <code>password</code>: <code>changeme</code> <code>enabled</code>: + * <code>true</code> + * + * If a password is not provided, please ensure you change the default + * password ASAP for security reasons! + * + * @param info passed from Jersey + * @param user the user defined in the JSON payload + * @return A response stating success or failure of user creation + */ + @POST + @Consumes("application/json") + @Produces("application/json") + public Response createUser(@Context UriInfo info, User user) { + LOG.info("POST /auth/v1/users (create a user with the specified payload"); + + // The "enabled" field is optional, and defaults to true. + if (user.isEnabled() == null) { + user.setEnabled(true); + } + + // The "name" field is required. + final String userName = user.getName(); + if (userName == null) { + return missingRequiredField("name"); + } + // The "name" field has a maximum length. + if (userName.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN); + } + + // The "domain field is required. + final String domainId = user.getDomainid(); + if (domainId == null) { + return missingRequiredField("domain"); + } + // The "domain" field has a maximum length. + if (domainId.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN); + } + + // The "description" field is optional and defaults to "". + final String userDescription = user.getDescription(); + if (userDescription == null) { + user.setDescription(DEFAULT_DESCRIPTION); + } + // The "description" field has a maximum length. + if (userDescription.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN); + } + + // The "email" field is optional and defaults to "". + final String userEmail = user.getEmail(); + if (userEmail == null) { + user.setEmail(DEFAULT_EMAIL); + } + if (userEmail.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN); + } + // TODO add a check on email format here. + + // The "password" field is optional and defautls to "changeme". + final String userPassword = user.getPassword(); + if (userPassword == null) { + user.setPassword(DEFAULT_PWD); + } else if (userPassword.length() > IdmLightApplication.MAX_FIELD_LEN) { + return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN); + } + + try { + // At this point, fields have been properly verified. Create the + // user account + final User createdUser = AAAIDMLightModule.getStore().writeUser(user); + user.setUserid(createdUser.getUserid()); + } catch (IDMStoreException se) { + return internalError("creating", se); + } + + // Redact the password and salt for security reasons. + redactUserPasswordInfo(user); + // TODO report back to the client a warning message to change the + // default password if none was specified. + return Response.status(201).entity(user).build(); + } + + /** + * REST endpoint to update a user account. + * + * @param info passed from Jersey + * @param user the user defined in the JSON payload + * @param id the unique id for the user that will be updated + * @return A response stating success or failure of the user update + */ + @PUT + @Path("/{id}") + @Consumes("application/json") + @Produces("application/json") + public Response putUser(@Context UriInfo info, User user, @PathParam("id") String id) { + + LOG.info("PUT /auth/v1/users/{} (Updates a user account)", id); + + try { + user.setUserid(id); + + if (checkInputFieldLength(user.getPassword())) { + return providedFieldTooLong("password", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getName())) { + return providedFieldTooLong("name", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getDescription())) { + return providedFieldTooLong("description", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getEmail())) { + return providedFieldTooLong("email", IdmLightApplication.MAX_FIELD_LEN); + } + + if (checkInputFieldLength(user.getDomainid())) { + return providedFieldTooLong("domain", IdmLightApplication.MAX_FIELD_LEN); + } + + user = AAAIDMLightModule.getStore().updateUser(user); + if (user == null) { + return new IDMError(404, String.format("User not found for id %s", id), "").response(); + } + + IdmLightProxy.clearClaimCache(); + + // Redact the password and salt for security reasons. + redactUserPasswordInfo(user); + return Response.status(200).entity(user).build(); + } catch (IDMStoreException se) { + return internalError("updating", se); + } + } + + /** + * REST endpoint to delete a user account. + * + * @param info passed from Jersey + * @param id the unique id of the user which is being deleted + * @return A response stating success or failure of user deletion + */ + @DELETE + @Path("/{id}") + public Response deleteUser(@Context UriInfo info, @PathParam("id") String id) { + LOG.info("DELETE /auth/v1/users/{} (Delete a user account)", id); + + try { + final User user = AAAIDMLightModule.getStore().deleteUser(id); + + if (user == null) { + return new IDMError(404, + String.format("Error deleting user. " + + "Couldn't find user with id %s", id), + "").response(); + } + } catch (IDMStoreException se) { + return internalError("deleting", se); + } + + // Successfully deleted the user; report success to the client. + IdmLightProxy.clearClaimCache(); + return Response.status(204).build(); + } + + /** + * Creates a <code>Response</code> related to an internal server error. + * + * @param verbal such as "creating", "deleting", "updating" + * @param e The exception, which is propagated in the response + * @return A response containing internal error with specific reasoning + */ + private Response internalError(final String verbal, final Exception e) { + LOG.error("There was an internal error {} the user", verbal, e); + return new IDMError(500, + String.format("There was an internal error %s the user", verbal), + e.getMessage()).response(); + } + + /** + * Creates a <code>Response</code> related to the user not providing a + * required field. + * + * @param fieldName the name of the field which is missing + * @return A response explaining that the request is missing a field + */ + private Response missingRequiredField(final String fieldName) { + + return new IDMError(400, + String.format("%s is required to create the user account. " + + "Please provide a %s in your payload.", fieldName, fieldName), + "").response(); + } + + /** + * Creates a <code>Response</code> related to the user providing a field + * that is too long. + * + * @param fieldName the name of the field that is too long + * @param maxFieldLength the maximum length of <code>fieldName</code> + * @return A response containing the bad field and the maximum field length + */ + private Response providedFieldTooLong(final String fieldName, final int maxFieldLength) { + + return new IDMError(400, + getProvidedFieldTooLongMessage(fieldName, maxFieldLength), + "").response(); + } + + /** + * Creates the client-facing message related to the user providing a field + * that is too long. + * + * @param fieldName the name of the field that is too long + * @param maxFieldLength the maximum length of <code>fieldName</code> + * @return + */ + private static String getProvidedFieldTooLongMessage(final String fieldName, + final int maxFieldLength) { + + return String.format("The provided {} field is too long. " + + "The max length is {}.", fieldName, maxFieldLength); + } + + /** + * Prepares a user account for output by redacting the appropriate fields. + * This method side-effects the <code>user</code> parameter. + * + * @param user the user account which will have fields redacted + */ + private static void redactUserPasswordInfo(final User user) { + user.setPassword(REDACTED_PASSWORD); + user.setSalt(REDACTED_SALT); + } + + /** + * Validate the input field length + * + * @param inputField + * @return true if input field bigger than the MAX_FIELD_LEN + */ + private boolean checkInputFieldLength(final String inputField) { + return inputField != null && (inputField.length() > IdmLightApplication.MAX_FIELD_LEN); + } +} |