Projet

Général

Profil

0003-views-add-a-user_info-endpoint.patch

Benjamin Dauvergne, 08 juillet 2015 00:50

Télécharger (10 ko)

Voir les différences:

Subject: [PATCH 3/3] views: add a user_info endpoint

The endpoint supports JSON with CORS or JSONP with Referer validation.
Browser or proxy not sending Referer headers will be forbidden to access
the view. Cross-origin check are disabled when DEBUG=True. It also means
that just viewing it in you browser is forbidden (as the browser will
not send the Referer or Origin header).
 src/authentic2/a2_rbac/models.py     |  9 ++++++++
 src/authentic2/cors.py               | 44 ++++++++++++++++++++++++++++++++++++
 src/authentic2/custom_user/models.py | 13 +++++++++++
 src/authentic2/decorators.py         | 39 +++++++++++++++++++++++++++++++-
 src/authentic2/idp/saml/__init__.py  | 10 ++++++++
 src/authentic2/models.py             | 12 ++++++++++
 src/authentic2/tests.py              |  9 ++++++++
 src/authentic2/urls.py               |  1 +
 src/authentic2/views.py              | 10 ++++++++
 9 files changed, 146 insertions(+), 1 deletion(-)
 create mode 100644 src/authentic2/cors.py
src/authentic2/a2_rbac/models.py
147 147
        verbose_name_plural = _('roles')
148 148
        ordering = ('ou', 'service', 'name',)
149 149

  
150
    def to_json(self):
151
        return {
152
            'uuid': self.uuid,
153
            'name': self.name,
154
            'slug': self.slug,
155
            'is_admin': bool(self.admin_scope_ct and self.admin_scope_id),
156
            'is_service': bool(self.service),
157
        }
158

  
150 159

  
151 160
class RoleParenting(RoleParentingAbstractBase):
152 161
    class Meta:
src/authentic2/cors.py
1
from .decorators import SessionCache
2
import urlparse
3

  
4
from django.conf import settings
5

  
6
from . import plugins
7

  
8

  
9
def make_origin(url):
10
    '''Build origin of an URL'''
11
    parsed = urlparse.urlparse(url)
12
    if ':' in parsed.netloc:
13
        host, port = parsed.netloc.split(':', 1)
14
        if parsed.scheme == 'http' and port == 80:
15
            port = None
16
        if parsed.scheme == 'https' and port == 443:
17
            port = None
18
    else:
19
        host, port = parsed.netloc, None
20
    result = '%s://%s' % (parsed.scheme, host)
21
    if port:
22
        result += ':%s' % port
23
    return result
24

  
25

  
26
@SessionCache(timeout=60, args=(1,))
27
def check_origin(request, origin):
28
    '''Decide if an origin is authorized to do a CORS request'''
29
    if settings.DEBUG:
30
        return True
31
    request_origin = make_origin(request.build_absolute_uri())
32
    if origin == 'null':
33
        return False
34
    if not origin:
35
        return False
36
    if origin == request_origin:
37
        return True
38
    for plugin in plugins.get_plugins():
39
        if hasattr(plugin, 'check_origin'):
40
            if plugin.check_origin(request, origin):
41
                return True
42
    return False
43

  
44

  
src/authentic2/custom_user/models.py
13 13

  
14 14
from authentic2 import utils, validators, app_settings
15 15
from authentic2.decorators import errorcollector
16
from authentic2.models import Service
16 17

  
17 18
from .managers import UserManager
18 19

  
......
128 129

  
129 130
    def natural_key(self):
130 131
        return (self.uuid,)
132

  
133
    def to_json(self):
134
        return {
135
            'uuid': self.uuid,
136
            'username': self.username,
137
            'email': self.email,
138
            'first_name': self.first_name,
139
            'last_name': self.last_name,
140
            'is_superuser': self.is_superuser,
141
            'roles': [role.to_json() for role in self.roles_and_parents().filter(service__isnull=True)],
142
            'services': [service.to_json(user=self) for service in Service.objects.all()],
143
        }
src/authentic2/decorators.py
1
import re
2
from json import dumps as json_dumps
1 3
from contextlib import contextmanager
2 4
import time
3 5
from functools import wraps
4 6

  
5 7
from django.contrib.auth.decorators import login_required
6 8
from django.views.debug import technical_404_response
7
from django.http import Http404
9
from django.http import Http404, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
8 10
from django.core.cache import cache as django_cache
9 11

  
10 12
from . import utils, app_settings, middleware
......
234 236
        yield
235 237
    except ValidationError, e:
236 238
        e.update_error_dict(error_dict)
