Revision 72bf812d

b/snf-django-lib/snf_django/lib/api/__init__.py
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

  
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

  
45
from astakosclient import AstakosClient
46
from django.conf import settings
47
from snf_django.lib.api import faults
48

  
49

  
50
log = getLogger(__name__)
51

  
52

  
53
def get_token(request):
54
    """Get the Authentication Token of a request."""
55
    token = request.GET.get("X-Auth-Token", None)
56
    if not token:
57
        token = request.META.get("HTTP_X_AUTH_TOKEN", None)
58
    return token
59

  
60

  
61
def api_method(http_method=None, token_required=True, user_required=True,
62
               logger=None, format_allowed=True):
63
    """Decorator function for views that implement an API method."""
64
    if not logger:
65
        logger = log
66

  
67
    def decorator(func):
68
        @wraps(func)
69
        def wrapper(request, *args, **kwargs):
70
            try:
71
                # Get the requested serialization format
72
                request.serialization = get_serialization(request,
73
                                                          format_allowed)
74

  
75
                # Check HTTP method
76
                if http_method and request.method != http_method:
77
                    raise faults.BadRequest("Method not allowed")
78

  
79
                # Get authentication token
80
                request.x_auth_token = None
81
                if token_required or user_required:
82
                    token = get_token(request)
83
                    if not token:
84
                        msg = "Access denied. No authentication token"
85
                        raise faults.Unauthorized(msg)
86
                    request.x_auth_token = token
87

  
88
                # Authenticate
89
                if user_required:
90
                    assert(token_required), "Can not get user without token"
91
                    astakos = AstakosClient(settings.ASTAKOS_URL,
92
                                            use_pool=True,
93
                                            logger=logger)
94
                    user_info = astakos.get_user_info(token)
95
                    request.user_uniq = user_info["uuid"]
96
                    request.user = user_info
97

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

  
101
                # Fill in response variables
102
                update_response_headers(request, response)
103
                return response
104
            except faults.Fault, fault:
105
                if fault.code >= 500:
106
                    logger.exception("API ERROR")
107
                return render_fault(request, fault)
108
            except:
109
                logger.exception("Unexpected ERROR")
110
                fault = faults.InternalServerError("Unexpected ERROR")
111
                return render_fault(request, fault)
112
        return wrapper
113
    return decorator
114

  
115

  
116
def get_serialization(request, format_allowed=True):
117
    """Return the serialization format requested.
118

  
119
    Valid formats are 'json' and 'xml' and 'text'
120
    """
121

  
122
    if not format_allowed:
123
        return "text"
124

  
125
    # Try to get serialization from 'format' parameter
126
    _format = request.GET.get("format")
127
    if _format:
128
        if _format == "json":
129
            return "json"
130
        elif _format == "xml":
131
            return "xml"
132

  
133
    # Try to get serialization from path
134
    path = request.path
135
    if path.endswith(".json"):
136
        return "json"
137
    elif path.endswith(".xml"):
138
        return "xml"
139

  
140
    for item in request.META.get("HTTP_ACCEPT", "").split(","):
141
        accept, sep, rest = item.strip().partition(";")
142
        if accept == "application/json":
143
            return "json"
144
        elif accept == "applcation/xml":
145
            return "xml"
146

  
147
    return "json"
148

  
149

  
150
def update_response_headers(request, response):
151
    if not response.has_header("Content-Type"):
152
        serialization = request.serialization
153
        if serialization == "xml":
154
            response["Content-Type"] = "application/xml; charset=UTF-8"
155
        elif serialization == "json":
156
            response["Content-Type"] = "application/json; charset=UTF-8"
157
        elif serialization == "text":
158
            response["Content-Type"] = "text/plain; charset=UTF-8"
159
        else:
160
            raise ValueError("Unknown serialization format '%s'" %
161
                             serialization)
162

  
163
    if settings.DEBUG or settings.TEST:
164
        response["Date"] = format_date_time(time())
165

  
166
    if not response.has_header("Content-Length"):
167
        response["Content-Length"] = len(response.content)
168

  
169
    cache.add_never_cache_headers(response)
170
    # Fix Vary and Cache-Control Headers. Issue: #3448
171
    cache.patch_vary_headers(response, ('X-Auth-Token',))
172
    cache.patch_cache_control(response, no_cache=True, no_store=True,
173
                              must_revalidate=True)
174

  
175

  
176
def render_fault(request, fault):
177
    """Render an API fault to an HTTP response."""
178
    # If running in debug mode add exception information to fault details
179
    if settings.DEBUG or settings.TEST:
180
        fault.details = format_exc()
181

  
182
    try:
183
        serialization = request.serialization
184
    except AttributeError:
185
        request.serialization = "json"
186
        serialization = "json"
187

  
188
    # Serialize the fault data to xml or json
189
    if serialization == "xml":
190
        data = render_to_string("fault.xml", {"fault": fault})
191
    else:
192
        d = {fault.name: {"code": fault.code,
193
                          "message": fault.message,
194
                          "details": fault.details}
195
            }
196
        data = json.dumps(d)
197

  
198
    response = HttpResponse(data, status=fault.code)
199
    update_response_headers(request, response)
200
    return response
201

  
202

  
203
def not_found(request):
204
    raise faults.BadRequest('Not found.')
205

  
206

  
207
def method_not_allowed(request):
208
    raise faults.BadRequest('Method not allowed')
b/snf-django-lib/snf_django/lib/api/utils.py
1
# Copyright 2011-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

  
34
import datetime
35
from dateutil.parser import parse as date_parse
36
from django.utils import simplejson as json
37

  
38
from django.conf import settings
39
from snf_django.lib.api import faults
40

  
41

  
42
class UTC(datetime.tzinfo):
43
    """
44
    Helper UTC time information object.
45
    """
46

  
47
    def utcoffset(self, dt):
48
        return datetime.timedelta(0)
49

  
50
    def tzname(self, dt):
51
        return 'UTC'
52

  
53
    def dst(self, dt):
54
        return datetime.timedelta(0)
55

  
56

  
57
def isoformat(d):
58
    """Return an ISO8601 date string that includes a timezone.
59

  
60
    >>> from datetime import datetime
61
    >>> d = datetime(2012, 8, 10, 00, 59, 59)
62
    >>> isoformat(d)
63
    '2012-08-10T00:59:59+00:00'
64
    """
65

  
66
    return d.replace(tzinfo=UTC()).isoformat()
67

  
68

  
69
def isoparse(s):
70
    """Parse an ISO8601 date string into a datetime object."""
71

  
72
    if not s:
73
        return None
74

  
75
    try:
76
        since = date_parse(s)
77
        utc_since = since.astimezone(UTC()).replace(tzinfo=None)
78
    except ValueError:
79
        raise faults.BadRequest('Invalid changes-since parameter.')
80

  
81
    now = datetime.datetime.now()
82
    if utc_since > now:
83
        raise faults.BadRequest('changes-since value set in the future.')
84

  
85
    if now - utc_since > datetime.timedelta(seconds=settings.POLL_LIMIT):
86
        raise faults.BadRequest('Too old changes-since value.')
87

  
88
    return utc_since
89

  
90

  
91
def get_request_dict(request):
92
    """Return data sent by the client as python dictionary.
93

  
94
    Only JSON format is supported
95

  
96
    """
97
    data = request.raw_post_data
98
    content_type = request.META.get("CONTENT_TYPE")
99
    if content_type.startswith("application/json"):
100
        try:
101
            return json.loads(data)
102
        except ValueError:
103
            raise faults.BadRequest("Invalid JSON data")
104
    else:
105
        raise faults.BadRequest("Unsupported Content-type: '%s'", content_type)

Also available in: Unified diff