Statistics
| Branch: | Tag: | Revision:

root / snf-django-lib / snf_django / lib / api / __init__.py @ 2aba7764

History | View | Annotate | Download (12.7 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
import sys
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
django_logger = getLogger("django.request")
55

    
56

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

    
64

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

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

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

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

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

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

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

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

    
121
                # Fill in response variables
122
                update_response_headers(request, response)
123
                return response
124
            except faults.Fault as fault:
125
                if fault.code >= 500:
126
                    django_logger.error("Unexpected API Error: %s",
127
                                        request.path,
128
                                        exc_info=sys.exc_info(),
129
                                        extra={
130
                                            "status_code": fault.code,
131
                                            "request": request})
132
                return render_fault(request, fault)
133
            except AstakosClientException as err:
134
                fault = faults.Fault(message=err.message,
135
                                     details=err.details,
136
                                     code=err.status)
137
                if fault.code >= 500:
138
                    django_logger.error("Unexpected AstakosClient Error: %s",
139
                                        request.path,
140
                                        exc_info=sys.exc_info(),
141
                                        extra={
142
                                            "status_code": fault.code,
143
                                            "request": request})
144
                return render_fault(request, fault)
145
            except:
146
                django_logger.error("Internal Server Error: %s", request.path,
147
                                    exc_info=sys.exc_info(),
148
                                    extra={
149
                                        "status_code": '500',
150
                                        "request": request})
151
                fault = faults.InternalServerError("Unexpected error")
152
                return render_fault(request, fault)
153
        return csrf.csrf_exempt(wrapper)
154
    return decorator
155

    
156

    
157
def get_serialization(request, format_allowed=True,
158
                      default_serialization="json"):
159
    """Return the serialization format requested.
160

161
    Valid formats are 'json' and 'xml' and 'text'
162
    """
163

    
164
    if not format_allowed:
165
        return "text"
166

    
167
    # Try to get serialization from 'format' parameter
168
    _format = request.GET.get("format")
169
    if _format:
170
        if _format == "json":
171
            return "json"
172
        elif _format == "xml":
173
            return "xml"
174

    
175
    # Try to get serialization from path
176
    path = request.path
177
    if path.endswith(".json"):
178
        return "json"
179
    elif path.endswith(".xml"):
180
        return "xml"
181

    
182
    for item in request.META.get("HTTP_ACCEPT", "").split(","):
183
        accept, sep, rest = item.strip().partition(";")
184
        if accept == "application/json":
185
            return "json"
186
        elif accept == "application/xml":
187
            return "xml"
188

    
189
    return default_serialization
190

    
191

    
192
def update_response_headers(request, response):
193
    if not getattr(response, "override_serialization", False):
194
        serialization = request.serialization
195
        if serialization == "xml":
196
            response["Content-Type"] = "application/xml; charset=UTF-8"
197
        elif serialization == "json":
198
            response["Content-Type"] = "application/json; charset=UTF-8"
199
        elif serialization == "text":
200
            response["Content-Type"] = "text/plain; charset=UTF-8"
201
        else:
202
            raise ValueError("Unknown serialization format '%s'" %
203
                             serialization)
204

    
205
    if settings.DEBUG or getattr(settings, "TEST", False):
206
        response["Date"] = format_date_time(time())
207

    
208
    if not response.has_header("Content-Length"):
209
        _base_content_is_iter = getattr(response, '_base_content_is_iter',
210
                                        None)
211
        if (_base_content_is_iter is not None and not _base_content_is_iter):
212
            response["Content-Length"] = len(response.content)
213
        else:
214
            if not (response.has_header('Content-Type') and
215
                    response['Content-Type'].startswith(
216
                        'multipart/byteranges')):
217
                # save response content from been consumed if it is an iterator
218
                response._container, data = itertools.tee(response._container)
219
                response["Content-Length"] = len(str(data))
220

    
221
    cache.add_never_cache_headers(response)
222
    # Fix Vary and Cache-Control Headers. Issue: #3448
223
    cache.patch_vary_headers(response, ('X-Auth-Token',))
224
    cache.patch_cache_control(response, no_cache=True, no_store=True,
225
                              must_revalidate=True)
226

    
227

    
228
def render_fault(request, fault):
229
    """Render an API fault to an HTTP response."""
230
    # If running in debug mode add exception information to fault details
231
    if settings.DEBUG or getattr(settings, "TEST", False):
232
        fault.details = format_exc()
233

    
234
    try:
235
        serialization = request.serialization
236
    except AttributeError:
237
        request.serialization = "json"
238
        serialization = "json"
239

    
240
    # Serialize the fault data to xml or json
241
    if serialization == "xml":
242
        data = render_to_string("fault.xml", {"fault": fault})
243
    else:
244
        d = {fault.name: {"code": fault.code,
245
                          "message": fault.message,
246
                          "details": fault.details}}
247
        data = json.dumps(d)
248

    
249
    response = HttpResponse(data, status=fault.code)
250
    if response.status_code == 405 and hasattr(fault, 'allowed_methods'):
251
        response['Allow'] = ','.join(fault.allowed_methods)
252
    update_response_headers(request, response)
253
    return response
254

    
255

    
256
@api_method(token_required=False, user_required=False)
257
def api_endpoint_not_found(request):
258
    raise faults.BadRequest("API endpoint not found")
259

    
260

    
261
@api_method(token_required=False, user_required=False)
262
def api_method_not_allowed(request, allowed_methods):
263
    raise faults.NotAllowed("Method not allowed",
264
                            allowed_methods=allowed_methods)
265

    
266

    
267
def allow_jsonp(key='callback'):
268
    """
269
    Wrapper to enable jsonp responses.
270
    """
271
    def wrapper(func):
272
        @wraps(func)
273
        def view_wrapper(request, *args, **kwargs):
274
            response = func(request, *args, **kwargs)
275
            if 'content-type' in response._headers and \
276
               response._headers['content-type'][1] == 'application/json':
277
                callback_name = request.GET.get(key, None)
278
                if callback_name:
279
                    response.content = "%s(%s)" % (callback_name,
280
                                                   response.content)
281
                    response._headers['content-type'] = ('Content-Type',
282
                                                         'text/javascript')
283
            return response
284
        return view_wrapper
285
    return wrapper
286

    
287

    
288
def user_in_groups(permitted_groups, logger=None):
289
    """Check that the request user belongs to one of permitted groups.
290

291
    Django view wrapper to check that the already identified request user
292
    belongs to one of the allowed groups.
293

294
    """
295
    if not logger:
296
        logger = log
297

    
298
    def decorator(func):
299
        @wraps(func)
300
        def wrapper(request, *args, **kwargs):
301
            if hasattr(request, "user") and request.user is not None:
302
                groups = request.user["access"]["user"]["roles"]
303
                groups = [g["name"] for g in groups]
304
            else:
305
                raise faults.Forbidden
306

    
307
            common_groups = set(groups) & set(permitted_groups)
308

    
309
            if not common_groups:
310
                msg = ("Not allowing access to '%s' by user '%s'. User does"
311
                       " not belong to a valid group. User groups: %s,"
312
                       " Required groups %s"
313
                       % (request.path, request.user, groups,
314
                          permitted_groups))
315
                logger.error(msg)
316
                raise faults.Forbidden
317

    
318
            logger.info("User '%s' in groups '%s' accessed view '%s'",
319
                        request.user_uniq, groups, request.path)
320

    
321
            return func(request, *args, **kwargs)
322
        return wrapper
323
    return decorator