Projet

Général

Profil

0001-a2_rbac-move-role-attributes-to-real-model-fields-69.patch

Valentin Deniaud, 25 octobre 2022 17:36

Télécharger (25,9 ko)

Voir les différences:

Subject: [PATCH] a2_rbac: move role attributes to real model fields (#69895)

 src/authentic2/a2_rbac/admin.py               |  5 --
 .../migrations/0034_new_role_fields.py        | 36 ++++++++
 .../migrations/0035_populate_role_fields.py   | 61 +++++++++++++
 .../migrations/0036_delete_roleattribute.py   | 16 ++++
 src/authentic2/a2_rbac/models.py              | 23 ++---
 src/authentic2/app_settings.py                |  1 -
 .../attributes_ng/sources/service_roles.py    | 71 ----------------
 src/authentic2/data_transfer.py               | 22 ++---
 src/authentic2/manager/forms.py               | 39 +--------
 tests/test_a2_rbac.py                         | 85 ++++++++++++++-----
 tests/test_data_transfer.py                   | 17 ++--
 tests/test_idp_saml2.py                       | 10 +--
 tests/test_manager.py                         | 15 ++--
 13 files changed, 208 insertions(+), 193 deletions(-)
 create mode 100644 src/authentic2/a2_rbac/migrations/0034_new_role_fields.py
 create mode 100644 src/authentic2/a2_rbac/migrations/0035_populate_role_fields.py
 create mode 100644 src/authentic2/a2_rbac/migrations/0036_delete_roleattribute.py
 delete mode 100644 src/authentic2/attributes_ng/sources/service_roles.py
src/authentic2/a2_rbac/admin.py
38 38
        return super().get_queryset(request).filter(direct=True)
39 39

  
40 40

  
41
class RoleAttributeInline(admin.TabularInline):
42
    model = models.RoleAttribute
43

  
44

  
45 41
class RoleAdmin(admin.ModelAdmin):
46 42
    inlines = [RoleChildInline, RoleParentInline]
47 43
    fields = (
......
62 58
    list_display = ('__str__', 'slug', 'ou', 'service', 'admin_scope')
63 59
    list_select_related = True
64 60
    list_filter = ['ou', 'service']
65
    inlines = [RoleAttributeInline]
66 61

  
67 62

  
68 63
class OrganizationalUnitAdmin(admin.ModelAdmin):
src/authentic2/a2_rbac/migrations/0034_new_role_fields.py
1
# Generated by Django 2.2.26 on 2022-10-25 09:35
2

  
3
import django
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('a2_rbac', '0033_remove_old_operation_fk'),
11
    ]
12

  
13
    operations = [
14
        migrations.AddField(
15
            model_name='role',
16
            name='details',
17
            field=models.TextField(blank=True, verbose_name='Role details (frontoffice)'),
18
        ),
19
        migrations.AddField(
20
            model_name='role',
21
            name='emails',
22
            field=django.contrib.postgres.fields.ArrayField(
23
                base_field=models.EmailField(max_length=254), default=list, size=None
24
            ),
25
        ),
26
        migrations.AddField(
27
            model_name='role',
28
            name='emails_to_members',
29
            field=models.BooleanField(default=True, verbose_name='Emails to members'),
30
        ),
31
        migrations.AddField(
32
            model_name='role',
33
            name='is_superuser',
34
            field=models.BooleanField(default=False),
35
        ),
36
    ]
src/authentic2/a2_rbac/migrations/0035_populate_role_fields.py
1
import json
2

  
3
from django.db import migrations
4

  
5

  
6
def populate_role_fields(apps, schema_editor):
7
    Role = apps.get_model('a2_rbac', 'Role')
8

  
9
    fields = {'details', 'emails', 'emails_to_members', 'is_superuser'}
10
    roles = list(Role.objects.all().prefetch_related('attributes'))
11
    for role in roles:
12
        for attribute in role.attributes.all():
13
            if attribute.name not in fields:
14
                continue
15
            try:
16
                value = json.loads(attribute.value)
17
            except json.JSONDecodeError:
18
                continue
19

  
20
            if attribute.name == 'emails':
21
                if not isinstance(value, list):
22
                    continue
23
                value = [x[:254] for x in value]
24

  
25
            if attribute.name == 'details' and not isinstance(value, str):
26
                continue
27

  
28
            if attribute.name in ('emails_to_members', 'is_superuser') and not isinstance(value, bool):
29
                continue
30

  
31
            setattr(role, attribute.name, value)
32

  
33
    Role.objects.bulk_update(roles, fields, batch_size=1000)
34

  
35

  
36
def reverse_populate_role_fields(apps, schema_editor):
37
    Role = apps.get_model('a2_rbac', 'Role')
38
    RoleAttribute = apps.get_model('a2_rbac', 'RoleAttribute')
39

  
40
    fields = ['details', 'emails', 'emails_to_members']
41
    attributes = []
42
    for role in Role.objects.all():
43
        for field in fields:
44
            attributes.append(
45
                RoleAttribute(
46
                    role_id=role.pk, name=field, kind='json', value=json.dumps(getattr(role, field))
47
                )
48
            )
49

  
50
    RoleAttribute.objects.bulk_create(attributes, batch_size=1000)
51

  
52

  
53
class Migration(migrations.Migration):
54

  
55
    dependencies = [
56
        ('a2_rbac', '0034_new_role_fields'),
57
    ]
58

  
59
    operations = [
60
        migrations.RunPython(populate_role_fields, reverse_code=reverse_populate_role_fields),
61
    ]
src/authentic2/a2_rbac/migrations/0036_delete_roleattribute.py
1
# Generated by Django 2.2.26 on 2022-10-25 10:33
2

  
3
from django.db import migrations
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('a2_rbac', '0035_populate_role_fields'),
10
    ]
