Projet

Général

Profil

0001-data_transfer-validate-uniqueness-of-roles-41342.patch

Benjamin Dauvergne, 10 avril 2020 15:25

Télécharger (15,1 ko)

Voir les différences:

Subject: [PATCH 1/2] data_transfer: validate uniqueness of roles (#41342)

 src/authentic2/data_transfer.py      | 80 +++++++++++++++++++++-------
 src/authentic2/manager/views.py      | 12 ++---
 src/authentic2/utils/lazy.py         | 26 +++++++++
 tests/test_data_transfer.py          | 33 +++++++++---
 tests/test_import_export_site_cmd.py |  9 ++--
 5 files changed, 124 insertions(+), 36 deletions(-)
 create mode 100644 src/authentic2/utils/lazy.py
src/authentic2/data_transfer.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from functools import wraps
18

  
19
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
17 20
from django.contrib.contenttypes.models import ContentType
21
from django.utils.translation import ugettext_lazy as _
22
from django.utils.text import format_lazy
18 23

  
19 24
from django_rbac.models import Operation
20 25
from django_rbac.utils import (
21 26
    get_ou_model, get_role_model, get_role_parenting_model, get_permission_model)
27

  
28
from authentic2.decorators import errorcollector
22 29
from authentic2.a2_rbac.models import RoleAttribute
30
from authentic2.utils.lazy import lazy_join
23 31

  
24 32

  
25 33
def update_model(obj, d):
26 34
    for attr, value in d.items():
27 35
        setattr(obj, attr, value)
36
    errors = {}
37
    with errorcollector(errors):
38
        if hasattr(obj, 'validate'):
39
            obj.validate()
40

  
41
    with errorcollector(errors):
42
        if hasattr(obj, 'validate_unique'):
43
            obj.validate_unique()
44
    if errors:
45
        errorlist = []
46
        for key, messages in list(errors.items()):
47
            if key == NON_FIELD_ERRORS:
48
                errorlist.extend(messages)
49
            else:
50
                value = getattr(obj, key)
51

  
52
                def error_list(messages):
53
                    for message in messages:
54
                        if isinstance(message, ValidationError):
55
                            yield message.message
56
                        else:
57
                            yield message
58
                for message in error_list(messages):
59
                    errorlist.append(format_lazy(u'{}="{}": {}', obj.__class__._meta.get_field(key).verbose_name, value, message))
60
        raise ValidationError(errorlist)
28 61
    obj.save()
29 62

  
30 63

  
......
128 161
        self.role_attributes_update = role_attributes_update
129 162

  
130 163

  
131
class DataImportError(Exception):
132
    pass
133

  
134

  
135 164
class RoleDeserializer(object):
136 165
    def __init__(self, d, import_context):
137 166
        self._import_context = import_context
......
151 180
            else:
152 181
                self._role_d[key] = value
153 182

  
183
    def wraps_validationerror(func):
184
        @wraps(func)
185
        def f(self, *args, **kwargs):
186
            try:
187
                return func(self, *args, **kwargs)
188
            except ValidationError as e:
189
                raise ValidationError(_('Role "%s": %s') % (
190
                    self._role_d.get('name', self._role_d.get('slug')),
191
                    lazy_join(', ', [v.message for v in e.error_list])))
192
        return f
193

  
194
    @wraps_validationerror
154 195
    def deserialize(self):
155 196
        ou_d = self._role_d['ou']
156 197
        has_ou = bool(ou_d)
157 198
        ou = None if not has_ou else search_ou(ou_d)
158 199
        if has_ou and not ou:
159
            raise DataImportError(
160
                "Can't import role because missing Organizational Unit: %s" % ou_d)
200
            raise ValidationError(_("Can't import role because missing Organizational Unit: %s") % ou_d)
161 201

  
162 202
        kwargs = self._role_d.copy()
163 203
        kwargs.pop('ou', None)
......
172 212
            update_model(self._obj, kwargs)
173 213
        else:  # Create role
174 214
            if 'uuid' in kwargs and not kwargs['uuid']:
175
                raise DataImportError("Cannot import role '%s' with empty uuid"
176
                                      % kwargs.get('name'))
215
                raise ValidationError(_("Cannot import role '%s' with empty uuid") % kwargs.get('name'))
177 216
            self._obj = get_role_model().objects.create(**kwargs)
178 217
            status = 'created'
179 218

  
......
184 223
        self._obj.get_admin_role()
185 224
        return self._obj, status
186 225

  
226
    @wraps_validationerror
187 227
    def attributes(self):
188 228
        """ Update attributes (delete everything then create)
189 229
        """
......
199 239

  
200 240
        return created, deleted
201 241

  
242
    @wraps_validationerror
202 243
    def parentings(self):
203 244
        """ Update parentings (delete everything then create)
204 245
        """
......
212 253
            for parent_d in self._parents:
213 254
                parent = search_role(parent_d)
214 255
                if not parent:
215
                    raise DataImportError("Could not find role: %s" % parent_d)
256
                    raise ValidationError(_("Could not find parent role: %s") % parent_d)
216 257
                created.append(Parenting.objects.create(
217 258
                    child=self._obj, direct=True, parent=parent))
218 259

  
219 260
        return created, deleted
220 261

  
262
    @wraps_validationerror
221 263
    def permissions(self):
222 264
        """ Update permissions (delete everything then create)
223 265
        """
......
300 342
    result = ImportResult()
301 343

  
302 344
    if not isinstance(json_d, dict):
303
        raise DataImportError('Export file is invalid: not a dictionnary')
345
        raise ValidationError(_('Import file is invalid: not a dictionnary'))
304 346

  
305 347
    if import_context.import_ous:
306 348
        for ou_d in json_d.get('ous', []):
307 349
            result.update_ous(*import_ou(ou_d))
308 350

  
309 351
    if import_context.import_roles:
310
        roles_ds = [RoleDeserializer(role_d, import_context) for role_d in json_d.get('roles', [])
311
                    if not role_d['slug'].startswith('_')]
352
        roles_ds = []
353
        for role_d in json_d.get('roles', []):
354
            # ignore internal roles
355
            if role_d['slug'].startswith('_'):
356
                continue
357
            roles_ds.append(RoleDeserializer(role_d, import_context))
312 358

  
313 359
        for ds in roles_ds:
314 360
            result.update_roles(*ds.deserialize())
......
326 372
                result.update_permissions(*ds.permissions())
327 373

  
328 374
        if import_context.ou_delete_orphans:
329
            raise DataImportError(
330
                "Unsupported context value for ou_delete_orphans : %s" % (
331
                    import_context.ou_delete_orphans))
375
            raise ValidationError(_("Unsupported context value for ou_delete_orphans : %s") % (
376
                import_context.ou_delete_orphans))
332 377

  
333 378
        if import_context.role_delete_orphans:
334 379
            # FIXME : delete each role that is in DB but not in the export
335
            raise DataImportError(
336
                "Unsupported context value for role_delete_orphans : %s" % (
337
                    import_context.role_delete_orphans))
380
            raise ValidationError(_("Unsupported context value for role_delete_orphans : %s") % (
381
                import_context.role_delete_orphans))
338 382

  
339 383
    return result
src/authentic2/manager/views.py
17 17
import json
18 18
import inspect
19 19

  
20
from django.core.exceptions import PermissionDenied
20
from django.core.exceptions import PermissionDenied, ValidationError
21 21
from django.db import transaction
22 22
from django.views.generic.base import ContextMixin
23 23
from django.views.generic import (FormView, UpdateView, CreateView, DeleteView, TemplateView,
......
40 40

  
41 41
from django_rbac.utils import get_ou_model
42 42

  
43
from authentic2.data_transfer import export_site, import_site, DataImportError, ImportContext
43
from authentic2.data_transfer import export_site, import_site, ImportContext
44 44
from authentic2.forms.profile import modelform_factory
45 45
from authentic2.utils import redirect, batch_queryset
46 46
from authentic2.decorators import json as json_view
......
708 708

  
709 709
    def form_valid(self, form):
710 710
        try:
711
            json_site = json.loads(force_text(
712
                    self.request.FILES['site_json'].read()))
711
            json_site = json.loads(
712
                force_text(self.request.FILES['site_json'].read()))
713 713
        except ValueError:
714 714
            form.add_error('site_json', _('File is not in the expected JSON format.'))
715 715
            return self.form_invalid(form)
......
717 717
        try:
718 718
            with transaction.atomic():
719 719
                import_site(json_site, ImportContext())
720
        except DataImportError as e:
721
            form.add_error('site_json', six.text_type(e))
720
        except ValidationError as e:
721
            form.add_error('site_json', e)
722 722
            return self.form_invalid(form)
723 723

  
724 724
        return super(SiteImportView, self).form_valid(form)
src/authentic2/utils/lazy.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2020 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.utils.text import format_lazy
18

  
19

  
20
def lazy_join(join, args):
21
    if not args:
22
        return ''
23

  
24
    fstring = '{}' + ''.join([join + '{}'] * (len(args) - 1))
25
    return format_lazy(fstring, *args)
26

  
tests/test_data_transfer.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from django_rbac.utils import get_role_model, get_ou_model
18 17
import pytest
19 18

  
19
from django.core.exceptions import ValidationError
20

  
21
from django_rbac.utils import get_role_model, get_ou_model
22

  
20 23
from authentic2.a2_rbac.models import RoleParenting
21 24
from authentic2.data_transfer import (
22 25
    export_site,
23 26
    ExportContext,
24
    DataImportError,
25 27
    export_roles,
26 28
    import_site,
27 29
    export_ous,
......
159 161
        'uuid': get_hex_uuid(), 'name': 'some role', 'description': 'role description',
160 162
        'slug': 'some-role', 'ou': {'slug': 'some-ou'}, 'service': None},
161 163
        ImportContext())
162
    with pytest.raises(DataImportError):
164
    with pytest.raises(ValidationError):
163 165
        rd.deserialize()
164 166

  
165 167

  
......
255 257
        'uuid': get_hex_uuid(), 'ou': None, 'service': None}
