Projet

Général

Profil

0001-auth_saml-migrate-JSON-fields-to-models-67025.patch

Valentin Deniaud, 17 août 2022 11:43

Télécharger (17,8 ko)

Voir les différences:

Subject: [PATCH 1/8] auth_saml: migrate JSON fields to models (#67025)

 ..._samlattributelookup_setattributeaction.py | 137 +++++++++++++++++
 .../migrations/0005_migrate_jsonfields.py     | 140 ++++++++++++++++++
 src/authentic2_auth_saml/models.py            |  50 +++++++
 tests/test_auth_saml.py                       |  93 ++++++++++++
 4 files changed, 420 insertions(+)
 create mode 100644 src/authentic2_auth_saml/migrations/0004_addroleaction_renameattributeaction_samlattributelookup_setattributeaction.py
 create mode 100644 src/authentic2_auth_saml/migrations/0005_migrate_jsonfields.py
src/authentic2_auth_saml/migrations/0004_addroleaction_renameattributeaction_samlattributelookup_setattributeaction.py
1
# Generated by Django 2.2.26 on 2022-08-16 15:29
2

  
3
import django.db.models.deletion
4
from django.conf import settings
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        migrations.swappable_dependency(settings.RBAC_ROLE_MODEL),
12
        ('authentic2_auth_saml', '0003_auto_20220726_1713'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='SetAttributeAction',
18
            fields=[
19
                (
20
                    'id',
21
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
22
                ),
23
                ('attribute', models.CharField(max_length=256, verbose_name='User attribute name')),
24
                ('saml_attribute', models.CharField(max_length=1024, verbose_name='SAML attribute name')),
25
                (
26
                    'mandatory',
27
                    models.BooleanField(
28
                        default=False, help_text='Deny login if action fails.', verbose_name='Mandatory'
29
                    ),
30
                ),
31
                (
32
                    'authenticator',
33
                    models.ForeignKey(
34
                        on_delete=django.db.models.deletion.CASCADE,
35
                        related_name='set_attribute_actions',
36
                        to='authentic2_auth_saml.SAMLAuthenticator',
37
                    ),
38
                ),
39
            ],
40
            options={
41
                'verbose_name': 'Set an attribute',
42
                'default_related_name': 'set_attribute_actions',
43
            },
44
        ),
45
        migrations.CreateModel(
46
            name='SAMLAttributeLookup',
47
            fields=[
48
                (
49
                    'id',
50
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
51
                ),
52
                ('user_field', models.CharField(max_length=256, verbose_name='User field')),
53
                ('saml_attribute', models.CharField(max_length=1024, verbose_name='SAML attribute')),
54
                ('ignore_case', models.BooleanField(default=False, verbose_name='Ignore case')),
55
                (
56
                    'authenticator',
57
                    models.ForeignKey(
58
                        on_delete=django.db.models.deletion.CASCADE,
59
                        related_name='attribute_lookups',
60
                        to='authentic2_auth_saml.SAMLAuthenticator',
61
                    ),
62
                ),
63
            ],
64
            options={
65
                'verbose_name': 'Attribute lookup',
66
                'default_related_name': 'attribute_lookups',
67
            },
68
        ),
69
        migrations.CreateModel(
70
            name='RenameAttributeAction',
71
            fields=[
72
                (
73
                    'id',
74
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
75
                ),
76
                ('from_name', models.CharField(max_length=1024, verbose_name='From')),
77
                ('to_name', models.CharField(max_length=64, verbose_name='To')),
78
                (
79
                    'authenticator',
80
                    models.ForeignKey(
81
                        on_delete=django.db.models.deletion.CASCADE,
82
                        related_name='rename_attribute_actions',
83
                        to='authentic2_auth_saml.SAMLAuthenticator',
84
                    ),
85
                ),
86
            ],
87
            options={
88
                'verbose_name': 'Rename an attribute',
89
                'default_related_name': 'rename_attribute_actions',
90
            },
91
        ),
92
        migrations.CreateModel(
93
            name='AddRoleAction',
94
            fields=[
95
                (
96
                    'id',
97
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
98
                ),
99
                (
100
                    'condition',
101
                    models.CharField(
102
                        editable=False,
103
                        blank=True,
104
                        max_length=256,
105
                    ),
106
                ),
107
                (
108
                    'mandatory',
109
                    models.BooleanField(
110
                        editable=False,
111
                        default=False,
112
                    ),
113
                ),
114
                (
115
                    'authenticator',
116
                    models.ForeignKey(
117
                        on_delete=django.db.models.deletion.CASCADE,
118
                        related_name='add_role_actions',
119
                        to='authentic2_auth_saml.SAMLAuthenticator',
120
                    ),
121
                ),
122
                (
123
                    'role',
124
                    models.ForeignKey(
125
                        on_delete=django.db.models.deletion.CASCADE,
126
                        related_name='add_role_actions',
127
                        to=settings.RBAC_ROLE_MODEL,
128
                        verbose_name='Role',
129
                    ),
130
                ),
131
            ],
132
            options={
133
                'verbose_name': 'Add a role',
134
                'default_related_name': 'add_role_actions',
135
            },
136
        ),
137
    ]
