Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.3 kB)

1 72bf812d Christos Stavrakakis
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2 72bf812d Christos Stavrakakis
#
3 72bf812d Christos Stavrakakis
# Redistribution and use in source and binary forms, with or
4 72bf812d Christos Stavrakakis
# without modification, are permitted provided that the following
5 72bf812d Christos Stavrakakis
# conditions are met:
6 72bf812d Christos Stavrakakis
#
7 72bf812d Christos Stavrakakis
#   1. Redistributions of source code must retain the above
8 72bf812d Christos Stavrakakis
#      copyright notice, this list of conditions and the following
9 72bf812d Christos Stavrakakis
#      disclaimer.
10 72bf812d Christos Stavrakakis
#
11 72bf812d Christos Stavrakakis
#   2. Redistributions in binary form must reproduce the above
12 72bf812d Christos Stavrakakis
#      copyright notice, this list of conditions and the following
13 72bf812d Christos Stavrakakis
#      disclaimer in the documentation and/or other materials
14 72bf812d Christos Stavrakakis
#      provided with the distribution.
15 72bf812d Christos Stavrakakis
#
16 72bf812d Christos Stavrakakis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 72bf812d Christos Stavrakakis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 72bf812d Christos Stavrakakis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 72bf812d Christos Stavrakakis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 72bf812d Christos Stavrakakis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 72bf812d Christos Stavrakakis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 72bf812d Christos Stavrakakis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 72bf812d Christos Stavrakakis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 72bf812d Christos Stavrakakis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 72bf812d Christos Stavrakakis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 72bf812d Christos Stavrakakis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 72bf812d Christos Stavrakakis
# POSSIBILITY OF SUCH DAMAGE.
28 72bf812d Christos Stavrakakis
#
29 72bf812d Christos Stavrakakis
# The views and conclusions contained in the software and
30 72bf812d Christos Stavrakakis
# documentation are those of the authors and should not be
31 72bf812d Christos Stavrakakis
# interpreted as representing official policies, either expressed
32 72bf812d Christos Stavrakakis
# or implied, of GRNET S.A.
33 aad21b81 Christos Stavrakakis
import sys
34 72bf812d Christos Stavrakakis
from functools import wraps
35 72bf812d Christos Stavrakakis
from traceback import format_exc
36 72bf812d Christos Stavrakakis
from time import time
37 72bf812d Christos Stavrakakis
from logging import getLogger
38 72bf812d Christos Stavrakakis
from wsgiref.handlers import format_date_time
39 72bf812d Christos Stavrakakis
40 72bf812d Christos Stavrakakis
from django.http import HttpResponse
41 72bf812d Christos Stavrakakis
from django.utils import cache
42 72bf812d Christos Stavrakakis
from django.utils import simplejson as json
43 72bf812d Christos Stavrakakis
from django.template.loader import render_to_string
44 81e19e70 Kostas Papadimitriou
from django.views.decorators import csrf
45 72bf812d Christos Stavrakakis
46 72bf812d Christos Stavrakakis
from astakosclient import AstakosClient
47 6c1c0738 Ilias Tsitsimpis
from astakosclient.errors import AstakosClientException
48 72bf812d Christos Stavrakakis
from django.conf import settings
49 72bf812d Christos Stavrakakis
from snf_django.lib.api import faults
50 72bf812d Christos Stavrakakis
51 4d244f4a Sofia Papagiannaki
import itertools
52 72bf812d Christos Stavrakakis
53 72bf812d Christos Stavrakakis
log = getLogger(__name__)
54 93505a12 Christos Stavrakakis
django_logger = getLogger("django.request")
55 72bf812d Christos Stavrakakis
56 72bf812d Christos Stavrakakis
57 72bf812d Christos Stavrakakis
def get_token(request):
58 72bf812d Christos Stavrakakis
    """Get the Authentication Token of a request."""
59 72bf812d Christos Stavrakakis
    token = request.GET.get("X-Auth-Token", None)
