__init__.py 13.3 KB
Newer Older
1
# Copyright 2012-2014 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
# 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.
33
import sys
34
35
36
37
38
39
40
41
42
43
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
44
from django.views.decorators import csrf
45
46

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

51
import itertools
52
53

log = getLogger(__name__)
54
django_logger = getLogger("django.request")
55
56
57
58
59
60
61
62
63
64
65


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,
66
               logger=None, format_allowed=True, astakos_auth_url=None,
67
               serializations=None, strict_serlization=False):
68
69
70
71
    """Decorator function for views that implement an API method."""
    if not logger:
        logger = log

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

74
75
76
77
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            try:
78
79
80
81
82
                # Explicitly set request encoding to UTF-8 instead of relying
                # to the DEFAULT_CHARSET setting. See:
                # https://docs.djangoproject.com/en/1.4/ref/unicode/#form-submission
                request.encoding = 'utf-8'

83
                # Get the requested serialization format
84
                serialization = get_serialization(
85
                    request, format_allowed, serializations[0])
86
87
88
89
90
91
92
93
94
95

                # If guessed serialization is not supported, fallback to
                # the default serialization or return an API error in case
                # strict serialization flag is set.
                if not serialization in serializations:
                    if strict_serlization:
                        raise faults.BadRequest(("%s serialization not "
                                                "supported") % serialization)
                    serialization = serializations[0]
                request.serialization = serialization
96
97
98

                # Check HTTP method
                if http_method and request.method != http_method:
99
100
                    raise faults.NotAllowed("Method not allowed",
                                            allowed_methods=[http_method])
101
102
103
104
105
106
107
108
109
110
111
112
113

                # 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"
114
115
116
117
118
119
120
121
122
                    astakos_url = astakos_auth_url
                    if astakos_url is None:
                        try:
                            astakos_url = settings.ASTAKOS_AUTH_URL
                        except AttributeError:
                            logger.error("Cannot authenticate without having"
                                         " an Astakos Authentication URL")
                            raise
                    astakos = AstakosClient(token, astakos_url,
123
                                            use_pool=True,
124
                                            retry=2,
125
                                            logger=logger)
126
127
                    user_info = astakos.authenticate()
                    request.user_uniq = user_info["access"]["user"]["id"]
128
129
130
131
132
133
134
135
                    request.user = user_info

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

                # Fill in response variables
                update_response_headers(request, response)
                return response
136
            except faults.Fault as fault:
137
                if fault.code >= 500:
138
139
140
141
142
143
                    django_logger.error("Unexpected API Error: %s",
                                        request.path,
                                        exc_info=sys.exc_info(),
                                        extra={
                                            "status_code": fault.code,
                                            "request": request})
144
                return render_fault(request, fault)
145
146
147
148
149
            except AstakosClientException as err:
                fault = faults.Fault(message=err.message,
                                     details=err.details,
                                     code=err.status)
                if fault.code >= 500:
150
151
152
153
154
155
                    django_logger.error("Unexpected AstakosClient Error: %s",
                                        request.path,
                                        exc_info=sys.exc_info(),
                                        extra={
                                            "status_code": fault.code,
                                            "request": request})
156
                return render_fault(request, fault)
157
            except:
158
159
160
161
162
                django_logger.error("Internal Server Error: %s", request.path,
                                    exc_info=sys.exc_info(),
                                    extra={
                                        "status_code": '500',
                                        "request": request})
163
                fault = faults.InternalServerError("Unexpected error")
164
                return render_fault(request, fault)
165
        return csrf.csrf_exempt(wrapper)
166
167
168
    return decorator


Christos Stavrakakis's avatar
Christos Stavrakakis committed
169
170
def get_serialization(request, format_allowed=True,
                      default_serialization="json"):
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
    """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"
198
        elif accept == "application/xml":
199
200
            return "xml"

201
    return default_serialization
202
203
204


def update_response_headers(request, response):
205
    if not getattr(response, "override_serialization", False):
206
207
208
209
210
211
212
213
214
215
216
        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)

217
    if settings.DEBUG or getattr(settings, "TEST", False):
218
219
220
        response["Date"] = format_date_time(time())

    if not response.has_header("Content-Length"):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
221
222
        _base_content_is_iter = getattr(response, '_base_content_is_iter',
                                        None)
223
        if (_base_content_is_iter is not None and not _base_content_is_iter):
224
225
            response["Content-Length"] = len(response.content)
        else:
226
227
228
229
230
231
            if not (response.has_header('Content-Type') and
                    response['Content-Type'].startswith(
                        'multipart/byteranges')):
                # save response content from been consumed if it is an iterator
                response._container, data = itertools.tee(response._container)
                response["Content-Length"] = len(str(data))
232
233
234
235
236
237
238
239
240
241
242

    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
243
    if settings.DEBUG or getattr(settings, "TEST", False):
244
245
246
247
248
249
250
251
252
253
254
255
256
257
        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,
258
                          "details": fault.details}}
259
260
261
        data = json.dumps(d)

    response = HttpResponse(data, status=fault.code)
262
263
    if response.status_code == 405 and hasattr(fault, 'allowed_methods'):
        response['Allow'] = ','.join(fault.allowed_methods)
264
265
266
267
    update_response_headers(request, response)
    return response


268
269
270
@api_method(token_required=False, user_required=False)
def api_endpoint_not_found(request):
    raise faults.BadRequest("API endpoint not found")
271
272


273
@api_method(token_required=False, user_required=False)
274
275
276
def api_method_not_allowed(request, allowed_methods):
    raise faults.NotAllowed("Method not allowed",
                            allowed_methods=allowed_methods)
277
278
279
280
281
282
283


def allow_jsonp(key='callback'):
    """
    Wrapper to enable jsonp responses.
    """
    def wrapper(func):
284
        @wraps(func)
285
286
287
288
289
290
291
292
293
294
295
296
297
        def view_wrapper(request, *args, **kwargs):
            response = func(request, *args, **kwargs)
            if 'content-type' in response._headers and \
               response._headers['content-type'][1] == 'application/json':
                callback_name = request.GET.get(key, None)
                if callback_name:
                    response.content = "%s(%s)" % (callback_name,
                                                   response.content)
                    response._headers['content-type'] = ('Content-Type',
                                                         'text/javascript')
            return response
        return view_wrapper
    return wrapper
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335


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

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

    """
    if not logger:
        logger = log

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

            common_groups = set(groups) & set(permitted_groups)

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

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

            return func(request, *args, **kwargs)
        return wrapper
    return decorator