Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / util.py @ e7726e14

History | View | Annotate | Download (11.1 kB)

1
# Copyright 2011-2012 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 logging
35
import datetime
36
import time
37
import urllib
38

    
39
from urlparse import urlparse
40
from datetime import tzinfo, timedelta
41

    
42
from django.http import HttpResponse, HttpResponseBadRequest, urlencode, \
43
                        HttpResponseRedirect
44
from django.template import RequestContext
45
from django.contrib.auth import authenticate
46
from django.core.urlresolvers import reverse
47
from django.shortcuts import redirect
48
from django.core.exceptions import ValidationError, ObjectDoesNotExist
49
from django.utils.translation import ugettext as _
50
from django.core.urlresolvers import reverse
51

    
52
from astakos.im.models import AstakosUser, Invitation
53
from astakos.im.functions import login
54
from astakos.im import settings
55

    
56
import astakos.im.messages as astakos_messages
57

    
58
logger = logging.getLogger(__name__)
59

    
60

    
61
class UTC(tzinfo):
62
    def utcoffset(self, dt):
63
        return timedelta(0)
64

    
65
    def tzname(self, dt):
66
        return 'UTC'
67

    
68
    def dst(self, dt):
69
        return timedelta(0)
70

    
71

    
72
def isoformat(d):
73
    """Return an ISO8601 date string that includes a timezone."""
74

    
75
    return d.replace(tzinfo=UTC()).isoformat()
76

    
77

    
78
def epoch(datetime):
79
    return int(time.mktime(datetime.timetuple()) * 1000)
80

    
81

    
82
def get_context(request, extra_context=None, **kwargs):
83
    extra_context = extra_context or {}
84
    extra_context.update(kwargs)
85
    return RequestContext(request, extra_context)
86

    
87

    
88
def get_invitation(request):
89
    """
90
    Returns the invitation identified by the ``code``.
91

92
    Raises ValueError if the invitation is consumed or there is another account
93
    associated with this email.
94
    """
95
    code = request.GET.get('code')
96
    if request.method == 'POST':
97
        code = request.POST.get('code')
98
    if not code:
99
        return
100
    invitation = Invitation.objects.get(code=code)
101
    if invitation.is_consumed:
102
        raise ValueError(_(astakos_messages.INVITATION_CONSUMED_ERR))
103
    if reserved_email(invitation.username):
104
        email = invitation.username
105
        raise ValueError(_(astakos_messages.EMAIL_RESERVED) % locals())
106
    return invitation
107

    
108

    
109
def restrict_next(url, domain=None, allowed_schemes=()):
110
    """
111
    Utility method to validate that provided url is safe to be used as the
112
    redirect location of an http redirect response. The method parses the
113
    provided url and identifies if it conforms CORS against provided domain
114
    AND url scheme matches any of the schemes in `allowed_schemes` parameter.
115
    If verirication succeeds sanitized safe url is returned. Consider using
116
    the method's result in the response location header and not the originally
117
    provided url. If verification fails the method returns None.
118

119
    >>> print restrict_next('/im/feedback', '.okeanos.grnet.gr')
120
    /im/feedback
121
    >>> print restrict_next('pithos.okeanos.grnet.gr/im/feedback',
122
    ...                     '.okeanos.grnet.gr')
123
    //pithos.okeanos.grnet.gr/im/feedback
124
    >>> print restrict_next('https://pithos.okeanos.grnet.gr/im/feedback',
125
    ...                     '.okeanos.grnet.gr')
126
    https://pithos.okeanos.grnet.gr/im/feedback
127
    >>> print restrict_next('pithos://127.0.0.1', '.okeanos.grnet.gr')
128
    None
129
    >>> print restrict_next('pithos://127.0.0.1', '.okeanos.grnet.gr',
130
    ...                     allowed_schemes=('pithos'))
131
    None
132
    >>> print restrict_next('pithos://127.0.0.1', '127.0.0.1',
133
    ...                     allowed_schemes=('pithos'))
134
    pithos://127.0.0.1
135
    >>> print restrict_next('node1.example.com', '.okeanos.grnet.gr')
136
    None
137
    >>> print restrict_next('//node1.example.com', '.okeanos.grnet.gr')
138
    None
139
    >>> print restrict_next('https://node1.example.com', '.okeanos.grnet.gr')
140
    None
141
    >>> print restrict_next('https://node1.example.com')
142
    https://node1.example.com
143
    >>> print restrict_next('//node1.example.com')
144
    //node1.example.com
145
    >>> print restrict_next('node1.example.com')
146
    //node1.example.com
147
    >>> print restrict_next('node1.example.com', allowed_schemes=('pithos',))
148
    None
149
    >>> print restrict_next('pithos://localhost', 'localhost',
150
    ...                     allowed_schemes=('pithos',))
151
    pithos://localhost
152
    """
153
    if not url:
154
        return None
155

    
156
    parts = urlparse(url, scheme='http')
157
    if not parts.netloc and not parts.path.startswith('/'):
158
        # fix url if does not conforms RFC 1808
159
        url = '//%s' % url
160
        parts = urlparse(url, scheme='http')
161

    
162
    if not domain and not allowed_schemes:
163
        return url
164

    
165
    # domain validation
166
    if domain:
167
        if not parts.netloc:
168
            return url
169
        if parts.netloc.endswith(domain):
170
            return url
171
        else:
172
            return None
173

    
174
    # scheme validation
175
    if allowed_schemes:
176
        if parts.scheme in allowed_schemes:
177
            return url
178

    
179
    return None
