Commit ee570a7a authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Implement single decorator for handling API calls

Create a single decorator for API methods to be used be accross all
synnefo. This decorator does the following:

* Proper logging of 5xx faults and unexpected errors
* Authentication with Astakos using snf-astakos-client
* Sets proper HTTP response and cache control headers

Also move some common functions from various apps to
'snf_django.lib.api.utils'

Refs #3358 #3448
parent c977c625
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from functools import wraps
from traceback import format_exc
from time import time
from logging import getLogger
from wsgiref.handlers import format_date_time
from django.http import HttpResponse
from django.utils import cache
from django.utils import simplejson as json
from django.template.loader import render_to_string
from astakosclient import AstakosClient
from django.conf import settings
from snf_django.lib.api import faults
log = getLogger(__name__)
def get_token(request):
"""Get the Authentication Token of a request."""
token = request.GET.get("X-Auth-Token", None)
if not token:
token = request.META.get("HTTP_X_AUTH_TOKEN", None)
return token
def api_method(http_method=None, token_required=True, user_required=True,
logger=None, format_allowed=True):
"""Decorator function for views that implement an API method."""
if not logger:
logger = log
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
try:
# Get the requested serialization format
request.serialization = get_serialization(request,
format_allowed)
# Check HTTP method
if http_method and request.method != http_method:
raise faults.BadRequest("Method not allowed")
# Get authentication token
request.x_auth_token = None
if token_required or user_required:
token = get_token(request)
if not token:
msg = "Access denied. No authentication token"
raise faults.Unauthorized(msg)
request.x_auth_token = token
# Authenticate
if user_required:
assert(token_required), "Can not get user without token"
astakos = AstakosClient(settings.ASTAKOS_URL,
use_pool=True,
logger=logger)
user_info = astakos.get_user_info(token)
request.user_uniq = user_info["uuid"]
request.user = user_info
# Get the response object
response = func(request, *args, **kwargs)
# Fill in response variables
update_response_headers(request, response)
return response
except faults.Fault, fault:
if fault.code >= 500:
logger.exception("API ERROR")
return render_fault(request, fault)
except:
logger.exception("Unexpected ERROR")
fault = faults.InternalServerError("Unexpected ERROR")
return render_fault(request, fault)
return wrapper
return decorator
def get_serialization(request, format_allowed=True):
"""Return the serialization format requested.
Valid formats are 'json' and 'xml' and 'text'
"""
if not format_allowed:
return "text"
# Try to get serialization from 'format' parameter
_format = request.GET.get("format")
if _format:
if _format == "json":
return "json"
elif _format == "xml":
return "xml"
# Try to get serialization from path
path = request.path
if path.endswith(".json"):
return "json"
elif path.endswith(".xml"):
return "xml"
for item in request.META.get("HTTP_ACCEPT", "").split(","):
accept, sep, rest = item.strip().partition(";")
if accept == "application/json":
return "json"
elif accept == "applcation/xml":
return "xml"
return "json"
def update_response_headers(request, response):
if not response.has_header("Content-Type"):
serialization = request.serialization
if serialization == "xml":
response["Content-Type"] = "application/xml; charset=UTF-8"
elif serialization == "json":
response["Content-Type"] = "application/json; charset=UTF-8"
elif serialization == "text":
response["Content-Type"] = "text/plain; charset=UTF-8"
else:
raise ValueError("Unknown serialization format '%s'" %
serialization)
if settings.DEBUG or settings.TEST:
response["Date"] = format_date_time(time())
if not response.has_header("Content-Length"):
response["Content-Length"] = len(response.content)
cache.add_never_cache_headers(response)
# Fix Vary and Cache-Control Headers. Issue: #3448
cache.patch_vary_headers(response, ('X-Auth-Token',))
cache.patch_cache_control(response, no_cache=True, no_store=True,
must_revalidate=True)
def render_fault(request, fault):
"""Render an API fault to an HTTP response."""
# If running in debug mode add exception information to fault details
if settings.DEBUG or settings.TEST:
fault.details = format_exc()
try:
serialization = request.serialization
except AttributeError:
request.serialization = "json"
serialization = "json"
# Serialize the fault data to xml or json
if serialization == "xml":
data = render_to_string("fault.xml", {"fault": fault})
else:
d = {fault.name: {"code": fault.code,
"message": fault.message,
"details": fault.details}
}
data = json.dumps(d)
response = HttpResponse(data, status=fault.code)
update_response_headers(request, response)
return response
def not_found(request):
raise faults.BadRequest('Not found.')
def method_not_allowed(request):
raise faults.BadRequest('Method not allowed')
# Copyright 2011-2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import datetime
from dateutil.parser import parse as date_parse
from django.utils import simplejson as json
from django.conf import settings
from snf_django.lib.api import faults
class UTC(datetime.tzinfo):
"""
Helper UTC time information object.
"""
def utcoffset(self, dt):
return datetime.timedelta(0)
def tzname(self, dt):
return 'UTC'
def dst(self, dt):
return datetime.timedelta(0)
def isoformat(d):
"""Return an ISO8601 date string that includes a timezone.
>>> from datetime import datetime
>>> d = datetime(2012, 8, 10, 00, 59, 59)
>>> isoformat(d)
'2012-08-10T00:59:59+00:00'
"""
return d.replace(tzinfo=UTC()).isoformat()
def isoparse(s):
"""Parse an ISO8601 date string into a datetime object."""
if not s:
return None
try:
since = date_parse(s)
utc_since = since.astimezone(UTC()).replace(tzinfo=None)
except ValueError:
raise faults.BadRequest('Invalid changes-since parameter.')
now = datetime.datetime.now()
if utc_since > now:
raise faults.BadRequest('changes-since value set in the future.')
if now - utc_since > datetime.timedelta(seconds=settings.POLL_LIMIT):
raise faults.BadRequest('Too old changes-since value.')
return utc_since
def get_request_dict(request):
"""Return data sent by the client as python dictionary.
Only JSON format is supported
"""
data = request.raw_post_data
content_type = request.META.get("CONTENT_TYPE")
if content_type.startswith("application/json"):
try:
return json.loads(data)
except ValueError:
raise faults.BadRequest("Invalid JSON data")
else:
raise faults.BadRequest("Unsupported Content-type: '%s'", content_type)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment