Projet

Général

Profil

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

Valentin Deniaud, 16 août 2022 14:12

Télécharger (18 ko)

Voir les différences:

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

 ..._samlattributelookup_setattributeaction.py | 136 +++++++++++++++++
 .../migrations/0005_auto_20220727_1704.py     | 140 ++++++++++++++++++
 src/authentic2_auth_saml/models.py            |  44 ++++++
 tests/test_auth_saml.py                       |  93 ++++++++++++
 4 files changed, 413 insertions(+)
 create mode 100644 src/authentic2_auth_saml/migrations/0004_addroleaction_renameattributeaction_samlattributelookup_setattributeaction.py
 create mode 100644 src/authentic2_auth_saml/migrations/0005_auto_20220727_1704.py
src/authentic2_auth_saml/migrations/0004_addroleaction_renameattributeaction_samlattributelookup_setattributeaction.py
1
# Generated by Django 2.2.26 on 2022-08-11 13:16
2

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

  
7
import authentic2.utils.evaluate
8

  
9

  
10
class Migration(migrations.Migration):
11

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

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

  
230
    class Meta:
231
        verbose_name = _('Attribute lookup')
232

  
233
class RenameAttributeAction(models.Model):
234
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
235
    from_name = models.CharField(_('From'), max_length=128)
236
    to_name = models.CharField(_('To'), max_length=32)
237

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

  
242

  
243
class SetAttributeAction(models.Model):
244
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
245
    attribute = models.CharField(_('User attribute name'), max_length=32)
246
    saml_attribute = models.CharField(_('SAML attribute name'), max_length=128)
247
    mandatory = models.BooleanField(_('Mandatory'), default=False, help_text=_('Deny login if action fails.'))
248

  
249
    class Meta:
250
        default_related_name = 'set_attribute_actions'
251
        verbose_name = _('Set an attribute')
252

  
253

  
254
class AddRoleAction(models.Model):
255
    authenticator = models.ForeignKey(SAMLAuthenticator, on_delete=models.CASCADE)
256
    role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.CASCADE)
257
    condition = models.CharField(_('Condition'), max_length=256, blank=True)
258
    mandatory = models.BooleanField(_('Mandatory'), default=False, help_text=_('Deny login if action fails.'))
259

  
260
    class Meta:
261
        default_related_name = 'add_role_actions'
262
        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_auto_20220727_1704'),
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' * 129, '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' * 128
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
-