authentic/tests/test_manager_authenticators.py

884 lines
35 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 json
import pytest
from django import VERSION as DJ_VERSION
from django.utils.html import escape
from webtest import Upload
from authentic2.a2_rbac.models import Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.apps.authenticators.models import AddRoleAction, BaseAuthenticator, LoginPasswordAuthenticator
from authentic2.models import Attribute
from authentic2_auth_fc.models import FcAuthenticator
from authentic2_auth_oidc.models import OIDCClaimMapping, OIDCProvider
from authentic2_auth_saml.models import SAMLAuthenticator, SetAttributeAction
from .utils import assert_event, login, logout
def test_authenticators_authorization(app, simple_user, simple_role, admin, superuser):
simple_user.roles.add(simple_role.get_admin_role()) # grant user access to /manage/
resp = login(app, simple_user, path='/manage/')
assert 'Authenticators' not in resp.text
app.get('/manage/authenticators/', status=403)
role = Role.objects.get(name='Manager of authenticators')
simple_user.roles.add(role)
resp = app.get('/manage/')
resp = resp.click('Authentication frontends')
assert 'Authenticators' in resp.text
logout(app)
resp = login(app, admin, path='/manage/')
assert 'Authentication frontends' in resp.text
resp = resp.click('Authentication frontends')
assert 'Authenticators' in resp.text
logout(app)
resp = login(app, superuser, path='/manage/')
assert 'Authentication frontends' in resp.text
resp = resp.click('Authentication frontends')
assert 'Authenticators' in resp.text
def test_authenticators_password(app, superuser_or_admin, settings):
resp = login(app, superuser_or_admin, path='/manage/authenticators/')
# Password authenticator already exists
assert 'Password' in resp.text
authenticator = LoginPasswordAuthenticator.objects.get()
resp = resp.click('Configure')
assert 'Show condition: None' in resp.text
# cannot delete password authenticator
assert 'Delete' not in resp.text
assert 'configuration is not complete' not in resp.text
app.get('/manage/authenticators/%s/delete/' % authenticator.pk, status=403)
resp = resp.click('Edit')
assert list(resp.form.fields) == [
'csrfmiddlewaretoken',
'show_condition',
'button_description',
'registration_open',
'min_password_strength',
'password_min_length',
'remember_me',
'include_ou_selector',
'password_regex',
'password_regex_error_msg',
'login_exponential_retry_timeout_duration',
'login_exponential_retry_timeout_factor',
'login_exponential_retry_timeout_max_duration',
'login_exponential_retry_timeout_min_duration',
'emails_ip_ratelimit',
'sms_ip_ratelimit',
'emails_address_ratelimit',
'sms_number_ratelimit',
None,
]
resp.form['show_condition'] = '}'
resp = resp.form.submit()
assert "could not parse expression: unmatched" in resp.text
resp.form['show_condition'] = "'backoffice' in login_hint or remote_addr == '1.2.3.4'"
resp = resp.form.submit().follow()
assert 'Click "Edit" to change configuration.' not in resp.text
if DJ_VERSION[0] <= 2:
assert (
"Show condition: &#39;backoffice&#39; in login_hint or remote_addr == &#39;1.2.3.4&#39;"
in resp.text
)
else:
# html-rendered quote characters change in django 3 onwards…
assert (
"Show condition: &#x27;backoffice&#x27; in login_hint or remote_addr == &#x27;1.2.3.4&#x27;"
in resp.text
)
assert_event('authenticator.edit', user=superuser_or_admin, session=app.session)
resp = resp.click('Edit')
resp.form['show_condition'] = "remote_addr in dnsbl('ddns.entrouvert.org')"
resp = resp.form.submit().follow()
assert 'dnsbl' in resp.text
# password authenticator cannot be disabled
assert 'Disable' not in resp.text
app.get('/manage/authenticators/%s/toggle/' % authenticator.pk, status=403)
resp = resp.click('Journal')
assert resp.text.count('edit (show_condition)') == 2
# cannot add another password authenticator
resp = app.get('/manage/authenticators/add/')
assert 'Password' not in resp.text
# phone authn management feature flag is activated
settings.A2_ALLOW_PHONE_AUTHN_MANAGEMENT = True
phone1 = Attribute.objects.create(
name='another_phone',
kind='phone_number',
label='Another phone',
)
phone2 = Attribute.objects.create(
name='yet_another_phone',
kind='fr_phone_number',
label='Yet another phone',
)
resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
resp.form['accept_email_authentication'] = False
resp.form['accept_phone_authentication'] = True
assert resp.form['phone_identifier_field'].options == [
(str(phone1.id), False, 'Another phone'),
(str(phone2.id), False, 'Yet another phone'),
]
resp.form['phone_identifier_field'] = phone2.id
resp.form.submit()
authenticator.refresh_from_db()
assert authenticator.accept_email_authentication is False
assert authenticator.accept_phone_authentication is True
assert authenticator.phone_identifier_field == phone2
@pytest.mark.freeze_time('2022-04-19 14:00')
def test_authenticators_password_export(app, superuser):
resp = login(app, superuser, path='/manage/authenticators/')
assert LoginPasswordAuthenticator.objects.count() == 1
resp = resp.click('Configure')
resp = resp.click('Export')
assert resp.headers['content-type'] == 'application/json'
assert (
resp.headers['content-disposition']
== 'attachment; filename="export_password_authenticator_20220419.json"'
)
authenticator_json = json.loads(resp.text)
assert authenticator_json == {
'authenticator_type': 'authenticators.loginpasswordauthenticator',
'name': '',
'slug': 'password-authenticator',
'show_condition': '',
'button_description': '',
'button_label': 'Login',
'registration_open': True,
'remember_me': None,
'include_ou_selector': False,
'min_password_strength': None,
'password_min_length': 8,
'password_regex': '',
'password_regex_error_msg': '',
'login_exponential_retry_timeout_duration': 1,
'login_exponential_retry_timeout_factor': 1.8,
'login_exponential_retry_timeout_max_duration': 3600,
'login_exponential_retry_timeout_min_duration': 10,
'emails_ip_ratelimit': '10/h',
'sms_ip_ratelimit': '10/h',
'emails_address_ratelimit': '3/d',
'sms_number_ratelimit': '10/h',
'ou': None,
'related_objects': [],
'accept_email_authentication': True,
'accept_phone_authentication': False,
}
resp = app.get('/manage/authenticators/')
resp = resp.click('Import')
authenticator_json['button_description'] = 'test'
resp.form['authenticator_json'] = Upload(
'export.json', json.dumps(authenticator_json).encode(), 'application/json'
)
resp = resp.form.submit()
assert LoginPasswordAuthenticator.objects.count() == 1
assert LoginPasswordAuthenticator.objects.get().button_description == 'test'
@pytest.mark.freeze_time('2022-04-19 14:00')
def test_authenticators_oidc(app, superuser, ou1, ou2):
resp = login(app, superuser, path='/manage/authenticators/')
resp = resp.click('Add new authenticator')
resp.form['name'] = 'Test'
resp.form['authenticator'] = 'oidc'
resp = resp.form.submit()
assert '/edit/' in resp.location
assert_event('authenticator.creation', user=superuser, session=app.session)
provider = OIDCProvider.objects.filter(slug='test', ou=get_default_ou()).get()
resp = app.get(provider.get_absolute_url())
assert 'extra-actions-menu-opener' in resp.text
assert 'Creation date: April 19, 2022, 2 p.m.' in resp.text
assert 'Last modification date: April 19, 2022, 2 p.m.' in resp.text
assert 'Issuer' not in resp.text
assert 'Enable' not in resp.text
assert 'configuration is not complete' in resp.text
app.get('/manage/authenticators/%s/toggle/' % provider.pk, status=403)
resp = resp.click('Edit')
assert 'enabled' not in resp.form.fields
assert 'last_sync_time' not in resp.form.fields
assert resp.pyquery('input#id_client_id').val() == ''
assert resp.pyquery('input#id_client_secret').val() == ''
resp.form['ou'] = ou1.pk
resp.form['issuer'] = 'https://oidc.example.com'
resp.form['scopes'] = 'profile email'
resp.form['strategy'] = 'create'
resp.form['authorization_endpoint'] = 'https://oidc.example.com/authorize'
resp.form['token_endpoint'] = 'https://oidc.example.com/token'
resp.form['userinfo_endpoint'] = 'https://oidc.example.com/user_info'
resp.form['idtoken_algo'] = 2
resp.form['button_label'] = 'Test'
resp.form['button_description'] = 'test'
resp.form['client_id'] = 'auie'
resp.form['client_secret'] = 'tsrn'
resp = resp.form.submit().follow()
assert_event('authenticator.edit', user=superuser, session=app.session)
assert 'Issuer: https://oidc.example.com' in resp.text
assert 'Scopes: profile email' in resp.text
resp = app.get('/manage/authenticators/')
assert 'OpenID Connect - Test' in resp.text
assert 'class="section disabled"' in resp.text
assert 'OIDC provider linked to' not in resp.text
resp = resp.click('Configure', index=1)
resp = resp.click('Enable').follow()
assert 'Authenticator has been enabled.' in resp.text
assert_event('authenticator.enable', user=superuser, session=app.session)
resp = resp.click('Journal')
assert 'enable' in resp.text
assert (
'edit (ou, issuer, scopes, strategy, client_id, button_label, idtoken_algo, '
'client_secret, token_endpoint, userinfo_endpoint, button_description, authorization_endpoint)'
in resp.text
)
assert 'creation' in resp.text
resp = app.get('/manage/authenticators/')
assert 'class="section disabled"' not in resp.text
assert 'OIDC provider linked to https://oidc.example.com with scopes profile, email.' not in resp.text
# same name
resp = resp.click('Add new authenticator')
resp.form['name'] = 'test'
resp.form['authenticator'] = 'oidc'
resp = resp.form.submit().follow()
assert OIDCProvider.objects.filter(slug='test-1').count() == 1
OIDCProvider.objects.filter(slug='test-1').delete()
# no name
resp = app.get('/manage/authenticators/add/')
resp.form['authenticator'] = 'oidc'
resp = resp.form.submit()
assert 'This field is required' in resp.text
resp = app.get('/manage/authenticators/')
resp = resp.click('Configure', index=1)
resp = resp.click('Disable').follow()
assert 'Authenticator has been disabled.' in resp.text
assert_event('authenticator.disable', user=superuser, session=app.session)
resp = app.get('/manage/authenticators/')
assert 'class="section disabled"' in resp.text
resp = resp.click('Configure', index=1)
resp = resp.click('Delete')
resp = resp.form.submit().follow()
assert not OIDCProvider.objects.filter(slug='test').exists()
assert_event('authenticator.deletion', user=superuser, session=app.session)
def test_authenticators_oidc_claims(app, superuser):
authenticator = OIDCProvider.objects.create(slug='idp1')
resp = login(app, superuser, path=authenticator.get_absolute_url())
resp = resp.click('Add', href='claim')
resp.form['claim'] = 'email'
resp.form['attribute'].select(text='Email address (email)')
resp.form['verified'].select(text='verified claim')
resp.form['required'] = True
resp.form['idtoken_claim'] = True
resp = resp.form.submit()
assert_event('authenticator.related_object.creation', user=superuser, session=app.session)
assert '#open:oidcclaimmapping' in resp.location
resp = resp.follow()
assert 'email → Email address (email), verified, required, idtoken' in resp.text
resp = resp.click('email')
resp.form['attribute'].select(text='First name (first_name)')
resp = resp.form.submit().follow()
assert 'email → First name (first_name), verified, required, idtoken' in resp.text
assert_event('authenticator.related_object.edit', user=superuser, session=app.session)
resp = resp.click('Remove')
resp = resp.form.submit().follow()
assert 'email' not in resp.text
assert_event('authenticator.related_object.deletion', user=superuser, session=app.session)
def test_authenticators_oidc_claims_disabled_attribute(app, superuser):
authenticator = OIDCProvider.objects.create(slug='idp1')
attr = Attribute.objects.create(kind='string', name='test_attribute', label='Test attribute')
resp = login(app, superuser, path=authenticator.get_absolute_url())
resp = resp.click('Add', href='claim')
assert resp.pyquery('select#id_attribute option[value=test_attribute]')
attr.disabled = True
attr.save()
resp = app.get(authenticator.get_absolute_url())
resp = resp.click('Add', href='claim')
assert not resp.pyquery('select#id_attribute option[value=test_attribute]')
def test_authenticators_oidc_add_role(app, superuser, role_ou1):
authenticator = OIDCProvider.objects.create(slug='idp1')
resp = login(app, superuser, path=authenticator.get_absolute_url())
resp = resp.click('Add', href='role')
resp.form['role'] = role_ou1.pk
resp = resp.form.submit().follow()
assert 'role_ou1' in resp.text
def test_authenticators_oidc_export(app, superuser, simple_role):
authenticator = OIDCProvider.objects.create(slug='idp1', order=42, ou=get_default_ou(), enabled=True)
OIDCClaimMapping.objects.create(authenticator=authenticator, claim='test', attribute='hop')
AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)
resp = login(app, superuser, path=authenticator.get_absolute_url())
export_resp = resp.click('Export')
resp = app.get('/manage/authenticators/import/')
resp.form['authenticator_json'] = Upload('export.json', export_resp.body, 'application/json')
resp = resp.form.submit()
assert '/authenticators/%s/' % authenticator.pk in resp.location
resp = resp.follow()
assert 'Authenticator has been updated.' in resp.text
assert OIDCProvider.objects.count() == 1
assert OIDCClaimMapping.objects.count() == 1
assert AddRoleAction.objects.count() == 1
OIDCProvider.objects.all().delete()
OIDCClaimMapping.objects.all().delete()
AddRoleAction.objects.all().delete()
resp = app.get('/manage/authenticators/import/')
resp.form['authenticator_json'] = Upload('export.json', export_resp.body, 'application/json')
resp = resp.form.submit().follow()
assert 'Authenticator has been created.' in resp.text
authenticator = OIDCProvider.objects.get()
assert authenticator.slug == 'idp1'
assert authenticator.order == 1
assert authenticator.ou == get_default_ou()
assert authenticator.enabled is False
assert OIDCClaimMapping.objects.filter(
authenticator=authenticator, claim='test', attribute='hop'
).exists()
assert AddRoleAction.objects.filter(authenticator=authenticator, role=simple_role).exists()
def test_authenticators_oidc_import_errors(app, superuser, simple_role):
resp = login(app, superuser, path='/manage/authenticators/import/')
resp.form['authenticator_json'] = Upload('export.json', b'not-json', 'application/json')
resp = resp.form.submit()
assert 'File is not in the expected JSON format.' in resp.text
resp.form['authenticator_json'] = Upload('export.json', b'{}', 'application/json')
resp = resp.form.submit()
assert escape('Missing "authenticator_type" key.') in resp.text
resp.form['authenticator_json'] = Upload(
'export.json', b'{"authenticator_type": "xxx"}', 'application/json'
)
resp = resp.form.submit()
assert 'Invalid authenticator_type: xxx.' in resp.text
resp.form['authenticator_json'] = Upload(
'export.json', b'{"authenticator_type": "x.y"}', 'application/json'
)
resp = resp.form.submit()
assert 'Unknown authenticator_type: x.y.' in resp.text
authenticator = OIDCProvider.objects.create(slug='idp1', order=42, ou=get_default_ou(), enabled=True)
AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)
export_resp = app.get('/manage/authenticators/%s/export/' % authenticator.pk)
export = json.loads(export_resp.text)
del export['slug']
resp.form['authenticator_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
resp = resp.form.submit()
assert 'Missing slug.' in resp.text
export = json.loads(export_resp.text)
export['ou'] = {'slug': 'xxx'}
resp.form['authenticator_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
resp = resp.form.submit()
assert escape("Organization unit not found: {'slug': 'xxx'}.") in resp.text
export = json.loads(export_resp.text)
del export['related_objects'][0]['object_type']
resp.form['authenticator_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
resp = resp.form.submit()
assert escape('Missing "object_type" key.') in resp.text
export = json.loads(export_resp.text)
del export['related_objects'][0]['role']
resp.form['authenticator_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
resp = resp.form.submit()
assert escape('Missing "role" key in add role action.') in resp.text
export = json.loads(export_resp.text)
export['related_objects'][0]['role'] = {'slug': 'xxx'}
resp.form['authenticator_json'] = Upload('export.json', json.dumps(export).encode(), 'application/json')
resp = resp.form.submit()
assert escape("Role not found: {'slug': 'xxx'}.") in resp.text
def test_authenticators_oidc_related_objects_permissions(app, simple_user, simple_role):
authenticator = OIDCProvider.objects.create(slug='idp1', order=42, ou=get_default_ou(), enabled=True)
authenticator.save()
mapping = OIDCClaimMapping.objects.create(authenticator=authenticator, claim='test', attribute='hop')
action = AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)
simple_user.roles.add(simple_role.get_admin_role()) # grant user access to /manage/
role = Role.objects.get(name='Manager of authenticators')
login(app, simple_user, path='/')
app.get(authenticator.get_absolute_url(), status=403)
app.get(f'/manage/authenticators/{authenticator.pk}/oidcclaimmapping/{mapping.pk}/edit/', status=403)
app.get(f'/manage/authenticators/{authenticator.pk}/addroleaction/{action.pk}/delete/', status=403)
app.get(f'/manage/authenticators/{authenticator.pk}/addroleaction/add/', status=403)
simple_user.roles.add(role)
app.get(authenticator.get_absolute_url())
app.get(f'/manage/authenticators/{authenticator.pk}/oidcclaimmapping/{mapping.pk}/edit/')
app.get(f'/manage/authenticators/{authenticator.pk}/addroleaction/{action.pk}/delete/')
app.get(f'/manage/authenticators/{authenticator.pk}/addroleaction/add/')
def test_authenticators_fc(app, superuser):
resp = login(app, superuser, path='/manage/authenticators/')
resp = resp.click('Add new authenticator')
resp.form['authenticator'] = 'fc'
resp = resp.form.submit()
assert '/edit/' in resp.location
provider = FcAuthenticator.objects.get()
assert provider.order == -1
resp = app.get(provider.get_absolute_url())
assert 'extra-actions-menu-opener' in resp.text
assert 'Platform: Integration' in resp.text
assert 'Scopes: profile (profile), email (email)' in resp.text
assert 'Client ID' not in resp.text
assert 'Client Secret' not in resp.text
resp = resp.click('Edit')
assert list(resp.form.fields) == [
'csrfmiddlewaretoken',
'show_condition',
'platform',
'client_id',
'client_secret',
'scopes',
'link_by_email',
None,
]
assert 'phone' not in resp.pyquery('#id_scopes').html()
assert 'address' not in resp.pyquery('#id_scopes').html()
resp.form['platform'] = 'prod'
resp.form['client_id'] = '211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6k'
resp.form['client_secret'] = '211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6d'
resp.form['scopes'] = ['given_name', 'birthdate']
resp = resp.form.submit().follow()
assert 'Platform: Production' in resp.text
assert 'Scopes: given name (given_name), birthdate (birthdate)' in resp.text
assert 'Client ID: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6k' in resp.text
assert 'Client Secret: 211286433e39cce01db448d80181bdfd005554b19cd51b3fe7943f6b3b86ab6d' in resp.text
resp = app.get('/manage/authenticators/')
assert 'FranceConnect' in resp.text
assert 'class="section disabled"' in resp.text
resp = resp.click('Configure', index=1)
resp = resp.click('Enable').follow()
assert 'Authenticator has been enabled.' in resp.text
resp = app.get('/manage/authenticators/')
assert 'class="section disabled"' not in resp.text
provider.refresh_from_db()
provider.scopes.extend(['phone', 'address']) # deprecated scopes
provider.save()
resp = app.get(provider.get_absolute_url())
resp = resp.click('Edit')
resp.form.submit().follow()
provider.refresh_from_db()
assert 'phone' not in provider.scopes
assert 'address' not in provider.scopes
def test_authenticators_saml(app, superuser, ou1, ou2):
resp = login(app, superuser, path='/manage/authenticators/')
resp = resp.click('Add new authenticator')
resp.form['name'] = 'Test'
resp.form['authenticator'] = 'saml'
resp = resp.form.submit()
authenticator = SAMLAuthenticator.objects.filter(slug='test').get()
resp = app.get(authenticator.get_absolute_url())
assert 'Create user if their username does not already exists: Yes' in resp.text
assert 'Metadata file path' not in resp.text
assert 'Enable' not in resp.text
assert 'configuration is not complete' in resp.text
resp = resp.click('Edit')
assert resp.pyquery('button#tab-general').attr('class') == 'pk-tabs--button-marker'
assert not resp.pyquery('button#tab-advanced').attr('class')
assert 'ou' not in resp.form.fields
resp = resp.form.submit()
assert 'One of the metadata fields must be filled.' in resp.text
resp.form['metadata_url'] = 'https://example.com/metadata.xml'
resp = resp.form.submit().follow()
assert 'Metadata URL: https://example.com/metadata.xml' in resp.text
resp = resp.click('Enable').follow()
assert 'Authenticator has been enabled.' in resp.text
resp = resp.click('Edit')
resp.form['attribute_mapping'] = '[{"attribute": "email", "saml_attribute": "mail", "mandatory": false}]'
resp = resp.form.submit().follow()
authenticator.refresh_from_db()
assert authenticator.attribute_mapping == [
{"attribute": "email", "saml_attribute": "mail", "mandatory": False}
]
resp = resp.click('Edit')
assert resp.pyquery('button#tab-advanced').attr('class') == 'pk-tabs--button-marker'
resp = app.get(authenticator.get_absolute_url())
resp = resp.click('Journal')
assert 'edit (metadata_url)' in resp.text
def test_authenticators_saml_hide_metadata_url_advanced_fields(app, superuser, ou1, ou2):
authenticator = SAMLAuthenticator.objects.create(slug='idp1')
resp = login(app, superuser)
resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
assert 'Metadata cache time' not in resp.text
assert 'Metadata HTTP timeout' not in resp.text
resp.form['metadata_url'] = 'https://example.com/metadata.xml'
resp = resp.form.submit().follow()
resp = resp.click('Edit')
assert 'Metadata cache time' in resp.text
assert 'Metadata HTTP timeout' in resp.text
def test_authenticators_saml_validate_metadata(app, superuser):
authenticator = SAMLAuthenticator.objects.create(slug='idp1')
resp = login(app, superuser)
resp = app.get('/manage/authenticators/%s/edit/' % authenticator.pk)
resp.form['metadata'] = 'invalid'
resp.form['metadata'] = '<a/>'
resp = resp.form.submit()
assert 'Invalid metadata, missing tag {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor' in resp.text
resp.form[
'metadata'
] = '<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"></ns0:EntityDescriptor>'
resp = resp.form.submit()
assert 'Invalid metadata, missing entityID' in resp.text
resp.form['metadata'] = (
'<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata"'
' entityID="https://example.com"></ns0:EntityDescriptor>'
)
resp.form.submit(status=302)
def test_authenticators_saml_view_metadata(app, superuser):
authenticator = SAMLAuthenticator.objects.create(slug='idp1')
resp = login(app, superuser)
resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
assert 'Metadata (XML):' not in resp.text
assert app.get('/manage/authenticators/%s/metadata.xml' % authenticator.pk, status=404)
authenticator.metadata = '<a><b></b></a>'
authenticator.save()
resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
assert 'Metadata (XML):' in resp.text
resp = resp.click('View metadata')
assert resp.text == '<a><b></b></a>'
def test_authenticators_saml_missing_signing_key(app, superuser, settings):
authenticator = SAMLAuthenticator.objects.create(slug='idp1')
resp = login(app, superuser)
resp = app.get(authenticator.get_absolute_url())
assert 'Signing key is missing' in resp.text
settings.MELLON_PRIVATE_KEY = 'xxx'
settings.MELLON_PUBLIC_KEYS = ['yyy']
resp = app.get(authenticator.get_absolute_url())
assert 'Signing key is missing' not in resp.text
def test_authenticators_saml_no_name_display(app, superuser, ou1, ou2):
SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
resp = login(app, superuser, path='/manage/authenticators/')
assert 'SAML - idp1' in resp.text
def test_authenticators_saml_name_id_format_select(app, superuser):
authenticator = SAMLAuthenticator.objects.create(metadata_url='https://example.com/meta.xml', slug='idp1')
resp = login(app, superuser, path='/manage/authenticators/%s/edit/' % authenticator.pk)
resp.form['name_id_policy_format'].select(
text='Persistent (urn:oasis:names:tc:SAML:2.0:nameid-format:persistent)'
)
resp.form.submit().follow()
authenticator.refresh_from_db()
assert authenticator.name_id_policy_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
def test_authenticators_saml_attribute_lookup(app, superuser):
authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
resp = login(app, superuser, path=authenticator.get_absolute_url())
resp = resp.click('Add', href='samlattributelookup')
resp.form['user_field'].select(text='Email address (email)')
resp.form['saml_attribute'] = 'mail'
resp = resp.form.submit()
assert_event('authenticator.related_object.creation', user=superuser, session=app.session)
assert '#open:samlattributelookup' in resp.location
resp = resp.follow()
assert escape('"mail" (from "Email address (email)")') in resp.text
resp = resp.click('mail')
resp.form['ignore_case'] = True
resp = resp.form.submit().follow()
assert escape('"mail" (from "Email address (email)"), case insensitive') in resp.text
assert_event('authenticator.related_object.edit', user=superuser, session=app.session)
Attribute.objects.create(kind='string', name='test', label='Test')
resp = resp.click('mail')
resp.form['user_field'].select(text='Test (test)')
resp = resp.form.submit().follow()
assert escape('"mail" (from "Test (test)"), case insensitive') in resp.text
resp = resp.click('Remove', href='samlattributelookup')
resp = resp.form.submit().follow()
assert 'Test (test)' not in resp.text
assert_event('authenticator.related_object.deletion', user=superuser, session=app.session)
def test_authenticators_saml_set_attribute(app, superuser):
authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
resp = login(app, superuser, path=authenticator.get_absolute_url())
resp = resp.click('Add', href='setattributeaction')
resp.form['user_field'].select(text='Email address (email)')
resp.form['saml_attribute'] = 'mail'
resp = resp.form.submit().follow()
assert escape('"Email address (email)" from "mail"') in resp.text
resp = resp.click('mail')
resp.form['mandatory'] = True
resp = resp.form.submit().follow()
assert escape('"Email address (email)" from "mail" (mandatory)') in resp.text
def test_authenticators_saml_add_role(app, superuser, role_ou1, role_ou2):
authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
resp = login(app, superuser, path=authenticator.get_absolute_url())
resp = resp.click('Add', href='addroleaction')
assert [x[2] for x in resp.form['role'].options] == ['---------', 'OU1 - role_ou1', 'OU2 - role_ou2']
resp.form['role'] = role_ou1.pk
resp = resp.form.submit().follow()
assert 'role_ou1' in resp.text
resp = resp.click('role_ou1')
resp.form['role'] = role_ou2.pk
resp = resp.form.submit().follow()
assert 'role_ou1' not in resp.text
assert 'role_ou2' in resp.text
def test_authenticators_saml_export(app, superuser, simple_role):
authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
SetAttributeAction.objects.create(authenticator=authenticator, user_field='test', saml_attribute='hop')
AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)
resp = login(app, superuser, path=authenticator.get_absolute_url())
export_resp = resp.click('Export')
SAMLAuthenticator.objects.all().delete()
SetAttributeAction.objects.all().delete()
AddRoleAction.objects.all().delete()
resp = app.get('/manage/authenticators/import/')
resp.form['authenticator_json'] = Upload('export.json', export_resp.body, 'application/json')
resp = resp.form.submit().follow()
authenticator = SAMLAuthenticator.objects.get()
assert authenticator.slug == 'idp1'
assert authenticator.metadata == 'meta1.xml'
assert SetAttributeAction.objects.filter(
authenticator=authenticator, user_field='test', saml_attribute='hop'
).exists()
assert AddRoleAction.objects.filter(authenticator=authenticator, role=simple_role).exists()
def test_authenticators_order(app, superuser):
resp = login(app, superuser, path='/manage/authenticators/')
saml_authenticator = SAMLAuthenticator.objects.create(name='Test', slug='test', enabled=True, order=42)
SAMLAuthenticator.objects.create(name='Test disabled', slug='test-disabled', enabled=False)
fc_authenticator = FcAuthenticator.objects.create(slug='fc-authenticator', enabled=True, order=-1)
password_authenticator = LoginPasswordAuthenticator.objects.get()
assert fc_authenticator.order == -1
assert password_authenticator.order == 0
assert saml_authenticator.order == 42
resp = resp.click('Edit order')
assert resp.text.index('FranceConnect') < resp.text.index('Password') < resp.text.index('SAML - Test')
assert 'SAML - Test disabled' not in resp.text
resp.form['order'] = '%s,%s,%s' % (saml_authenticator.pk, password_authenticator.pk, fc_authenticator.pk)
resp.form.submit()
fc_authenticator.refresh_from_db()
password_authenticator.refresh_from_db()
saml_authenticator.refresh_from_db()
assert fc_authenticator.order == 2
assert password_authenticator.order == 1
assert saml_authenticator.order == 0
def test_authenticators_add_last(app, superuser):
resp = login(app, superuser, path='/manage/authenticators/')
BaseAuthenticator.objects.all().delete()
resp = resp.click('Add new authenticator')
resp.form['name'] = 'Test'
resp.form['authenticator'] = 'saml'
resp.form.submit()
authenticator = SAMLAuthenticator.objects.get()
assert authenticator.order == 1
authenticator.order = 42
authenticator.save()
resp = app.get('/manage/authenticators/add/')
resp.form['name'] = 'Test 2'
resp.form['authenticator'] = 'saml'
resp.form.submit()
authenticator = SAMLAuthenticator.objects.filter(slug='test-2').get()
assert authenticator.order == 43
def test_authenticators_configuration_info(app, superuser, ou1, ou2):
resp = login(app, superuser, path='/manage/authenticators/')
resp = resp.click('Add new authenticator')
assert resp.text.count('infonotice') == 2
assert '<div class="infonotice saml idp-info">' in resp.text
assert '<div class="infonotice oidc idp-info">' in resp.text
assert resp.text.count('Configuration information for your identity provider') == 2
# saml
assert (
'Metadata URL:<br><a href="https://testserver/accounts/saml/metadata/" rel="nofollow">'
'https://testserver/accounts/saml/metadata/</a>'
) in resp.text
assert 'Commonly expected attributes:' in resp.text
assert 'Email (email)' in resp.text
authenticator = SAMLAuthenticator.objects.create(metadata='meta1.xml', slug='idp1')
SetAttributeAction.objects.create(
authenticator=authenticator, user_field='email', saml_attribute='mail', mandatory=True
)
SetAttributeAction.objects.create(
authenticator=authenticator, user_field='first_name', saml_attribute='given_name'
)
resp = app.get('/manage/authenticators/%s/detail/' % authenticator.pk)
assert 'Information for configuration' in resp.text
assert 'Expected attributes' in resp.text
assert 'mail (mandatory)' in resp.text
assert 'given_name' in resp.text
# oidc
authenticator = OIDCProvider.objects.create(slug='idp2')
for url in ('/manage/authenticators/add/', '/manage/authenticators/%s/detail/' % authenticator.pk):
resp = app.get(url)
assert (
'Redirect URI (redirect_uri):<br><a href="https://testserver/accounts/oidc/callback/" '
'rel="nofollow">https://testserver/accounts/oidc/callback/</a>'
) in resp.text
assert (
'Redirect URI after logout (post_logout_redirect_uri):<br><a href="https://testserver/logout/" '
'rel="nofollow">https://testserver/logout/</a>'
) in resp.text