Projet

Général

Profil

0009-api-add-endpoints-to-manage-role-inheritance-62013.patch

Benjamin Dauvergne, 13 mai 2022 17:27

Télécharger (21,3 ko)

Voir les différences:

Subject: [PATCH 9/9] api: add endpoints to manage role inheritance (#62013)

 src/authentic2/api_urls.py  |  10 ++
 src/authentic2/api_views.py | 137 ++++++++++++++++-
 tests/api/test_roles.py     | 297 ++++++++++++++++++++++++++++++++++++
 3 files changed, 440 insertions(+), 4 deletions(-)
 create mode 100644 tests/api/test_roles.py
src/authentic2/api_urls.py
42 42
        api_views.role_memberships,
43 43
        name='a2-api-role-members',
44 44
    ),
45
    url(
46
        r'^roles/(?P<role_uuid>[0-9a-z]{32})/parents/$',
47
        api_views.roles_parents,
48
        name='a2-api-role-parents',
49
    ),
50
    url(
51
        r'^roles/(?P<role_uuid>[\w+]*)/relationships/parents/$',
52
        api_views.roles_parents_relationships,
53
        name='a2-api-role-parents-relationships',
54
    ),
45 55
    url(r'^check-password/$', api_views.check_password, name='a2-api-check-password'),
46 56
    url(r'^validate-password/$', api_views.validate_password, name='a2-api-validate-password'),
47 57
    url(r'^address-autocomplete/$', api_views.address_autocomplete, name='a2-api-address-autocomplete'),
src/authentic2/api_views.py
40 40
from requests.exceptions import RequestException
41 41
from rest_framework import authentication, pagination, permissions, serializers, status
42 42
from rest_framework.authentication import SessionAuthentication
43
from rest_framework.exceptions import AuthenticationFailed, PermissionDenied, ValidationError
43
from rest_framework.exceptions import AuthenticationFailed, ErrorDetail, PermissionDenied, ValidationError
44 44
from rest_framework.fields import CreateOnlyDefault
45 45
from rest_framework.filters import BaseFilterBackend
46 46
from rest_framework.generics import GenericAPIView
......
54 54
from authentic2.compat.drf import action
55 55

  
56 56
from . import api_mixins, app_settings, decorators, hooks
57
from .a2_rbac.models import OrganizationalUnit, Role
57
from .a2_rbac.models import OrganizationalUnit, Role, RoleParenting
58 58
from .a2_rbac.utils import get_default_ou
59 59
from .custom_user.models import Profile, ProfileType, User
60 60
from .journal_event_types import UserLogin, UserRegistration
61 61
from .models import Attribute, PasswordReset, Service
62 62
from .passwords import get_password_checker
63 63
from .utils import misc as utils_misc
64
from .utils.api import DjangoRBACPermission, NaturalKeyRelatedField
64 65
from .utils.lookups import Unaccent
65 66

  
66 67
# Retro-compatibility with older Django versions
......
918 919
    filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
919 920
    filterset_class = RolesFilter
920 921
    lookup_field = 'uuid'
922
    queryset = Role.objects.all()
921 923

  
922
    def get_queryset(self):
923
        return self.request.user.filter_by_perm('a2_rbac.view_role', Role.objects.all())
924
    def filter_queryset(self, queryset):
925
        queryset = super().filter_queryset(queryset)
926
        return self.request.user.filter_by_perm('a2_rbac.view_role', queryset)
924 927

  
925 928
    def perform_destroy(self, instance):
926 929
        if not self.request.user.has_perm(perm='a2_rbac.delete_role', obj=instance):
......
1196 1199
role_memberships = RoleMembershipsAPI.as_view()
1197 1200

  
1198 1201

  
1202
class PublikMixin:
1203
    def finalize_response(self, request, response, *args, **kwargs):
1204
        '''Adapt error response to Publik schema'''
1205
        response = super().finalize_response(request, response, *args, **kwargs)
