Projet

Général

Profil

0003-api-new-user-API-7862.patch

Benjamin Dauvergne, 13 novembre 2015 11:03

Télécharger (8,82 ko)

Voir les différences:

Subject: [PATCH 3/4] api: new user API (#7862)

You can list/add/change users. Security is enforced by basic authentication,
session authentication and role permissions:
- custom_user.view_user for listing,
- custom_user.add_user for creating,
- custom_user.change_user for updating,
- custom_user.delete_user for deleting.
 setup.py                    |   1 +
 src/authentic2/api_urls.py  |   1 +
 src/authentic2/api_views.py | 126 +++++++++++++++++++++++++++++++++++++++++++-
 src/authentic2/settings.py  |   9 ++++
 tox.ini                     |   4 +-
 5 files changed, 138 insertions(+), 3 deletions(-)
setup.py
118 118
        'six>=1',
119 119
        'Markdown>=2.1',
120 120
        'python-ldap',
121
        'django-filter',
121 122
      ],
122 123
      extras_require = {
123 124
          'idp-openid': ['python-openid'],
src/authentic2/api_urls.py
10 10
                       url(r'^user/$', api_views.user,
11 11
                           name='a2-api-user'),
12 12
)
13
urlpatterns += api_views.router.urls
src/authentic2/api_views.py
1 1
'''Views for Authentic2 API'''
2
import json
2 3
import smtplib
3 4

  
4 5
from django.db import models
......
11 12
from django_rbac.utils import get_ou_model
12 13

  
13 14
from rest_framework import serializers
15
from rest_framework.viewsets import ModelViewSet
16
from rest_framework.routers import SimpleRouter
14 17
from rest_framework.generics import GenericAPIView
15 18
from rest_framework.response import Response
16
from rest_framework import authentication, permissions, status
19
from rest_framework import permissions, status
20
from rest_framework.exceptions import PermissionDenied
17 21

  
18 22
from . import utils, decorators
23
from .models import Attribute, AttributeValue
19 24

  
20 25

  
21 26
class HasUserAddPermission(permissions.BasePermission):
......
83 88

  
84 89

  
85 90
class BaseRpcView(RpcMixin, GenericAPIView):
86
    authentication_classes = (authentication.BasicAuthentication,)
87 91
    permission_classes = (permissions.IsAuthenticated,
88 92
                          HasUserAddPermission)
89 93

  
......
200 204
    if request.user.is_anonymous():
201 205
        return {}
202 206
    return request.user.to_json()
207

  
208

  
209
_class_cache = {}
210

  
211

  
212
def attributes_hash(attributes):
213
    attributes = sorted(attributes, key=lambda at: at.name)
214
    return hash(tuple((at.name, at.required) for at in attributes))
215

  
216

  
217
def get_user_class():
218
    attributes = Attribute.objects.filter(kind='string')
219
    key = 'user-class-%s' % attributes_hash(attributes)
220
    if key not in _class_cache:
221
        user_class = get_user_model()
222

  
223
        class Meta:
224
            proxy = True
225
        fields = {
226
            'Meta': Meta,
227
            '__module__': user_class.__module__,
228
        }
229
        for at in attributes:
230
            def new_property(at):
231
                def get_property(self):
232
                    try:
233
                        return json.loads(
234
                            AttributeValue.objects.with_owner(self).get(attribute=at).content)
235
                    except AttributeValue.DoesNotExist:
236
                        return ''
237

  
238
                def set_property(self, value):
239
                    at.set_value(self, value)
240
                return property(get_property, set_property)
241
            fields[at.name] = new_property(at)
242
        _class_cache[key] = type('NewUserClass', (user_class,), fields)
243
    return _class_cache[key]
244

  
245

  
246
class BaseUserSerializer(serializers.ModelSerializer):
247
    ou = serializers.SlugRelatedField(
248
        queryset=get_ou_model().objects.all(),
249
        slug_field='slug',
250
        required=False, allow_null=True)
251
    date_joined = serializers.DateTimeField(read_only=True)
252
    last_login = serializers.DateTimeField(read_only=True)
253

  
254
    def check_perm(self, perm, ou):
255
        self.context['view'].check_perm(perm, ou)
256

  
257
    def create(self, validated_data):
258
        extra_field = {}
259
        for at in Attribute.objects.filter(kind='string'):
260
            if at.name in validated_data:
261
                extra_field[at.name] = validated_data.pop(at.name)