src/authentic2_auth_saml/migrations/0005_migrate_jsonfields.py
1
# Generated by Django 2.2.26 on 2022-07-27 15:04
2

  
3
from django.core.exceptions import MultipleObjectsReturned
4
from django.db import migrations
5

  
6

  
7
def get_key(obj, name, max_length=None, default=''):
8
    setting = obj.get(name)
9

  
10
    expected_type = type(default)
11
    if not isinstance(setting, expected_type):
12
        setting = None
13

  
14
    if setting is None:
15
        setting = default
16

  
17
    return setting[:max_length] if max_length else setting
18

  
19

  
20
def get_ou(role_desc, ou_model):
21
    ou_desc = role_desc.get('ou')
22
    if ou_desc is None:
23
        return None
24
    if not isinstance(ou_desc, dict):
25
        return
26
    slug = ou_desc.get('slug')
27
    name = ou_desc.get('name')
28
    if slug:
29
        if not isinstance(slug, str):
30
            return
31
        try:
32
            return ou_model.objects.get(slug=slug)
33
        except ou_model.DoesNotExist:
34
            return
35
    elif name:
36
        if not isinstance(name, str):
37
            return
38
        try:
39
            return ou_model.objects.get(name=name)
40
        except ou_model.DoesNotExist:
41
            pass
42

  
43

  
44
def get_role(mapping, role_model, ou_model):
45
    role_desc = mapping.get('role')
46
    if not role_desc or not isinstance(role_desc, dict):
47
        return
48
    slug = role_desc.get('slug')
49
    name = role_desc.get('name')
50
    ou = get_ou(role_desc, ou_model)
51

  
52
    kwargs = {}
53
    if ou:
54
        kwargs['ou'] = ou
55

  
56
    if slug:
57
        if not isinstance(slug, str):
58
            return
59
        kwargs['slug'] = slug
60
    elif name:
61
        if not isinstance(name, str):
62
            return
63
        kwargs['name'] = name
64
    else:
65
        return
66

  
67
    try:
68
        return role_model.objects.get(**kwargs)
69
    except role_model.DoesNotExist:
70
        pass
71
    except MultipleObjectsReturned:
72
        pass
73

  
74

  
75
def migrate_jsonfields(apps, schema_editor):
76
    SAMLAuthenticator = apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
77
    SAMLAttributeLookup = apps.get_model('authentic2_auth_saml', 'SAMLAttributeLookup')
78
    SetAttributeAction = apps.get_model('authentic2_auth_saml', 'SetAttributeAction')
79
    AddRoleAction = apps.get_model('authentic2_auth_saml', 'AddRoleAction')
80
    RenameAttributeAction = apps.get_model('authentic2_auth_saml', 'RenameAttributeAction')
81
    Role = apps.get_model('a2_rbac', 'Role')
82
    OU = apps.get_model('a2_rbac', 'OrganizationalUnit')
83

  
84
    for authenticator in SAMLAuthenticator.objects.all():
85
        for obj in authenticator.lookup_by_attributes:
