Statistics
| Branch: | Tag: | Revision:

root / snf-django-lib / snf_django / lib / api / __init__.py @ 381a548c

History | View | Annotate | Download (10.5 kB)

1
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from functools import wraps
35
from traceback import format_exc
36
from time import time
37
from logging import getLogger
38
from wsgiref.handlers import format_date_time
39

    
40
from django.http import HttpResponse
41
from django.utils import cache
42
from django.utils import simplejson as json
43
from django.template.loader import render_to_string
44
from django.views.decorators import csrf
45

    
46
from astakosclient import AstakosClient
47
from astakosclient.errors import AstakosClientException
48
from django.conf import settings
49
from snf_django.lib.api import faults
50

    
51
import itertools
52

    
53
log = getLogger(__name__)
54

    
55

    
56
def get_token(request):
57
    """Get the Authentication Token of a request."""
58
    token = request.GET.get("X-Auth-Token", None)
59
    if not token:
60
        token = request.META.get("HTTP_X_AUTH_TOKEN", None)
61
    return token
62

    
63

    
64
def api_method(http_method=None, token_required=True, user_required=True,
65
               logger=None, format_allowed=True, astakos_auth_url=None,
66
               serializations=None, strict_serlization=False):
67
    """Decorator function for views that implement an API method."""
68
    if not logger:
69
        logger = log
70

    
71
    serializations = serializations or ['json', 'xml']
72

    
73
    def decorator(func):
74
        @wraps(func)
75
        def wrapper(request, *args, **kwargs):
76
            try:
77
                # Get the requested serialization format
78
                serialization = get_serialization(
79
                    request, format_allowed, serializations[0])
80

    
81
                # If guessed serialization is not supported, fallback to
82
                # the default serialization or return an API error in case
83
                # strict serialization flag is set.
84
                if not serialization in serializations:
85
                    if strict_serlization:
86
                        raise faults.BadRequest(("%s serialization not "
87
                                                "supported") % serialization)
88
                    serialization = serializations[0]
89
                request.serialization = serialization
90

    
91
                # Check HTTP method
92
                if http_method and request.method != http_method:
93
                    raise faults.NotAllowed("Method not allowed",
94
                                            allowed_methods=[http_method])
95

    
96
                # Get authentication token
97
                request.x_auth_token = None
98
                if token_required or user_required:
99
                    token = get_token(request)
100
                    if not token:
101
                        msg = "Access denied. No authentication token"
102
                        raise faults.Unauthorized(msg)
103
                    request.x_auth_token = token
104

    
105
                # Authenticate
106
                if user_required:
107
                    assert(token_required), "Can not get user without token"
108
                    astakos = astakos_auth_url or settings.ASTAKOS_AUTH_URL
109
                    astakos = AstakosClient(token, astakos,
110
                                            use_pool=True,
111
                                            retry=2,
112
                                            logger=logger)
113
                    user_info = astakos.authenticate()
114
                    request.user_uniq = user_info["access"]["user"]["id"]
115
                    request.user = user_info
116

    
117
                # Get the response object
118
                response = func(request, *args, **kwargs)
119

    
120
                # Fill in response variables
121
                update_response_headers(request, response)
122
                return response
123
            except faults.Fault, fault:
124
                if fault.code >= 500:
125
                    logger.exception("API ERROR")
126
                return render_fault(request, fault)
127
            except AstakosClientException as err:
128
                fault = faults.Fault(message=err.message,
129
                                     details=err.details,
130
                                     code=err.status)
131
                if fault.code >= 500:
132
                    logger.exception("Astakos ERROR")
133
                return render_fault(request, fault)
134
            except:
135
                logger.exception("Unexpected ERROR")
136
                fault = faults.InternalServerError("Unexpected error")
137
                return render_fault(request, fault)
138
        return csrf.csrf_exempt(wrapper)
139
    return decorator
140

    
141

    
142
def get_serialization(request, format_allowed=True,
143
                      default_serialization="json"):
144
    """Return the serialization format requested.
145

146
    Valid formats are 'json' and 'xml' and 'text'
147
    """
148

    
149
    if not format_allowed:
150
        return "text"
151

    
152
    # Try to get serialization from 'format' parameter
153
    _format = request.GET.get("format")
154
    if _format:
155
        if _format == "json":
