Statistics
| Branch: | Tag: | Revision:

root / snf-django-lib / snf_django / lib / api / __init__.py @ 981d3b0d

History | View | Annotate | Download (10 kB)

1 72bf812d Christos Stavrakakis
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2 72bf812d Christos Stavrakakis
#
3 72bf812d Christos Stavrakakis
# Redistribution and use in source and binary forms, with or
4 72bf812d Christos Stavrakakis
# without modification, are permitted provided that the following
5 72bf812d Christos Stavrakakis
# conditions are met:
6 72bf812d Christos Stavrakakis
#
7 72bf812d Christos Stavrakakis
#   1. Redistributions of source code must retain the above
8 72bf812d Christos Stavrakakis
#      copyright notice, this list of conditions and the following
9 72bf812d Christos Stavrakakis
#      disclaimer.
10 72bf812d Christos Stavrakakis
#
11 72bf812d Christos Stavrakakis
#   2. Redistributions in binary form must reproduce the above
12 72bf812d Christos Stavrakakis
#      copyright notice, this list of conditions and the following
13 72bf812d Christos Stavrakakis
#      disclaimer in the documentation and/or other materials
14 72bf812d Christos Stavrakakis
#      provided with the distribution.
15 72bf812d Christos Stavrakakis
#
16 72bf812d Christos Stavrakakis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 72bf812d Christos Stavrakakis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 72bf812d Christos Stavrakakis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 72bf812d Christos Stavrakakis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 72bf812d Christos Stavrakakis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 72bf812d Christos Stavrakakis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 72bf812d Christos Stavrakakis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 72bf812d Christos Stavrakakis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 72bf812d Christos Stavrakakis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 72bf812d Christos Stavrakakis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 72bf812d Christos Stavrakakis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 72bf812d Christos Stavrakakis
# POSSIBILITY OF SUCH DAMAGE.
28 72bf812d Christos Stavrakakis
#
29 72bf812d Christos Stavrakakis
# The views and conclusions contained in the software and
30 72bf812d Christos Stavrakakis
# documentation are those of the authors and should not be
31 72bf812d Christos Stavrakakis
# interpreted as representing official policies, either expressed
32 72bf812d Christos Stavrakakis
# or implied, of GRNET S.A.
33 72bf812d Christos Stavrakakis
34 72bf812d Christos Stavrakakis
from functools import wraps
35 72bf812d Christos Stavrakakis
from traceback import format_exc
36 72bf812d Christos Stavrakakis
from time import time
37 72bf812d Christos Stavrakakis
from logging import getLogger
38 72bf812d Christos Stavrakakis
from wsgiref.handlers import format_date_time
39 72bf812d Christos Stavrakakis
40 72bf812d Christos Stavrakakis
from django.http import HttpResponse
41 72bf812d Christos Stavrakakis
from django.utils import cache
42 72bf812d Christos Stavrakakis
from django.utils import simplejson as json
43 72bf812d Christos Stavrakakis
from django.template.loader import render_to_string
44 81e19e70 Kostas Papadimitriou
from django.views.decorators import csrf
45 72bf812d Christos Stavrakakis
46 72bf812d Christos Stavrakakis
from astakosclient import AstakosClient
47 6c1c0738 Ilias Tsitsimpis
from astakosclient.errors import AstakosClientException
48 72bf812d Christos Stavrakakis
from django.conf import settings
49 72bf812d Christos Stavrakakis
from snf_django.lib.api import faults
50 72bf812d Christos Stavrakakis
51 981d3b0d Sofia Papagiannaki
import itertools
52 72bf812d Christos Stavrakakis
53 72bf812d Christos Stavrakakis
log = getLogger(__name__)
54 72bf812d Christos Stavrakakis
55 72bf812d Christos Stavrakakis
56 72bf812d Christos Stavrakakis
def get_token(request):
57 72bf812d Christos Stavrakakis
    """Get the Authentication Token of a request."""