86
            saml_attribute = get_key(obj, 'saml_attribute', 1024)
87
            user_field = get_key(obj, 'user_field', 256)
88
            if saml_attribute and user_field:
89
                SAMLAttributeLookup.objects.create(
90
                    authenticator=authenticator,
91
                    saml_attribute=saml_attribute,
92
                    user_field=user_field,
93
                    ignore_case=get_key(obj, 'ignore-case', default=False),
94
                )
95
        for obj in authenticator.a2_attribute_mapping:
96
            action = obj.get('action') or ''
97
            action = action.replace('_', '-')
98
            if not action or action == 'set-attribute':
99
                attribute = get_key(obj, 'attribute', 256)
100
                saml_attribute = get_key(obj, 'saml_attribute', 1024)
101
                if attribute and saml_attribute:
102
                    SetAttributeAction.objects.create(
103
                        authenticator=authenticator,
104
                        attribute=attribute,
105
                        saml_attribute=saml_attribute,
106
                        mandatory=get_key(obj, 'mandatory', default=False),
107
                    )
108
            elif action == 'rename':
109
                from_name = get_key(obj, 'from', 1024)
110
                to_name = get_key(obj, 'to', 64)
111
                if from_name and to_name:
112
                    RenameAttributeAction.objects.create(
113
                        authenticator=authenticator,
114
                        from_name=from_name,
115
                        to_name=to_name,
116
                    )
117
            elif action in ('toggle-role', 'add-role'):
118
                role = get_role(obj, Role, OU)
119
                if role:
120
                    AddRoleAction.objects.create(
121
                        authenticator=authenticator,
122
                        role=role,
123
                        condition=get_key(obj, 'condition', 256),
124
                        mandatory=get_key(obj, 'mandatory', default=False),
125
                    )
126

  
127

  
128
class Migration(migrations.Migration):
129

  
130
    dependencies = [
131
        (
132
            'authentic2_auth_saml',
133
            '0004_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
134
        ),
135
        ('a2_rbac', '0029_use_unique_constraints'),
136
    ]
137

  
138
    operations = [
139
        migrations.RunPython(migrate_jsonfields, reverse_code=migrations.RunPython.noop),
140
    ]
src/authentic2_auth_saml/models.py
19 19
from django.db import models
20 20
from django.utils.translation import gettext_lazy as _
21 21

  
22
from authentic2.a2_rbac.models import Role
22 23
from authentic2.apps.authenticators.models import BaseAuthenticator
23 24
from authentic2.utils.misc import redirect_to_login
24 25

  
......
216 217

  
217 218
    def profile(self, request, *args, **kwargs):
218 219
        return views.profile(request, *args, **kwargs)
220

  
221

  
222
class SAMLRelatedObjectBase(models.Model):
223
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
224

  
225
    def __repr__(self):
226
        return '%s (%s)' % (self._meta.object_name, self.pk)
227

  
228
    class Meta:
229
        abstract = True
230

  
231

  
232
class RenameAttributeAction(SAMLRelatedObjectBase):
233
    from_name = models.CharField(_('From'), max_length=1024)
234
    to_name = models.CharField(_('To'), max_length=64)
235

  
236
    class Meta:
237
        default_related_name = 'rename_attribute_actions'
238
        verbose_name = _('Rename an attribute')
239

  
240

  
241
class SAMLAttributeLookup(SAMLRelatedObjectBase):
242
    user_field = models.CharField(_('User field'), max_length=256)
243
    saml_attribute = models.CharField(_('SAML attribute'), max_length=1024)
244
    ignore_case = models.BooleanField(_('Ignore case'), default=False)
245

  
246
    class Meta:
247
        default_related_name = 'attribute_lookups'
248
        verbose_name = _('Attribute lookup')
249

  
250

  
251
class SetAttributeAction(SAMLRelatedObjectBase):
252
    attribute = models.CharField(_('User attribute name'), max_length=256)
253
    saml_attribute = models.CharField(_('SAML attribute name'), max_length=1024)
254
    mandatory = models.BooleanField(_('Mandatory'), default=False, help_text=_('Deny login if action fails.'))