256 258
    rd = RoleDeserializer(child_role_dict, ImportContext())
257 259
    rd.deserialize()
258
    with pytest.raises(DataImportError) as excinfo:
260
    with pytest.raises(ValidationError) as excinfo:
259 261
        rd.parentings()
260 262

  
261
    assert "Could not find role" in str(excinfo.value)
263
    assert "Could not find parent role" in str(excinfo.value)
262 264

  
263 265

  
264 266
def test_role_deserializer_permissions(db):
......
473 475
def test_import_roles_role_delete_orphans(db):
474 476
    roles = [{
475 477
        'name': 'some role', 'description': 'some role description', 'slug': '_some-role'}]
476
    with pytest.raises(DataImportError):
478
    with pytest.raises(ValidationError):
477 479
        import_site({'roles': roles}, ImportContext(role_delete_orphans=True))
478 480

  
479 481

  
......
512 514
    import_site(d, ImportContext(import_roles=False, import_ous=False))
513 515
    assert Role.objects.exclude(slug__startswith='_').count() == 0
514 516
    assert OU.objects.exclude(slug='default').count() == 0
515
    with pytest.raises(DataImportError) as e:
517
    with pytest.raises(ValidationError) as e:
516 518
        import_site(d, ImportContext(import_roles=True, import_ous=False))
