Project

General

Profile

0001-Add-API-method-to-generate-auto-login-link.patch

Benjamin Renard, 04 May 2017 11:55 AM

Download (9.3 KB)

View differences:

Subject: [PATCH] Add API method to generate auto-login link

 src/authentic2/api_urls.py     |   4 ++
 src/authentic2/api_views.py    | 139 ++++++++++++++++++++++++++++++++++++++++-
 src/authentic2/app_settings.py |   3 +
 3 files changed, 145 insertions(+), 1 deletion(-)
src/authentic2/api_urls.py
11 11
                           name='a2-api-user'),
12 12
                       url(r'^roles/(?P<role_uuid>[\w+]*)/members/(?P<member_uuid>[\w+]*)/$', api_views.roles, 
13 13
                           name='a2-api-role-member'),
14
                       url(r'^user/autologin$', api_views.userautologin,
15
                           name='a2-api-user-autologin'),
16
                       url(r'^user/autologin/(?P<token>[\w: -]+)$', api_views.userautologinview,
17
                           name='autologin'),
14 18
)
15 19
urlpatterns += api_views.router.urls
src/authentic2/api_views.py
1 1
'''Views for Authentic2 API'''
2 2
import logging
3 3
import smtplib
4
import datetime
5
from hashlib import sha1
4 6

  
7
from django import http
5 8
from django.db import models
6
from django.contrib.auth import get_user_model
9
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
10
from django.core import signing
11
from django.core.cache import cache
7 12
from django.core.exceptions import MultipleObjectsReturned
8 13
from django.utils.translation import ugettext as _
14
from django.utils.timezone import now
15
from django.utils.dateparse import parse_datetime
9 16
from django.views.decorators.vary import vary_on_headers
10 17
from django.views.decorators.cache import cache_control
11 18
from django.shortcuts import get_object_or_404
19
from django.views.generic.base import RedirectView
20
from django.core.urlresolvers import reverse
12 21

  
13 22
from django_rbac.utils import get_ou_model, get_role_model
14 23

  
......
29 38
from .models import Attribute, PasswordReset
30 39
from .a2_rbac.utils import get_default_ou
31 40

  
41
from utils import login
42
from . import app_settings
43

  
32 44

  
33 45
class HasUserAddPermission(permissions.BasePermission):
34 46
    def has_permission(self, request, view):
......
36 48
            return False
37 49
        return True
38 50

  
51
class IsSuperUser(permissions.BasePermission):
52
    """
53
    Allows access only to super users.
54
    """
55

  
56
    def has_permission(self, request, view):
57
        return request.user and request.user.is_superuser
39 58

  
40 59
class RegistrationSerializer(serializers.Serializer):
41 60
    '''Register RPC payload'''
......
244 263

  
245 264
password_change = PasswordChange.as_view()
246 265

  
266
class UserAutoLoginSerializer(serializers.Serializer):
267
    '''UserAutoLogin RPC payload'''
268
    uuid = serializers.CharField(
269
        required=False, allow_null=True)
270
    email = serializers.EmailField(
271
        required=False, allow_null=True)
272
    ou = serializers.SlugRelatedField(
273
        queryset=get_ou_model().objects.all(),
274
        slug_field='slug',
275
        required=False, allow_null=True)
276
    expire_duration = serializers.IntegerField(
277
        min_value=1, max_value=app_settings.A2_AUTH_TOKEN_MAX_LIFEMENT,
278
        required=False, allow_null=False)
279
    next_url = serializers.CharField(
280
        required=False, allow_null=True)
281

  
282
    def validate(self, data):
283
        User = get_user_model()
284
        if data.get('uuid'):
285
            qs = User.objects.filter(uuid=data['uuid'])
286
        elif data.get('email'):
287
            qs = User.objects.filter(email=data['email'])
288
        else:
289
            raise serializers.ValidationError("you must provide user's uuid or email")
290
        if data.get('ou'):
291
            qs = qs.filter(ou=data.get('ou'))
292
        try:
293
            self.user = qs.get()
294
            data['uuid'] = self.user.uuid
295
        except User.DoesNotExist:
296
            raise serializers.ValidationError('no user found')
297
        except MultipleObjectsReturned:
298
            raise serializers.ValidationError('more than one user found')
299
        return data
300

  
301

  
302
class UserAutoLogin(BaseRpcView):
303
    permission_classes = (permissions.IsAuthenticated,
304
                          IsSuperUser)
305

  
306
    serializer_class = UserAutoLoginSerializer
307

  
308
    def rpc(self, request, serializer):
309
        data = {}
310
        data['uuid'] = serializer.validated_data.get('uuid')
