From 00cb384e8e65e7b046cbf35e103001350160ba1f Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Thu, 5 Jul 2012 21:53:36 +0100 Subject: [PATCH] heat API : Return auth errors in AWS format Update EC2 authentication code to return HeatAPIException subclasses, so the API error response on auth failure is aligned with AWS responses. ref #125 Change-Id: Iafa33e7aed4d77f2255b4d879192b9d15a2395aa Signed-off-by: Steven Hardy --- heat/api/v1/__init__.py | 39 +++++++++++++++++++++++++++++++-------- heat/api/v1/exception.py | 34 ++++++++++++++++++++++++++++++++++ heat/common/wsgi.py | 14 +++++++++----- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/heat/api/v1/__init__.py b/heat/api/v1/__init__.py index 8f0efbd4..e5f1d117 100644 --- a/heat/api/v1/__init__.py +++ b/heat/api/v1/__init__.py @@ -29,6 +29,7 @@ from webob import Request import webob from heat import utils from heat.common import context +from heat.api.v1 import exception logger = logging.getLogger(__name__) @@ -43,17 +44,28 @@ class EC2Token(wsgi.Middleware): @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): # Read request signature and access id. + # If we find KeyStoneCreds in the params we ignore a key error + # here so that we can use both authentication methods. + # Returning here just means the user didn't supply AWS + # authentication and we'll let the app try native keystone next. logger.info("Checking AWS credentials..") try: signature = req.params['Signature'] + except KeyError: + logger.info("No AWS Signature found.") + if 'KeyStoneCreds' in req.params: + return self.application + else: + raise exception.HeatIncompleteSignatureError() + + try: access = req.params['AWSAccessKeyId'] except KeyError: - # We ignore a key error here so that we can use both - # authentication methods. Returning here just means - # the user didn't supply AWS authentication and we'll let - # the app try native keystone next. - logger.info("No AWS credentials found.") - return self.application + logger.info("No AWSAccessKeyId found.") + if 'KeyStoneCreds' in req.params: + return self.application + else: + raise exception.HeatMissingAuthenticationTokenError() logger.info("AWS credentials found, checking against keystone.") # Make a copy of args for authentication and signature verification. @@ -99,9 +111,20 @@ class EC2Token(wsgi.Middleware): token_id = result['access']['token']['id'] logger.info("AWS authentication successful.") except (AttributeError, KeyError): - # FIXME: Should be 404 I think. logger.info("AWS authentication failure.") - raise webob.exc.HTTPBadRequest() + # Try to extract the reason for failure so we can return the + # appropriate AWS error via raising an exception + try: + reason = result['error']['message'] + except KeyError: + reason = None + + if reason == "EC2 access key not found.": + raise exception.HeatInvalidClientTokenIdError() + elif reason == "EC2 signature not supplied.": + raise exception.HeatSignatureError() + else: + raise exception.HeatAccessDeniedError() # Authenticated! req.headers['X-Auth-EC2-Creds'] = creds_json diff --git a/heat/api/v1/exception.py b/heat/api/v1/exception.py index c643a763..247a7f3f 100644 --- a/heat/api/v1/exception.py +++ b/heat/api/v1/exception.py @@ -18,6 +18,7 @@ """Heat API exception subclasses - maps API response errors to AWS Errors""" import webob.exc +from heat.common import wsgi class HeatAPIException(webob.exc.HTTPError): @@ -32,6 +33,17 @@ class HeatAPIException(webob.exc.HTTPError): explanation = "Generic HeatAPIException, please use specific subclasses!" err_type = "Sender" + def __init__(self, detail=None): + ''' + Overload HTTPError constructor, so we can create a default serialized + body. This is required because not all error responses are processed + by the wsgi controller (ie auth errors, which are further up the + paste pipeline. We serialize in XML by default (as AWS does) + ''' + webob.exc.HTTPError.__init__(self, detail=detail) + serializer = wsgi.XMLResponseSerializer() + serializer.default(self, self.get_unserialized_body()) + def get_unserialized_body(self): ''' Return a dict suitable for serialization in the wsgi controller @@ -190,3 +202,25 @@ class HeatThrottlingError(HeatAPIException): code = 400 title = "Throttling" explanation = "Request was denied due to request throttling" + + +# Not documented in the AWS docs, authentication failure errors +class HeatAccessDeniedError(HeatAPIException): + ''' + This is the response given when authentication fails due to user + IAM group memberships meaning we deny access + ''' + code = 403 + title = "AccessDenied" + explanation = "User is not authorized to perform action" + + +class HeatSignatureError(HeatAPIException): + ''' + This is the response given when authentication fails due to + a bad signature + ''' + code = 403 + title = "SignatureDoesNotMatch" + explanation = ("The request signature we calculated does not match the " + + "signature you provided") diff --git a/heat/common/wsgi.py b/heat/common/wsgi.py index a29f2956..07924f93 100644 --- a/heat/common/wsgi.py +++ b/heat/common/wsgi.py @@ -546,14 +546,18 @@ class Resource(object): # Here we should get API exceptions derived from HeatAPIException # these implement get_unserialized_body(), which allow us to get # a dict containing the unserialized error response. + # We only need to serialize for JSON content_type, as the + # exception body is pre-serialized to the default XML in the + # HeatAPIException constructor # If we get something else here (e.g a webob.exc exception), # this will fail, and we just return it without serializing, # which will not conform to the expected AWS error response format - try: - err_body = action_result.get_unserialized_body() - serializer.default(action_result, err_body) - except: - logging.warning("Unable to serialize exception response") + if content_type == "JSON": + try: + err_body = action_result.get_unserialized_body() + serializer.default(action_result, err_body) + except: + logging.warning("Unable to serialize exception response") return action_result -- 2.45.2