60 72bf812d Christos Stavrakakis
    if not token:
61 72bf812d Christos Stavrakakis
        token = request.META.get("HTTP_X_AUTH_TOKEN", None)
62 72bf812d Christos Stavrakakis
    return token
63 72bf812d Christos Stavrakakis
64 72bf812d Christos Stavrakakis
65 72bf812d Christos Stavrakakis
def api_method(http_method=None, token_required=True, user_required=True,
66 ef57e622 Ilias Tsitsimpis
               logger=None, format_allowed=True, astakos_auth_url=None,
67 d0d9a3f5 Kostas Papadimitriou
               serializations=None, strict_serlization=False):
68 72bf812d Christos Stavrakakis
    """Decorator function for views that implement an API method."""
69 72bf812d Christos Stavrakakis
    if not logger:
70 72bf812d Christos Stavrakakis
        logger = log
71 72bf812d Christos Stavrakakis
72 d0d9a3f5 Kostas Papadimitriou
    serializations = serializations or ['json', 'xml']
73 d0d9a3f5 Kostas Papadimitriou
74 72bf812d Christos Stavrakakis
    def decorator(func):
75 72bf812d Christos Stavrakakis
        @wraps(func)
76 72bf812d Christos Stavrakakis
        def wrapper(request, *args, **kwargs):
77 72bf812d Christos Stavrakakis
            try:
78 72bf812d Christos Stavrakakis
                # Get the requested serialization format
79 d0d9a3f5 Kostas Papadimitriou
                serialization = get_serialization(
80 f9662798 Kostas Papadimitriou
                    request, format_allowed, serializations[0])
81 d0d9a3f5 Kostas Papadimitriou
82 d0d9a3f5 Kostas Papadimitriou
                # If guessed serialization is not supported, fallback to
83 d0d9a3f5 Kostas Papadimitriou
                # the default serialization or return an API error in case
84 d0d9a3f5 Kostas Papadimitriou
                # strict serialization flag is set.
85 d0d9a3f5 Kostas Papadimitriou
                if not serialization in serializations:
86 d0d9a3f5 Kostas Papadimitriou
                    if strict_serlization:
87 d0d9a3f5 Kostas Papadimitriou
                        raise faults.BadRequest(("%s serialization not "
88 d0d9a3f5 Kostas Papadimitriou
                                                "supported") % serialization)
89 d0d9a3f5 Kostas Papadimitriou
                    serialization = serializations[0]
90 d0d9a3f5 Kostas Papadimitriou
                request.serialization = serialization
91 72bf812d Christos Stavrakakis
92 72bf812d Christos Stavrakakis
                # Check HTTP method
93 72bf812d Christos Stavrakakis
                if http_method and request.method != http_method:
94 72bf812d Christos Stavrakakis
                    raise faults.BadRequest("Method not allowed")
95 72bf812d Christos Stavrakakis
96 72bf812d Christos Stavrakakis
                # Get authentication token
97 72bf812d Christos Stavrakakis
                request.x_auth_token = None
98 72bf812d Christos Stavrakakis
                if token_required or user_required:
99 72bf812d Christos Stavrakakis
                    token = get_token(request)
100 72bf812d Christos Stavrakakis
                    if not token:
101 72bf812d Christos Stavrakakis
                        msg = "Access denied. No authentication token"
102 72bf812d Christos Stavrakakis
                        raise faults.Unauthorized(msg)
103 72bf812d Christos Stavrakakis
                    request.x_auth_token = token
104 72bf812d Christos Stavrakakis
105 72bf812d Christos Stavrakakis
                # Authenticate
106 72bf812d Christos Stavrakakis
                if user_required:
107 72bf812d Christos Stavrakakis
                    assert(token_required), "Can not get user without token"
108 ef57e622 Ilias Tsitsimpis
                    astakos = astakos_auth_url or settings.ASTAKOS_AUTH_URL