517 519
    assert 'missing Organizational' in e.value.args[0]
518 520
    assert Role.objects.exclude(slug__startswith='_').count() == 0
......
540 542
    d = export_site(ExportContext(export_ous=False))
541 543
    assert 'ous' not in d
542 544

  
545

  
546
def test_role_validate_unique(db):
547
    ou = OU.objects.create(name='ou', slug='ou')
548
    Role.objects.create(name='role1', slug='role1', ou=ou)
549
    Role.objects.create(name='role2', slug='role2', ou=ou)
550

  
551
    data = {
552
        'roles': [
553
            {
554
                'name': 'role1',
555
                'slug': 'role2',
556
                'ou': {'slug': 'ou'},
557
            }
558
        ]
559
    }
560
    with pytest.raises(ValidationError, match=r'Role "role1": name="role1": Name already used'):
561
        import_site(data)
tests/test_import_export_site_cmd.py
17 17
import random
18 18
import json
19 19

  
20
from django.core.exceptions import ValidationError
21

  
20 22
from django.utils import six
21 23
from django.utils.six.moves import builtins as __builtin__
22 24
from django.core import management
......
122 124

  
123 125

  
124 126
def test_import_site_cmd_unhandled_context_option(db, monkeypatch, capsys, json_fixture):
125
    from authentic2.data_transfer import DataImportError
126

  
127 127
    content = {
128 128
        'roles': [
129 129
            {
......
138 138

  
139 139
    Role.objects.create(uuid='dqfewrvesvews2532', slug='role-slug', name='role-name')
140 140

  
141
    with pytest.raises(DataImportError):
141
    with pytest.raises(ValidationError):
142 142
        management.call_command(
143 143
            'import_site', '-o', 'role-delete-orphans', json_fixture(content))
144 144

  
......
205 205

  
206 206

  
207 207
def test_import_site_empty_uuids(db, monkeypatch, json_fixture):
208
    from authentic2.data_transfer import DataImportError
209
    with pytest.raises(DataImportError):
208
    with pytest.raises(ValidationError):
210 209
        management.call_command('import_site', json_fixture({
211 210
            'roles': [
212 211
                {
213
-