156
            return "json"
157
        elif _format == "xml":
158
            return "xml"
159

    
160
    # Try to get serialization from path
161
    path = request.path
162
    if path.endswith(".json"):
163
        return "json"
164
    elif path.endswith(".xml"):
165
        return "xml"
166

    
167
    for item in request.META.get("HTTP_ACCEPT", "").split(","):
168
        accept, sep, rest = item.strip().partition(";")
169
        if accept == "application/json":
170
            return "json"
171
        elif accept == "application/xml":
172
            return "xml"
173

    
174
    return default_serialization
175

    
176

    
177
def update_response_headers(request, response):
178
    if not getattr(response, "override_serialization", False):
179
        serialization = request.serialization
180
        if serialization == "xml":
181
            response["Content-Type"] = "application/xml; charset=UTF-8"
182
        elif serialization == "json":
183
            response["Content-Type"] = "application/json; charset=UTF-8"
184
        elif serialization == "text":
185
            response["Content-Type"] = "text/plain; charset=UTF-8"
186
        else:
187
            raise ValueError("Unknown serialization format '%s'" %
188
                             serialization)
189

    
190
    if settings.DEBUG or getattr(settings, "TEST", False):
191
        response["Date"] = format_date_time(time())
192

    
193
    if not response.has_header("Content-Length"):
194
        _base_content_is_iter = getattr(response, '_base_content_is_iter',
195
                                        None)
196
        if (_base_content_is_iter is not None and not _base_content_is_iter):
197
            response["Content-Length"] = len(response.content)
198
        else:
199
            if not (response.has_header('Content-Type') and
200
                    response['Content-Type'].startswith(
201
                        'multipart/byteranges')):
202
                # save response content from been consumed if it is an iterator
203
                response._container, data = itertools.tee(response._container)
204
                response["Content-Length"] = len(str(data))
205

    
206
    cache.add_never_cache_headers(response)
207
    # Fix Vary and Cache-Control Headers. Issue: #3448
208
    cache.patch_vary_headers(response, ('X-Auth-Token',))
209
    cache.patch_cache_control(response, no_cache=True, no_store=True,
210
                              must_revalidate=True)
211

    
212

    
213
def render_fault(request, fault):
214
    """Render an API fault to an HTTP response."""
215
    # If running in debug mode add exception information to fault details
216
    if settings.DEBUG or getattr(settings, "TEST", False):
217
        fault.details = format_exc()
218

    
219
    try:
220
        serialization = request.serialization
221
    except AttributeError:
222
        request.serialization = "json"
223
        serialization = "json"
224

    
225
    # Serialize the fault data to xml or json
226
    if serialization == "xml":
227
        data = render_to_string("fault.xml", {"fault": fault})
228
    else:
229
        d = {fault.name: {"code": fault.code,
230
                          "message": fault.message,
231
                          "details": fault.details}}
232
        data = json.dumps(d)
233

    
234
    response = HttpResponse(data, status=fault.code)
235
    if response.status_code == 405 and hasattr(fault, 'allowed_methods'):
236
        response['Allow'] = ','.join(fault.allowed_methods)
237
    update_response_headers(request, response)
238
    return response
239

    
240

    
241
@api_method(token_required=False, user_required=False)
242
def api_endpoint_not_found(request):
243
    raise faults.BadRequest("API endpoint not found")
244

    
245

    
246
@api_method(token_required=False, user_required=False)
247
def api_method_not_allowed(request, allowed_methods):
248
    raise faults.NotAllowed("Method not allowed",
249
                            allowed_methods=allowed_methods)
250

    
251

    
252
def allow_jsonp(key='callback'):
253
    """
254
    Wrapper to enable jsonp responses.
255
    """
256
    def wrapper(func):
257
        def view_wrapper(request, *args, **kwargs):
258
            response = func(request, *args, **kwargs)
259
            if 'content-type' in response._headers and \
260
               response._headers['content-type'][1] == 'application/json':
261
                callback_name = request.GET.get(key, None)
262
                if callback_name:
263
                    response.content = "%s(%s)" % (callback_name,
264
                                                   response.content)
265
                    response._headers['content-type'] = ('Content-Type',
266
                                                         'text/javascript')
267
            return response
268
        return view_wrapper
269
    return wrapper