11

  
12
    operations = [
13
        migrations.DeleteModel(
14
            name='RoleAttribute',
15
        ),
16
    ]
src/authentic2/a2_rbac/models.py
23 23
from django.contrib.auth import get_user_model
24 24
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
25 25
from django.contrib.contenttypes.models import ContentType
26
from django.contrib.postgres.fields import ArrayField
26 27
from django.core.exceptions import ValidationError
27 28
from django.core.validators import MinValueValidator
28 29
from django.db import models
......
385 386
        to=rbac_utils.get_permission_model_name(), related_name='roles', blank=True
386 387
    )
387 388
    name = models.TextField(verbose_name=_('name'))
389
    details = models.TextField(_('Role details (frontoffice)'), blank=True)
390
    emails = ArrayField(models.EmailField(), default=list)
391
    emails_to_members = models.BooleanField(_('Emails to members'), default=True)
392
    is_superuser = models.BooleanField(default=False)
388 393
    admin_scope_ct = models.ForeignKey(
389 394
        to='contenttypes.ContentType',
390 395
        null=True,
......
737 742
        return '{} {}> {}'.format(self.parent.name, '-' if self.direct else '~', self.child.name)
738 743

  
739 744

  
740
class RoleAttribute(models.Model):
741
    KINDS = (('string', _('string')),)
742
    role = models.ForeignKey(
743
        to=Role, verbose_name=_('role'), related_name='attributes', on_delete=models.CASCADE
744
    )
745
    name = models.CharField(max_length=64, verbose_name=_('name'))
746
    kind = models.CharField(max_length=32, choices=KINDS, verbose_name=_('kind'))
747
    value = models.TextField(verbose_name=_('value'))
748

  
749
    class Meta:
750
        verbose_name = 'role attribute'
751
        verbose_name_plural = _('role attributes')
752
        unique_together = (('role', 'name', 'kind', 'value'),)
753

  
754
    def to_json(self):
755
        return {'name': self.name, 'kind': self.kind, 'value': self.value}
756

  
757

  
758 745
class Operation(models.Model):
759 746
    slug = models.CharField(max_length=32, verbose_name=_('slug'), unique=True)
760 747

  
src/authentic2/app_settings.py
101 101
            'authentic2.attributes_ng.sources.function',
102 102
            'authentic2.attributes_ng.sources.django_user',
103 103
            'authentic2.attributes_ng.sources.ldap',
104
            'authentic2.attributes_ng.sources.service_roles',
105 104
        ),
