diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APICredentialError.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APICredentialError.java deleted file mode 100644 index f60b7028e..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APICredentialError.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2015 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.rest; - -import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; - -/** - * Represents an error related to either invalid or insufficient credentials - * submitted to a REST endpoint. - * - * @author Michael Jumper - */ -public class APICredentialError extends APIError { - - /** - * The required credentials. - */ - private final CredentialsInfo info; - - /** - * The type of error that occurred. - */ - private final Type type; - - /** - * All possible types of credential errors. - */ - public enum Type { - - /** - * The credentials provided were invalid. - */ - INVALID, - - /** - * The credentials provided were not necessarily invalid, but were not - * sufficient to determine validity. - */ - INSUFFICIENT - - } - - /** - * Create a new APICredentialError with the specified error message and - * credentials information. - * - * @param type - * The type of error that occurred. - * - * @param message - * The error message. - * - * @param info - * An object which describes the required credentials. - */ - public APICredentialError(Type type, String message, CredentialsInfo info) { - super(message); - this.type = type; - this.info = info; - } - - /** - * Returns the type of error that occurred. - * - * @return - * The type of error that occurred. - */ - public Type getType() { - return type; - } - - /** - * Returns an object which describes the required credentials. - * - * @return - * An object which describes the required credentials. - */ - public CredentialsInfo getInfo() { - return info; - } - -} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java index f08c2e402..3c62725c8 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/APIError.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,31 +22,161 @@ package org.glyptodon.guacamole.net.basic.rest; +import java.util.Collection; +import javax.ws.rs.core.Response; +import org.glyptodon.guacamole.form.Parameter; + /** - * A simple object to represent an error to be sent from the REST API. + * Describes an error that occurred within a REST endpoint. + * * @author James Muehlner + * @author Michael Jumper */ public class APIError { - + /** * The error message. */ private final String message; /** - * Get the error message. - * @return The error message. + * All expected request parameters, if any. + */ + private final Collection expected; + + /** + * The type of error that occurred. + */ + private final Type type; + + /** + * All possible types of REST API errors. + */ + public enum Type { + + /** + * The requested operation could not be performed because the request + * itself was malformed. + */ + BAD_REQUEST(Response.Status.BAD_REQUEST), + + /** + * The credentials provided were invalid. + */ + INVALID_CREDENTIALS(Response.Status.FORBIDDEN), + + /** + * The credentials provided were not necessarily invalid, but were not + * sufficient to determine validity. + */ + INSUFFICIENT_CREDENTIALS(Response.Status.FORBIDDEN), + + /** + * An internal server error has occurred. + */ + INTERNAL_ERROR(Response.Status.INTERNAL_SERVER_ERROR), + + /** + * An object related to the request does not exist. + */ + NOT_FOUND(Response.Status.NOT_FOUND), + + /** + * Permission was denied to perform the requested operation. + */ + PERMISSION_DENIED(Response.Status.FORBIDDEN); + + /** + * The HTTP status associated with this error type. + */ + private final Response.Status status; + + /** + * Defines a new error type associated with the given HTTP status. + * + * @param status + * The HTTP status to associate with the error type. + */ + Type(Response.Status status) { + this.status = status; + } + + /** + * Returns the HTTP status associated with this error type. + * + * @return + * The HTTP status associated with this error type. + */ + public Response.Status getStatus() { + return status; + } + + } + + /** + * Create a new APIError with the specified error message. + * + * @param type + * The type of error that occurred. + * + * @param message + * The error message. + */ + public APIError(Type type, String message) { + this.type = type; + this.message = message; + this.expected = null; + } + + /** + * Create a new APIError with the specified error message and parameter + * information. + * + * @param type + * The type of error that occurred. + * + * @param message + * The error message. + * + * @param expected + * All parameters expected in the original request, or now required as + * a result of the original request. + */ + public APIError(Type type, String message, Collection expected) { + this.type = type; + this.message = message; + this.expected = expected; + } + + /** + * Returns the type of error that occurred. + * + * @return + * The type of error that occurred. + */ + public Type getType() { + return type; + } + + /** + * Returns an object which describes the required credentials. + * + * @return + * An object which describes the required credentials. + */ + public Collection getExpected() { + return expected; + } + + /** + * Returns a human-readable error message describing the error that + * occurred. + * + * @return + * A human-readable error message. */ public String getMessage() { return message; } - - /** - * Create a new APIError with the specified error message. - * @param message The error message. - */ - public APIError(String message) { - this.message = message; - } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java index 8d344c68d..741ed15df 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java @@ -27,6 +27,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; @@ -61,11 +62,11 @@ public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { if (message == null) message = "Permission denied."; - throw new HTTPException(Response.Status.FORBIDDEN, new APICredentialError( - APICredentialError.Type.INSUFFICIENT, - message, - e.getCredentialsInfo() - )); + throw new HTTPException( + APIError.Type.INSUFFICIENT_CREDENTIALS, + message, + e.getCredentialsInfo().getParameters() + ); } // The provided credentials are wrong @@ -76,11 +77,11 @@ public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { if (message == null) message = "Permission denied."; - throw new HTTPException(Response.Status.FORBIDDEN, new APICredentialError( - APICredentialError.Type.INVALID, - message, - e.getCredentialsInfo() - )); + throw new HTTPException( + APIError.Type.INVALID_CREDENTIALS, + message, + e.getCredentialsInfo().getParameters() + ); } // Generic permission denied @@ -91,10 +92,28 @@ public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { if (message == null) message = "Permission denied."; - throw new HTTPException(Response.Status.FORBIDDEN, message); + throw new HTTPException( + APIError.Type.PERMISSION_DENIED, + message + ); } + // Arbitrary resource not found + catch (GuacamoleResourceNotFoundException e) { + + // Generate default message + String message = e.getMessage(); + if (message == null) + message = "Not found."; + + throw new HTTPException( + APIError.Type.NOT_FOUND, + message + ); + + } + // Arbitrary bad requests catch (GuacamoleClientException e) { @@ -103,7 +122,10 @@ public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { if (message == null) message = "Invalid request."; - throw new HTTPException(Response.Status.BAD_REQUEST, message); + throw new HTTPException( + APIError.Type.BAD_REQUEST, + message + ); } @@ -116,7 +138,10 @@ public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { message = "Unexpected server error."; logger.debug("Unexpected exception in REST endpoint.", e); - throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, message); + throw new HTTPException( + APIError.Type.INTERNAL_ERROR, + message + ); } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java index b4d00d540..1f00556f9 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/HTTPException.java @@ -22,37 +22,64 @@ package org.glyptodon.guacamole.net.basic.rest; +import java.util.Collection; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import org.glyptodon.guacamole.form.Parameter; /** - * An exception that will result in the given HTTP Status and message or entity - * being returned from the API layer. - * + * An exception that will result in the given HTTP Status and error being + * returned from the API layer. All error messages have the same format which + * is defined by APIError. + * * @author James Muehlner + * @author Michael Jumper */ public class HTTPException extends WebApplicationException { - + /** - * Construct a new HTTPException with the given HTTP status and entity. - * - * @param status The HTTP Status to use for the response. - * @param entity The entity to use as the body of the response. + * Construct a new HTTPException with the given error. All information + * associated with this new exception will be extracted from the given + * APIError. + * + * @param error + * The error that occurred. */ - public HTTPException(Status status, Object entity) { - super(Response.status(status).entity(entity).build()); + public HTTPException(APIError error) { + super(Response.status(error.getType().getStatus()).entity(error).build()); } - + /** - * Construct a new HTTPException with the given HTTP status and message. The - * message will be wrapped in an APIError container. - * - * @param status The HTTP Status to use for the response. - * @param message The message to build the response entity with. + * Creates a new HTTPException with the given type and message. The + * corresponding APIError will be created from the provided information. + * + * @param type + * The type of error that occurred. + * + * @param message + * A human-readable message describing the error. */ - public HTTPException(Status status, String message) { - super(Response.status(status).entity(new APIError(message)).build()); + public HTTPException(APIError.Type type, String message) { + this(new APIError(type, message)); } - + + /** + * Creates a new HTTPException with the given type, message, and + * parameter information. The corresponding APIError will be created from + * the provided information. + * + * @param type + * The type of error that occurred. + * + * @param message + * A human-readable message describing the error. + * + * @param expected + * All parameters expected in the original request, or now required as + * a result of the original request. + */ + public HTTPException(APIError.Type type, String message, Collection expected) { + this(new APIError(type, message, expected)); + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java index af7d01b9e..059173776 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java @@ -35,7 +35,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response.Status; import javax.xml.bind.DatatypeConverter; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; @@ -44,6 +43,7 @@ import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.glyptodon.guacamole.net.basic.GuacamoleSession; +import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIRequest; import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.HTTPException; @@ -290,7 +290,7 @@ public class TokenRESTService { GuacamoleSession session = tokenSessionMap.remove(authToken); if (session == null) - throw new HTTPException(Status.NOT_FOUND, "No such token."); + throw new HTTPException(APIError.Type.NOT_FOUND, "No such token."); session.invalidate(); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java index 85f124476..8bae0cf27 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -39,8 +39,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; @@ -53,6 +51,7 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; +import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.remove; @@ -276,12 +275,12 @@ public class UserRESTService { // Validate data and path are sane if (!user.getUsername().equals(username)) - throw new HTTPException(Response.Status.BAD_REQUEST, + throw new HTTPException(APIError.Type.BAD_REQUEST, "Username in path does not match username provided JSON data."); // A user may not use this endpoint to modify himself if (userContext.self().getIdentifier().equals(user.getUsername())) { - throw new HTTPException(Response.Status.FORBIDDEN, + throw new HTTPException(APIError.Type.PERMISSION_DENIED, "Permission denied."); } @@ -336,7 +335,7 @@ public class UserRESTService { // Verify that the old password was correct if (authProvider.getUserContext(credentials) == null) { - throw new HTTPException(Response.Status.FORBIDDEN, + throw new HTTPException(APIError.Type.PERMISSION_DENIED, "Permission denied."); } @@ -467,7 +466,7 @@ public class UserRESTService { // Unsupported patch operation default: - throw new HTTPException(Status.BAD_REQUEST, + throw new HTTPException(APIError.Type.BAD_REQUEST, "Unsupported patch operation: \"" + operation + "\""); } @@ -586,7 +585,7 @@ public class UserRESTService { // Otherwise, the path is not supported else - throw new HTTPException(Status.BAD_REQUEST, "Unsupported patch path: \"" + path + "\""); + throw new HTTPException(APIError.Type.BAD_REQUEST, "Unsupported patch path: \"" + path + "\""); } // end for each patch operation diff --git a/guacamole/src/main/webapp/app/rest/types/Error.js b/guacamole/src/main/webapp/app/rest/types/Error.js new file mode 100644 index 000000000..fe2a599ff --- /dev/null +++ b/guacamole/src/main/webapp/app/rest/types/Error.js @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Service which defines the Error class. + */ +angular.module('rest').factory('Error', [function defineError() { + + /** + * The object returned by REST API calls when an error occurs. + * + * @constructor + * @param {Error|Object} [template={}] + * The object whose properties should be copied within the new + * Error. + */ + var Error = function Error(template) { + + // Use empty object by default + template = template || {}; + + /** + * A human-readable message describing the error that occurred. + * + * @type String + */ + this.message = template.message; + + /** + * The type string defining which values this parameter may contain, + * as well as what properties are applicable. Valid types are listed + * within Error.Type. + * + * @type String + * @default Error.Type.INTERNAL_ERROR + */ + this.type = template.type || Error.Type.INTERNAL_ERROR; + + /** + * Any parameters which were expected in the original request, or are + * now expected as a result of the original request, if any. If no + * such information is available, this will be null. + * + * @type Field[] + */ + this.expected = template.expected; + + }; + + /** + * All valid field types. + */ + Error.Type = { + + /** + * The requested operation could not be performed because the request + * itself was malformed. + * + * @type String + */ + BAD_REQUEST : 'BAD_REQUEST', + + /** + * The credentials provided were invalid. + * + * @type String + */ + INVALID_CREDENTIALS : 'INVALID_CREDENTIALS', + + /** + * The credentials provided were not necessarily invalid, but were not + * sufficient to determine validity. + * + * @type String + */ + INSUFFICIENT_CREDENTIALS : 'INSUFFICIENT_CREDENTIALS', + + /** + * An internal server error has occurred. + * + * @type String + */ + INTERNAL_ERROR : 'INTERNAL_ERROR', + + /** + * An object related to the request does not exist. + * + * @type String + */ + NOT_FOUND : 'NOT_FOUND', + + /** + * Permission was denied to perform the requested operation. + * + * @type String + */ + PERMISSION_DENIED : 'PERMISSION_DENIED' + + }; + + return Error; + +}]); \ No newline at end of file