58 72bf812d Christos Stavrakakis
    token = request.GET.get("X-Auth-Token", None)
59 72bf812d Christos Stavrakakis
    if not token:
60 72bf812d Christos Stavrakakis
        token = request.META.get("HTTP_X_AUTH_TOKEN", None)
61 72bf812d Christos Stavrakakis
    return token
62 72bf812d Christos Stavrakakis
63 72bf812d Christos Stavrakakis
64 72bf812d Christos Stavrakakis
def api_method(http_method=None, token_required=True, user_required=True,
65 6b560707 Sofia Papagiannaki
               logger=None, format_allowed=True, astakos_url=None,
66 d0d9a3f5 Kostas Papadimitriou
               serializations=None, strict_serlization=False):
67 72bf812d Christos Stavrakakis
    """Decorator function for views that implement an API method."""
68 72bf812d Christos Stavrakakis
    if not logger:
69 72bf812d Christos Stavrakakis
        logger = log
70 72bf812d Christos Stavrakakis
71 d0d9a3f5 Kostas Papadimitriou
    serializations = serializations or ['json', 'xml']
72 d0d9a3f5 Kostas Papadimitriou
73 72bf812d Christos Stavrakakis
    def decorator(func):
74 72bf812d Christos Stavrakakis
        @wraps(func)
75 72bf812d Christos Stavrakakis
        def wrapper(request, *args, **kwargs):
76 72bf812d Christos Stavrakakis
            try:
77 72bf812d Christos Stavrakakis
                # Get the requested serialization format
78 d0d9a3f5 Kostas Papadimitriou
                serialization = get_serialization(
79 f9662798 Kostas Papadimitriou
                    request, format_allowed, serializations[0])
80 d0d9a3f5 Kostas Papadimitriou
81 d0d9a3f5 Kostas Papadimitriou
                # If guessed serialization is not supported, fallback to
82 d0d9a3f5 Kostas Papadimitriou
                # the default serialization or return an API error in case
83 d0d9a3f5 Kostas Papadimitriou
                # strict serialization flag is set.
84 d0d9a3f5 Kostas Papadimitriou
                if not serialization in serializations:
85 d0d9a3f5 Kostas Papadimitriou
                    if strict_serlization:
86 d0d9a3f5 Kostas Papadimitriou
                        raise faults.BadRequest(("%s serialization not "
87 d0d9a3f5 Kostas Papadimitriou
                                                "supported") % serialization)
88 d0d9a3f5 Kostas Papadimitriou
                    serialization = serializations[0]
89 d0d9a3f5 Kostas Papadimitriou
                request.serialization = serialization
90 72bf812d Christos Stavrakakis
91 72bf812d Christos Stavrakakis
                # Check HTTP method
92 72bf812d Christos Stavrakakis
                if http_method and request.method != http_method:
93 72bf812d Christos Stavrakakis
                    raise faults.BadRequest("Method not allowed")
94 72bf812d Christos Stavrakakis
95 72bf812d Christos Stavrakakis
                # Get authentication token
96 72bf812d Christos Stavrakakis
                request.x_auth_token = None
97 72bf812d Christos Stavrakakis
                if token_required or user_required:
98 72bf812d Christos Stavrakakis
                    token = get_token(request)
99 72bf812d Christos Stavrakakis
                    if not token:
100 72bf812d Christos Stavrakakis
                        msg = "Access denied. No authentication token"
101 72bf812d Christos Stavrakakis
                        raise faults.Unauthorized(msg)
102 72bf812d Christos Stavrakakis
                    request.x_auth_token = token
103 72bf812d Christos Stavrakakis
104 72bf812d Christos Stavrakakis
                # Authenticate
105 72bf812d Christos Stavrakakis
                if user_required:
106 72bf812d Christos Stavrakakis
                    assert(token_required), "Can not get user without token"
107 e3ff6830 Georgios D. Tsoukalas
                    astakos = astakos_url or settings.ASTAKOS_BASE_URL