106 105
        definition='List of attribute backend classes or modules',
107 106
    ),
src/authentic2/attributes_ng/sources/service_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
from django.utils.translation import gettext_lazy as _
18

  
19
from authentic2.a2_rbac.models import Role
20

  
21
from ...decorators import to_list
22
from ...models import Service
23

  
24

  
25
@to_list
26
def get_instances(ctx):
27
    return [None]
28

  
29

  
30
@to_list
31
def get_attribute_names(instance, ctx):
32
    service = ctx.get('service')
33
    if not isinstance(service, Service):
34
        return
35
    names = []
36
    for service_role in Role.objects.filter(service=service).prefetch_related('attributes'):
37
        for service_role_attribute in service_role.attributes.all():
38
            if service_role_attribute.name in names:
39
                continue
40
            names.append(service_role_attribute.name)
41
    names.sort()
42
    for name in names:
43
        yield (name, '%s (%s)' % (name, _('role attribute')))
44

  
45

  
46
def get_dependencies(instance, ctx):
47
    return (
48
        'user',
49
        'service',
50
    )
51

  
52

  
53
def get_attributes(instance, ctx):
54
    user = ctx.get('user')
55
    service = ctx.get('service')
56
    if not user or not service:
57
        return ctx
58
    ctx = ctx.copy()
59
    roles = Role.objects.for_user(user).filter(service=service).prefetch_related('attributes')
60
    for service_role in roles:
61
        for service_role_attribute in service_role.attributes.all():
62
            name = service_role_attribute.name
63
            value = service_role_attribute.value
64
            values = ctx.get(name, [])
65
            if not isinstance(values, (list, tuple, set)):
66
                values = [values]
67
            values = set(values)
68
            if value not in values:
69
                values.add(value)
70
            ctx[name] = values
71
    return ctx
src/authentic2/data_transfer.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17

  
18
import json
18 19
import uuid
19 20
from functools import wraps
20 21

  
......
24 25
from django.utils.text import format_lazy
25 26
from django.utils.translation import gettext_lazy as _
26 27

  
27
from authentic2.a2_rbac.models import (
28
    Operation,
29
    OrganizationalUnit,
30
    Permission,
31
    Role,
32
    RoleAttribute,
33
    RoleParenting,
34
)
28
from authentic2.a2_rbac.models import Operation, OrganizationalUnit, Permission, Role, RoleParenting
35 29
from authentic2.a2_rbac.utils import get_default_ou
36 30
from authentic2.decorators import errorcollector
37 31
from authentic2.utils.lazy import lazy_join
......
110 104

  
111 105
def export_roles(context):
112 106
    """Serialize roles in role_queryset"""
113
    return [role.export_json(attributes=True, parents=True, permissions=True) for role in context.role_qs]
107
    return [role.export_json(parents=True, permissions=True) for role in context.role_qs]
114 108

  
115 109

  
116 110
def search_ou(ou_d):
......
144 138
                         be deleted
145 139

  
146 140

  
147
    role_attributes_update: for each role in the import data,
141
    role_attributes_update: legacy, for each role in the import data,
148 142
                            attributes will deleted and re-created
149 143

  
150 144

  
......
277 271

  
278 272
    @wraps_validationerror
279 273
    def attributes(self):
280
        """Update attributes (delete everything then create)"""
274
        """Compatibility with old import files, set Role fields using attributes data"""
281 275
        created, deleted = [], []
282
        for attr in self._obj.attributes.all():
283
            attr.delete()
284
            deleted.append(attr)
285 276
        # Create attributes
286 277
        if self._attributes:
287 278
            for attr_dict in self._attributes:
288
                attr_dict['role'] = self._obj
289
                created.append(RoleAttribute.objects.create(**attr_dict))
279
                setattr(self._obj, attr_dict['name'], json.loads(attr_dict['value']))
290 280

  
291 281
        return created, deleted