1206
        if isinstance(response.data, dict) and 'err' not in response.data:
1207
            if list(response.data.keys()) == ['detail'] and isinstance(response.data['detail'], ErrorDetail):
1208
                response.data = {
1209
                    'err': 1,
1210
                    'err_class': response.data['detail'].code,
1211
                    'err_desc': str(response.data['detail']),
1212
                }
1213
            elif 'errors' in response.data:
1214
                response.data['err'] = 1
1215
                response.data.pop('result', None)
1216
                response.data['err_desc'] = response.data.pop('errors')
1217
        return response
1218

  
1219

  
1220
class RoleParentSerializer(RoleSerializer):
1221
    direct = serializers.BooleanField(read_only=True)
1222

  
1223
    class Meta(RoleSerializer.Meta):
1224
        fields = RoleSerializer.Meta.fields + ('direct',)
1225

  
1226

  
1227
class RolesParentsAPI(PublikMixin, GenericAPIView):
1228
    permission_classes = [
1229
        DjangoRBACPermission(
1230
            perms_map={
1231
                'GET': [],
1232
            },
1233
            object_perms_map={
1234
                'GET': ['a2_rbac.view_role'],
1235
            },
1236
        )
1237
    ]
1238
    serializer_class = RoleParentSerializer
1239
    queryset = Role.objects.all()
1240

  
1241
    def get(self, request, *, role_uuid, **kwargs):
1242
        direct = None if 'all' in self.request.GET else True
1243
        role = get_object_or_404(Role, uuid=role_uuid)
1244
        self.check_object_permissions(self.request, role)
1245
        qs = self.get_queryset()
1246
        qs = self.queryset.filter(pk=role.pk)
1247
        qs = qs.parents(include_self=False, annotate=True, direct=direct)
1248
        qs = request.user.filter_by_perm('a2_rbac.search_role', qs)
1249
        qs = qs.order_by('id')
1250
        serializer = self.get_serializer(qs, many=True)
1251
        return Response({'err': 0, 'data': serializer.data})
1252

  
1253

  
1254
roles_parents = RolesParentsAPI.as_view()
1255

  
1256

  
1257
class RoleParentingSerializer(serializers.ModelSerializer):
1258
    parent = NaturalKeyRelatedField(queryset=Role.objects.all())
1259
    direct = serializers.BooleanField(read_only=True)
1260

  
1261
    class Meta:
1262
        model = RoleParenting
1263
        fields = [
1264
            'parent',
1265
            'direct',
1266
        ]
1267

  
1268

  
1269
class RolesParentsRelationshipsAPI(PublikMixin, GenericAPIView):
1270
    permission_classes = [
1271
        DjangoRBACPermission(
1272
            perms_map={
1273
                'GET': [],
1274
                'POST': [],
1275
                'DELETE': [],
1276
            },
1277
            object_perms_map={
1278
                'GET': ['a2_rbac.view_role'],
1279
                'POST': ['a2_rbac.manage_members_role'],
1280
                'DELETE': ['a2_rbac.manage_members_role'],
1281
            },
1282
        )
1283
    ]
1284
    serializer_class = RoleParentingSerializer
1285
    queryset = RoleParenting.alive.all()
1286

  
1287
    def filter_queryset(self, queryset):
1288
        if 'all' in self.request.GET:
1289
            qs = queryset.filter(child__uuid=self.kwargs['role_uuid'])
1290
        else:
1291
            qs = queryset.filter(child__uuid=self.kwargs['role_uuid'], direct=True)
1292
        qs = qs.filter(parent__in=self.request.user.filter_by_perm('a2_rbac.view_role', Role.objects.all()))
1293
        qs = qs.order_by('id')
1294
        return qs
1295

  
1296
    def get(self, request, *, role_uuid, **kwargs):
1297
        role = get_object_or_404(Role, uuid=role_uuid)
1298
        self.check_object_permissions(self.request, role)
1299
        return self.list()