239

  
240

  
241
def json(func):
242
    '''Convert view to a JSON or JSON web-service supporting CORS'''
243
    from . import cors
244
    @wraps(func)
245
    def f(request, *args, **kwargs):
246
        # 1. check origin
247
        origin = request.META.get('HTTP_ORIGIN')
248
        if origin is None:
249
            origin = request.META.get('HTTP_REFERER')
250
            if origin:
251
                origin = cors.make_origin(origin)
252
        if not cors.check_origin(request, origin):
253
            return HttpResponseForbidden('bad origin')
254
        # 2. build response
255
        result = func(request, *args, **kwargs)
256
        json_str = json_dumps(result)
257
        response = HttpResponse(content_type='application/json')
258
        for variable in ('jsonpCallback', 'callback'):
259
            if variable in request.GET:
260
                identifier = request.GET[variable]
261
                if not re.match(r'^[$a-zA-Z_][0-9a-zA-Z_$]*$', identifier):
262
                    return HttpResponseBadRequest('invalid JSONP callback name')
263
                json_str = '%s(%s);' % (identifier, json_str)
264
                break
265
        else:
266
            response['Access-Control-Allow-Origin'] = origin
267
            response['Access-Control-Allow-Credentials'] = 'true'
268
            response['Access-Control-Allow-Headers'] = 'x-requested-with'
269
        response.write(json_str)
270
        return response
271
    return f
272

  
273

  
src/authentic2/idp/saml/__init__.py
32 32
    def get_idp_backends(self):
33 33
        return ['authentic2.idp.saml.backend.SamlBackend']
34 34

  
35
    def check_origin(self, request, origin):
36
        from authentic2.cors import make_origin
37
        from authentic2.saml.models import LibertySession
38
        for session in LibertySession.objects.filter(
39
                django_session_key=request.session.session_key):
40
            provider_origin = make_origin(session.provider_id)
41
            if origin == provider_origin:
42
                return True
43

  
44

  
35 45
from django.apps import AppConfig
36 46
class SAML2IdPConfig(AppConfig):
37 47
    name = 'authentic2.idp.saml'
src/authentic2/models.py
277 277

  
278 278
    def __unicode__(self):
279 279
        return self.name
280

  
281
    def to_json(self, user=None):
282
        if user:
283
            roles = user.roles_and_parents().filter(service=self)
284
        else:
285
            roles = self.roles.all()
286
        return {
287
            'name': self.name,
288
            'slug': self.slug,
289
            'ou': unicode(self.ou) if self.ou else None,
290
            'roles': [role.to_json() for role in roles],
291
        }
src/authentic2/tests.py
651 651

  
652 652
        def f():
653 653
            return random.random()
654
        def f2(a, b):
655
            return a
654 656
        # few chances the same value comme two times in a row
655 657
        self.assertNotEquals(f(), f())
656 658

  
......
669 671
        # null timeout, no cache
670 672
        h = GlobalCache(timeout=0)(f)
671 673
        self.assertNotEquals(h(), h())
674
        # vary on second arg
675
        i = GlobalCache(hostname_vary=False, args=(1,))(f2)
676
        for a in range(1, 10):
677
            self.assertEquals(i(a, 1), 1)
678
        for a in range(2, 10):
679
            self.assertEquals(i(a, a), a)
680

  
672 681

  
673 682
    def test_django_cache(self):
674 683
        client = Client()
src/authentic2/urls.py
19 19
    url(r'^logout/$', 'logout', name='auth_logout'),
20 20
    url(r'^redirect/(.*)', 'redirect', name='auth_redirect'),
21 21
    url(r'^accounts/', include('authentic2.profile_urls')),
22
    url(r'^user_info/', 'user_info', name='user_info'),
22 23
)
23 24

  
24 25
not_homepage_patterns += patterns('',
src/authentic2/views.py
31 31
from django.contrib.auth.decorators import login_required
32 32
from django.db.models.fields import FieldDoesNotExist
33 33
from django.db.models.query import Q
34
from django.views.decorators.vary import vary_on_headers
34 35

  
35 36

  
36 37
# FIXME: this decorator has nothing to do with an idp, should be moved in the
......
509 510
    messages.warning(request, 'Un warning')
510 511
    messages.error(request, 'Une erreur')
511 512
    return HttpResponseRedirect(next_url)
513

  
514
@vary_on_headers('Cookie', 'Origin', 'Referer')
515
@decorators.json
516
def user_info(request):
517
    if request.user.is_anonymous():
518
        return {}
519
    return request.user.to_json()
520

  
521

  
512
-