262
        self.check_perm('custom_user.add_user', validated_data.get('ou'))
263
        instance = super(BaseUserSerializer, self).create(validated_data)
264
        for key, value in extra_field.iteritems():
265
            setattr(instance, key, value)
266
        if 'password' in validated_data:
267
            instance.set_password(validated_data['password'])
268
            instance.save()
269
        return instance
270

  
271
    def update(self, instance, validated_data):
272
        extra_field = {}
273
        for at in Attribute.objects.filter(kind='string'):
274
            if at.name in validated_data:
275
                extra_field[at.name] = validated_data.pop(at.name)
276
        # Double check: to move an user from one ou into another you must be administrator of both
277
        self.check_perm('custom_user.change_user', instance.ou)
278
        self.check_perm('custom_user.change_user', validated_data.get('ou'))
279
        super(BaseUserSerializer, self).update(instance, validated_data)
280
        for key, value in extra_field.iteritems():
281
            setattr(instance, key, value)
282
        if 'password' in validated_data:
283
            instance.set_password(validated_data['password'])
284
            instance.save()
285
        return instance
286

  
287
    class Meta:
288
        model = get_user_class()
289
        exclude = ('date_joined', 'user_permissions', 'groups', 'last_login')
290

  
291

  
292
class UsersAPI(ModelViewSet):
293
    filter_fields = ['username', 'first_name', 'last_name']
294
    ordering_fields = ['username', 'first_name', 'last_name']
295

  
296
    def get_serializer_class(self):
297
        attributes = Attribute.objects.filter(kind='string')
298
        key = 'user-serializer-%s' % attributes_hash(attributes)
299
        if key not in _class_cache:
300
            attrs = {}
301
            for at in attributes:
302
                attrs[at.name] = serializers.CharField(required=at.required)
303
            _class_cache[key] = type('UserSerializer', (BaseUserSerializer,), attrs)
304
        return _class_cache[key]
305

  
306
    def get_queryset(self):
307
        User = get_user_class()
308
        return self.request.user.filter_by_perm(['custom_user.view_user'], User.objects.all())
309

  
310
    def check_perm(self, perm, ou):
311
        if ou:
312
            if not self.request.user.has_ou_perm(perm, ou):
313
                raise PermissionDenied(u'You do not have permission %s in %s' % (perm, ou))
314
        else:
315
            if not self.request.user.has_perm(perm):
316
                raise PermissionDenied(u'You do not have permission %s' % perm)
317

  
318
    def perform_destroy(self, instance):
319
        self.check_perm('custom_user.delete_user', instance.ou)
320
        super(UsersAPI, self).perform_destroy(instance)
321

  
322

  
323
router = SimpleRouter()
324
router.register(r'users', UsersAPI, base_name='a2-api-users')
src/authentic2/settings.py
259 259
# Django REST Framework
260 260
REST_FRAMEWORK = {
261 261
    'NON_FIELD_ERRORS_KEY': '__all__',
262
    'DEFAULT_AUTHENTICATION_CLASSES': (
263
        'rest_framework.authentication.BasicAuthentication',
264
        'rest_framework.authentication.SessionAuthentication',
265
    ),
266
    'DEFAULT_FILTER_BACKENDS': (
267
        'rest_framework.filters.DjangoFilterBackend',
268
        'rest_framework.filters.OrderingFilter',
269
    ),
270
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
262 271
}
263 272

  
264 273
#
tox.ini
13 13
    DJANGO_SETTINGS_MODULE=authentic2.settings
14 14
commands =
15 15
    ./getlasso.sh
16
    py.test --junitxml=django17.xml --cov-report xml --cov=src/ --cov-config .coveragerc --ignore=src/django_rbac --nomigrations src/
16
    py.test --junitxml=django17.xml --cov-report xml --cov=src/ --cov-config .coveragerc --ignore=src/django_rbac --nomigrations src/ tests/
17 17
    mv coverage.xml django17-coverage.xml
18 18
#    coverage run --source=. -a authentic2-ctl test -t src --settings=django_rbac.test_settings src/django_rbac/
19 19
usedevelop = True
......
26 26
  cssselect
27 27
  pylint==1.4.0
28 28
  astroid==1.3.2
29
  django-webtest
30
  WebTest
29 31

  
30 32
[testenv:rbac-django17]
31 33
# django.contrib.auth is not tested it does not work with our templates
32
-