1300

  
1301
    def list(self):
1302
        queryset = self.filter_queryset(self.get_queryset())
1303
        serializer = self.get_serializer(queryset, many=True)
1304
        return Response({'err': 0, 'data': serializer.data})
1305

  
1306
    def post(self, request, *, role_uuid, **kwargs):
1307
        serializer = self.get_serializer(data=request.data)
1308
        serializer.is_valid(raise_exception=True)
1309
        parent = serializer.validated_data['parent']
1310
        self.check_object_permissions(self.request, parent)
1311
        child = get_object_or_404(Role.objects.all(), uuid=role_uuid)
1312
        child.add_parent(parent)
1313
        return self.list()
1314

  
1315
    def delete(self, request, *, role_uuid, **kwargs):
1316
        serializer = self.get_serializer(data=request.data)
1317
        serializer.is_valid(raise_exception=True)
1318
        parent = serializer.validated_data['parent']
1319
        self.check_object_permissions(self.request, parent)
1320
        role = get_object_or_404(Role, uuid=role_uuid)
1321
        role.remove_parent(parent)
1322
        return self.list()
1323

  
1324

  
1325
roles_parents_relationships = RolesParentsRelationshipsAPI.as_view()
1326

  
1327

  
1199 1328
class BaseOrganizationalUnitSerializer(serializers.ModelSerializer):
1200 1329
    slug = serializers.SlugField(
1201 1330
        required=False,
tests/api/test_roles.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 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
import pytest
18

  
19
from authentic2.a2_rbac.models import Role
20
from authentic2.api_views import OrganizationalUnit as OU
21
from authentic2.api_views import RoleParentingSerializer, RoleParentSerializer
22
from tests.utils import scoped_db_fixture
23

  
24

  
25
@scoped_db_fixture(scope='module', autouse=True)
26
def roles():
27
    class Namespace:
28
        parent = Role.objects.create(name='parent', uuid='a' * 32)
29
        child = Role.objects.create(name='child', uuid='1' * 32)
30
        parent.add_child(child)
31
        grandchild = Role.objects.create(name='grandchild', uuid='2' * 32)
32
        child.add_child(grandchild)
33

  
34
    return Namespace
35

  
36

  
37
class TestSerializer:
38
    def test_role_parent(self):
39
        ou = OU(name='OU', slug='ou', uuid='2' * 32)
40
        role = Role(uuid='1' * 32, name='Role', slug='role', ou=ou)
41
        role.direct = True
42
        assert RoleParentSerializer(role).data == {
43
            'uuid': '1' * 32,
44
            'name': 'Role',
45
            'slug': 'role',
46
            'direct': True,
47
            'ou': 'ou',
48
        }
49

  
50
    def test_role_parenting(self, db, roles):
51
        assert RoleParentingSerializer(roles.parent.child_relation.first()).data == {
52
            'parent': {'service': None, 'slug': 'parent', 'ou': None, 'name': 'parent', 'uuid': 'a' * 32},
53
            'direct': True,
54
        }
55

  
56

  
57
class TestViews:
58
    class TestParents:
59
        def test_not_authenticated(self, db, app, admin, roles):
60
            app.get('/api/roles/%s/parents/' % roles.grandchild.uuid, status=401)
61

  
62
        class TestAuthenticated:
63
            @pytest.fixture
64
            def app(self, app, admin):
65
                app.authorization = ('Basic', (admin.username, admin.username))
66
                return app
67

  
68
            def test_default(self, app, roles):
69
                resp = app.get('/api/roles/%s/parents/' % roles.grandchild.uuid)
70
                assert resp.json == {
71
                    'err': 0,
72
                    'data': [
73
                        {
74
                            'uuid': '1' * 32,
75
                            'name': 'child',
76
                            'slug': 'child',
77
                            'ou': None,
78
                            'direct': True,
79
                        }
80
                    ],
81
                }
82

  
83
            def test_all(self, app, roles):
84
                resp = app.get('/api/roles/%s/parents/?all' % roles.grandchild.uuid)
85
                assert resp.json == {
86
                    'err': 0,
87
                    'data': [
88
                        {
89
                            'uuid': 'a' * 32,
90
                            'name': 'parent',
91
                            'slug': 'parent',
92
                            'ou': None,
93
                            'direct': False,
94
                        },
95
                        {
96
                            'uuid': '1' * 32,
97
                            'name': 'child',
98
                            'slug': 'child',
99
                            'ou': None,
100
                            'direct': True,
101
                        },
102
                    ],
103
                }
104

  
105
            def test_permission(self, app, simple_user, roles):
106
                role = Role.objects.create(name='admin')
107
                role.members.add(simple_user)
108
                app.authorization = ('Basic', (simple_user.username, simple_user.username))
109
                app.get('/api/roles/%s/parents/' % roles.grandchild.uuid, status=403)
110
                role.add_permission(roles.grandchild, 'view')
111
                resp = app.get('/api/roles/%s/parents/?all' % roles.grandchild.uuid, status=200)
112
                assert not resp.json['data']
113
                role.add_permission(roles.child, 'view')
114
                resp = app.get('/api/roles/%s/parents/?all' % roles.grandchild.uuid, status=200)
115
                assert len(resp.json['data']) == 1
116
                role.add_permission(roles.parent, 'view')
117
                resp = app.get('/api/roles/%s/parents/?all' % roles.grandchild.uuid, status=200)
118
                assert len(resp.json['data']) == 2
119

  
120
    class TestParentsRelationships:
121
        def test_not_authenticated(self, db, app, admin, roles):
122
            app.get('/api/roles/%s/relationships/parents/' % roles.parent.uuid, status=401)
123

  
124
        class TestAuthenticated:
125
            @pytest.fixture
126
            def app(self, app, admin):
127
                app.authorization = ('Basic', (admin.username, admin.username))
128
                return app
129

  
130
            def test_default(self, app, roles):
131
                resp = app.get('/api/roles/%s/relationships/parents/' % roles.grandchild.uuid)
132
                assert resp.json == {
133
                    'err': 0,
134
                    'data': [
135
                        {
136
                            'parent': {
137
                                'uuid': '1' * 32,
138
                                'name': 'child',
139
                                'slug': 'child',
140
                                'ou': None,
141
                                'service': None,
142
                            },
143
                            'direct': True,
144
                        }
145
                    ],
146
                }
147

  
148
            def test_all(self, app, roles):
149
                resp = app.get('/api/roles/%s/relationships/parents/?all' % roles.grandchild.uuid)
150
                assert resp.json == {
151
                    'err': 0,
152
                    'data': [
153
                        {
154
                            'parent': {
155
                                'uuid': '1' * 32,
156
                                'name': 'child',
157
                                'slug': 'child',
158
                                'ou': None,
159
                                'service': None,
160
                            },
161
                            'direct': True,
162
                        },
163
                        {
164
                            'parent': {
165
                                'uuid': 'a' * 32,
166
                                'name': 'parent',
167
                                'slug': 'parent',
168
                                'ou': None,
169
                                'service': None,
170
                            },
171
                            'direct': False,
172
                        },
173
                    ],
174
                }
175

  
176
            @pytest.mark.parametrize(
177
                'role_description',
178
                [{'uuid': 'a' * 32}, {'slug': 'parent'}, {'name': 'parent'}],
179
                ids=['by_uuid', 'by_slug', 'by_name'],
180
            )
181
            def test_create(self, role_description, app, roles):
182
                assert set(roles.parent.children(include_self=False, direct=True)) == {roles.child}
183
                resp = app.post_json(
184
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
185
                    params={'parent': role_description},
186
                )
187
                assert resp.json == {
188
                    'err': 0,
189
                    'data': [
190
                        {
191
                            'parent': {
192
                                'uuid': '1' * 32,
193
                                'name': 'child',
194
                                'slug': 'child',
195
                                'ou': None,
196
                                'service': None,
197
                            },
198
                            'direct': True,
199
                        },
200
                        {
201
                            'parent': {
202
                                'uuid': 'a' * 32,
203
                                'name': 'parent',
204
                                'slug': 'parent',
205
                                'ou': None,
206
                                'service': None,
207
                            },
208
                            'direct': True,
209
                        },
210
                    ],
211
                }
212
                assert set(roles.parent.children(include_self=False, direct=True)) == {
213
                    roles.child,
214
                    roles.grandchild,
215
                }
216

  
217
            def test_delete(self, app, roles):
218
                assert set(roles.parent.children(include_self=False, direct=True)) == {roles.child}
219
                resp = app.delete_json(
220
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
221
                    params={'parent': {'uuid': roles.child.uuid}},
222
                )
223
                assert resp.json == {'err': 0, 'data': []}
224
                assert not set(roles.grandchild.children(include_self=False, direct=True))
225

  
226
        class TestPermission:
227
            @pytest.fixture
228
            def user(self, simple_user):
229
                return simple_user
230

  
231
            @pytest.fixture
232
            def admin_role(self, user):
233
                role = Role.objects.create(name='admin')
234
                role.members.add(user)
235
                return role
236

  
237
            @pytest.fixture
238
            def app(self, app, user):
239
                app.authorization = ('Basic', (user.username, user.username))
240
                return app
241

  
242
            def test_list(self, app, admin_role, roles):
243
                resp = app.get('/api/roles/%s/relationships/parents/' % roles.grandchild.uuid, status=403)
244
                assert resp.json == {
245
                    'err': 1,
246
                    'err_class': 'permission_denied',
247
                    'err_desc': 'You do not have permission to perform this action.',
248
                }
249

  
250
                admin_role.add_permission(roles.grandchild, 'view')
251
                resp = app.get('/api/roles/%s/relationships/parents/?all' % roles.grandchild.uuid, status=200)
252
                assert not resp.json['data']
253
                admin_role.add_permission(roles.child, 'view')
254
                resp = app.get('/api/roles/%s/relationships/parents/?all' % roles.grandchild.uuid, status=200)
255
                assert len(resp.json['data']) == 1
256
                admin_role.add_permission(roles.parent, 'view')
257
                resp = app.get('/api/roles/%s/relationships/parents/?all' % roles.grandchild.uuid, status=200)
258
                assert len(resp.json['data']) == 2
259

  
260
            def test_create(self, app, admin_role, roles):
261
                app.post_json(
262
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
263
                    params={'parent': {'uuid': roles.parent.uuid}},
264
                    status=403,
265
                )
266
                admin_role.add_permission(roles.grandchild, 'manage_members')
267
                app.post_json(
268
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
269
                    params={'parent': {'uuid': roles.parent.uuid}},
270
                    status=403,
271
                )
272
                admin_role.add_permission(roles.parent, 'manage_members')
273
                app.post_json(
274
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
275
                    params={'parent': {'uuid': roles.parent.uuid}},
276
                    status=200,
277
                )
278

  
279
            def test_delete(self, app, admin_role, roles):
280
                app.delete_json(
281
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
282
                    params={'parent': {'uuid': roles.child.uuid}},
283
                    status=403,
284
                )
285
                admin_role.add_permission(roles.grandchild, 'manage_members')
286
                app.delete_json(
287
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
288
                    params={'parent': {'uuid': roles.child.uuid}},
289
                    status=403,
290
                )
291
                admin_role.add_permission(roles.child, 'manage_members')
292
                app.delete_json(
293
                    '/api/roles/%s/relationships/parents/' % roles.grandchild.uuid,
294
                    params={'parent': {'uuid': roles.child.uuid}},
295
                    status=200,
296
                )
297
                assert not set(roles.grandchild.parents(include_self=False, direct=True))
0
-