109 ef57e622 Ilias Tsitsimpis
                    astakos = AstakosClient(token, astakos,
110 726cb37f Ilias Tsitsimpis
                                            use_pool=True,
111 5e903a58 Christos Stavrakakis
                                            retry=2,
112 726cb37f Ilias Tsitsimpis
                                            logger=logger)
113 b4b82ec4 Giorgos Korfiatis
                    user_info = astakos.authenticate()
114 b4b82ec4 Giorgos Korfiatis
                    request.user_uniq = user_info["access"]["user"]["id"]
115 72bf812d Christos Stavrakakis
                    request.user = user_info
116 72bf812d Christos Stavrakakis
117 72bf812d Christos Stavrakakis
                # Get the response object
118 72bf812d Christos Stavrakakis
                response = func(request, *args, **kwargs)
119 72bf812d Christos Stavrakakis
120 72bf812d Christos Stavrakakis
                # Fill in response variables
121 72bf812d Christos Stavrakakis
                update_response_headers(request, response)
122 72bf812d Christos Stavrakakis
                return response
123 aad21b81 Christos Stavrakakis
            except faults.Fault as fault:
124 72bf812d Christos Stavrakakis
                if fault.code >= 500:
125 aad21b81 Christos Stavrakakis
                    logger.error("Unexpected API Error: %s", request.path,
126 aad21b81 Christos Stavrakakis
                                 exc_info=sys.exc_info(),
127 aad21b81 Christos Stavrakakis
                                 extra={
128 aad21b81 Christos Stavrakakis
                                     "status_code": fault.code,
129 aad21b81 Christos Stavrakakis
                                     "request": request})
130 72bf812d Christos Stavrakakis
                return render_fault(request, fault)
131 726cb37f Ilias Tsitsimpis
            except AstakosClientException as err:
132 726cb37f Ilias Tsitsimpis
                fault = faults.Fault(message=err.message,
133 726cb37f Ilias Tsitsimpis
                                     details=err.details,
134 726cb37f Ilias Tsitsimpis
                                     code=err.status)
135 726cb37f Ilias Tsitsimpis
                if fault.code >= 500:
136 aad21b81 Christos Stavrakakis
                    logger.error("Unexpected AstakosClient Error: %s",
137 aad21b81 Christos Stavrakakis
                                 request.path,
138 aad21b81 Christos Stavrakakis
                                 exc_info=sys.exc_info(),
139 aad21b81 Christos Stavrakakis
                                 extra={
140 aad21b81 Christos Stavrakakis
                                     "status_code": fault.code,
141 aad21b81 Christos Stavrakakis
                                     "request": request})
142 726cb37f Ilias Tsitsimpis
                return render_fault(request, fault)
143 72bf812d Christos Stavrakakis
            except:
144 93505a12 Christos Stavrakakis
                django_logger.error("Internal Server Error: %s", request.path,
145 93505a12 Christos Stavrakakis
                                    exc_info=sys.exc_info(),
146 93505a12 Christos Stavrakakis
                                    extra={
147 93505a12 Christos Stavrakakis
                                        "status_code": '500',
148 93505a12 Christos Stavrakakis
                                        "request": request})
149 499d9bfe Christos Stavrakakis
                fault = faults.InternalServerError("Unexpected error")
150 72bf812d Christos Stavrakakis
                return render_fault(request, fault)
151 81e19e70 Kostas Papadimitriou
        return csrf.csrf_exempt(wrapper)
152 72bf812d Christos Stavrakakis
    return decorator
153 72bf812d Christos Stavrakakis
154 72bf812d Christos Stavrakakis
155 d546637f Christos Stavrakakis
def get_serialization(request, format_allowed=True,
156 d546637f Christos Stavrakakis
                      default_serialization="json"):
157 72bf812d Christos Stavrakakis
    """Return the serialization format requested.
158 72bf812d Christos Stavrakakis

159 72bf812d Christos Stavrakakis
    Valid formats are 'json' and 'xml' and 'text'
160 72bf812d Christos Stavrakakis
    """