108 726cb37f Ilias Tsitsimpis
                    astakos = AstakosClient(astakos,
109 726cb37f Ilias Tsitsimpis
                                            use_pool=True,
110 f089ffca Christos Stavrakakis
                                            retry=2,
111 726cb37f Ilias Tsitsimpis
                                            logger=logger)
112 726cb37f Ilias Tsitsimpis
                    user_info = astakos.get_user_info(token)
113 72bf812d Christos Stavrakakis
                    request.user_uniq = user_info["uuid"]
114 72bf812d Christos Stavrakakis
                    request.user = user_info
115 72bf812d Christos Stavrakakis
116 72bf812d Christos Stavrakakis
                # Get the response object
117 72bf812d Christos Stavrakakis
                response = func(request, *args, **kwargs)
118 72bf812d Christos Stavrakakis
119 72bf812d Christos Stavrakakis
                # Fill in response variables
120 72bf812d Christos Stavrakakis
                update_response_headers(request, response)
121 72bf812d Christos Stavrakakis
                return response
122 72bf812d Christos Stavrakakis
            except faults.Fault, fault:
123 72bf812d Christos Stavrakakis
                if fault.code >= 500:
124 72bf812d Christos Stavrakakis
                    logger.exception("API ERROR")
125 72bf812d Christos Stavrakakis
                return render_fault(request, fault)
126 726cb37f Ilias Tsitsimpis
            except AstakosClientException as err:
127 726cb37f Ilias Tsitsimpis
                fault = faults.Fault(message=err.message,
128 726cb37f Ilias Tsitsimpis
                                     details=err.details,
129 726cb37f Ilias Tsitsimpis
                                     code=err.status)
130 726cb37f Ilias Tsitsimpis
                if fault.code >= 500:
131 255cef57 Christos Stavrakakis
                    logger.exception("Astakos ERROR")
132 726cb37f Ilias Tsitsimpis
                return render_fault(request, fault)
133 72bf812d Christos Stavrakakis
            except:
134 72bf812d Christos Stavrakakis
                logger.exception("Unexpected ERROR")
135 499d9bfe Christos Stavrakakis
                fault = faults.InternalServerError("Unexpected error")
136 72bf812d Christos Stavrakakis
                return render_fault(request, fault)
137 81e19e70 Kostas Papadimitriou
        return csrf.csrf_exempt(wrapper)
138 72bf812d Christos Stavrakakis
    return decorator
139 72bf812d Christos Stavrakakis
140 72bf812d Christos Stavrakakis
141 6b560707 Sofia Papagiannaki
def get_serialization(request, format_allowed=True, default_serialization="json"):
142 72bf812d Christos Stavrakakis
    """Return the serialization format requested.
143 72bf812d Christos Stavrakakis

144 72bf812d Christos Stavrakakis
    Valid formats are 'json' and 'xml' and 'text'
145 72bf812d Christos Stavrakakis
    """
146 72bf812d Christos Stavrakakis
147 72bf812d Christos Stavrakakis
    if not format_allowed:
148 72bf812d Christos Stavrakakis
        return "text"
149 72bf812d Christos Stavrakakis
150 72bf812d Christos Stavrakakis
    # Try to get serialization from 'format' parameter
151 72bf812d Christos Stavrakakis
    _format = request.GET.get("format")
152 72bf812d Christos Stavrakakis
    if _format:
153 72bf812d Christos Stavrakakis
        if _format == "json":
154 72bf812d Christos Stavrakakis
            return "json"
155 72bf812d Christos Stavrakakis
        elif _format == "xml":
156 72bf812d Christos Stavrakakis
            return "xml"
157 72bf812d Christos Stavrakakis
158 72bf812d Christos Stavrakakis
    # Try to get serialization from path
159 72bf812d Christos Stavrakakis
    path = request.path
160 72bf812d Christos Stavrakakis
    if path.endswith(".json"):
