commit 72bf812df963e0191cc0a03648c390e9f82fe458
Author: Christos Stavrakakis <cstavr@grnet.gr>
Date:   Fri Apr 5 19:29:04 2013 +0300

    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

diff --git a/snf-django-lib/snf_django/lib/__init__.py b/snf-django-lib/snf_django/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/snf-django-lib/snf_django/lib/api/__init__.py b/snf-django-lib/snf_django/lib/api/__init__.py
new file mode 100644
index 0000000..87a9b90
--- /dev/null
+++ b/snf-django-lib/snf_django/lib/api/__init__.py
@@ -0,0 +1,208 @@
+# 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')
diff --git a/snf-django-lib/snf_django/lib/api/utils.py b/snf-django-lib/snf_django/lib/api/utils.py
new file mode 100644
index 0000000..904af6f
--- /dev/null
+++ b/snf-django-lib/snf_django/lib/api/utils.py
@@ -0,0 +1,105 @@
+# 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)
