authentic/tests/test_auth_saml.py

250 lines
8.0 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 pytest
import lasso
from django.contrib.auth import get_user_model
from authentic2.models import Attribute
from authentic2_auth_saml.adapters import MappingError
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"]')
def test_provision_attributes(db, caplog):
from authentic2_auth_saml.adapters import AuthenticAdapter
adapter = AuthenticAdapter()
User = get_user_model()
Attribute.objects.create(kind='title', name='title', label='title')
user = User.objects.create()
idp = {
'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',
}
]
}
saml_attributes = {
u'issuer': 'https://idp.com/',
u'name_id_content': 'xxx',
u'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
u'mail': [u'john.doe@example.com'],
u'title': [u'Mr.'],
u'http://fucking/attribute/givenName': ['John'],
}
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'
user.delete()
# on missing mandatory attribute, no user is created
del saml_attributes['mail']
assert adapter.lookup_user(idp, saml_attributes) is None
# simulate no attribute value
saml_attributes['first_name'] = []
mapping = {
'attribute': 'first_name',
'saml_attribute': 'first_name',
}
with pytest.raises(MappingError, match='no value for first_name'):
adapter.action_set_attribute(user, idp, saml_attributes, mapping)
@pytest.mark.parametrize('action_name', ['add-role', 'toggle-role'])
def test_provision_add_role(db, simple_role, action_name):
from authentic2_auth_saml.adapters import AuthenticAdapter
adapter = AuthenticAdapter()
User = get_user_model()
user = User.objects.create()
idp = {
'A2_ATTRIBUTE_MAPPING': [
{
'action': action_name,
'role': {
'name': simple_role.name,
'ou': {
'name': simple_role.ou.name,
},
},
'condition': "roles == 'A'",
}
]
}
saml_attributes = {
'issuer': 'https://idp.com/',
'name_id_content': 'xxx',
'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
}
user = adapter.lookup_user(idp, saml_attributes)
user.refresh_from_db()
assert simple_role not in user.roles.all()
assert user.ou.default is True
user.delete()
# if a toggle-role is mandatory, failure to evaluate condition block user creation
assert idp['A2_ATTRIBUTE_MAPPING'][-1]['action'] == action_name
idp['A2_ATTRIBUTE_MAPPING'][-1]['mandatory'] = True
assert adapter.lookup_user(idp, saml_attributes) is None
saml_attributes['roles'] = ['A']
user = adapter.lookup_user(idp, saml_attributes)
user.refresh_from_db()
assert simple_role in user.roles.all()
user.delete()
idp['A2_ATTRIBUTE_MAPPING'][-1]['condition'] = "'A' in roles__list"
user = adapter.lookup_user(idp, saml_attributes)
user.refresh_from_db()
assert simple_role in user.roles.all()
saml_attributes['roles'] = []
adapter.provision(user, idp, saml_attributes)
# condition failed, so role should be removed
assert simple_role not in user.roles.all()
user.delete()
# on missing mandatory attribute, no user is created
del saml_attributes['mail']
assert adapter.lookup_user(idp, saml_attributes) is None
# simulate no attribute value
saml_attributes['first_name'] = []
attribute_mapping = [
{
'mandatory': True,
'attribute': 'first_name',
'saml_attribute': 'first_name',
}
]
# fail fast
with pytest.raises(MappingError, match='no value.*first_name'):
adapter.apply_attribute_mapping(user, idp, saml_attributes, attribute_mapping)
# or log a warning
caplog.clear()
del attribute_mapping[0]['mandatory']
adapter.apply_attribute_mapping(user, idp, saml_attributes, attribute_mapping)
assert re.match('.*no value.*first_name', caplog.records[0].message)
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']