Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.2 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

    
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.BadRequest("Method not allowed")
94

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

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

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

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

    
153

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

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

    
161
    if not format_allowed:
162
        return "text"
163

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

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

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

    
186
    return default_serialization
187

    
188

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

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

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

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

    
224

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

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

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

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

    
250

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

    
255

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

    
260

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

    
281

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

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

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

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

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

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

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

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