161 72bf812d Christos Stavrakakis
162 72bf812d Christos Stavrakakis
    if not format_allowed:
163 72bf812d Christos Stavrakakis
        return "text"
164 72bf812d Christos Stavrakakis
165 72bf812d Christos Stavrakakis
    # Try to get serialization from 'format' parameter
166 72bf812d Christos Stavrakakis
    _format = request.GET.get("format")
167 72bf812d Christos Stavrakakis
    if _format:
168 72bf812d Christos Stavrakakis
        if _format == "json":
169 72bf812d Christos Stavrakakis
            return "json"
170 72bf812d Christos Stavrakakis
        elif _format == "xml":
171 72bf812d Christos Stavrakakis
            return "xml"
172 72bf812d Christos Stavrakakis
173 72bf812d Christos Stavrakakis
    # Try to get serialization from path
174 72bf812d Christos Stavrakakis
    path = request.path
175 72bf812d Christos Stavrakakis
    if path.endswith(".json"):
176 72bf812d Christos Stavrakakis
        return "json"
177 72bf812d Christos Stavrakakis
    elif path.endswith(".xml"):
178 72bf812d Christos Stavrakakis
        return "xml"
179 72bf812d Christos Stavrakakis
180 72bf812d Christos Stavrakakis
    for item in request.META.get("HTTP_ACCEPT", "").split(","):
181 72bf812d Christos Stavrakakis
        accept, sep, rest = item.strip().partition(";")
182 72bf812d Christos Stavrakakis
        if accept == "application/json":
183 72bf812d Christos Stavrakakis
            return "json"
184 c50825bf Sofia Papagiannaki
        elif accept == "application/xml":
185 72bf812d Christos Stavrakakis
            return "xml"
186 72bf812d Christos Stavrakakis
187 6b560707 Sofia Papagiannaki
    return default_serialization
188 72bf812d Christos Stavrakakis
189 72bf812d Christos Stavrakakis
190 72bf812d Christos Stavrakakis
def update_response_headers(request, response):
191 b698d39d Christos Stavrakakis
    if not getattr(response, "override_serialization", False):
192 72bf812d Christos Stavrakakis
        serialization = request.serialization
193 72bf812d Christos Stavrakakis
        if serialization == "xml":
194 72bf812d Christos Stavrakakis
            response["Content-Type"] = "application/xml; charset=UTF-8"
195 72bf812d Christos Stavrakakis
        elif serialization == "json":
196 72bf812d Christos Stavrakakis
            response["Content-Type"] = "application/json; charset=UTF-8"
197 72bf812d Christos Stavrakakis
        elif serialization == "text":
198 72bf812d Christos Stavrakakis
            response["Content-Type"] = "text/plain; charset=UTF-8"
199 72bf812d Christos Stavrakakis
        else:
200 72bf812d Christos Stavrakakis
            raise ValueError("Unknown serialization format '%s'" %
201 72bf812d Christos Stavrakakis
                             serialization)
202 72bf812d Christos Stavrakakis
203 47ef53d5 Christos Stavrakakis
    if settings.DEBUG or getattr(settings, "TEST", False):
204 72bf812d Christos Stavrakakis
        response["Date"] = format_date_time(time())
205 72bf812d Christos Stavrakakis
206 72bf812d Christos Stavrakakis
    if not response.has_header("Content-Length"):
207 d546637f Christos Stavrakakis
        _base_content_is_iter = getattr(response, '_base_content_is_iter',
208 d546637f Christos Stavrakakis
                                        None)
209 6b256427 Christos Stavrakakis
        if (_base_content_is_iter is not None and not _base_content_is_iter):
210 4d244f4a Sofia Papagiannaki
            response["Content-Length"] = len(response.content)
211 4d244f4a Sofia Papagiannaki
        else:
