authentic/tests/auth_saml/test_migrations.py

429 lines
19 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
def test_saml_authenticator_data_migration(migration, settings):
app = 'authentic2_auth_saml'
migrate_from = [(app, '0001_initial')]
migrate_to = [(app, '0002_auto_20220608_1559')]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
settings.A2_AUTH_SAML_ENABLE = True
settings.MELLON_METADATA_CACHE_TIME = 42
settings.MELLON_METADATA_HTTP_TIMEOUT = 42
settings.MELLON_PROVISION = False
settings.MELLON_VERIFY_SSL_CERTIFICATE = True
settings.MELLON_TRANSIENT_FEDERATION_ATTRIBUTE = None
settings.MELLON_USERNAME_TEMPLATE = 'test'
settings.MELLON_NAME_ID_POLICY_ALLOW_CREATE = False
settings.MELLON_FORCE_AUTHN = True
settings.MELLON_ADD_AUTHNREQUEST_NEXT_URL_EXTENSION = False
settings.MELLON_GROUP_ATTRIBUTE = 'role'
settings.MELLON_CREATE_GROUP = True
settings.MELLON_ERROR_URL = 'https://example.com/error/'
settings.MELLON_AUTHN_CLASSREF = ('class1', 'class2')
settings.MELLON_LOGIN_HINTS = ['hint1', 'hint2']
settings.AUTH_FRONTENDS_KWARGS = {
'saml': {
'priority': 1,
'show_condition': {
'0': 'first condition',
'1': 'second condition',
},
}
}
settings.MELLON_IDENTITY_PROVIDERS = [
{
'METADATA': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
'REALM': 'test',
'METADATA_CACHE_TIME': 43,
'METADATA_HTTP_TIMEOUT': 43,
'PROVISION': True,
'LOOKUP_BY_ATTRIBUTES': [],
},
{
'METADATA_PATH': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
'NAME_ID_POLICY_ALLOW_CREATE': True,
'FORCE_AUTHN': False,
'ADD_AUTHNREQUEST_NEXT_URL_EXTENSION': True,
'A2_ATTRIBUTE_MAPPING': [
{
'attribute': 'email',
'saml_attribute': 'mail',
},
],
'LOOKUP_BY_ATTRIBUTES': [{'saml_attribute': 'email', 'user_field': 'email'}],
},
{
'METADATA_URL': 'https://example.com/metadata.xml',
'SLUG': 'third',
'ATTRIBUTE_MAPPING': {'email': 'attributes[mail][0]'},
'SUPERUSER_MAPPING': {'roles': 'Admin'},
},
]
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
first_authenticator, second_authenticator, third_authenticator = SAMLAuthenticator.objects.all()
assert first_authenticator.slug == '0'
assert first_authenticator.order == 1
assert first_authenticator.show_condition == 'first condition'
assert first_authenticator.enabled is True
assert first_authenticator.metadata_path == os.path.join(os.path.dirname(__file__), 'metadata.xml')
assert first_authenticator.metadata_url == ''
assert first_authenticator.metadata_cache_time == 43
assert first_authenticator.metadata_http_timeout == 43
assert first_authenticator.provision is True
assert first_authenticator.verify_ssl_certificate is True
assert first_authenticator.transient_federation_attribute == ''
assert first_authenticator.realm == 'test'
assert first_authenticator.username_template == 'test'
assert first_authenticator.name_id_policy_format == ''
assert first_authenticator.name_id_policy_allow_create is False
assert first_authenticator.force_authn is True
assert first_authenticator.add_authnrequest_next_url_extension is False
assert first_authenticator.group_attribute == 'role'
assert first_authenticator.create_group is True
assert first_authenticator.error_url == 'https://example.com/error/'
assert first_authenticator.error_redirect_after_timeout == 120
assert first_authenticator.authn_classref == 'class1, class2'
assert first_authenticator.login_hints == 'hint1, hint2'
assert first_authenticator.lookup_by_attributes == []
assert first_authenticator.a2_attribute_mapping == []
assert first_authenticator.attribute_mapping == {}
assert first_authenticator.superuser_mapping == {}
assert second_authenticator.slug == '1'
assert second_authenticator.order == 1
assert second_authenticator.show_condition == 'second condition'
assert second_authenticator.enabled is True
assert second_authenticator.metadata_path == os.path.join(os.path.dirname(__file__), 'metadata.xml')
assert second_authenticator.metadata_url == ''
assert second_authenticator.metadata_cache_time == 42
assert second_authenticator.metadata_http_timeout == 42
assert second_authenticator.provision is False
assert second_authenticator.verify_ssl_certificate is True
assert second_authenticator.transient_federation_attribute == ''
assert second_authenticator.realm == 'saml'
assert second_authenticator.username_template == 'test'
assert second_authenticator.name_id_policy_format == ''
assert second_authenticator.name_id_policy_allow_create is True
assert second_authenticator.force_authn is False
assert second_authenticator.add_authnrequest_next_url_extension is True
assert second_authenticator.group_attribute == 'role'
assert second_authenticator.create_group is True
assert second_authenticator.error_url == 'https://example.com/error/'
assert second_authenticator.error_redirect_after_timeout == 120
assert second_authenticator.authn_classref == 'class1, class2'
assert second_authenticator.login_hints == 'hint1, hint2'
assert second_authenticator.lookup_by_attributes == [{'saml_attribute': 'email', 'user_field': 'email'}]
assert second_authenticator.a2_attribute_mapping == [
{
'attribute': 'email',
'saml_attribute': 'mail',
},
]
assert first_authenticator.attribute_mapping == {}
assert first_authenticator.superuser_mapping == {}
assert third_authenticator.slug == 'third'
assert third_authenticator.order == 1
assert third_authenticator.show_condition == ''
assert third_authenticator.enabled is True
assert third_authenticator.metadata_path == ''
assert third_authenticator.metadata_url == 'https://example.com/metadata.xml'
assert third_authenticator.metadata_cache_time == 42
assert third_authenticator.metadata_http_timeout == 42
assert third_authenticator.provision is False
assert third_authenticator.verify_ssl_certificate is True
assert third_authenticator.transient_federation_attribute == ''
assert third_authenticator.realm == 'saml'
assert third_authenticator.username_template == 'test'
assert third_authenticator.name_id_policy_format == ''
assert third_authenticator.name_id_policy_format == ''
assert third_authenticator.name_id_policy_allow_create is False
assert third_authenticator.force_authn is True
assert third_authenticator.group_attribute == 'role'
assert third_authenticator.create_group is True
assert third_authenticator.error_url == 'https://example.com/error/'
assert third_authenticator.error_redirect_after_timeout == 120
assert third_authenticator.authn_classref == 'class1, class2'
assert third_authenticator.login_hints == 'hint1, hint2'
assert third_authenticator.lookup_by_attributes == [
{'saml_attribute': 'email', 'user_field': 'email', 'ignore-case': True},
{'saml_attribute': 'username', 'user_field': 'username'},
]
assert third_authenticator.a2_attribute_mapping == []
assert third_authenticator.attribute_mapping == {'email': 'attributes[mail][0]'}
assert third_authenticator.superuser_mapping == {'roles': 'Admin'}
def test_saml_authenticator_data_migration_empty_configuration(migration, settings):
app = 'authentic2_auth_saml'
migrate_from = [(app, '0001_initial')]
migrate_to = [(app, '0002_auto_20220608_1559')]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
assert not SAMLAuthenticator.objects.exists()
def test_saml_authenticator_data_migration_bad_settings(migration, settings):
app = 'authentic2_auth_saml'
migrate_from = [(app, '0001_initial')]
migrate_to = [(app, '0002_auto_20220608_1559')]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
settings.AUTH_FRONTENDS_KWARGS = {'saml': {'priority': None, 'show_condition': None}}
settings.MELLON_METADATA_CACHE_TIME = 2**16
settings.MELLON_METADATA_HTTP_TIMEOUT = -1
settings.MELLON_PROVISION = None
settings.MELLON_USERNAME_TEMPLATE = 42
settings.MELLON_GROUP_ATTRIBUTE = None
settings.MELLON_ERROR_URL = 'a' * 500
settings.MELLON_AUTHN_CLASSREF = 'not-a-list'
settings.MELLON_IDENTITY_PROVIDERS = [
{
'METADATA': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
'ERROR_REDIRECT_AFTER_TIMEOUT': -1,
'SUPERUSER_MAPPING': 'not-a-dict',
},
]
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
authenticator = SAMLAuthenticator.objects.get()
assert authenticator.slug == '0'
assert authenticator.order == 3
assert authenticator.show_condition == ''
assert authenticator.enabled is False
assert authenticator.metadata_cache_time == 3600
assert authenticator.metadata_http_timeout == 10
assert authenticator.provision is True
assert authenticator.username_template == '{attributes[name_id_content]}@{realm}'
assert authenticator.group_attribute == ''
assert authenticator.error_url == 'a' * 200
assert authenticator.error_redirect_after_timeout == 120
assert authenticator.authn_classref == ''
assert authenticator.superuser_mapping == {}
def test_saml_authenticator_data_migration_json_fields(migration, settings):
migrate_from = [
(
'authentic2_auth_saml',
'0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
),
('a2_rbac', '0036_delete_roleattribute'),
]
migrate_to = [
('authentic2_auth_saml', '0006_migrate_jsonfields'),
('a2_rbac', '0036_delete_roleattribute'),
]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
Role = old_apps.get_model('a2_rbac', 'Role')
OU = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
ou = OU.objects.create(name='Test OU', slug='test-ou')
role = Role.objects.create(name='Test role', slug='test-role', ou=ou)
SAMLAuthenticator.objects.create(
metadata='meta1.xml',
slug='idp1',
lookup_by_attributes=[
{'saml_attribute': 'email', 'user_field': 'email'},
{'saml_attribute': 'saml_name', 'user_field': 'first_name', 'ignore-case': True},
],
a2_attribute_mapping=[
{
'attribute': 'email',
'saml_attribute': 'mail',
'mandatory': True,
},
{'action': 'rename', 'from': 'a' * 1025, 'to': 'first_name'},
{
'attribute': 'first_name',
'saml_attribute': 'first_name',
},
{
'attribute': 'invalid',
'saml_attribute': '',
},
{
'attribute': 'invalid',
'saml_attribute': None,
},
{
'attribute': 'invalid',
},
{
'action': 'add-role',
'role': {
'name': role.name,
'ou': {
'name': role.ou.name,
},
},
'condition': "roles == 'A'",
},
],
)
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
authenticator = SAMLAuthenticator.objects.get()
attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
assert attribute_lookup1.saml_attribute == 'email'
assert attribute_lookup1.user_field == 'email'
assert attribute_lookup1.ignore_case is False
assert attribute_lookup2.saml_attribute == 'saml_name'
assert attribute_lookup2.user_field == 'first_name'
assert attribute_lookup2.ignore_case is True
set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
assert set_attribute1.attribute == 'email'
assert set_attribute1.saml_attribute == 'mail'
assert set_attribute1.mandatory is True
assert set_attribute2.attribute == 'first_name'
assert set_attribute2.saml_attribute == 'first_name'
assert set_attribute2.mandatory is False
rename_attribute = authenticator.rename_attribute_actions.get()
assert rename_attribute.from_name == 'a' * 1024
assert rename_attribute.to_name == 'first_name'
add_role = authenticator.add_role_actions.get()
assert add_role.role.pk == role.pk
assert add_role.condition == "roles == 'A'"
assert add_role.mandatory is False
def test_saml_authenticator_data_migration_json_fields_log_errors(migration, settings, caplog):
migrate_from = [
(
'authentic2_auth_saml',
'0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
),
('a2_rbac', '0036_delete_roleattribute'),
]
migrate_to = [
('authentic2_auth_saml', '0006_migrate_jsonfields'),
('a2_rbac', '0036_delete_roleattribute'),
]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
SAMLAuthenticator.objects.create(
metadata='meta1.xml',
slug='idp1',
lookup_by_attributes=[{'saml_attribute': 'email', 'user_field': 'email'}],
a2_attribute_mapping=['bad'],
)
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
authenticator = SAMLAuthenticator.objects.get()
assert not authenticator.attribute_lookups.exists()
assert caplog.messages == [
'could not create related objects for authenticator SAMLAuthenticator object (%s)' % authenticator.pk,
'attribute mapping for SAMLAuthenticator object (%s): ["bad"]' % authenticator.pk,
'lookup by attributes for SAMLAuthenticator object (%s): [{"user_field": "email", "saml_attribute": "email"}]'
% authenticator.pk,
]
def test_saml_authenticator_data_migration_rename_attributes(migration, settings):
migrate_from = [('authentic2_auth_saml', '0008_auto_20220913_1105')]
migrate_to = [('authentic2_auth_saml', '0009_statically_rename_attributes')]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
RenameAttributeAction = old_apps.get_model('authentic2_auth_saml', 'RenameAttributeAction')
SetAttributeAction = old_apps.get_model('authentic2_auth_saml', 'SetAttributeAction')
SAMLAttributeLookup = old_apps.get_model('authentic2_auth_saml', 'SAMLAttributeLookup')
authenticator = SAMLAuthenticator.objects.create(slug='idp1')
RenameAttributeAction.objects.create(
authenticator=authenticator, from_name='http://nice/attribute/givenName', to_name='first_name'
)
SAMLAttributeLookup.objects.create(
authenticator=authenticator, user_field='first_name', saml_attribute='first_name'
)
SAMLAttributeLookup.objects.create(
authenticator=authenticator, user_field='title', saml_attribute='title'
)
SetAttributeAction.objects.create(
authenticator=authenticator, user_field='first_name', saml_attribute='first_name'
)
SetAttributeAction.objects.create(authenticator=authenticator, user_field='title', saml_attribute='title')
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
authenticator = SAMLAuthenticator.objects.get()
attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
assert attribute_lookup1.saml_attribute == 'http://nice/attribute/givenName'
assert attribute_lookup1.user_field == 'first_name'
assert attribute_lookup2.saml_attribute == 'title'
assert attribute_lookup2.user_field == 'title'
set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
assert set_attribute1.saml_attribute == 'http://nice/attribute/givenName'
assert set_attribute1.user_field == 'first_name'
assert set_attribute2.saml_attribute == 'title'
assert set_attribute2.user_field == 'title'
def test_saml_authenticator_data_migration_metadata_file_to_db(migration, settings):
migrate_from = [('authentic2_auth_saml', '0012_move_add_role_action')]
migrate_to = [('authentic2_auth_saml', '0014_remove_samlauthenticator_metadata_path')]
old_apps = migration.before(migrate_from)
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
SAMLAuthenticator.objects.create(
slug='idp1', metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml')
)
SAMLAuthenticator.objects.create(slug='idp2', metadata='xxx')
SAMLAuthenticator.objects.create(slug='idp3', metadata_url='https://example.com')
SAMLAuthenticator.objects.create(slug='idp4', metadata_path='/unknown/')
new_apps = migration.apply(migrate_to)
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
authenticator = SAMLAuthenticator.objects.get(slug='idp1')
assert authenticator.metadata.startswith('<?xml version="1.0"?>')
assert authenticator.metadata.endswith('</EntityDescriptor>\n')
assert SAMLAuthenticator.objects.filter(slug='idp2', metadata='xxx').count() == 1
assert SAMLAuthenticator.objects.filter(slug='idp3', metadata_url='https://example.com').count() == 1