GUAC-1161: Generalize APICredentialError into APIError. Provide consistent error responses for all REST endpoints.

This commit is contained in:
Michael Jumper
2015-04-20 14:43:55 -07:00
parent c80a203e97
commit 74883ae121
7 changed files with 358 additions and 157 deletions

View File

@@ -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;
}
}

View File

@@ -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<Parameter> 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<Parameter> 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<Parameter> 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;
}
}

View File

@@ -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
);
}

View File

@@ -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<Parameter> expected) {
this(new APIError(type, message, expected));
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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;
}]);