255

  
256
    class Meta:
257
        default_related_name = 'set_attribute_actions'
258
        verbose_name = _('Set an attribute')
259

  
260

  
261
class AddRoleAction(SAMLRelatedObjectBase):
262
    role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.CASCADE)
263
    condition = models.CharField(editable=False, max_length=256, blank=True)
264
    mandatory = models.BooleanField(editable=False, default=False)
265

  
266
    class Meta:
267
        default_related_name = 'add_role_actions'
268
        verbose_name = _('Add a role')
tests/test_auth_saml.py
576 576
    assert authenticator.error_redirect_after_timeout == 120
577 577
    assert authenticator.authn_classref == ''
578 578
    assert authenticator.superuser_mapping == {}
579

  
580

  
581
def test_saml_authenticator_data_migration_json_fields(migration, settings):
582
    migrate_from = [
583
        (
584
            'authentic2_auth_saml',
585
            '0004_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
586
        ),
587
        ('a2_rbac', '0029_use_unique_constraints'),
588
    ]
589
    migrate_to = [
590
        ('authentic2_auth_saml', '0005_migrate_jsonfields'),
591
        ('a2_rbac', '0029_use_unique_constraints'),
592
    ]
593

  
594
    old_apps = migration.before(migrate_from)
595
    SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
596
    Role = old_apps.get_model('a2_rbac', 'Role')
597
    OU = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
598

  
599
    ou = OU.objects.create(name='Test OU', slug='test-ou')
600
    role = Role.objects.create(name='Test role', slug='test-role', ou=ou)
601

  
602
    SAMLAuthenticator.objects.create(
603
        metadata='meta1.xml',
604
        slug='idp1',
605
        lookup_by_attributes=[
606
            {'saml_attribute': 'email', 'user_field': 'email'},
607
            {'saml_attribute': 'saml_name', 'user_field': 'first_name', 'ignore-case': True},
608
        ],
609
        a2_attribute_mapping=[
610
            {
611
                'attribute': 'email',
612
                'saml_attribute': 'mail',
613
                'mandatory': True,
614
            },
615
            {'action': 'rename', 'from': 'a' * 1025, 'to': 'first_name'},
616
            {
617
                'attribute': 'first_name',
618
                'saml_attribute': 'first_name',
619
            },
620
            {
621
                'attribute': 'invalid',
622
                'saml_attribute': '',
623
            },
624
            {
625
                'attribute': 'invalid',
626
                'saml_attribute': None,
627
            },
628
            {
629
                'attribute': 'invalid',
630
            },
631
            {
632
                'action': 'add-role',
633
                'role': {
634
                    'name': role.name,
635
                    'ou': {
636
                        'name': role.ou.name,
637
                    },
638
                },
639
                'condition': "roles == 'A'",
640
            },
641
        ],
642
    )
643

  
644
    new_apps = migration.apply(migrate_to)
645
    SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
646
    authenticator = SAMLAuthenticator.objects.get()
647

  
648
    attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
649
    assert attribute_lookup1.saml_attribute == 'email'
650
    assert attribute_lookup1.user_field == 'email'
651
    assert attribute_lookup1.ignore_case is False
652
    assert attribute_lookup2.saml_attribute == 'saml_name'
653
    assert attribute_lookup2.user_field == 'first_name'
654
    assert attribute_lookup2.ignore_case is True
655

  
656
    set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
657
    assert set_attribute1.attribute == 'email'
658
    assert set_attribute1.saml_attribute == 'mail'
659
    assert set_attribute1.mandatory is True
660
    assert set_attribute2.attribute == 'first_name'
661
    assert set_attribute2.saml_attribute == 'first_name'
662
    assert set_attribute2.mandatory is False
663

  
664
    rename_attribute = authenticator.rename_attribute_actions.get()
665
    assert rename_attribute.from_name == 'a' * 1024
666
    assert rename_attribute.to_name == 'first_name'
667

  
668
    add_role = authenticator.add_role_actions.get()
669
    assert add_role.role.pk == role.pk
670
    assert add_role.condition == "roles == 'A'"
671
    assert add_role.mandatory is False
579
-