161 72bf812d Christos Stavrakakis
        return "json"
162 72bf812d Christos Stavrakakis
    elif path.endswith(".xml"):
163 72bf812d Christos Stavrakakis
        return "xml"
164 72bf812d Christos Stavrakakis
165 72bf812d Christos Stavrakakis
    for item in request.META.get("HTTP_ACCEPT", "").split(","):
166 72bf812d Christos Stavrakakis
        accept, sep, rest = item.strip().partition(";")
167 72bf812d Christos Stavrakakis
        if accept == "application/json":
168 72bf812d Christos Stavrakakis
            return "json"
169 c50825bf Sofia Papagiannaki
        elif accept == "application/xml":
170 72bf812d Christos Stavrakakis
            return "xml"
171 72bf812d Christos Stavrakakis
172 6b560707 Sofia Papagiannaki
    return default_serialization
173 72bf812d Christos Stavrakakis
174 72bf812d Christos Stavrakakis
175 72bf812d Christos Stavrakakis
def update_response_headers(request, response):
176 b698d39d Christos Stavrakakis
    if not getattr(response, "override_serialization", False):
177 72bf812d Christos Stavrakakis
        serialization = request.serialization
178 72bf812d Christos Stavrakakis
        if serialization == "xml":
179 72bf812d Christos Stavrakakis
            response["Content-Type"] = "application/xml; charset=UTF-8"
180 72bf812d Christos Stavrakakis
        elif serialization == "json":
181 72bf812d Christos Stavrakakis
            response["Content-Type"] = "application/json; charset=UTF-8"
182 72bf812d Christos Stavrakakis
        elif serialization == "text":
183 72bf812d Christos Stavrakakis
            response["Content-Type"] = "text/plain; charset=UTF-8"
184 72bf812d Christos Stavrakakis
        else:
185 72bf812d Christos Stavrakakis
            raise ValueError("Unknown serialization format '%s'" %
186 72bf812d Christos Stavrakakis
                             serialization)
187 72bf812d Christos Stavrakakis
188 47ef53d5 Christos Stavrakakis
    if settings.DEBUG or getattr(settings, "TEST", False):
189 72bf812d Christos Stavrakakis
        response["Date"] = format_date_time(time())
190 72bf812d Christos Stavrakakis
191 72bf812d Christos Stavrakakis
    if not response.has_header("Content-Length"):
192 981d3b0d Sofia Papagiannaki
        if response._is_string:
193 981d3b0d Sofia Papagiannaki
            response["Content-Length"] = len(response.content)
194 981d3b0d Sofia Papagiannaki
        else:
195 981d3b0d Sofia Papagiannaki
            if not (response.has_header('Content-Type') and
196 981d3b0d Sofia Papagiannaki
                    response['Content-Type'].startswith(
197 981d3b0d Sofia Papagiannaki
                        'multipart/byteranges')):
198 981d3b0d Sofia Papagiannaki
                # save response content from been consumed if it is an iterator
199 981d3b0d Sofia Papagiannaki
                response._container, data = itertools.tee(response._container)
200 981d3b0d Sofia Papagiannaki
                response["Content-Length"] = len(str(data))
201 72bf812d Christos Stavrakakis
202 72bf812d Christos Stavrakakis
    cache.add_never_cache_headers(response)
203 72bf812d Christos Stavrakakis
    # Fix Vary and Cache-Control Headers. Issue: #3448
204 72bf812d Christos Stavrakakis
    cache.patch_vary_headers(response, ('X-Auth-Token',))
205 72bf812d Christos Stavrakakis
    cache.patch_cache_control(response, no_cache=True, no_store=True,
206 72bf812d Christos Stavrakakis
                              must_revalidate=True)
207 72bf812d Christos Stavrakakis
208 72bf812d Christos Stavrakakis
209 72bf812d Christos Stavrakakis
def render_fault(request, fault):
210 72bf812d Christos Stavrakakis
    """Render an API fault to an HTTP response."""