292 282

  
src/authentic2/manager/forms.py
31 31
from django.utils.translation import pgettext
32 32
from django_select2.forms import HeavySelect2Widget
33 33

  
34
from authentic2.a2_rbac.models import Operation, OrganizationalUnit, Permission, Role, RoleAttribute
34
from authentic2.a2_rbac.models import Operation, OrganizationalUnit, Permission, Role
35 35
from authentic2.a2_rbac.utils import generate_slug, get_default_ou
36 36
from authentic2.custom_user.backends import DjangoRBACBackend
37 37
from authentic2.forms.fields import (
......
603 603

  
604 604

  
605 605
class RoleEditForm(SlugMixin, HideOUFieldMixin, LimitQuerysetFormMixin, CssClass, forms.ModelForm):
606
    ou = forms.ModelChoiceField(
607
        queryset=OrganizationalUnit.objects, required=True, label=_('Organizational unit')
608
    )
609
    details = forms.CharField(
610
        label=_('Role details (frontoffice)'), widget=forms.Textarea, initial='', required=False
611
    )
612 606
    emails = CommaSeparatedCharField(
613 607
        label=_('Emails'),
614 608
        item_validators=[EmailValidator()],
615 609
        required=False,
616 610
        help_text=_('Emails must be separated by commas.'),
617 611
    )
618
    emails_to_members = forms.BooleanField(required=False, initial=True, label=_('Emails to members'))
619 612

  
620 613
    class Meta:
621 614
        model = Role
622
        fields = ('name', 'slug', 'ou', 'description')
615
        fields = ('name', 'slug', 'ou', 'description', 'details', 'emails', 'emails_to_members')
623 616
        widgets = {
624 617
            'name': forms.TextInput(),
625 618
        }
626 619

  
627 620
    def __init__(self, *args, **kwargs):
628
        instance = kwargs.get('instance')
629
        if instance:
630
            fields = [x.name for x in Role._meta.get_fields()]
631
            initial = kwargs.setdefault('initial', {})
632
            role_attributes = RoleAttribute.objects.filter(role=instance, kind='json')
633
            for role_attribute in role_attributes:
634
                if role_attribute.name in fields:
635
                    continue
636
                initial[role_attribute.name] = json.loads(role_attribute.value)
637 621
        super().__init__(*args, **kwargs)
638

  
639
    def save(self, commit=True):
640
        fields = [x.name for x in Role._meta.get_fields()]
641
        assert commit
642
        instance = super().save(commit=commit)
643
        for field in self.cleaned_data:
644
            if field in fields:
645
                continue
646
            value = json.dumps(self.cleaned_data[field])
647
            ra, created = RoleAttribute.objects.get_or_create(
648
                role=instance, name=field, kind='json', defaults={'value': value}
649
            )
650
            if not created and ra.value != value:
651
                ra.value = value
652
                ra.save()
653
        instance.save()
654
        return instance
622
        if 'ou' in self.fields:
623
            self.fields['ou'].required = True
655 624

  
656 625

  
657 626
class OUEditForm(SlugMixin, CssClass, forms.ModelForm):
tests/test_a2_rbac.py
21 21

  
22 22
from authentic2.a2_rbac.models import CHANGE_OP, MANAGE_MEMBERS_OP, Operation
23 23
from authentic2.a2_rbac.models import OrganizationalUnit as OU
24
from authentic2.a2_rbac.models import Permission, Role, RoleAttribute
24
from authentic2.a2_rbac.models import Permission, Role
25 25
from authentic2.a2_rbac.utils import get_default_ou
26 26
from authentic2.custom_user.models import User
27 27
from authentic2.models import Service
......
182 182
    assert role_dict['service'] == {'slug': service.slug, 'ou': {'uuid': ou.uuid, 'slug': 'ou', 'name': 'ou'}}
183 183

  
184 184

  
185
def test_role_with_attributes_export_json(db):
186
    role = Role.objects.create(name='some role')
187
    attr1 = RoleAttribute.objects.create(role=role, name='attr1_name', kind='string', value='attr1_value')
188
    attr2 = RoleAttribute.objects.create(role=role, name='attr2_name', kind='string', value='attr2_value')
189

  
190
    role_dict = role.export_json(attributes=True)
191
    attributes = role_dict['attributes']
192
    assert len(attributes) == 2
193

  
194
    expected_attr_names = {attr1.name, attr2.name}
195
    for attr_dict in attributes:
196
        assert attr_dict['name'] in expected_attr_names
197
        expected_attr_names.remove(attr_dict['name'])
198
        target_attr = RoleAttribute.objects.filter(name=attr_dict['name']).first()
199
        assert attr_dict['kind'] == target_attr.kind
200
        assert attr_dict['value'] == target_attr.value
201

  
202

  
203 185
def test_role_with_parents_export_json(db):
204 186
    grand_parent_role = Role.objects.create(name='test grand parent role', slug='test-grand-parent-role')
205 187
    parent_1_role = Role.objects.create(name='test parent 1 role', slug='test-parent-1-role')
......
740 722
        ).count()