212 981d3b0d Sofia Papagiannaki
            if not (response.has_header('Content-Type') and
213 981d3b0d Sofia Papagiannaki
                    response['Content-Type'].startswith(
214 981d3b0d Sofia Papagiannaki
                        'multipart/byteranges')):
215 981d3b0d Sofia Papagiannaki
                # save response content from been consumed if it is an iterator
216 981d3b0d Sofia Papagiannaki
                response._container, data = itertools.tee(response._container)
217 981d3b0d Sofia Papagiannaki
                response["Content-Length"] = len(str(data))
218 72bf812d Christos Stavrakakis
219 72bf812d Christos Stavrakakis
    cache.add_never_cache_headers(response)
220 72bf812d Christos Stavrakakis
    # Fix Vary and Cache-Control Headers. Issue: #3448
221 72bf812d Christos Stavrakakis
    cache.patch_vary_headers(response, ('X-Auth-Token',))
222 72bf812d Christos Stavrakakis
    cache.patch_cache_control(response, no_cache=True, no_store=True,
223 72bf812d Christos Stavrakakis
                              must_revalidate=True)
224 72bf812d Christos Stavrakakis
225 72bf812d Christos Stavrakakis
226 72bf812d Christos Stavrakakis
def render_fault(request, fault):
227 72bf812d Christos Stavrakakis
    """Render an API fault to an HTTP response."""
228 72bf812d Christos Stavrakakis
    # If running in debug mode add exception information to fault details
229 47ef53d5 Christos Stavrakakis
    if settings.DEBUG or getattr(settings, "TEST", False):
230 72bf812d Christos Stavrakakis
        fault.details = format_exc()
231 72bf812d Christos Stavrakakis
232 72bf812d Christos Stavrakakis
    try:
233 72bf812d Christos Stavrakakis
        serialization = request.serialization
234 72bf812d Christos Stavrakakis
    except AttributeError:
235 72bf812d Christos Stavrakakis
        request.serialization = "json"
236 72bf812d Christos Stavrakakis
        serialization = "json"
237 72bf812d Christos Stavrakakis
238 72bf812d Christos Stavrakakis
    # Serialize the fault data to xml or json
239 72bf812d Christos Stavrakakis
    if serialization == "xml":
240 72bf812d Christos Stavrakakis
        data = render_to_string("fault.xml", {"fault": fault})
241 72bf812d Christos Stavrakakis
    else:
242 72bf812d Christos Stavrakakis
        d = {fault.name: {"code": fault.code,
243 72bf812d Christos Stavrakakis
                          "message": fault.message,
244 93c6900c Ilias Tsitsimpis
                          "details": fault.details}}
245 72bf812d Christos Stavrakakis
        data = json.dumps(d)
246 72bf812d Christos Stavrakakis
247 72bf812d Christos Stavrakakis
    response = HttpResponse(data, status=fault.code)
248 72bf812d Christos Stavrakakis
    update_response_headers(request, response)
249 72bf812d Christos Stavrakakis
    return response
250 72bf812d Christos Stavrakakis
251 72bf812d Christos Stavrakakis
252 269ac8de Christos Stavrakakis
@api_method(token_required=False, user_required=False)
253 269ac8de Christos Stavrakakis
def api_endpoint_not_found(request):
254 269ac8de Christos Stavrakakis
    raise faults.BadRequest("API endpoint not found")
255 72bf812d Christos Stavrakakis
256 72bf812d Christos Stavrakakis
257 269ac8de Christos Stavrakakis
@api_method(token_required=False, user_required=False)
258 269ac8de Christos Stavrakakis
def api_method_not_allowed(request):
259 72bf812d Christos Stavrakakis
    raise faults.BadRequest('Method not allowed')
260 fced411d Kostas Papadimitriou
261 fced411d Kostas Papadimitriou
262 fced411d Kostas Papadimitriou
def allow_jsonp(key='callback'):
263 fced411d Kostas Papadimitriou
    """
264 fced411d Kostas Papadimitriou
    Wrapper to enable jsonp responses.
265 fced411d Kostas Papadimitriou
    """
