authentic/tests/test_auth_saml.py

272 lines
9.3 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 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
import re
import lasso
import pytest
from django.contrib.auth import get_user_model
from mellon.models import UserSAMLIdentifier
from authentic2.custom_user.models import DeletedUser
from authentic2.models import Attribute
from authentic2_auth_saml.adapters import AuthenticAdapter, MappingError
User = get_user_model()
def test_providers_on_login_page(db, app, settings):
settings.A2_AUTH_SAML_ENABLE = True
PROVIDERS = [
{'METADATA': 'meta1.xml', 'ENTITY_ID': 'idp1'},
]
settings.MELLON_IDENTITY_PROVIDERS = PROVIDERS
response = app.get('/login/')
assert response.pyquery('button[name="login-saml-0"]')
assert not response.pyquery('button[name="login-saml-1"]')
PROVIDERS = [
{'METADATA': 'meta1.xml', 'ENTITY_ID': 'idp1', 'SLUG': 'idp1'},
]
settings.MELLON_IDENTITY_PROVIDERS = PROVIDERS
response = app.get('/login/')
assert response.pyquery('button[name="login-saml-idp1"]')
assert not response.pyquery('button[name="login-saml-1"]')
PROVIDERS.append({'METADATA': 'meta1.xml', 'ENTITY_ID': 'idp1'})
response = app.get('/login/')
# two frontends should be present on login page
assert response.pyquery('button[name="login-saml-idp1"]')
assert response.pyquery('button[name="login-saml-1"]')
@pytest.fixture
def adapter():
return AuthenticAdapter()
@pytest.fixture
def idp():
return {
'A2_ATTRIBUTE_MAPPING': [
{
'attribute': 'email',
'saml_attribute': 'mail',
'mandatory': True,
},
{'action': 'rename', 'from': 'http://fucking/attribute/givenName', 'to': 'first_name'},
{
'attribute': 'title',
'saml_attribute': 'title',
},
{
'attribute': 'first_name',
'saml_attribute': 'first_name',
},
]
}
@pytest.fixture
def title_attribute(db):
return Attribute.objects.create(kind='title', name='title', label='title')
@pytest.fixture
def saml_attributes():
return {
'issuer': 'https://idp.com/',
'name_id_content': 'xxx',
'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
'mail': ['john.doe@example.com'],
'title': ['Mr.'],
'http://fucking/attribute/givenName': ['John'],
}
@pytest.fixture
def user(db):
return User.objects.create()
def test_lookup_user_ok(adapter, idp, saml_attributes, title_attribute):
assert User.objects.count() == 0
user = adapter.lookup_user(idp, saml_attributes)
user.refresh_from_db()
assert user.email == 'john.doe@example.com'
assert user.attributes.title == 'Mr.'
assert user.first_name == 'John'
assert user.attributes.title == 'Mr.'
assert user.ou.default is True
def test_lookup_user_missing_mandatory_attribute(adapter, idp, saml_attributes, title_attribute):
del saml_attributes['mail']
assert User.objects.count() == 0
assert adapter.lookup_user(idp, saml_attributes) is None
assert User.objects.count() == 0
def test_apply_attribute_mapping_missing_attribute_logged(
caplog, adapter, idp, saml_attributes, title_attribute, user
):
caplog.set_level('WARNING')
saml_attributes['http://fucking/attribute/givenName'] = []
adapter.apply_attribute_mapping(user, idp, saml_attributes, idp['A2_ATTRIBUTE_MAPPING'])
assert re.match('.*no value.*first_name', caplog.records[-1].message)
def test_apply_attribute_mapping_missing_attribute_exception(
adapter, idp, saml_attributes, title_attribute, user
):
saml_attributes['http://fucking/attribute/givenName'] = []
idp['A2_ATTRIBUTE_MAPPING'][-1]['mandatory'] = True
with pytest.raises(MappingError, match='no value'):
adapter.apply_attribute_mapping(user, idp, saml_attributes, idp['A2_ATTRIBUTE_MAPPING'])
@pytest.mark.parametrize('action_name', ['add-role', 'toggle-role'])
class TestAddRole:
@pytest.fixture
def idp(self, action_name, simple_role):
return {
'A2_ATTRIBUTE_MAPPING': [
{
'action': action_name,
'role': {
'name': simple_role.name,
'ou': {
'name': simple_role.ou.name,
},
},
'condition': "roles == 'A'",
}
]
}
@pytest.fixture
def saml_attributes(self):
return {
'issuer': 'https://idp.com/',
'name_id_content': 'xxx',
'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
}
def test_lookup_user_condition_fails(self, adapter, simple_role, idp, saml_attributes):
user = adapter.lookup_user(idp, saml_attributes)
assert simple_role not in user.roles.all()
def test_lookup_user_condition_success(self, adapter, simple_role, idp, saml_attributes):
saml_attributes['roles'] = ['A']
user = adapter.lookup_user(idp, saml_attributes)
assert simple_role in user.roles.all()
def test_lookup_user_mandatory_condition(self, adapter, simple_role, idp, saml_attributes):
# if a toggle-role is mandatory, failure to evaluate condition block user creation
idp['A2_ATTRIBUTE_MAPPING'][0]['mandatory'] = True
assert adapter.lookup_user(idp, saml_attributes) is None
def test_lookup_user_mandatory(self, adapter, simple_role, idp, saml_attributes):
idp['A2_ATTRIBUTE_MAPPING'][0]['mandatory'] = True
saml_attributes['roles'] = ['A']
user = adapter.lookup_user(idp, saml_attributes)
assert simple_role in user.roles.all()
def test_lookup_user_use_list(self, adapter, simple_role, idp, saml_attributes):
idp['A2_ATTRIBUTE_MAPPING'][0]['condition'] = "'A' in roles__list"
saml_attributes['roles'] = ['A']
user = adapter.lookup_user(idp, saml_attributes)
assert simple_role in user.roles.all()
def test_lookup_user_add_and_remove(self, adapter, simple_role, idp, saml_attributes, caplog):
saml_attributes['roles'] = ['A']
user = adapter.lookup_user(idp, saml_attributes)
assert simple_role in user.roles.all()
saml_attributes['roles'] = []
adapter.provision(user, idp, saml_attributes)
# condition failed, so role should be removed
user.refresh_from_db()
assert simple_role not in user.roles.all()
def test_login_with_conditionnal_authenticators(db, app, settings, caplog):
settings.A2_AUTH_SAML_ENABLE = True
settings.MELLON_IDENTITY_PROVIDERS = [
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
]
response = app.get('/login/')
assert 'login-saml-0' in response
settings.AUTH_FRONTENDS_KWARGS = {'saml': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
response = app.get('/login/')
assert 'login-saml-0' not in response
settings.MELLON_IDENTITY_PROVIDERS.append(
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
)
settings.AUTH_FRONTENDS_KWARGS = {'saml': {'show_condition': {'0': 'remote_addr==\'0.0.0.0\''}}}
response = app.get('/login/')
assert 'login-saml-0' not in response
assert 'login-saml-1' in response
settings.AUTH_FRONTENDS_KWARGS = {
'saml': {'show_condition': {'0': 'remote_addr==\'0.0.0.0\'', '1': 'remote_addr==\'0.0.0.0\''}}
}
response = app.get('/login/')
assert 'login-saml-0' not in response
assert 'login-saml-1' not in response
def test_login_autorun(db, app, settings):
response = app.get('/login/')
settings.A2_AUTH_SAML_ENABLE = True
settings.MELLON_IDENTITY_PROVIDERS = [
{"METADATA": os.path.join(os.path.dirname(__file__), 'metadata.xml')}
]
# hide password block
settings.AUTH_FRONTENDS_KWARGS = {'password': {'show_condition': 'remote_addr==\'0.0.0.0\''}}
response = app.get('/login/', status=302)
assert '/accounts/saml/login/?entityID=' in response['Location']
def test_save_account_on_delete_user(db):
user = User.objects.create()
UserSAMLIdentifier.objects.create(user=user, issuer='https://idp1.com/', name_id='1234')
UserSAMLIdentifier.objects.create(user=user, issuer='https://idp2.com/', name_id='4567')
user.delete()
assert UserSAMLIdentifier.objects.count() == 0
deleted_user = DeletedUser.objects.get()
assert deleted_user.old_data.get('saml_accounts') == [
{
'issuer': 'https://idp1.com/',
'name_id': '1234',
},
{
'issuer': 'https://idp2.com/',
'name_id': '4567',
},
]