741 723
        == 1
742 724
    )
725

  
726

  
727
def test_a2_rbac_role_attribute_migration(migration, settings):
728
    migrate_from = [('a2_rbac', '0034_new_role_fields')]
729
    migrate_to = [('a2_rbac', '0036_delete_roleattribute')]
730

  
731
    old_apps = migration.before(migrate_from)
732
    Role = old_apps.get_model('a2_rbac', 'Role')
733
    RoleAttribute = old_apps.get_model('a2_rbac', 'RoleAttribute')
734

  
735
    role = Role.objects.create(name='role', slug='1')
736
    RoleAttribute.objects.create(role=role, kind='json', name='details', value='"abc"')
737
    RoleAttribute.objects.create(role=role, kind='json', name='emails', value='["a@a.com", "b@b.com"]')
738
    RoleAttribute.objects.create(role=role, kind='json', name='emails_to_members', value='false')
739
    RoleAttribute.objects.create(role=role, kind='string', name='is_superuser', value='true')
740

  
741
    role = Role.objects.create(name='role_default_values', slug='2')
742
    RoleAttribute.objects.create(role=role, kind='json', name='details', value='""')
743
    RoleAttribute.objects.create(role=role, kind='json', name='emails', value='[]')
744
    RoleAttribute.objects.create(role=role, kind='json', name='emails_to_members', value='true')
745
    RoleAttribute.objects.create(role=role, kind='string', name='is_superuser', value='false')
746

  
747
    role = Role.objects.create(name='role_no_attribute', slug='3')
748

  
749
    role = Role.objects.create(name='role_bad_attributes', slug='4')
750
    RoleAttribute.objects.create(role=role, kind='json', name='details', value='bad')
751
    RoleAttribute.objects.create(role=role, kind='json', name='emails', value='true')
752
    RoleAttribute.objects.create(role=role, kind='json', name='emails_to_members', value='bad')
753
    RoleAttribute.objects.create(role=role, kind='string', name='unknown', value='xxx')
754

  
755
    role = Role.objects.create(name='role_one_attribute', slug='5')
756
    RoleAttribute.objects.create(role=role, kind='json', name='details', value='"xxx"')
757

  
758
    new_apps = migration.apply(migrate_to)
759
    Role = new_apps.get_model('a2_rbac', 'Role')
760

  
761
    role = Role.objects.get(name='role')
762
    assert role.details == 'abc'
763
    assert role.emails == ['a@a.com', 'b@b.com']
764
    assert role.emails_to_members is False
765
    assert role.is_superuser is True
766

  
767
    role = Role.objects.get(name='role_default_values')
768
    assert role.details == ''
769
    assert role.emails == []
770
    assert role.emails_to_members is True
771
    assert role.is_superuser is False
772

  
773
    role = Role.objects.get(name='role_no_attribute')
774
    assert role.details == ''
775
    assert role.emails == []
776
    assert role.emails_to_members is True
777
    assert role.is_superuser is False
778

  
779
    role = Role.objects.get(name='role_bad_attributes')
780
    assert role.details == ''
781
    assert role.emails == []
782
    assert role.emails_to_members is True
783
    assert role.is_superuser is False
784

  
785
    role = Role.objects.get(name='role_one_attribute')
786
    assert role.details == 'xxx'
787
    assert role.emails == []
788
    assert role.emails_to_members is True
789
    assert role.is_superuser is False