266 fced411d Kostas Papadimitriou
    def wrapper(func):
267 bda47e03 Christos Stavrakakis
        @wraps(func)
268 fced411d Kostas Papadimitriou
        def view_wrapper(request, *args, **kwargs):
269 fced411d Kostas Papadimitriou
            response = func(request, *args, **kwargs)
270 fced411d Kostas Papadimitriou
            if 'content-type' in response._headers and \
271 fced411d Kostas Papadimitriou
               response._headers['content-type'][1] == 'application/json':
272 fced411d Kostas Papadimitriou
                callback_name = request.GET.get(key, None)
273 fced411d Kostas Papadimitriou
                if callback_name:
274 fced411d Kostas Papadimitriou
                    response.content = "%s(%s)" % (callback_name,
275 fced411d Kostas Papadimitriou
                                                   response.content)
276 fced411d Kostas Papadimitriou
                    response._headers['content-type'] = ('Content-Type',
277 fced411d Kostas Papadimitriou
                                                         'text/javascript')
278 fced411d Kostas Papadimitriou
            return response
279 fced411d Kostas Papadimitriou
        return view_wrapper
280 fced411d Kostas Papadimitriou
    return wrapper
281 bda47e03 Christos Stavrakakis
282 bda47e03 Christos Stavrakakis
283 bda47e03 Christos Stavrakakis
def user_in_groups(permitted_groups, logger=None):
284 bda47e03 Christos Stavrakakis
    """Check that the request user belongs to one of permitted groups.
285 bda47e03 Christos Stavrakakis

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

289 bda47e03 Christos Stavrakakis
    """
290 bda47e03 Christos Stavrakakis
    if not logger:
291 bda47e03 Christos Stavrakakis
        logger = log
292 bda47e03 Christos Stavrakakis
293 bda47e03 Christos Stavrakakis
    def decorator(func):
294 bda47e03 Christos Stavrakakis
        @wraps(func)
295 bda47e03 Christos Stavrakakis
        def wrapper(request, *args, **kwargs):
296 bda47e03 Christos Stavrakakis
            if hasattr(request, "user") and request.user is not None:
297 bda47e03 Christos Stavrakakis
                groups = request.user["access"]["user"]["roles"]
298 bda47e03 Christos Stavrakakis
                groups = [g["name"] for g in groups]
299 bda47e03 Christos Stavrakakis
            else:
300 bda47e03 Christos Stavrakakis
                raise faults.Forbidden
301 bda47e03 Christos Stavrakakis
302 bda47e03 Christos Stavrakakis
            common_groups = set(groups) & set(permitted_groups)
303 bda47e03 Christos Stavrakakis
304 bda47e03 Christos Stavrakakis
            if not common_groups:
305 bda47e03 Christos Stavrakakis
                msg = ("Not allowing access to '%s' by user '%s'. User does"
306 bda47e03 Christos Stavrakakis
                       " not belong to a valid group. User groups: %s,"
307 bda47e03 Christos Stavrakakis
                       " Required groups %s"
308 bda47e03 Christos Stavrakakis
                       % (request.path, request.user, groups,
309 bda47e03 Christos Stavrakakis
                          permitted_groups))
310 bda47e03 Christos Stavrakakis
                logger.error(msg)
311 bda47e03 Christos Stavrakakis
                raise faults.Forbidden
312 bda47e03 Christos Stavrakakis
313 bda47e03 Christos Stavrakakis
            logger.info("User '%s' in groups '%s' accessed view '%s'",
314 bda47e03 Christos Stavrakakis
                        request.user_uniq, groups, request.path)
315 bda47e03 Christos Stavrakakis
316 bda47e03 Christos Stavrakakis
            return func(request, *args, **kwargs)
317 bda47e03 Christos Stavrakakis
        return wrapper
318 bda47e03 Christos Stavrakakis
    return decorator