292 lines
9.6 KiB
Python
292 lines
9.6 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 datetime
|
|
import os
|
|
import re
|
|
|
|
import pytest
|
|
|
|
import lasso
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils.timezone import now
|
|
|
|
from mellon.models import UserSAMLIdentifier
|
|
|
|
from authentic2.models import Attribute
|
|
from authentic2.custom_user.models import DeletedUser
|
|
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.mark_as_deleted()
|
|
User.objects.cleanup(threshold=0, timestamp=now() + datetime.timedelta(seconds=1))
|
|
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',
|
|
}
|
|
]
|