311
        expire_duration = serializer.validated_data.get('expire_duration', app_settings.A2_AUTH_TOKEN_DEFAULT_LIFETIME)
312
        data['expire'] = (now()+datetime.timedelta(seconds=expire_duration)).isoformat()
313
        if serializer.validated_data.get('ou'):
314
            data['ou'] = serializer.validated_data.get('ou')
315
        data[REDIRECT_FIELD_NAME] = serializer.validated_data.get('next_url')
316
        token = signing.dumps(data)
317
        autologinurl = request.build_absolute_uri(
318
            reverse('autologin', kwargs={'token': token}))
319
        return {'autologinurl': autologinurl}, status.HTTP_200_OK
320

  
321
userautologin = UserAutoLogin.as_view()
322

  
323
class UserAutoLoginView(RedirectView):
324

  
325
    def __init__(self, *args, **kwargs):
326
        super(UserAutoLoginView, self).__init__(*args, **kwargs)
327
        self.logger = logging.getLogger(__name__)
328

  
329
    def valid_token(self, token):
330
        try:
331
            result = signing.loads(token.replace(' ', ''))
332
            expire = result.get('expire')
333
            if expire:
334
                if now()>=parse_datetime(expire):
335
                    self.logger.error(u'Specified token is expired')
336
                else:
337
                    cache_first_use_key = 'token-%s' % sha1(token)
338
                    if cache.get(cache_first_use_key):
339
                        if now()>=(parse_datetime(cache.get(cache_first_use_key)+datetime.timedelta(0,app_settings.A2_AUTH_TOKEN_LIFETIME_AFTER_FIRST_USE))):
340
                            self.logger.error(u'Specified token was already used and expired')
341
                            return None
342
                    else:
343
                        cache.set(cache_first_use_key, now().isoformat(), app_settings.A2_AUTH_TOKEN_MAX_LIFEMENT)
344
                    return result
345
            else:
346
                self.logger.error(u'No expire in token')
347
        except signing.BadSignature:
348
            self.logger.error(u'Specified token is invalid')
349
        return None
350

  
351
    def get_next_url(self, *args, **kwargs):
352
            if kwargs.get('token') and kwargs.get('token').get(REDIRECT_FIELD_NAME):
353
                return kwargs.get('token').get(REDIRECT_FIELD_NAME)
354
            return reverse('auth_homepage')
355

  
356
    def get(self, request, *args, **kwargs):
357
        token = self.valid_token(kwargs.get('token'))
358

  
359
        url = None
360
        if token:
361
            self.logger.info(u"Valid token : %s" % token)
362
            User = get_user_model()
363
            qs = User.objects.filter(uuid=token['uuid'])
364
            if token.get('ou'):
365
                qs = qs.filter(ou=token['ou'])
366
            try:
367
                user = qs.get()
368
                self.logger.info(u"User : %s" % user)
369
                user.backend = 'authentic2.backends.models_backend.ModelBackend'
370
                login(request, user, 'token')
371
                url = self.get_next_url(token = token)
372
                self.logger.info(u"Redirect to : %s" % url)
373
            except User.DoesNotExist:
374
                self.logger.error(u'User %s from token does not exists' % token['uuid'])
375
            except MultipleObjectsReturned:
376
                self.logger.error(u"More than one user return from token's uuid (%s)" % token['uuid'])
377

  
378
        if not url:
379
            url = reverse('auth_login')
380

  
381
        return http.HttpResponseRedirect(url)
382

  
383
userautologinview = UserAutoLoginView.as_view()
247 384

  
248 385
@vary_on_headers('Cookie', 'Origin', 'Referer')
249 386
@cache_control(private=True, max_age=60)
src/authentic2/app_settings.py
144 144
    A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'),
145 145
    A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'),
146 146
    A2_AUTH_PASSWORD_ENABLE=Setting(default=True, definition='Activate login/password authentication', names=('AUTH_PASSWORD',)),
147
    A2_AUTH_TOKEN_DEFAULT_LIFETIME=Setting(default=1800, definition='Default lifetime of autologin tokens'),
148
    A2_AUTH_TOKEN_MAX_LIFEMENT=Setting(default=172800, definition='Maximum allowed lifetime of autologin tokens'),
149
    A2_AUTH_TOKEN_LIFETIME_AFTER_FIRST_USE=Setting(default=3600, definition='Maximum allowed lifetime of autologin tokens after first use'),
147 150
    A2_LOGIN_FAILURE_COUNT_BEFORE_WARNING=Setting(default=0,
148 151
            definition='Failure count before logging a warning to '
149 152
            'authentic2.user_login_failure. No warning will be send if value is '
150
-