0001-auth_saml-migrate-JSON-fields-to-models-67025.patch
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 |
- |