1 |
|
# authentic2 - versatile identity manager
|
2 |
|
# Copyright (C) 2010-2019 Entr'ouvert
|
3 |
|
#
|
4 |
|
# This program is free software: you can redistribute it and/or modify it
|
5 |
|
# under the terms of the GNU Affero General Public License as published
|
6 |
|
# by the Free Software Foundation, either version 3 of the License, or
|
7 |
|
# (at your option) any later version.
|
8 |
|
#
|
9 |
|
# This program is distributed in the hope that it will be useful,
|
10 |
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11 |
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12 |
|
# GNU Affero General Public License for more details.
|
13 |
|
#
|
14 |
|
# You should have received a copy of the GNU Affero General Public License
|
15 |
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16 |
|
|
17 |
|
import os
|
18 |
|
import re
|
19 |
|
from unittest import mock
|
20 |
|
|
21 |
|
import lasso
|
22 |
|
import pytest
|
23 |
|
from django.contrib.auth import get_user_model
|
24 |
|
from mellon.adapters import UserCreationError
|
25 |
|
from mellon.models import Issuer, UserSAMLIdentifier
|
26 |
|
|
27 |
|
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
|
28 |
|
from authentic2.custom_user.models import DeletedUser
|
29 |
|
from authentic2.models import Attribute
|
30 |
|
from authentic2_auth_saml.adapters import AuthenticAdapter, MappingError
|
31 |
|
from authentic2_auth_saml.models import (
|
32 |
|
AddRoleAction,
|
33 |
|
SAMLAttributeLookup,
|
34 |
|
SAMLAuthenticator,
|
35 |
|
SetAttributeAction,
|
36 |
|
)
|
37 |
|
|
38 |
|
from .utils import login
|
39 |
|
|
40 |
|
User = get_user_model()
|
41 |
|
|
42 |
|
|
43 |
|
@pytest.fixture
|
44 |
|
def patched_adapter(monkeypatch):
|
45 |
|
def load_idp(self, settings, order):
|
46 |
|
settings['ENTITY_ID'] = 'idp1'
|
47 |
|
return settings
|
48 |
|
|
49 |
|
monkeypatch.setattr(AuthenticAdapter, 'load_idp', load_idp)
|
50 |
|
|
51 |
|
|
52 |
|
def test_providers_on_login_page(db, app, settings):
|
53 |
|
SAMLAuthenticator.objects.create(
|
54 |
|
enabled=True,
|
55 |
|
metadata='meta1.xml',
|
56 |
|
slug='idp1',
|
57 |
|
button_label='Test label',
|
58 |
|
button_description='This is a test.',
|
59 |
|
)
|
60 |
|
|
61 |
|
response = app.get('/login/')
|
62 |
|
assert response.pyquery('button[name="login-saml-idp1"]')
|
63 |
|
assert not response.pyquery('button[name="login-saml-1"]')
|
64 |
|
assert 'SAML' in response.text
|
65 |
|
|
66 |
|
SAMLAuthenticator.objects.create(enabled=True, metadata='meta1.xml', slug='idp2')
|
67 |
|
response = app.get('/login/')
|
68 |
|
# two frontends should be present on login page
|
69 |
|
assert response.pyquery('button[name="login-saml-idp1"]')
|
70 |
|
assert response.pyquery('button[name="login-saml-idp2"]')
|
71 |
|
assert 'Test label' in response.text
|
72 |
|
assert 'This is a test.' in response.text
|
73 |
|
|
74 |
|
|
75 |
|
@pytest.fixture
|
76 |
|
def adapter():
|
77 |
|
return AuthenticAdapter()
|
78 |
|
|
79 |
|
|
80 |
|
@pytest.fixture
|
81 |
|
def idp(db):
|
82 |
|
authenticator = SAMLAuthenticator.objects.create(
|
83 |
|
enabled=True,
|
84 |
|
metadata='meta1.xml',
|
85 |
|
slug='idp1',
|
86 |
|
)
|
87 |
|
SetAttributeAction.objects.create(
|
88 |
|
authenticator=authenticator,
|
89 |
|
user_field='email',
|
90 |
|
saml_attribute='mail',
|
91 |
|
mandatory=True,
|
92 |
|
)
|
93 |
|
SetAttributeAction.objects.create(
|
94 |
|
authenticator=authenticator,
|
95 |
|
user_field='title',
|
96 |
|
saml_attribute='title',
|
97 |
|
)
|
98 |
|
SetAttributeAction.objects.create(
|
99 |
|
authenticator=authenticator,
|
100 |
|
user_field='first_name',
|
101 |
|
saml_attribute='http://nice/attribute/givenName',
|
102 |
|
)
|
103 |
|
return authenticator.settings
|
104 |
|
|
105 |
|
|
106 |
|
@pytest.fixture
|
107 |
|
def title_attribute(db):
|
108 |
|
return Attribute.objects.create(kind='title', name='title', label='title')
|
109 |
|
|
110 |
|
|
111 |
|
@pytest.fixture
|
112 |
|
def saml_attributes():
|
113 |
|
return {
|
114 |
|
'issuer': 'https://idp.com/',
|
115 |
|
'name_id_content': 'xxx',
|
116 |
|
'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
|
117 |
|
'mail': ['john.doe@example.com'],
|
118 |
|
'title': ['Mr.'],
|
119 |
|
'http://nice/attribute/givenName': ['John'],
|
120 |
|
}
|
121 |
|
|
122 |
|
|
123 |
|
@pytest.fixture
|
124 |
|
def user(db):
|
125 |
|
return User.objects.create()
|
126 |
|
|
127 |
|
|
128 |
|
def test_lookup_user_ok(adapter, idp, saml_attributes, title_attribute):
|
129 |
|
assert User.objects.count() == 0
|
130 |
|
|
131 |
|
user = adapter.lookup_user(idp, saml_attributes)
|
132 |
|
user.refresh_from_db()
|
133 |
|
assert user.email == 'john.doe@example.com'
|
134 |
|
assert user.attributes.title == 'Mr.'
|
135 |
|
assert user.first_name == 'John'
|
136 |
|
assert user.attributes.title == 'Mr.'
|
137 |
|
assert user.ou.default is True
|
138 |
|
|
139 |
|
|
140 |
|
def test_lookup_user_missing_mandatory_attribute(adapter, idp, saml_attributes, title_attribute):
|
141 |
|
del saml_attributes['mail']
|
142 |
|
|
143 |
|
assert User.objects.count() == 0
|
144 |
|
assert adapter.lookup_user(idp, saml_attributes) is None
|
145 |
|
assert User.objects.count() == 0
|
146 |
|
|
147 |
|
|
148 |
|
def test_apply_attribute_mapping_missing_attribute_logged(
|
149 |
|
caplog, adapter, idp, saml_attributes, title_attribute, user
|
150 |
|
):
|
151 |
|
caplog.set_level('WARNING')
|
152 |
|
saml_attributes['http://nice/attribute/givenName'] = []
|
153 |
|
adapter.provision_a2_attributes(user, idp, saml_attributes)
|
154 |
|
assert re.match('.*no value.*first_name', caplog.records[-1].message)
|
155 |
|
|
156 |
|
|
157 |
|
def test_apply_attribute_mapping_missing_attribute_exception(
|
158 |
|
adapter, idp, saml_attributes, title_attribute, user, rf
|
159 |
|
):
|
160 |
|
saml_attributes['http://nice/attribute/givenName'] = []
|
161 |
|
SetAttributeAction.objects.filter(user_field='first_name').update(mandatory=True)
|
162 |
|
with pytest.raises(MappingError, match='no value'):
|
163 |
|
adapter.provision_a2_attributes(user, idp, saml_attributes)
|
164 |
|
|
165 |
|
request = rf.get('/')
|
166 |
|
request._messages = mock.Mock()
|
167 |
|
adapter.request = request
|
168 |
|
with pytest.raises(UserCreationError):
|
169 |
|
adapter.finish_create_user(idp, saml_attributes, user)
|
170 |
|
request._messages.add.assert_called_once_with(
|
171 |
|
40, 'User creation failed: no value for attribute "first_name".', ''
|
172 |
|
)
|
173 |
|
|
174 |
|
|
175 |
|
@pytest.mark.parametrize('action_name', ['add-role', 'toggle-role'])
|
176 |
|
class TestAddRole:
|
177 |
|
@pytest.fixture
|
178 |
|
def idp(self, action_name, simple_role):
|
179 |
|
authenticator = SAMLAuthenticator.objects.create(
|
180 |
|
enabled=True,
|
181 |
|
metadata='meta1.xml',
|
182 |
|
slug='idp1',
|
183 |
|
)
|
184 |
|
AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)
|
185 |
|
return authenticator.settings
|
186 |
|
|
187 |
|
@pytest.fixture
|
188 |
|
def saml_attributes(self):
|
189 |
|
return {
|
190 |
|
'issuer': 'https://idp.com/',
|
191 |
|
'name_id_content': 'xxx',
|
192 |
|
'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
|
193 |
|
}
|
194 |
|
|
195 |
|
def test_lookup_user_success(self, adapter, simple_role, idp, saml_attributes):
|
196 |
|
user = adapter.lookup_user(idp, saml_attributes)
|
197 |
|
assert simple_role in user.roles.all()
|
198 |
|
|
199 |
|
|
200 |
|
def test_login_with_conditionnal_authenticators(db, app, settings, caplog):
|
201 |
|
authenticator = SAMLAuthenticator.objects.create(
|
202 |
|
enabled=True, metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'), slug='idp1'
|
203 |
|
)
|
204 |
|
|
205 |
|
response = app.get('/login/')
|
206 |
|
assert 'login-saml-idp1' in response
|
207 |
|
|
208 |
|
authenticator.show_condition = 'remote_addr==\'0.0.0.0\''
|
209 |
|
authenticator.save()
|
210 |
|
response = app.get('/login/')
|
211 |
|
assert 'login-saml-idp1' not in response
|
212 |
|
|
213 |
|
authenticator2 = SAMLAuthenticator.objects.create(
|
214 |
|
enabled=True, metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'), slug='idp2'
|
215 |
|
)
|
216 |
|
response = app.get('/login/')
|
217 |
|
assert 'login-saml-idp1' not in response
|
218 |
|
assert 'login-saml-idp2' in response
|
219 |
|
|
220 |
|
authenticator2.show_condition = 'remote_addr==\'0.0.0.0\''
|
221 |
|
authenticator2.save()
|
222 |
|
response = app.get('/login/')
|
223 |
|
assert 'login-saml-idp1' not in response
|
224 |
|
assert 'login-saml-idp2' not in response
|
225 |
|
|
226 |
|
|
227 |
|
def test_login_condition_dnsbl(db, app, settings, caplog):
|
228 |
|
SAMLAuthenticator.objects.create(
|
229 |
|
enabled=True,
|
230 |
|
metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'),
|
231 |
|
slug='idp1',
|
232 |
|
show_condition='remote_addr in dnsbl(\'dnswl.example.com\')',
|
233 |
|
)
|
234 |
|
SAMLAuthenticator.objects.create(
|
235 |
|
enabled=True,
|
236 |
|
metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'),
|
237 |
|
slug='idp2',
|
238 |
|
show_condition='remote_addr not in dnsbl(\'dnswl.example.com\')',
|
239 |
|
)
|
240 |
|
with mock.patch('authentic2.utils.evaluate.check_dnsbl', return_value=True):
|
241 |
|
response = app.get('/login/')
|
242 |
|
assert 'login-saml-idp1' in response
|
243 |
|
assert 'login-saml-idp2' not in response
|
244 |
|
|
245 |
|
|
246 |
|
def test_login_autorun(db, app, settings, patched_adapter):
|
247 |
|
response = app.get('/login/')
|
248 |
|
|
249 |
|
authenticator = SAMLAuthenticator.objects.create(
|
250 |
|
enabled=True, metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'), slug='idp1'
|
251 |
|
)
|
252 |
|
# hide password block
|
253 |
|
LoginPasswordAuthenticator.objects.update_or_create(
|
254 |
|
slug='password-authenticator', defaults={'enabled': False}
|
255 |
|
)
|
256 |
|
response = app.get('/login/', status=302)
|
257 |
|
assert '/accounts/saml/login/?entityID=' in response['Location']
|
258 |
|
|
259 |
|
authenticator.slug = 'slug_with_underscore'
|
260 |
|
authenticator.save()
|
261 |
|
response = app.get('/login/', status=302)
|
262 |
|
assert '/accounts/saml/login/?entityID=' in response['Location']
|
263 |
|
|
264 |
|
|
265 |
|
def test_save_account_on_delete_user(db):
|
266 |
|
user = User.objects.create()
|
267 |
|
issuer1, _ = Issuer.objects.get_or_create(entity_id='https://idp1.com/')
|
268 |
|
UserSAMLIdentifier.objects.create(user=user, issuer=issuer1, name_id='1234')
|
269 |
|
issuer2, _ = Issuer.objects.get_or_create(entity_id='https://idp2.com/')
|
270 |
|
UserSAMLIdentifier.objects.create(user=user, issuer=issuer2, name_id='4567')
|
271 |
|
|
272 |
|
user.delete()
|
273 |
|
assert UserSAMLIdentifier.objects.count() == 0
|
274 |
|
|
275 |
|
deleted_user = DeletedUser.objects.get()
|
276 |
|
assert deleted_user.old_data.get('saml_accounts') == [
|
277 |
|
{
|
278 |
|
'issuer': 'https://idp1.com/',
|
279 |
|
'name_id': '1234',
|
280 |
|
},
|
281 |
|
{
|
282 |
|
'issuer': 'https://idp2.com/',
|
283 |
|
'name_id': '4567',
|
284 |
|
},
|
285 |
|
]
|
286 |
|
|
287 |
|
|
288 |
|
def test_manager_user_sidebar(app, superuser, simple_user):
|
289 |
|
login(app, superuser, '/manage/')
|
290 |
|
response = app.get('/manage/users/%s/' % simple_user.id)
|
291 |
|
assert 'SAML' not in response
|
292 |
|
|
293 |
|
issuer1, _ = Issuer.objects.get_or_create(entity_id='https://idp1.com/')
|
294 |
|
UserSAMLIdentifier.objects.create(user=simple_user, issuer=issuer1, name_id='1234')
|
295 |
|
|
296 |
|
response = app.get('/manage/users/%s/' % simple_user.id)
|
297 |
|
assert 'SAML' in response
|
298 |
|
assert 'https://idp1.com/' in response
|
299 |
|
assert '1234' in response
|
300 |
|
|
301 |
|
|
302 |
|
def test_saml_authenticator_settings(db):
|
303 |
|
authenticator = SAMLAuthenticator.objects.create(
|
304 |
|
enabled=True, metadata='meta1.xml', slug='idp1', authn_classref='a, b'
|
305 |
|
)
|
306 |
|
|
307 |
|
assert 'METADATA' in authenticator.settings
|
308 |
|
assert 'METADATA_PATH' not in authenticator.settings
|
309 |
|
assert 'METADATA_URL' not in authenticator.settings
|
310 |
|
assert authenticator.settings['AUTHN_CLASSREF'] == ['a', 'b']
|
311 |
|
|
312 |
|
authenticator.metadata = ''
|
313 |
|
authenticator.metadata_path = '/some/path/metadata.xml'
|
314 |
|
authenticator.save()
|
315 |
|
|
316 |
|
assert 'METADATA_PATH' in authenticator.settings
|
317 |
|
assert 'METADATA' not in authenticator.settings
|
318 |
|
assert 'METADATA_URL' not in authenticator.settings
|
319 |
|
|
320 |
|
authenticator.authn_classref = ''
|
321 |
|
authenticator.save()
|
322 |
|
|
323 |
|
assert authenticator.settings['AUTHN_CLASSREF'] == []
|
324 |
|
|
325 |
|
SAMLAttributeLookup.objects.create(
|
326 |
|
authenticator=authenticator,
|
327 |
|
user_field='email',
|
328 |
|
saml_attribute='mail',
|
329 |
|
)
|
330 |
|
assert authenticator.settings['LOOKUP_BY_ATTRIBUTES'] == [
|
331 |
|
{'saml_attribute': 'mail', 'user_field': 'email', 'ignore-case': False}
|
332 |
|
]
|
333 |
|
|
334 |
|
|
335 |
|
def test_saml_authenticator_data_migration(migration, settings):
|
336 |
|
app = 'authentic2_auth_saml'
|
337 |
|
migrate_from = [(app, '0001_initial')]
|
338 |
|
migrate_to = [(app, '0002_auto_20220608_1559')]
|
339 |
|
|
340 |
|
old_apps = migration.before(migrate_from)
|
341 |
|
SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
|
342 |
|
|
343 |
|
settings.A2_AUTH_SAML_ENABLE = True
|
344 |
|
settings.MELLON_METADATA_CACHE_TIME = 42
|
345 |
|
settings.MELLON_METADATA_HTTP_TIMEOUT = 42
|
346 |
|
settings.MELLON_PROVISION = False
|
347 |
|
settings.MELLON_VERIFY_SSL_CERTIFICATE = True
|
348 |
|
settings.MELLON_TRANSIENT_FEDERATION_ATTRIBUTE = None
|
349 |
|
settings.MELLON_USERNAME_TEMPLATE = 'test'
|
350 |
|
settings.MELLON_NAME_ID_POLICY_ALLOW_CREATE = False
|
351 |
|
settings.MELLON_FORCE_AUTHN = True
|
352 |
|
settings.MELLON_ADD_AUTHNREQUEST_NEXT_URL_EXTENSION = False
|
353 |
|
settings.MELLON_GROUP_ATTRIBUTE = 'role'
|
354 |
|
settings.MELLON_CREATE_GROUP = True
|
355 |
|
settings.MELLON_ERROR_URL = 'https://example.com/error/'
|
356 |
|
settings.MELLON_AUTHN_CLASSREF = ('class1', 'class2')
|
357 |
|
settings.MELLON_LOGIN_HINTS = ['hint1', 'hint2']
|
358 |
|
settings.AUTH_FRONTENDS_KWARGS = {
|
359 |
|
'saml': {
|
360 |
|
'priority': 1,
|
361 |
|
'show_condition': {
|
362 |
|
'0': 'first condition',
|
363 |
|
'1': 'second condition',
|
364 |
|
},
|
365 |
|
}
|
366 |
|
}
|
367 |
|
settings.MELLON_IDENTITY_PROVIDERS = [
|
368 |
|
{
|
369 |
|
'METADATA': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
|
370 |
|
'REALM': 'test',
|
371 |
|
'METADATA_CACHE_TIME': 43,
|
372 |
|
'METADATA_HTTP_TIMEOUT': 43,
|
373 |
|
'PROVISION': True,
|
374 |
|
'LOOKUP_BY_ATTRIBUTES': [],
|
375 |
|
},
|
376 |
|
{
|
377 |
|
'METADATA_PATH': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
|
378 |
|
'NAME_ID_POLICY_ALLOW_CREATE': True,
|
379 |
|
'FORCE_AUTHN': False,
|
380 |
|
'ADD_AUTHNREQUEST_NEXT_URL_EXTENSION': True,
|
381 |
|
'A2_ATTRIBUTE_MAPPING': [
|
382 |
|
{
|
383 |
|
'attribute': 'email',
|
384 |
|
'saml_attribute': 'mail',
|
385 |
|
},
|
386 |
|
],
|
387 |
|
'LOOKUP_BY_ATTRIBUTES': [{'saml_attribute': 'email', 'user_field': 'email'}],
|
388 |
|
},
|
389 |
|
{
|
390 |
|
'METADATA_URL': 'https://example.com/metadata.xml',
|
391 |
|
'SLUG': 'third',
|
392 |
|
'ATTRIBUTE_MAPPING': {'email': 'attributes[mail][0]'},
|
393 |
|
'SUPERUSER_MAPPING': {'roles': 'Admin'},
|
394 |
|
},
|
395 |
|
]
|
396 |
|
|
397 |
|
new_apps = migration.apply(migrate_to)
|
398 |
|
SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
|
399 |
|
first_authenticator, second_authenticator, third_authenticator = SAMLAuthenticator.objects.all()
|
400 |
|
assert first_authenticator.slug == '0'
|
401 |
|
assert first_authenticator.order == 1
|
402 |
|
assert first_authenticator.show_condition == 'first condition'
|
403 |
|
assert first_authenticator.enabled is True
|
404 |
|
assert first_authenticator.metadata_path == os.path.join(os.path.dirname(__file__), 'metadata.xml')
|
405 |
|
assert first_authenticator.metadata_url == ''
|
406 |
|
assert first_authenticator.metadata_cache_time == 43
|
407 |
|
assert first_authenticator.metadata_http_timeout == 43
|
408 |
|
assert first_authenticator.provision is True
|
409 |
|
assert first_authenticator.verify_ssl_certificate is True
|
410 |
|
assert first_authenticator.transient_federation_attribute == ''
|
411 |
|
assert first_authenticator.realm == 'test'
|
412 |
|
assert first_authenticator.username_template == 'test'
|
413 |
|
assert first_authenticator.name_id_policy_format == ''
|
414 |
|
assert first_authenticator.name_id_policy_allow_create is False
|
415 |
|
assert first_authenticator.force_authn is True
|
416 |
|
assert first_authenticator.add_authnrequest_next_url_extension is False
|
417 |
|
assert first_authenticator.group_attribute == 'role'
|
418 |
|
assert first_authenticator.create_group is True
|
419 |
|
assert first_authenticator.error_url == 'https://example.com/error/'
|
420 |
|
assert first_authenticator.error_redirect_after_timeout == 120
|
421 |
|
assert first_authenticator.authn_classref == 'class1, class2'
|
422 |
|
assert first_authenticator.login_hints == 'hint1, hint2'
|
423 |
|
assert first_authenticator.lookup_by_attributes == []
|
424 |
|
assert first_authenticator.a2_attribute_mapping == []
|
425 |
|
assert first_authenticator.attribute_mapping == {}
|
426 |
|
assert first_authenticator.superuser_mapping == {}
|
427 |
|
|
428 |
|
assert second_authenticator.slug == '1'
|
429 |
|
assert second_authenticator.order == 1
|
430 |
|
assert second_authenticator.show_condition == 'second condition'
|
431 |
|
assert second_authenticator.enabled is True
|
432 |
|
assert second_authenticator.metadata_path == os.path.join(os.path.dirname(__file__), 'metadata.xml')
|
433 |
|
assert second_authenticator.metadata_url == ''
|
434 |
|
assert second_authenticator.metadata_cache_time == 42
|
435 |
|
assert second_authenticator.metadata_http_timeout == 42
|
436 |
|
assert second_authenticator.provision is False
|
437 |
|
assert second_authenticator.verify_ssl_certificate is True
|
438 |
|
assert second_authenticator.transient_federation_attribute == ''
|
439 |
|
assert second_authenticator.realm == 'saml'
|
440 |
|
assert second_authenticator.username_template == 'test'
|
441 |
|
assert second_authenticator.name_id_policy_format == ''
|
442 |
|
assert second_authenticator.name_id_policy_allow_create is True
|
443 |
|
assert second_authenticator.force_authn is False
|
444 |
|
assert second_authenticator.add_authnrequest_next_url_extension is True
|
445 |
|
assert second_authenticator.group_attribute == 'role'
|
446 |
|
assert second_authenticator.create_group is True
|
447 |
|
assert second_authenticator.error_url == 'https://example.com/error/'
|
448 |
|
assert second_authenticator.error_redirect_after_timeout == 120
|
449 |
|
assert second_authenticator.authn_classref == 'class1, class2'
|
450 |
|
assert second_authenticator.login_hints == 'hint1, hint2'
|
451 |
|
assert second_authenticator.lookup_by_attributes == [{'saml_attribute': 'email', 'user_field': 'email'}]
|
452 |
|
assert second_authenticator.a2_attribute_mapping == [
|
453 |
|
{
|
454 |
|
'attribute': 'email',
|
455 |
|
'saml_attribute': 'mail',
|
456 |
|
},
|
457 |
|
]
|
458 |
|
assert first_authenticator.attribute_mapping == {}
|
459 |
|
assert first_authenticator.superuser_mapping == {}
|
460 |
|
|
461 |
|
assert third_authenticator.slug == 'third'
|
462 |
|
assert third_authenticator.order == 1
|
463 |
|
assert third_authenticator.show_condition == ''
|
464 |
|
assert third_authenticator.enabled is True
|
465 |
|
assert third_authenticator.metadata_path == ''
|
466 |
|
assert third_authenticator.metadata_url == 'https://example.com/metadata.xml'
|
467 |
|
assert third_authenticator.metadata_cache_time == 42
|
468 |
|
assert third_authenticator.metadata_http_timeout == 42
|
469 |
|
assert third_authenticator.provision is False
|
470 |
|
assert third_authenticator.verify_ssl_certificate is True
|
471 |
|
assert third_authenticator.transient_federation_attribute == ''
|
472 |
|
assert third_authenticator.realm == 'saml'
|
473 |
|
assert third_authenticator.username_template == 'test'
|
474 |
|
assert third_authenticator.name_id_policy_format == ''
|
475 |
|
assert third_authenticator.name_id_policy_format == ''
|
476 |
|
assert third_authenticator.name_id_policy_allow_create is False
|
477 |
|
assert third_authenticator.force_authn is True
|
478 |
|
assert third_authenticator.group_attribute == 'role'
|
479 |
|
assert third_authenticator.create_group is True
|
480 |
|
assert third_authenticator.error_url == 'https://example.com/error/'
|
481 |
|
assert third_authenticator.error_redirect_after_timeout == 120
|
482 |
|
assert third_authenticator.authn_classref == 'class1, class2'
|
483 |
|
assert third_authenticator.login_hints == 'hint1, hint2'
|
484 |
|
assert third_authenticator.lookup_by_attributes == [
|
485 |
|
{'saml_attribute': 'email', 'user_field': 'email', 'ignore-case': True},
|
486 |
|
{'saml_attribute': 'username', 'user_field': 'username'},
|
487 |
|
]
|
488 |
|
assert third_authenticator.a2_attribute_mapping == []
|
489 |
|
assert third_authenticator.attribute_mapping == {'email': 'attributes[mail][0]'}
|
490 |
|
assert third_authenticator.superuser_mapping == {'roles': 'Admin'}
|
491 |
|
|
492 |
|
|
493 |
|
def test_saml_authenticator_data_migration_empty_configuration(migration, settings):
|
494 |
|
app = 'authentic2_auth_saml'
|
495 |
|
migrate_from = [(app, '0001_initial')]
|
496 |
|
migrate_to = [(app, '0002_auto_20220608_1559')]
|
497 |
|
|
498 |
|
old_apps = migration.before(migrate_from)
|
499 |
|
SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
|
500 |
|
|
501 |
|
new_apps = migration.apply(migrate_to)
|
502 |
|
SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
|
503 |
|
assert not SAMLAuthenticator.objects.exists()
|
504 |
|
|
505 |
|
|
506 |
|
def test_saml_authenticator_data_migration_bad_settings(migration, settings):
|
507 |
|
app = 'authentic2_auth_saml'
|
508 |
|
migrate_from = [(app, '0001_initial')]
|
509 |
|
migrate_to = [(app, '0002_auto_20220608_1559')]
|
510 |
|
|
511 |
|
old_apps = migration.before(migrate_from)
|
512 |
|
SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
|
513 |
|
|
514 |
|
settings.AUTH_FRONTENDS_KWARGS = {"saml": {"priority": None, "show_condition": None}}
|
515 |
|
settings.MELLON_METADATA_CACHE_TIME = 2**16
|
516 |
|
settings.MELLON_METADATA_HTTP_TIMEOUT = -1
|
517 |
|
settings.MELLON_PROVISION = None
|
518 |
|
settings.MELLON_USERNAME_TEMPLATE = 42
|
519 |
|
settings.MELLON_GROUP_ATTRIBUTE = None
|
520 |
|
settings.MELLON_ERROR_URL = 'a' * 500
|
521 |
|
settings.MELLON_AUTHN_CLASSREF = 'not-a-list'
|
522 |
|
settings.MELLON_IDENTITY_PROVIDERS = [
|
523 |
|
{
|
524 |
|
'METADATA': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
|
525 |
|
'ERROR_REDIRECT_AFTER_TIMEOUT': -1,
|
526 |
|
'SUPERUSER_MAPPING': 'not-a-dict',
|
527 |
|
},
|
528 |
|
]
|
529 |
|
|
530 |
|
new_apps = migration.apply(migrate_to)
|
531 |
|
SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
|
532 |
|
authenticator = SAMLAuthenticator.objects.get()
|
533 |
|
assert authenticator.slug == '0'
|
534 |
|
assert authenticator.order == 3
|
535 |
|
assert authenticator.show_condition == ''
|
536 |
|
assert authenticator.enabled is False
|
537 |
|
assert authenticator.metadata_cache_time == 3600
|
538 |
|
assert authenticator.metadata_http_timeout == 10
|
539 |
|
assert authenticator.provision is True
|
540 |
|
assert authenticator.username_template == '{attributes[name_id_content]}@{realm}'
|
541 |
|
assert authenticator.group_attribute == ''
|
542 |
|
assert authenticator.error_url == 'a' * 200
|
543 |
|
assert authenticator.error_redirect_after_timeout == 120
|
544 |
|
assert authenticator.authn_classref == ''
|
545 |
|
assert authenticator.superuser_mapping == {}
|
546 |
|
|
547 |
|
|
548 |
|
def test_saml_authenticator_data_migration_json_fields(migration, settings):
|
549 |
|
migrate_from = [
|
550 |
|
(
|
551 |
|
'authentic2_auth_saml',
|
552 |
|
'0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
|
553 |
|
),
|
554 |
|
('a2_rbac', '0029_use_unique_constraints'),
|
555 |
|
]
|
556 |
|
migrate_to = [
|
557 |
|
('authentic2_auth_saml', '0006_migrate_jsonfields'),
|
558 |
|
('a2_rbac', '0029_use_unique_constraints'),
|
559 |
|
]
|
560 |
|
|
561 |
|
old_apps = migration.before(migrate_from)
|
562 |
|
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
|
563 |
|
Role = old_apps.get_model('a2_rbac', 'Role')
|
564 |
|
OU = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
|
565 |
|
|
566 |
|
ou = OU.objects.create(name='Test OU', slug='test-ou')
|
567 |
|
role = Role.objects.create(name='Test role', slug='test-role', ou=ou)
|
568 |
|
|
569 |
|
SAMLAuthenticator.objects.create(
|
570 |
|
metadata='meta1.xml',
|
571 |
|
slug='idp1',
|
572 |
|
lookup_by_attributes=[
|
573 |
|
{'saml_attribute': 'email', 'user_field': 'email'},
|
574 |
|
{'saml_attribute': 'saml_name', 'user_field': 'first_name', 'ignore-case': True},
|
575 |
|
],
|
576 |
|
a2_attribute_mapping=[
|
577 |
|
{
|
578 |
|
'attribute': 'email',
|
579 |
|
'saml_attribute': 'mail',
|
580 |
|
'mandatory': True,
|
581 |
|
},
|
582 |
|
{'action': 'rename', 'from': 'a' * 1025, 'to': 'first_name'},
|
583 |
|
{
|
584 |
|
'attribute': 'first_name',
|
585 |
|
'saml_attribute': 'first_name',
|
586 |
|
},
|
587 |
|
{
|
588 |
|
'attribute': 'invalid',
|
589 |
|
'saml_attribute': '',
|
590 |
|
},
|
591 |
|
{
|
592 |
|
'attribute': 'invalid',
|
593 |
|
'saml_attribute': None,
|
594 |
|
},
|
595 |
|
{
|
596 |
|
'attribute': 'invalid',
|
597 |
|
},
|
598 |
|
{
|
599 |
|
'action': 'add-role',
|
600 |
|
'role': {
|
601 |
|
'name': role.name,
|
602 |
|
'ou': {
|
603 |
|
'name': role.ou.name,
|
604 |
|
},
|
605 |
|
},
|
606 |
|
'condition': "roles == 'A'",
|
607 |
|
},
|
608 |
|
],
|
609 |
|
)
|
610 |
|
|
611 |
|
new_apps = migration.apply(migrate_to)
|
612 |
|
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
|
613 |
|
authenticator = SAMLAuthenticator.objects.get()
|
614 |
|
|
615 |
|
attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
|
616 |
|
assert attribute_lookup1.saml_attribute == 'email'
|
617 |
|
assert attribute_lookup1.user_field == 'email'
|
618 |
|
assert attribute_lookup1.ignore_case is False
|
619 |
|
assert attribute_lookup2.saml_attribute == 'saml_name'
|
620 |
|
assert attribute_lookup2.user_field == 'first_name'
|
621 |
|
assert attribute_lookup2.ignore_case is True
|
622 |
|
|
623 |
|
set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
|
624 |
|
assert set_attribute1.attribute == 'email'
|
625 |
|
assert set_attribute1.saml_attribute == 'mail'
|
626 |
|
assert set_attribute1.mandatory is True
|
627 |
|
assert set_attribute2.attribute == 'first_name'
|
628 |
|
assert set_attribute2.saml_attribute == 'first_name'
|
629 |
|
assert set_attribute2.mandatory is False
|
630 |
|
|
631 |
|
rename_attribute = authenticator.rename_attribute_actions.get()
|
632 |
|
assert rename_attribute.from_name == 'a' * 1024
|
633 |
|
assert rename_attribute.to_name == 'first_name'
|
634 |
|
|
635 |
|
add_role = authenticator.add_role_actions.get()
|
636 |
|
assert add_role.role.pk == role.pk
|
637 |
|
assert add_role.condition == "roles == 'A'"
|
638 |
|
assert add_role.mandatory is False
|
639 |
|
|
640 |
|
|
641 |
|
def test_saml_authenticator_data_migration_json_fields_log_errors(migration, settings, caplog):
|
642 |
|
migrate_from = [
|
643 |
|
(
|
644 |
|
'authentic2_auth_saml',
|
645 |
|
'0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
|
646 |
|
),
|
647 |
|
('a2_rbac', '0029_use_unique_constraints'),
|
648 |
|
]
|
649 |
|
migrate_to = [
|
650 |
|
('authentic2_auth_saml', '0006_migrate_jsonfields'),
|
651 |
|
('a2_rbac', '0029_use_unique_constraints'),
|
652 |
|
]
|
653 |
|
|
654 |
|
old_apps = migration.before(migrate_from)
|
655 |
|
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
|
656 |
|
|
657 |
|
SAMLAuthenticator.objects.create(
|
658 |
|
metadata='meta1.xml',
|
659 |
|
slug='idp1',
|
660 |
|
lookup_by_attributes=[{'saml_attribute': 'email', 'user_field': 'email'}],
|
661 |
|
a2_attribute_mapping=['bad'],
|
662 |
|
)
|
663 |
|
|
664 |
|
new_apps = migration.apply(migrate_to)
|
665 |
|
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
|
666 |
|
|
667 |
|
authenticator = SAMLAuthenticator.objects.get()
|
668 |
|
assert not authenticator.attribute_lookups.exists()
|
669 |
|
|
670 |
|
assert caplog.messages == [
|
671 |
|
'could not create related objects for authenticator SAMLAuthenticator object (%s)' % authenticator.pk,
|
672 |
|
'attribute mapping for SAMLAuthenticator object (%s): ["bad"]' % authenticator.pk,
|
673 |
|
'lookup by attributes for SAMLAuthenticator object (%s): [{"user_field": "email", "saml_attribute": "email"}]'
|
674 |
|
% authenticator.pk,
|
675 |
|
]
|
676 |
|
|
677 |
|
|
678 |
|
def test_saml_authenticator_data_migration_rename_attributes(migration, settings):
|
679 |
|
migrate_from = [('authentic2_auth_saml', '0008_auto_20220913_1105')]
|
680 |
|
migrate_to = [('authentic2_auth_saml', '0009_statically_rename_attributes')]
|
681 |
|
|
682 |
|
old_apps = migration.before(migrate_from)
|
683 |
|
SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
|
684 |
|
RenameAttributeAction = old_apps.get_model('authentic2_auth_saml', 'RenameAttributeAction')
|
685 |
|
SetAttributeAction = old_apps.get_model('authentic2_auth_saml', 'SetAttributeAction')
|
686 |
|
SAMLAttributeLookup = old_apps.get_model('authentic2_auth_saml', 'SAMLAttributeLookup')
|
687 |
|
|
688 |
|
authenticator = SAMLAuthenticator.objects.create(slug='idp1')
|
689 |
|
RenameAttributeAction.objects.create(
|
690 |
|
authenticator=authenticator, from_name='http://nice/attribute/givenName', to_name='first_name'
|
691 |
|
)
|
692 |
|
SAMLAttributeLookup.objects.create(
|
693 |
|
authenticator=authenticator, user_field='first_name', saml_attribute='first_name'
|
694 |
|
)
|
695 |
|
SAMLAttributeLookup.objects.create(
|
696 |
|
authenticator=authenticator, user_field='title', saml_attribute='title'
|
697 |
|
)
|
698 |
|
SetAttributeAction.objects.create(
|
699 |
|
authenticator=authenticator, user_field='first_name', saml_attribute='first_name'
|
700 |
|
)
|
701 |
|
SetAttributeAction.objects.create(authenticator=authenticator, user_field='title', saml_attribute='title')
|
702 |
|
|
703 |
|
new_apps = migration.apply(migrate_to)
|
704 |
|
SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
|
705 |
|
authenticator = SAMLAuthenticator.objects.get()
|
706 |
|
|
707 |
|
attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
|
708 |
|
assert attribute_lookup1.saml_attribute == 'http://nice/attribute/givenName'
|
709 |
|
assert attribute_lookup1.user_field == 'first_name'
|
710 |
|
assert attribute_lookup2.saml_attribute == 'title'
|
711 |
|
assert attribute_lookup2.user_field == 'title'
|
712 |
|
|
713 |
|
set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
|
714 |
|
assert set_attribute1.saml_attribute == 'http://nice/attribute/givenName'
|
715 |
|
assert set_attribute1.user_field == 'first_name'
|
716 |
|
assert set_attribute2.saml_attribute == 'title'
|
717 |
|
assert set_attribute2.user_field == 'title'
|
718 |
|
-
|