211 72bf812d Christos Stavrakakis
    # If running in debug mode add exception information to fault details
212 47ef53d5 Christos Stavrakakis
    if settings.DEBUG or getattr(settings, "TEST", False):
213 72bf812d Christos Stavrakakis
        fault.details = format_exc()
214 72bf812d Christos Stavrakakis
215 72bf812d Christos Stavrakakis
    try:
216 72bf812d Christos Stavrakakis
        serialization = request.serialization
217 72bf812d Christos Stavrakakis
    except AttributeError:
218 72bf812d Christos Stavrakakis
        request.serialization = "json"
219 72bf812d Christos Stavrakakis
        serialization = "json"
220 72bf812d Christos Stavrakakis
221 72bf812d Christos Stavrakakis
    # Serialize the fault data to xml or json
222 72bf812d Christos Stavrakakis
    if serialization == "xml":
223 72bf812d Christos Stavrakakis
        data = render_to_string("fault.xml", {"fault": fault})
224 72bf812d Christos Stavrakakis
    else:
225 72bf812d Christos Stavrakakis
        d = {fault.name: {"code": fault.code,
226 72bf812d Christos Stavrakakis
                          "message": fault.message,
227 93c6900c Ilias Tsitsimpis
                          "details": fault.details}}
228 72bf812d Christos Stavrakakis
        data = json.dumps(d)
229 72bf812d Christos Stavrakakis
230 72bf812d Christos Stavrakakis
    response = HttpResponse(data, status=fault.code)
231 72bf812d Christos Stavrakakis
    update_response_headers(request, response)
232 72bf812d Christos Stavrakakis
    return response
233 72bf812d Christos Stavrakakis
234 72bf812d Christos Stavrakakis
235 269ac8de Christos Stavrakakis
@api_method(token_required=False, user_required=False)
236 269ac8de Christos Stavrakakis
def api_endpoint_not_found(request):
237 269ac8de Christos Stavrakakis
    raise faults.BadRequest("API endpoint not found")
238 72bf812d Christos Stavrakakis
239 72bf812d Christos Stavrakakis
240 269ac8de Christos Stavrakakis
@api_method(token_required=False, user_required=False)
241 269ac8de Christos Stavrakakis
def api_method_not_allowed(request):
242 72bf812d Christos Stavrakakis
    raise faults.BadRequest('Method not allowed')
243 fced411d Kostas Papadimitriou
244 fced411d Kostas Papadimitriou
245 fced411d Kostas Papadimitriou
def allow_jsonp(key='callback'):
246 fced411d Kostas Papadimitriou
    """
247 fced411d Kostas Papadimitriou
    Wrapper to enable jsonp responses.
248 fced411d Kostas Papadimitriou
    """
249 fced411d Kostas Papadimitriou
    def wrapper(func):
250 fced411d Kostas Papadimitriou
        def view_wrapper(request, *args, **kwargs):
251 fced411d Kostas Papadimitriou
            response = func(request, *args, **kwargs)
252 fced411d Kostas Papadimitriou
            if 'content-type' in response._headers and \
253 fced411d Kostas Papadimitriou
               response._headers['content-type'][1] == 'application/json':
254 fced411d Kostas Papadimitriou
                callback_name = request.GET.get(key, None)
255 fced411d Kostas Papadimitriou
                if callback_name:
256 fced411d Kostas Papadimitriou
                    response.content = "%s(%s)" % (callback_name,
257 fced411d Kostas Papadimitriou
                                                   response.content)
258 fced411d Kostas Papadimitriou
                    response._headers['content-type'] = ('Content-Type',
259 fced411d Kostas Papadimitriou
                                                         'text/javascript')
260 fced411d Kostas Papadimitriou
            return response
261 fced411d Kostas Papadimitriou
        return view_wrapper
262 fced411d Kostas Papadimitriou
    return wrapper