Project

General

Profile

0001-api-accept-get-update_or_create-parameter-to-user-an.patch

Benjamin Dauvergne, 21 May 2019 01:40 PM

Download (9.49 KB)

View differences:

Subject: [PATCH] api: accept get/update_or_create parameter to user and role
 creation endpoint (fixes #22376)

 src/authentic2/api_mixins.py | 99 ++++++++++++++++++++++++++++++++++++
 src/authentic2/api_views.py  |  8 +--
 tests/test_api.py            | 65 ++++++++++++++++++++---
 3 files changed, 162 insertions(+), 10 deletions(-)
 create mode 100644 src/authentic2/api_mixins.py
src/authentic2/api_mixins.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2018 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.db import transaction
18

  
19
from rest_framework.serializers import raise_errors_on_nested_writes
20
from rest_framework.utils import model_meta
21

  
22

  
23
class GetOrCreateModelSerializer(object):
24
    def get_or_create(self, keys, validated_data):
25
        raise_errors_on_nested_writes('get_or_create', self, validated_data)
26

  
27
        ModelClass = self.Meta.model
28

  
29
        # Remove many-to-many relationships from validated_data.
30
        # They are not valid arguments to the default `.create()` method,
31
        # as they require that the instance has already been saved.
32
        info = model_meta.get_field_info(ModelClass)
33
        many_to_many = {}
34
        for field_name, relation_info in info.relations.items():
35
            if relation_info.to_many and (field_name in validated_data):
36
                many_to_many[field_name] = validated_data.pop(field_name)
37

  
38
        kwargs = {}
39
        defaults = kwargs['defaults'] = {}
40
        missing_keys = set(keys) - set(validated_data)
41
        if missing_keys:
42
            raise TypeError('Keys %s are missing' % missing_keys)
43
        for key, value in validated_data.items():
44
            if key in keys:
45
                kwargs[key] = value
46
            else:
47
                defaults[key] = value
48
        with transaction.atomic():
49
            instance, created = self.Meta.model.objects.get_or_create(**kwargs)
50
            if many_to_many and created:
51
                self.update(instance, many_to_many)
52
        return instance
53

  
54
    def update_or_create(self, keys, validated_data):
55
        raise_errors_on_nested_writes('update_or_create', self, validated_data)
56

  
57
        ModelClass = self.Meta.model
58

  
59
        # Remove many-to-many relationships from validated_data.
60
        # They are not valid arguments to the default `.create()` method,
61
        # as they require that the instance has already been saved.
62
        info = model_meta.get_field_info(ModelClass)
63
        many_to_many = {}
64
        for field_name, relation_info in info.relations.items():
65
            if relation_info.to_many and (field_name in validated_data):
66
                many_to_many[field_name] = validated_data.pop(field_name)
67

  
68
        kwargs = {}
69
        defaults = kwargs['defaults'] = {}
70
        missing_keys = set(keys) - set(validated_data)
71
        if missing_keys:
72
            raise TypeError('Keys %s are missing' % missing_keys)
73
        for key, value in validated_data.items():
74
            if key in keys:
75
                kwargs[key] = value
76
            else:
77
                defaults[key] = value
78
        with transaction.atomic():
79
            instance, created = self.Meta.model.objects.get_or_create(**kwargs)
80
            if many_to_many or not created:
81
                self.update(instance, validated_data)
82
        return instance
83

  
84
    def create(self, validated_data):
85
        try:
86
            keys = self.context['view'].request.GET.getlist('get_or_create')
87
        except Exception:
88
            pass
89
        else:
90
            if keys:
91
                return self.get_or_create(keys, validated_data)
92
        try:
93
            keys = self.context['view'].request.GET.getlist('update_or_create')
94
        except Exception:
95
            pass
96
        else:
97
            if keys:
98
                return self.update_or_create(keys, validated_data)
99
        return super(GetOrCreateModelSerializer, self).create(validated_data)
src/authentic2/api_views.py
46 46

  
47 47
from .passwords import get_password_checker
48 48
from .custom_user.models import User
49
from . import utils, decorators, attribute_kinds, app_settings, hooks
49
from . import (utils, decorators, attribute_kinds, app_settings, hooks,
50
               api_mixins)
50 51
from .models import Attribute, PasswordReset, Service
51 52
from .a2_rbac.utils import get_default_ou
52 53

  
......
321 322
    return request.user.to_json()
322 323

  
323 324

  
324
class BaseUserSerializer(serializers.ModelSerializer):
325
class BaseUserSerializer(api_mixins.GetOrCreateModelSerializer,
326
                         serializers.ModelSerializer):
325 327
    ou = serializers.SlugRelatedField(
326 328
        queryset=get_ou_model().objects.all(),
327 329
        slug_field='slug',
......
490 492
        exclude = ('date_joined', 'user_permissions', 'groups', 'last_login')
491 493

  
492 494

  
493
class RoleSerializer(serializers.ModelSerializer):
495
class RoleSerializer(api_mixins.GetOrCreateModelSerializer, serializers.ModelSerializer):
494 496
    ou = serializers.SlugRelatedField(
495 497
        many=False,
496 498
        required=False,
tests/test_api.py
22 22
import uuid
23 23

  
24 24

  
25
from django.core.urlresolvers import reverse
25
from django.contrib.auth.hashers import check_password
26 26
from django.contrib.auth import get_user_model
27 27
from django.contrib.contenttypes.models import ContentType
28
from authentic2.a2_rbac.utils import get_default_ou
29
from django_rbac.utils import get_role_model, get_ou_model
30
from django_rbac.models import SEARCH_OP
31
from authentic2.models import Service
32 28
from django.core import mail
33
from django.contrib.auth.hashers import check_password
29
from django.core.urlresolvers import reverse
34 30

  
35
from authentic2_idp_oidc.models import OIDCClient
31
from django_rbac.models import SEARCH_OP
32
from django_rbac.utils import get_role_model, get_ou_model
33

  
34
from authentic2.a2_rbac.models import Role
35
from authentic2.a2_rbac.utils import get_default_ou
36
from authentic2.models import Service
36 37

  
37 38
from utils import login, basic_authorization_header, get_link_from_mail
38 39

  
39 40
pytestmark = pytest.mark.django_db
40 41

  
42
User = get_user_model()
43

  
41 44

  
42 45
def test_api_user_simple(logged_app):
43 46
    resp = logged_app.get('/api/user/')
......
1146 1149
    assert response.json['checks'][3]['result'] is True
1147 1150
    assert response.json['checks'][4]['label'] == 'must contain "ok"'
1148 1151
    assert response.json['checks'][4]['result'] is True
1152

  
1153

  
1154
def test_api_users_get_or_create(settings, app, admin):
1155
    app.authorization = ('Basic', (admin.username, admin.username))
1156
    # test missing first_name
1157
    payload = {
1158
        'email': 'john.doe@example.net',
1159
        'first_name': 'John',
1160
        'last_name': 'Doe',
1161
    }
1162
    resp = app.post_json('/api/users/?get_or_create=email', params=payload, status=201)
1163
    id = resp.json['id']
1164
    assert User.objects.get(id=id).first_name == 'John'
1165
    assert User.objects.get(id=id).last_name == 'Doe'
1166

  
1167
    resp = app.post_json('/api/users/?get_or_create=email', params=payload, status=201)
1168
    assert id == resp.json['id']
1169
    assert User.objects.get(id=id).first_name == 'John'
1170
    assert User.objects.get(id=id).last_name == 'Doe'
1171

  
1172
    payload['first_name'] = 'Jane'
1173
    resp = app.post_json('/api/users/?update_or_create=email', params=payload, status=201)
1174
    assert id == resp.json['id']
1175
    assert User.objects.get(id=id).first_name == 'Jane'
1176
    assert User.objects.get(id=id).last_name == 'Doe'
1177

  
1178

  
1179
def test_api_roles_get_or_create(settings, ou1, app, admin):
1180
    app.authorization = ('Basic', (admin.username, admin.username))
1181
    # test missing first_name
1182
    payload = {
1183
        'ou_slug': 'ou1',
1184
        'name': 'Role 1',
1185
        'slug': 'role-1',
1186
    }
1187
    resp = app.post_json('/api/roles/?get_or_create=slug', params=payload, status=201)
1188
    uuid = resp.json['uuid']
1189
    assert Role.objects.get(uuid=uuid).name == 'Role 1'
1190
    assert Role.objects.get(uuid=uuid).slug == 'role-1'
1191

  
1192
    resp = app.post_json('/api/roles/?get_or_create=slug', params=payload, status=201)
1193
    assert uuid == resp.json['uuid']
1194

  
1195
    payload['name'] = 'Role 2'
1196
    resp = app.post_json('/api/roles/?update_or_create=slug', params=payload, status=201)
1197
    assert uuid == resp.json['uuid']
1198
    assert Role.objects.get(uuid=uuid).name == 'Role 2'
1199
    assert Role.objects.get(uuid=uuid).slug == 'role-1'
1149
-