tests/test_data_transfer.py
238 238
def test_role_deserializer_with_attributes(db):
239 239

  
240 240
    attributes_data = {
241
        'attr1_name': dict(name='attr1_name', kind='string', value='attr1_value'),
242
        'attr2_name': dict(name='attr2_name', kind='string', value='attr2_value'),
241
        'is_superuser': dict(name='is_superuser', kind='string', value='true'),
242
        'emails': dict(name='emails', kind='json', value='["a@a.com"]'),
243 243
    }
244 244
    rd = RoleDeserializer(
245 245
        {
......
254 254
        ImportContext(),
255 255
    )
256 256
    role, status = rd.deserialize()
257
    created, dummy = rd.attributes()
257
    rd.attributes()
258 258
    assert status == 'created'
259
    assert role.attributes.count() == 2
260
    assert len(created) == 2
261

  
262
    for attr in created:
263
        attr_dict = attributes_data[attr.name]
264
        assert attr_dict['name'] == attr.name
265
        assert attr_dict['kind'] == attr.kind
266
        assert attr_dict['value'] == attr.value
267
        del attributes_data[attr.name]
259
    assert role.is_superuser is True
260
    assert role.emails == ['a@a.com']
268 261

  
269 262

  
270 263
def test_role_deserializer_creates_admin_role(db):
tests/test_idp_saml2.py
173 173

  
174 174
        # Admin role
175 175
        self.admin_role = Role.objects.create(
176
            name='Administrator', slug='administrator', service=self.provider
176
            name='Administrator', slug='administrator', service=self.provider, is_superuser=True
177 177
        )
178
        self.admin_role.attributes.create(name='superuser', kind='string', value='true')
179 178

  
180 179
        # SAML attributes mapping
181 180
        self.saml_first_name_attribute = self.provider.attributes.create(
......
957 956
    service_role = Role.objects.create(
958 957
        name='Role of service', slug='role-of-service', ou=ou1, service=add_attributes_all.provider
959 958
    )
960

  
961
    service_role.attributes.create(name='is_admin', kind='string', value='true')
962 959
    user_ou1.roles.add(service_role)
963 960

  
964
    add_attributes_all.get_definitions.return_value.append(
965
        SAMLAttribute(name_format='basic', name='is_admin', attribute_name='is_admin'),
966
    )
967

  
968 961
    attributes = add_attributes_all(user_ou1)
969 962
    assert attributes == {
970 963
        'a2_role_names': {'Role of service', 'role_ou2'},
......
999 992
        'django_user_password': {'abba0b6ff456806bab66baed93e6d9c4'},
1000 993
        'django_user_username': {'john.doe'},
1001 994
        'django_user_uuid': {user_ou1.uuid},
1002
        'is_admin': {'true'},
1003 995
    }
1004 996

  
1005 997

  
tests/test_manager.py
166 166
    resp.form['emails'] = 'test@example.com'
167 167
    resp.form['emails_to_members'] = False
168 168
    resp = resp.form.submit().follow()
169
    assert set(simple_role.attributes.values_list('name', 'value')) == {
170
        ('emails_to_members', 'false'),
171
        ('emails', '["test@example.com"]'),
172
        ('details', '"xxx"'),
173
    }
169

  
170
    simple_role.refresh_from_db()
171
    assert simple_role.details == 'xxx'
172
    assert simple_role.emails == ['test@example.com']
173
    assert simple_role.emails_to_members is False
174 174

  
175 175
    resp = app.get('/manage/roles/%s/edit/' % simple_role.pk)
176 176
    resp.form['emails'] = 'test@example.com, hop@example.com'
177 177
    resp = resp.form.submit().follow()
178
    emails = simple_role.attributes.get(name='emails')
179
    assert set(json.loads(emails.value)) == {'test@example.com', 'hop@example.com'}
178

  
179
    simple_role.refresh_from_db()
180
    assert set(simple_role.emails) == {'test@example.com', 'hop@example.com'}
180 181

  
181 182
    resp = app.get('/manage/roles/%s/edit/' % simple_role.pk)
182 183
    resp.form['emails'] = 'xxx'
183
-