Statistics
| Branch: | Tag: | Revision:

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

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

    
154

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

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

    
162
    if not format_allowed:
163
        return "text"
164

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

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

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

    
187
    return default_serialization
188

    
189

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

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

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

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

    
225

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

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

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

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

    
251

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

    
256

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

    
261

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

    
282

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

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

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

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

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

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

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

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