Projet

Général

Profil

0005-misc-split-auth_saml-tests-69720.patch

Benjamin Dauvergne, 18 octobre 2022 20:36

Télécharger (39,3 ko)

Voir les différences:

Subject: [PATCH 05/10] misc: split auth_saml tests (#69720)

 tests/auth_saml/conftest.py        |  83 ++++++
 tests/auth_saml/metadata.xml       |  78 ++++++
 tests/auth_saml/private_key.pem    |  28 ++
 tests/auth_saml/public_key.pem     |  19 ++
 tests/auth_saml/test_adapter.py    |  98 +++++++
 tests/auth_saml/test_login.py      | 121 +++++++++
 tests/auth_saml/test_manager.py    |  33 +++
 tests/auth_saml/test_migrations.py | 402 +++++++++++++++++++++++++++++
 8 files changed, 862 insertions(+)
 create mode 100644 tests/auth_saml/conftest.py
 create mode 100644 tests/auth_saml/metadata.xml
 create mode 100644 tests/auth_saml/private_key.pem
 create mode 100644 tests/auth_saml/public_key.pem
 create mode 100644 tests/auth_saml/test_adapter.py
 create mode 100644 tests/auth_saml/test_login.py
 create mode 100644 tests/auth_saml/test_manager.py
 create mode 100644 tests/auth_saml/test_migrations.py
tests/auth_saml/conftest.py
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 pathlib
18

  
19
import lasso
20
import pytest
21

  
22
from authentic2.custom_user.models import User
23
from authentic2.models import Attribute
24
from authentic2_auth_saml.adapters import AuthenticAdapter
25
from authentic2_auth_saml.models import SAMLAuthenticator, SetAttributeAction
26

  
27

  
28
@pytest.fixture
29
def adapter():
30
    return AuthenticAdapter()
31

  
32

  
33
base_path = pathlib.Path(__file__).parent
34

  
35

  
36
@pytest.fixture
37
def idp(db, settings):
38
    settings.MELLON_PRIVATE_KEY = str((base_path / './private_key.pem').resolve())
39
    settings.MELLON_PUBLIC_KEY = str((base_path / './public_key.pem').resolve())
40
    authenticator = SAMLAuthenticator.objects.create(
41
        enabled=True,
42
        metadata=(base_path / './metadata.xml').read_text(),
43
        slug='idp1',
44
    )
45
    SetAttributeAction.objects.create(
46
        authenticator=authenticator,
47
        user_field='email',
48
        saml_attribute='mail',
49
        mandatory=True,
50
    )
51
    SetAttributeAction.objects.create(
52
        authenticator=authenticator,
53
        user_field='title',
54
        saml_attribute='title',
55
    )
56
    SetAttributeAction.objects.create(
57
        authenticator=authenticator,
58
        user_field='first_name',
59
        saml_attribute='http://nice/attribute/givenName',
60
    )
61
    return authenticator.settings
62

  
63

  
64
@pytest.fixture
65
def title_attribute(db):
66
    return Attribute.objects.create(kind='title', name='title', label='title')
67

  
68

  
69
@pytest.fixture
70
def saml_attributes():
71
    return {
72
        'issuer': 'https://idp.com/',
73
        'name_id_content': 'xxx',
74
        'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
75
        'mail': ['john.doe@example.com'],
76
        'title': ['Mr.'],
77
        'http://nice/attribute/givenName': ['John'],
78
    }
79

  
80

  
81
@pytest.fixture
82
def user(db):
83
    return User.objects.create()
tests/auth_saml/metadata.xml
1
<?xml version="1.0"?>
2
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
3
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
4
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
5
    entityID="http://idp5/metadata">
6
<IDPSSODescriptor
7
    WantAuthnRequestsSigned="true"
8
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
9
<KeyDescriptor use="signing">
10
    <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11
      <ds:X509Data><ds:X509Certificate>
12
MIIC/TCCAeWgAwIBAgIUe/2RmSPWPz90rF3xm4q+jPPrGlcwDQYJKoZIhvcNAQEL
13
BQAwDjEMMAoGA1UEAwwDSURQMB4XDTE5MDQwMzEwMDEwM1oXDTQ2MDgxOTEwMDEw
14
M1owDjEMMAoGA1UEAwwDSURQMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
15
AQEA39a9HT0PFMAy5Tcdik+LEWuDqqEdt8UrZr7TH/GpfAneDC0skDeHi9ErsEet
16
ZYuBkk7YDpNvpaXprhG7EWwO9LnBN5oxN7Jp7PEOyD8+v4GSKjySbmTubaGcR5F3
17
3EfPVp9yin79kN+iIi/VtoL6cacfzsIBjNmBBzs4RhIjoSce+0uTuV+EN73p5ZSt
18
mThamA/qnUeRDnVG5Y/hya3ldsg+rb6ObahnUYcAcP9sR/SKku3YQNVG0f4u1JYY
19
in7gKGZ8ty7YeVI6ulVNmG/fZXo8nw3OJ9VDG1Ye3yOz7tqhGCh9HjUsoVikPsoD
20
iDXQAgVynaEqo33SZGmgceGZowIDAQABo1MwUTAdBgNVHQ4EFgQUg0v91gd9cDen
21
b+C5YH/Kfj+1db4wHwYDVR0jBBgwFoAUg0v91gd9cDenb+C5YH/Kfj+1db4wDwYD
22
VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAiAvkSPyv5oCuJmETM/B/
23
HKd252g90yzdKM38gs1fFXt6IErcI5t6UlFZFrIs6K1yE5dEjgxFZFKFbakO618C
24
xdh6MI8obfhAbqCDLVSkWtm9M0HX1I1HxJ/b+0BR6RtT9w8gDRL4ZRb/+y+82GRH
25
Sm+9A8VXgWaTKRsUnRXUQPVXrQ4mU0R+f5tXpa1CVpH3Z8krYbvZSzB086alim12
26
5Kbe21CSN83wCZm0mjkKwFrrjCnKv3wSNqHHXQoYeGfON6B33d0rJRLwjIWJ7BDC
27
tkks8tLgCsYhKGNwprDy8Eo/lDCzQe03Ob1HPEh2XaENJoAx0XT6kJDyX41N8JPK
28
tA==
29
</ds:X509Certificate></ds:X509Data>
30
    </ds:KeyInfo>
31
  </KeyDescriptor>
32
  <ArtifactResolutionService isDefault="true" index="0"
33
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
34
    Location="http://idp5/artifact" />
35
  <SingleLogoutService
36
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
37
    Location="http://idp5/singleLogoutSOAP" />
38
  <SingleLogoutService
39
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
40
    Location="http://idp5/singleLogout"
41
    ResponseLocation="http://idp5/singleLogoutReturn" />
42
  <ManageNameIDService
43
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
44
    Location="http://idp5/manageNameIdSOAP" />
45
  <ManageNameIDService
46
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
47
    Location="http://idp5/manageNameId"
48
    ResponseLocation="http://idp5/manageNameIdReturn" />
49
  <SingleSignOnService
50
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
51
    Location="http://idp5/singleSignOn" />
52
  <SingleSignOnService
53
    Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
54
    Location="http://idp5/singleSignOnSOAP" />
55
</IDPSSODescriptor>
56
<AuthnAuthorityDescriptor
57
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
58
	<AuthnQueryService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://idp6/authnQueryService"/>
59
	<AssertionIDRequestService Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI" Location="http://idp6/authnAuthAssertionIDRequestService"/>
60
	<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
61
</AuthnAuthorityDescriptor>
62
<PDPDescriptor
63
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
64
	<AuthzService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://idp6/authzService"/>
65
	<AssertionIDRequestService Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI" Location="http://idp6/PDPAuthAssertionIDRequestService"/>
66
	<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:kerberos</NameIDFormat>
67
</PDPDescriptor>
68
<AttributeAuthorityDescriptor
69
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
70
	<AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://idp6/attributeService"/>
71
	<AssertionIDRequestService Binding="urn:oasis:names:tc:SAML:2.0:bindings:URI" Location="http://idp6/AttributeAuthAssertionIDRequestService"/>
72
	<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</NameIDFormat>
73
</AttributeAuthorityDescriptor>
74
<Organization>
75
   <OrganizationName xml:lang="en">Entr'ouvert</OrganizationName>
76
</Organization>
77

  
78
</EntityDescriptor>
tests/auth_saml/private_key.pem
1
-----BEGIN PRIVATE KEY-----
2
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJ4MpsYAEt52vT
3
sbQzo15UE1ZyUNb5sz77iaZZXX3k6pS6z4qOF83p2y7CpRWyjfPsqxVTTbP70BVe
4
yI9G4cPw2BeCoyNxPLTjh4J/DUT3z6iCatJfKVUKZNIYKBGPb7wAA1OHGk4MOGw6
5
gL0uC/m3yTN4Q8RhRD55GTHW0vb8PJOizWESr0ycvgIJyhmzhA++9DoAJj1jphj8
6
AZWLYWDcBMVo4D/6TvcwWtaxfj8eXmmbIzB+igsvQSLbIyRxzXJaMj7cYocDoSS4
7
eE8kRaL+ndUNd8gqNn6DpMbYolY24atjrT4+Zvqx3uDs0NMIN9rGagayNtTe+XTJ
8
FTx2nlL7AgMBAAECggEAVu2QvHHqkBWifJl8eu/R4mohQ0BEEWl5qV5wXvK/Dx9j
9
w70ycFUXuadDz1S+rxApBLP2jtRauAe17AZ4i5ETilXCaeJNlKkLSx5CturD0+F7
10
Mg1FYOyvTbZ0MSqvxQ/b6DWGdhqBmQmCsP5Wd8l4Ugc1PogPu8JjFEohB9v3tkyr
11
jdJJB5hHB34YfpXfSqbo1tNuU1CLyzaL+D02BND7ompKuQRLG1MtTuvZ/7IS4l5v
12
t1CVfdOUfPZeF9DJmFYrTaDHSrsCSIJl/djNOl2dn2rZnd/TDnuxg0UdSz4myjhj
13
xdhlXpDK+VheDHqDwVYDul4F5xc3fg4AI2cm+Hy+YQKBgQDjYP0SG770n7lEpI5S
14
K8c8D2BtQPX1Qj9wyNr69T4dIphfCRNJvaQnUxBXDBD/Tup82zbXWr7jOaCBWJy+
15
0Ik+nE+t85NY2sZcoBEd5dsYfriXYIjomhXzloNkwbR3HqN6DTbSqatLbwkdczw8
16
K8ZR6mlGX5F7jRSf7sH+NU4+HQKBgQDjShH/Q0X97YOY0An6QPWVVIhSp5zbyB2M
17
KVfAV5bVF8AaJJccoaGgM5HBsQBd7IrgCJodt51E6TpZK+Im/VOzG8sUmNA5MeuF
18
8XFJ0TRwLsFFlFFh2KN2uWYIIe3RFbXY0Kxil2YSOxNiHG2m5qkBucTgWoKG4kzF
19
mY9+RHjp9wKBgQCo6/UW7uX+dmr9RAM3qK5rQEEy6X/QpVbcQ1vr9SYgHwN0Fxnt
20
PqYlUOBiyuQVyFsMRw+HDjOiO72yWlKYr/RGP3oykTJ2YJHdXk1ZGNqcaAha7azI
21
oTCNttQGlqGrnWd11TtVZheMAwGSj3nAegTr9mofjgBW+YjJCGe2o4NtQQKBgChQ
22
MvD7laZ8QiMQgzSH4Qcjfypp4EB7NgJuMspCvtX86G26n7LMWEZ53xhjtJT0J42k
23
+PDcaGCYnWjDh9EyjW3vOA3nLMd4OzX+pQFawdpD8LPOosCgFB4ytA4tNmknWKGk
24
IW87OzdwkveL40b/Emrj8C963jveV4+UtQbITknxAoGAAMRnKZiY5oCUdNF3b5CF
25
u/c7rBBvXvD6LoD/I+DciW+vzzLTpyHXe0O1LXAXbh6Vhbdl/JUEqQ9GY0WHUF2i
26
GGsC/A8cUZPBW/KRTp9Gvba/cLVVJJMg17Yeif1vLBipKtjhBOe2J32oi2sFBzU5
27
ftuBuQmlGE+LRrRFtGu+fS8=
28
-----END PRIVATE KEY-----
tests/auth_saml/public_key.pem
1
-----BEGIN CERTIFICATE-----
2
MIIC/TCCAeWgAwIBAgIUUpf0AbWHRr58otdjpctRwYP/uEYwDQYJKoZIhvcNAQEL
3
BQAwDjEMMAoGA1UEAwwDSURQMB4XDTE5MDQwMzEwMDExMloXDTQ2MDgxOTEwMDEx
4
MlowDjEMMAoGA1UEAwwDSURQMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
5
AQEAyeDKbGABLedr07G0M6NeVBNWclDW+bM++4mmWV195OqUus+KjhfN6dsuwqUV
6
so3z7KsVU02z+9AVXsiPRuHD8NgXgqMjcTy044eCfw1E98+ogmrSXylVCmTSGCgR
7
j2+8AANThxpODDhsOoC9Lgv5t8kzeEPEYUQ+eRkx1tL2/DyTos1hEq9MnL4CCcoZ
8
s4QPvvQ6ACY9Y6YY/AGVi2Fg3ATFaOA/+k73MFrWsX4/Hl5pmyMwfooLL0Ei2yMk
9
cc1yWjI+3GKHA6EkuHhPJEWi/p3VDXfIKjZ+g6TG2KJWNuGrY60+Pmb6sd7g7NDT
10
CDfaxmoGsjbU3vl0yRU8dp5S+wIDAQABo1MwUTAdBgNVHQ4EFgQUuJZGqJa7ljZZ
11
LWZ3AqbvbdipCBIwHwYDVR0jBBgwFoAUuJZGqJa7ljZZLWZ3AqbvbdipCBIwDwYD
12
VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAPaaW02Y17FAIJv9wk7tc
13
MURL364jlaGrNYcv/og2uJw2WxKsCKahE8fY3Yu3fceSt6eMannWkvDpAGC9COIX
14
Lr9VKK9c2eUbFyeCWu4eizQaUTKtrQIyxyL1geQdmJZPcJfvDfJM4lUxt0gTx1R5
15
ouMwDAtIFfDpOKQyXthqeXoGrrraxHr+GzJcgdHeR9c4eiKXf7C1JEJhhv6a3zDz
16
v3uOwiLhlKIQ430623MK75jdEzo+2/aUzur8UttkRBdalumYR5SM+CKLhPYc9L6p
17
55pHYinL190yAjIDuY9WN+d+8C/2UrUI5iiHOc/D2kYCN8dJWDwhXlRKhRZ6f0jq
18
Lw==
19
-----END CERTIFICATE-----
tests/auth_saml/test_adapter.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 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 re
18
from unittest import mock
19

  
20
import lasso
21
import pytest
22
from mellon.adapters import UserCreationError
23

  
24
from authentic2.custom_user.models import User
25
from authentic2_auth_saml.adapters import MappingError
26
from authentic2_auth_saml.models import AddRoleAction, SAMLAuthenticator, SetAttributeAction
27

  
28

  
29
def test_lookup_user_ok(adapter, idp, saml_attributes, title_attribute):
30
    assert User.objects.count() == 0
31

  
32
    user = adapter.lookup_user(idp, saml_attributes)
33
    user.refresh_from_db()
34
    assert user.email == 'john.doe@example.com'
35
    assert user.attributes.title == 'Mr.'
36
    assert user.first_name == 'John'
37
    assert user.attributes.title == 'Mr.'
38
    assert user.ou.default is True
39

  
40

  
41
def test_lookup_user_missing_mandatory_attribute(adapter, idp, saml_attributes, title_attribute):
42
    del saml_attributes['mail']
43

  
44
    assert User.objects.count() == 0
45
    assert adapter.lookup_user(idp, saml_attributes) is None
46
    assert User.objects.count() == 0
47

  
48

  
49
def test_apply_attribute_mapping_missing_attribute_logged(
50
    caplog, adapter, idp, saml_attributes, title_attribute, user
51
):
52
    caplog.set_level('WARNING')
53
    saml_attributes['http://nice/attribute/givenName'] = []
54
    adapter.provision_a2_attributes(user, idp, saml_attributes)
55
    assert re.match('.*no value.*first_name', caplog.records[-1].message)
56

  
57

  
58
@pytest.mark.parametrize('action_name', ['add-role', 'toggle-role'])
59
class TestAddRole:
60
    @pytest.fixture
61
    def idp(self, action_name, simple_role):
62
        authenticator = SAMLAuthenticator.objects.create(
63
            enabled=True,
64
            metadata='meta1.xml',
65
            slug='idp1',
66
        )
67
        AddRoleAction.objects.create(authenticator=authenticator, role=simple_role)
68
        return authenticator.settings
69

  
70
    @pytest.fixture
71
    def saml_attributes(self):
72
        return {
73
            'issuer': 'https://idp.com/',
74
            'name_id_content': 'xxx',
75
            'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT,
76
        }
77

  
78
    def test_lookup_user_success(self, adapter, simple_role, idp, saml_attributes):
79
        user = adapter.lookup_user(idp, saml_attributes)
80
        assert simple_role in user.roles.all()
81

  
82

  
83
def test_apply_attribute_mapping_missing_attribute_exception(
84
    adapter, idp, saml_attributes, title_attribute, user, rf
85
):
86
    saml_attributes['http://nice/attribute/givenName'] = []
87
    SetAttributeAction.objects.filter(user_field='first_name').update(mandatory=True)
88
    with pytest.raises(MappingError, match='no value'):
89
        adapter.provision_a2_attributes(user, idp, saml_attributes)
90

  
91
    request = rf.get('/')
92
    request._messages = mock.Mock()
93
    adapter.request = request
94
    with pytest.raises(UserCreationError):
95
        adapter.finish_create_user(idp, saml_attributes, user)
96
    request._messages.add.assert_called_once_with(
97
        40, 'User creation failed: no value for attribute "first_name".', ''
98
    )
tests/auth_saml/test_login.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 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
from unittest import mock
19

  
20
import pytest
21

  
22
from authentic2.apps.authenticators.models import LoginPasswordAuthenticator
23
from authentic2_auth_saml.adapters import AuthenticAdapter
24
from authentic2_auth_saml.models import SAMLAuthenticator
25

  
26

  
27
@pytest.fixture
28
def patched_adapter(monkeypatch):
29
    def load_idp(self, settings, order):
30
        settings['ENTITY_ID'] = 'idp1'
31
        return settings
32

  
33
    monkeypatch.setattr(AuthenticAdapter, 'load_idp', load_idp)
34

  
35

  
36
def test_providers_on_login_page(db, app, settings):
37
    SAMLAuthenticator.objects.create(
38
        enabled=True,
39
        metadata='meta1.xml',
40
        slug='idp1',
41
        button_label='Test label',
42
        button_description='This is a test.',
43
    )
44

  
45
    response = app.get('/login/')
46
    assert response.pyquery('button[name="login-saml-idp1"]')
47
    assert not response.pyquery('button[name="login-saml-1"]')
48
    assert 'SAML' in response.text
49

  
50
    SAMLAuthenticator.objects.create(enabled=True, metadata='meta1.xml', slug='idp2')
51
    response = app.get('/login/')
52
    # two frontends should be present on login page
53
    assert response.pyquery('button[name="login-saml-idp1"]')
54
    assert response.pyquery('button[name="login-saml-idp2"]')
55
    assert 'Test label' in response.text
56
    assert 'This is a test.' in response.text
57

  
58

  
59
def test_login_with_conditionnal_authenticators(db, app, settings, caplog):
60
    authenticator = SAMLAuthenticator.objects.create(
61
        enabled=True, metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'), slug='idp1'
62
    )
63

  
64
    response = app.get('/login/')
65
    assert 'login-saml-idp1' in response
66

  
67
    authenticator.show_condition = 'remote_addr==\'0.0.0.0\''
68
    authenticator.save()
69
    response = app.get('/login/')
70
    assert 'login-saml-idp1' not in response
71

  
72
    authenticator2 = SAMLAuthenticator.objects.create(
73
        enabled=True, metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'), slug='idp2'
74
    )
75
    response = app.get('/login/')
76
    assert 'login-saml-idp1' not in response
77
    assert 'login-saml-idp2' in response
78

  
79
    authenticator2.show_condition = 'remote_addr==\'0.0.0.0\''
80
    authenticator2.save()
81
    response = app.get('/login/')
82
    assert 'login-saml-idp1' not in response
83
    assert 'login-saml-idp2' not in response
84

  
85

  
86
def test_login_condition_dnsbl(db, app, settings, caplog):
87
    SAMLAuthenticator.objects.create(
88
        enabled=True,
89
        metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'),
90
        slug='idp1',
91
        show_condition='remote_addr in dnsbl(\'dnswl.example.com\')',
92
    )
93
    SAMLAuthenticator.objects.create(
94
        enabled=True,
95
        metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'),
96
        slug='idp2',
97
        show_condition='remote_addr not in dnsbl(\'dnswl.example.com\')',
98
    )
99
    with mock.patch('authentic2.utils.evaluate.check_dnsbl', return_value=True):
100
        response = app.get('/login/')
101
    assert 'login-saml-idp1' in response
102
    assert 'login-saml-idp2' not in response
103

  
104

  
105
def test_login_autorun(db, app, settings, patched_adapter):
106
    response = app.get('/login/')
107

  
108
    authenticator = SAMLAuthenticator.objects.create(
109
        enabled=True, metadata_path=os.path.join(os.path.dirname(__file__), 'metadata.xml'), slug='idp1'
110
    )
111
    # hide password block
112
    LoginPasswordAuthenticator.objects.update_or_create(
113
        slug='password-authenticator', defaults={'enabled': False}
114
    )
115
    response = app.get('/login/', status=302)
116
    assert '/accounts/saml/login/?entityID=' in response['Location']
117

  
118
    authenticator.slug = 'slug_with_underscore'
119
    authenticator.save()
120
    response = app.get('/login/', status=302)
121
    assert '/accounts/saml/login/?entityID=' in response['Location']
tests/auth_saml/test_manager.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 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
from mellon.models import Issuer, UserSAMLIdentifier
18

  
19
from ..utils import login
20

  
21

  
22
def test_manager_user_sidebar(app, superuser, simple_user):
23
    login(app, superuser, '/manage/')
24
    response = app.get('/manage/users/%s/' % simple_user.id)
25
    assert 'SAML' not in response
26

  
27
    issuer1, _ = Issuer.objects.get_or_create(entity_id='https://idp1.com/')
28
    UserSAMLIdentifier.objects.create(user=simple_user, issuer=issuer1, name_id='1234')
29

  
30
    response = app.get('/manage/users/%s/' % simple_user.id)
31
    assert 'SAML' in response
32
    assert 'https://idp1.com/' in response
33
    assert '1234' in response
tests/auth_saml/test_migrations.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2022 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

  
19

  
20
def test_saml_authenticator_data_migration(migration, settings):
21
    app = 'authentic2_auth_saml'
22
    migrate_from = [(app, '0001_initial')]
23
    migrate_to = [(app, '0002_auto_20220608_1559')]
24

  
25
    old_apps = migration.before(migrate_from)
26
    SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
27

  
28
    settings.A2_AUTH_SAML_ENABLE = True
29
    settings.MELLON_METADATA_CACHE_TIME = 42
30
    settings.MELLON_METADATA_HTTP_TIMEOUT = 42
31
    settings.MELLON_PROVISION = False
32
    settings.MELLON_VERIFY_SSL_CERTIFICATE = True
33
    settings.MELLON_TRANSIENT_FEDERATION_ATTRIBUTE = None
34
    settings.MELLON_USERNAME_TEMPLATE = 'test'
35
    settings.MELLON_NAME_ID_POLICY_ALLOW_CREATE = False
36
    settings.MELLON_FORCE_AUTHN = True
37
    settings.MELLON_ADD_AUTHNREQUEST_NEXT_URL_EXTENSION = False
38
    settings.MELLON_GROUP_ATTRIBUTE = 'role'
39
    settings.MELLON_CREATE_GROUP = True
40
    settings.MELLON_ERROR_URL = 'https://example.com/error/'
41
    settings.MELLON_AUTHN_CLASSREF = ('class1', 'class2')
42
    settings.MELLON_LOGIN_HINTS = ['hint1', 'hint2']
43
    settings.AUTH_FRONTENDS_KWARGS = {
44
        'saml': {
45
            'priority': 1,
46
            'show_condition': {
47
                '0': 'first condition',
48
                '1': 'second condition',
49
            },
50
        }
51
    }
52
    settings.MELLON_IDENTITY_PROVIDERS = [
53
        {
54
            'METADATA': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
55
            'REALM': 'test',
56
            'METADATA_CACHE_TIME': 43,
57
            'METADATA_HTTP_TIMEOUT': 43,
58
            'PROVISION': True,
59
            'LOOKUP_BY_ATTRIBUTES': [],
60
        },
61
        {
62
            'METADATA_PATH': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
63
            'NAME_ID_POLICY_ALLOW_CREATE': True,
64
            'FORCE_AUTHN': False,
65
            'ADD_AUTHNREQUEST_NEXT_URL_EXTENSION': True,
66
            'A2_ATTRIBUTE_MAPPING': [
67
                {
68
                    'attribute': 'email',
69
                    'saml_attribute': 'mail',
70
                },
71
            ],
72
            'LOOKUP_BY_ATTRIBUTES': [{'saml_attribute': 'email', 'user_field': 'email'}],
73
        },
74
        {
75
            'METADATA_URL': 'https://example.com/metadata.xml',
76
            'SLUG': 'third',
77
            'ATTRIBUTE_MAPPING': {'email': 'attributes[mail][0]'},
78
            'SUPERUSER_MAPPING': {'roles': 'Admin'},
79
        },
80
    ]
81

  
82
    new_apps = migration.apply(migrate_to)
83
    SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
84
    first_authenticator, second_authenticator, third_authenticator = SAMLAuthenticator.objects.all()
85
    assert first_authenticator.slug == '0'
86
    assert first_authenticator.order == 1
87
    assert first_authenticator.show_condition == 'first condition'
88
    assert first_authenticator.enabled is True
89
    assert first_authenticator.metadata_path == os.path.join(os.path.dirname(__file__), 'metadata.xml')
90
    assert first_authenticator.metadata_url == ''
91
    assert first_authenticator.metadata_cache_time == 43
92
    assert first_authenticator.metadata_http_timeout == 43
93
    assert first_authenticator.provision is True
94
    assert first_authenticator.verify_ssl_certificate is True
95
    assert first_authenticator.transient_federation_attribute == ''
96
    assert first_authenticator.realm == 'test'
97
    assert first_authenticator.username_template == 'test'
98
    assert first_authenticator.name_id_policy_format == ''
99
    assert first_authenticator.name_id_policy_allow_create is False
100
    assert first_authenticator.force_authn is True
101
    assert first_authenticator.add_authnrequest_next_url_extension is False
102
    assert first_authenticator.group_attribute == 'role'
103
    assert first_authenticator.create_group is True
104
    assert first_authenticator.error_url == 'https://example.com/error/'
105
    assert first_authenticator.error_redirect_after_timeout == 120
106
    assert first_authenticator.authn_classref == 'class1, class2'
107
    assert first_authenticator.login_hints == 'hint1, hint2'
108
    assert first_authenticator.lookup_by_attributes == []
109
    assert first_authenticator.a2_attribute_mapping == []
110
    assert first_authenticator.attribute_mapping == {}
111
    assert first_authenticator.superuser_mapping == {}
112

  
113
    assert second_authenticator.slug == '1'
114
    assert second_authenticator.order == 1
115
    assert second_authenticator.show_condition == 'second condition'
116
    assert second_authenticator.enabled is True
117
    assert second_authenticator.metadata_path == os.path.join(os.path.dirname(__file__), 'metadata.xml')
118
    assert second_authenticator.metadata_url == ''
119
    assert second_authenticator.metadata_cache_time == 42
120
    assert second_authenticator.metadata_http_timeout == 42
121
    assert second_authenticator.provision is False
122
    assert second_authenticator.verify_ssl_certificate is True
123
    assert second_authenticator.transient_federation_attribute == ''
124
    assert second_authenticator.realm == 'saml'
125
    assert second_authenticator.username_template == 'test'
126
    assert second_authenticator.name_id_policy_format == ''
127
    assert second_authenticator.name_id_policy_allow_create is True
128
    assert second_authenticator.force_authn is False
129
    assert second_authenticator.add_authnrequest_next_url_extension is True
130
    assert second_authenticator.group_attribute == 'role'
131
    assert second_authenticator.create_group is True
132
    assert second_authenticator.error_url == 'https://example.com/error/'
133
    assert second_authenticator.error_redirect_after_timeout == 120
134
    assert second_authenticator.authn_classref == 'class1, class2'
135
    assert second_authenticator.login_hints == 'hint1, hint2'
136
    assert second_authenticator.lookup_by_attributes == [{'saml_attribute': 'email', 'user_field': 'email'}]
137
    assert second_authenticator.a2_attribute_mapping == [
138
        {
139
            'attribute': 'email',
140
            'saml_attribute': 'mail',
141
        },
142
    ]
143
    assert first_authenticator.attribute_mapping == {}
144
    assert first_authenticator.superuser_mapping == {}
145

  
146
    assert third_authenticator.slug == 'third'
147
    assert third_authenticator.order == 1
148
    assert third_authenticator.show_condition == ''
149
    assert third_authenticator.enabled is True
150
    assert third_authenticator.metadata_path == ''
151
    assert third_authenticator.metadata_url == 'https://example.com/metadata.xml'
152
    assert third_authenticator.metadata_cache_time == 42
153
    assert third_authenticator.metadata_http_timeout == 42
154
    assert third_authenticator.provision is False
155
    assert third_authenticator.verify_ssl_certificate is True
156
    assert third_authenticator.transient_federation_attribute == ''
157
    assert third_authenticator.realm == 'saml'
158
    assert third_authenticator.username_template == 'test'
159
    assert third_authenticator.name_id_policy_format == ''
160
    assert third_authenticator.name_id_policy_format == ''
161
    assert third_authenticator.name_id_policy_allow_create is False
162
    assert third_authenticator.force_authn is True
163
    assert third_authenticator.group_attribute == 'role'
164
    assert third_authenticator.create_group is True
165
    assert third_authenticator.error_url == 'https://example.com/error/'
166
    assert third_authenticator.error_redirect_after_timeout == 120
167
    assert third_authenticator.authn_classref == 'class1, class2'
168
    assert third_authenticator.login_hints == 'hint1, hint2'
169
    assert third_authenticator.lookup_by_attributes == [
170
        {'saml_attribute': 'email', 'user_field': 'email', 'ignore-case': True},
171
        {'saml_attribute': 'username', 'user_field': 'username'},
172
    ]
173
    assert third_authenticator.a2_attribute_mapping == []
174
    assert third_authenticator.attribute_mapping == {'email': 'attributes[mail][0]'}
175
    assert third_authenticator.superuser_mapping == {'roles': 'Admin'}
176

  
177

  
178
def test_saml_authenticator_data_migration_empty_configuration(migration, settings):
179
    app = 'authentic2_auth_saml'
180
    migrate_from = [(app, '0001_initial')]
181
    migrate_to = [(app, '0002_auto_20220608_1559')]
182

  
183
    old_apps = migration.before(migrate_from)
184
    SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
185

  
186
    new_apps = migration.apply(migrate_to)
187
    SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
188
    assert not SAMLAuthenticator.objects.exists()
189

  
190

  
191
def test_saml_authenticator_data_migration_bad_settings(migration, settings):
192
    app = 'authentic2_auth_saml'
193
    migrate_from = [(app, '0001_initial')]
194
    migrate_to = [(app, '0002_auto_20220608_1559')]
195

  
196
    old_apps = migration.before(migrate_from)
197
    SAMLAuthenticator = old_apps.get_model(app, 'SAMLAuthenticator')
198

  
199
    settings.AUTH_FRONTENDS_KWARGS = {"saml": {"priority": None, "show_condition": None}}
200
    settings.MELLON_METADATA_CACHE_TIME = 2**16
201
    settings.MELLON_METADATA_HTTP_TIMEOUT = -1
202
    settings.MELLON_PROVISION = None
203
    settings.MELLON_USERNAME_TEMPLATE = 42
204
    settings.MELLON_GROUP_ATTRIBUTE = None
205
    settings.MELLON_ERROR_URL = 'a' * 500
206
    settings.MELLON_AUTHN_CLASSREF = 'not-a-list'
207
    settings.MELLON_IDENTITY_PROVIDERS = [
208
        {
209
            'METADATA': os.path.join(os.path.dirname(__file__), 'metadata.xml'),
210
            'ERROR_REDIRECT_AFTER_TIMEOUT': -1,
211
            'SUPERUSER_MAPPING': 'not-a-dict',
212
        },
213
    ]
214

  
215
    new_apps = migration.apply(migrate_to)
216
    SAMLAuthenticator = new_apps.get_model(app, 'SAMLAuthenticator')
217
    authenticator = SAMLAuthenticator.objects.get()
218
    assert authenticator.slug == '0'
219
    assert authenticator.order == 3
220
    assert authenticator.show_condition == ''
221
    assert authenticator.enabled is False
222
    assert authenticator.metadata_cache_time == 3600
223
    assert authenticator.metadata_http_timeout == 10
224
    assert authenticator.provision is True
225
    assert authenticator.username_template == '{attributes[name_id_content]}@{realm}'
226
    assert authenticator.group_attribute == ''
227
    assert authenticator.error_url == 'a' * 200
228
    assert authenticator.error_redirect_after_timeout == 120
229
    assert authenticator.authn_classref == ''
230
    assert authenticator.superuser_mapping == {}
231

  
232

  
233
def test_saml_authenticator_data_migration_json_fields(migration, settings):
234
    migrate_from = [
235
        (
236
            'authentic2_auth_saml',
237
            '0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
238
        ),
239
        ('a2_rbac', '0029_use_unique_constraints'),
240
    ]
241
    migrate_to = [
242
        ('authentic2_auth_saml', '0006_migrate_jsonfields'),
243
        ('a2_rbac', '0029_use_unique_constraints'),
244
    ]
245

  
246
    old_apps = migration.before(migrate_from)
247
    SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
248
    Role = old_apps.get_model('a2_rbac', 'Role')
249
    OU = old_apps.get_model('a2_rbac', 'OrganizationalUnit')
250

  
251
    ou = OU.objects.create(name='Test OU', slug='test-ou')
252
    role = Role.objects.create(name='Test role', slug='test-role', ou=ou)
253

  
254
    SAMLAuthenticator.objects.create(
255
        metadata='meta1.xml',
256
        slug='idp1',
257
        lookup_by_attributes=[
258
            {'saml_attribute': 'email', 'user_field': 'email'},
259
            {'saml_attribute': 'saml_name', 'user_field': 'first_name', 'ignore-case': True},
260
        ],
261
        a2_attribute_mapping=[
262
            {
263
                'attribute': 'email',
264
                'saml_attribute': 'mail',
265
                'mandatory': True,
266
            },
267
            {'action': 'rename', 'from': 'a' * 1025, 'to': 'first_name'},
268
            {
269
                'attribute': 'first_name',
270
                'saml_attribute': 'first_name',
271
            },
272
            {
273
                'attribute': 'invalid',
274
                'saml_attribute': '',
275
            },
276
            {
277
                'attribute': 'invalid',
278
                'saml_attribute': None,
279
            },
280
            {
281
                'attribute': 'invalid',
282
            },
283
            {
284
                'action': 'add-role',
285
                'role': {
286
                    'name': role.name,
287
                    'ou': {
288
                        'name': role.ou.name,
289
                    },
290
                },
291
                'condition': "roles == 'A'",
292
            },
293
        ],
294
    )
295

  
296
    new_apps = migration.apply(migrate_to)
297
    SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
298
    authenticator = SAMLAuthenticator.objects.get()
299

  
300
    attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
301
    assert attribute_lookup1.saml_attribute == 'email'
302
    assert attribute_lookup1.user_field == 'email'
303
    assert attribute_lookup1.ignore_case is False
304
    assert attribute_lookup2.saml_attribute == 'saml_name'
305
    assert attribute_lookup2.user_field == 'first_name'
306
    assert attribute_lookup2.ignore_case is True
307

  
308
    set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
309
    assert set_attribute1.attribute == 'email'
310
    assert set_attribute1.saml_attribute == 'mail'
311
    assert set_attribute1.mandatory is True
312
    assert set_attribute2.attribute == 'first_name'
313
    assert set_attribute2.saml_attribute == 'first_name'
314
    assert set_attribute2.mandatory is False
315

  
316
    rename_attribute = authenticator.rename_attribute_actions.get()
317
    assert rename_attribute.from_name == 'a' * 1024
318
    assert rename_attribute.to_name == 'first_name'
319

  
320
    add_role = authenticator.add_role_actions.get()
321
    assert add_role.role.pk == role.pk
322
    assert add_role.condition == "roles == 'A'"
323
    assert add_role.mandatory is False
324

  
325

  
326
def test_saml_authenticator_data_migration_json_fields_log_errors(migration, settings, caplog):
327
    migrate_from = [
328
        (
329
            'authentic2_auth_saml',
330
            '0005_addroleaction_renameattributeaction_samlattributelookup_setattributeaction',
331
        ),
332
        ('a2_rbac', '0029_use_unique_constraints'),
333
    ]
334
    migrate_to = [
335
        ('authentic2_auth_saml', '0006_migrate_jsonfields'),
336
        ('a2_rbac', '0029_use_unique_constraints'),
337
    ]
338

  
339
    old_apps = migration.before(migrate_from)
340
    SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
341

  
342
    SAMLAuthenticator.objects.create(
343
        metadata='meta1.xml',
344
        slug='idp1',
345
        lookup_by_attributes=[{'saml_attribute': 'email', 'user_field': 'email'}],
346
        a2_attribute_mapping=['bad'],
347
    )
348

  
349
    new_apps = migration.apply(migrate_to)
350
    SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
351

  
352
    authenticator = SAMLAuthenticator.objects.get()
353
    assert not authenticator.attribute_lookups.exists()
354

  
355
    assert caplog.messages == [
356
        'could not create related objects for authenticator SAMLAuthenticator object (%s)' % authenticator.pk,
357
        'attribute mapping for SAMLAuthenticator object (%s): ["bad"]' % authenticator.pk,
358
        'lookup by attributes for SAMLAuthenticator object (%s): [{"user_field": "email", "saml_attribute": "email"}]'
359
        % authenticator.pk,
360
    ]
361

  
362

  
363
def test_saml_authenticator_data_migration_rename_attributes(migration, settings):
364
    migrate_from = [('authentic2_auth_saml', '0008_auto_20220913_1105')]
365
    migrate_to = [('authentic2_auth_saml', '0009_statically_rename_attributes')]
366

  
367
    old_apps = migration.before(migrate_from)
368
    SAMLAuthenticator = old_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
369
    RenameAttributeAction = old_apps.get_model('authentic2_auth_saml', 'RenameAttributeAction')
370
    SetAttributeAction = old_apps.get_model('authentic2_auth_saml', 'SetAttributeAction')
371
    SAMLAttributeLookup = old_apps.get_model('authentic2_auth_saml', 'SAMLAttributeLookup')
372

  
373
    authenticator = SAMLAuthenticator.objects.create(slug='idp1')
374
    RenameAttributeAction.objects.create(
375
        authenticator=authenticator, from_name='http://nice/attribute/givenName', to_name='first_name'
376
    )
377
    SAMLAttributeLookup.objects.create(
378
        authenticator=authenticator, user_field='first_name', saml_attribute='first_name'
379
    )
380
    SAMLAttributeLookup.objects.create(
381
        authenticator=authenticator, user_field='title', saml_attribute='title'
382
    )
383
    SetAttributeAction.objects.create(
384
        authenticator=authenticator, user_field='first_name', saml_attribute='first_name'
385
    )
386
    SetAttributeAction.objects.create(authenticator=authenticator, user_field='title', saml_attribute='title')
387

  
388
    new_apps = migration.apply(migrate_to)
389
    SAMLAuthenticator = new_apps.get_model('authentic2_auth_saml', 'SAMLAuthenticator')
390
    authenticator = SAMLAuthenticator.objects.get()
391

  
392
    attribute_lookup1, attribute_lookup2 = authenticator.attribute_lookups.all().order_by('pk')
393
    assert attribute_lookup1.saml_attribute == 'http://nice/attribute/givenName'
394
    assert attribute_lookup1.user_field == 'first_name'
395
    assert attribute_lookup2.saml_attribute == 'title'
396
    assert attribute_lookup2.user_field == 'title'
397

  
398
    set_attribute1, set_attribute2 = authenticator.set_attribute_actions.all().order_by('pk')
399
    assert set_attribute1.saml_attribute == 'http://nice/attribute/givenName'
400
    assert set_attribute1.user_field == 'first_name'
401
    assert set_attribute2.saml_attribute == 'title'
402
    assert set_attribute2.user_field == 'title'
0
-