180

    
181

    
182
def prepare_response(request, user, next='', renew=False):
183
    """Return the unique username and the token
184
       as 'X-Auth-User' and 'X-Auth-Token' headers,
185
       or redirect to the URL provided in 'next'
186
       with the 'user' and 'token' as parameters.
187

188
       Reissue the token even if it has not yet
189
       expired, if the 'renew' parameter is present
190
       or user has not a valid token.
191
    """
192
    renew = renew or (not user.auth_token)
193
    renew = renew or user.token_expired()
194
    if renew:
195
        user.renew_token(
196
            flush_sessions=True,
197
            current_key=request.session.session_key
198
        )
199
        try:
200
            user.save()
201
        except ValidationError, e:
202
            return HttpResponseBadRequest(e)
203

    
204
    next = restrict_next(next, domain=settings.COOKIE_DOMAIN)
205

    
206
    if settings.FORCE_PROFILE_UPDATE and \
207
            not user.is_verified and not user.is_superuser:
208
        params = ''
209
        if next:
210
            params = '?' + urlencode({'next': next})
211
        next = reverse('edit_profile') + params
212

    
213
    response = HttpResponse()
214

    
215
    # authenticate before login
216
    user = authenticate(email=user.email, auth_token=user.auth_token)
217
    login(request, user)
218
    request.session.set_expiry(user.auth_token_expires)
219

    
220
    if not next:
221
        next = settings.LOGIN_SUCCESS_URL
222

    
223
    response['Location'] = next
224
    response.status_code = 302
225
    return response
226

    
227
class lazy_string(object):
228
    def __init__(self, function, *args, **kwargs):
229
        self.function = function
230
        self.args = args
231
        self.kwargs = kwargs
232

    
233
    def __str__(self):
234
        if not hasattr(self, 'str'):
235
            self.str = self.function(*self.args, **self.kwargs)
236
        return self.str
237

    
238

    
239
def reverse_lazy(*args, **kwargs):
240
    return lazy_string(reverse, *args, **kwargs)
241

    
242

    
243
def reserved_email(email):
244
    return AstakosUser.objects.user_exists(email)
245

    
246

    
247
def reserved_verified_email(email):
248
    return AstakosUser.objects.verified_user_exists(email)
249

    
250

    
251
def get_query(request):
252
    try:
253
        return request.__getattribute__(request.method)
254
    except AttributeError:
255
        return {}
256

    
257
def get_properties(obj):
258
    def get_class_attr(_class, attr):
259
        try:
260
            return getattr(_class, attr)
261
        except AttributeError:
262
            return
263

    
264
    return (i for i in vars(obj.__class__) \
265
        if isinstance(get_class_attr(obj.__class__, i), property))
266

    
267
def model_to_dict(obj, exclude=None, include_empty=True):
268
    '''
269
        serialize model object to dict with related objects
270

271
        author: Vadym Zakovinko <vp@zakovinko.com>
272
        date: January 31, 2011
273
        http://djangosnippets.org/snippets/2342/
274
    '''
275

    
276
    if exclude is None:
277
        exclude = ['AutoField', 'ForeignKey', 'OneToOneField']
278
    tree = {}
279
    for field_name in obj._meta.get_all_field_names():
280
        try:
281
            field = getattr(obj, field_name)
282
        except (ObjectDoesNotExist, AttributeError):
283
            continue
284

    
285
        if field.__class__.__name__ in ['RelatedManager', 'ManyRelatedManager']:
286
            if field.model.__name__ in exclude:
287
                continue
288

    
289
            if field.__class__.__name__ == 'ManyRelatedManager':
290
                exclude.append(obj.__class__.__name__)
291
            subtree = []
292
            for related_obj in getattr(obj, field_name).all():
293
                value = model_to_dict(related_obj, exclude=exclude)
294
                if value or include_empty:
295
                    subtree.append(value)
296
            if subtree or include_empty:
297
                tree[field_name] = subtree
298
            continue
299

    
300
        field = obj._meta.get_field_by_name(field_name)[0]
301
        if field.__class__.__name__ in exclude:
302
            continue
303

    
304
        if field.__class__.__name__ == 'RelatedObject':
305
            exclude.append(field.model.__name__)
306
            tree[field_name] = model_to_dict(getattr(obj, field_name),
307
                                             exclude=exclude)
308
            continue
309

    
310
        value = getattr(obj, field_name)
311
        if field.__class__.__name__ == 'ForeignKey':
312
            value = unicode(value) if value is not None else value
313
        if value or include_empty:
314
            tree[field_name] = value
315
    properties = list(get_properties(obj))
316
    for p in properties:
317
       tree[p] = getattr(obj, p)
318
    tree['str_repr'] = obj.__str__()
319

    
320
    return tree
321

    
322
def login_url(request):
323
    attrs = {}
324
    for attr in ['login', 'key', 'code']:
325
        val = request.REQUEST.get(attr, None)
326
        if val:
327
            attrs[attr] = val
328
    return "%s?%s" % (reverse('login'), urllib.urlencode(attrs))
329

    
330

    
331
def redirect_back(request, default='index'):
332
    """
333
    Redirect back to referer if safe and possible.
334
    """
335
    referer = request.META.get('HTTP_REFERER')
336

    
337
    safedomain = settings.BASE_URL.replace("https://", "").replace(
338
        "http://", "")
339
    safe = restrict_next(referer, safedomain)
340
    # avoid redirect loop
341
    loops = referer == request.get_full_path()
342
    if referer and safe and not loops:
343
        return redirect(referer)
344
    return redirect(reverse(default))