Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.4 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.BadRequest("Method not allowed")
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 as fault:
124
                if fault.code >= 500:
125
                    django_logger.error("Unexpected API Error: %s",
126
                                        request.path,
127
                                        exc_info=sys.exc_info(),
128
                                        extra={
129
                                            "status_code": fault.code,
130
                                            "request": request})
131
                return render_fault(request, fault)
132
            except AstakosClientException as err:
133
                fault = faults.Fault(message=err.message,
134
                                     details=err.details,
135
                                     code=err.status)
136
                if fault.code >= 500:
137
                    django_logger.error("Unexpected AstakosClient Error: %s",
138
                                        request.path,
139
                                        exc_info=sys.exc_info(),
140
                                        extra={
141
                                            "status_code": fault.code,
142
                                            "request": request})
143
                return render_fault(request, fault)
144
            except:
145
                django_logger.error("Internal Server Error: %s", request.path,
146
                                    exc_info=sys.exc_info(),
147
                                    extra={
148
                                        "status_code": '500',
149
                                        "request": request})
150
                fault = faults.InternalServerError("Unexpected error")
151
                return render_fault(request, fault)
152
        return csrf.csrf_exempt(wrapper)
153
    return decorator
154

    
155

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

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

    
163
    if not format_allowed:
164
        return "text"
165

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

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

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

    
188
    return default_serialization
189

    
190

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

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

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

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

    
226

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

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

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

    
248
    response = HttpResponse(data, status=fault.code)
249
    update_response_headers(request, response)
250
    return response
251

    
252

    
253
@api_method(token_required=False, user_required=False)
254
def api_endpoint_not_found(request):
255
    raise faults.BadRequest("API endpoint not found")
256

    
257

    
258
@api_method(token_required=False, user_required=False)
259
def api_method_not_allowed(request):
260
    raise faults.BadRequest('Method not allowed')
261

    
262

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

    
283

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

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

290
    """
291
    if not logger:
292
        logger = log
293

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

    
303
            common_groups = set(groups) & set(permitted_groups)
304

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

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

    
317
            return func(request, *args, **kwargs)
318
        return wrapper
319
    return decorator