0002-profile-add-csam-account-un-linking-capabilities-608.patch
src/authentic2_auth_fedict/app_settings.py | ||
---|---|---|
36 | 36 |
def enable(self): |
37 | 37 |
return self._setting('ENABLE', False) |
38 | 38 | |
39 |
@property |
|
40 |
def PROVISION_VERIFIED_ATTRIBUTES(self): |
|
41 |
return self._setting('PROVISION_VERIFIED_ATTRIBUTES', ('first_name', 'last_name', 'email', 'title')) |
|
42 | ||
39 | 43 | |
40 | 44 |
import sys |
41 | 45 |
src/authentic2_auth_fedict/authenticators.py | ||
---|---|---|
49 | 49 |
def profile(self, request, *args, **kwargs): |
50 | 50 |
context = kwargs.get('context', {}).copy() |
51 | 51 |
user_saml_identifiers = request.user.saml_identifiers.all() |
52 |
if not user_saml_identifiers: |
|
53 |
return '' |
|
54 | 52 |
for user_saml_identifier in user_saml_identifiers: |
55 | 53 |
user_saml_identifier.idp = get_idp(user_saml_identifier.issuer) |
56 | 54 |
context['user_saml_identifiers'] = user_saml_identifiers |
src/authentic2_auth_fedict/backends.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import logging |
|
18 | ||
17 | 19 |
import lasso |
20 |
from django.contrib import messages |
|
21 |
from django.utils.translation import ugettext_lazy as _ |
|
18 | 22 |
from mellon.backends import SAMLBackend |
23 |
from mellon.utils import get_adapters, get_idp |
|
24 | ||
25 |
from authentic2_auth_fedict import app_settings |
|
26 | ||
27 |
logger = logging.getLogger(__name__) |
|
19 | 28 | |
20 | 29 | |
21 | 30 |
class FedictBackend(SAMLBackend): |
... | ... | |
28 | 37 |
# but we do not expose this detail to the service provider as all it |
29 | 38 |
# needs to know is "strong authentication". |
30 | 39 |
return lasso.SAML2_AUTHN_CONTEXT_SMARTCARD_PKI |
40 | ||
41 |
def authenticate(self, request=None, **credentials): |
|
42 |
saml_attributes = credentials.get('saml_attributes') or {} |
|
43 |
if 'issuer' not in saml_attributes: |
|
44 |
logger.debug('no idp in saml_attributes') |
|
45 |
return None |
|
46 |
idp = get_idp(saml_attributes['issuer']) |
|
47 |
if not idp: |
|
48 |
logger.debug('unknown idp %s', saml_attributes['issuer']) |
|
49 |
return None |
|
50 |
adapters = get_adapters(idp, request=request) |
|
51 |
for adapter in adapters: |
|
52 |
if not hasattr(adapter, 'authorize'): |
|
53 |
continue |
|
54 |
if not adapter.authorize(idp, saml_attributes): |
|
55 |
return |
|
56 |
for adapter in adapters: |
|
57 |
if hasattr(adapter, 'lookup_by_attributes'): |
|
58 |
user = adapter.lookup_by_attributes(idp, saml_attributes) |
|
59 |
old_user = None |
|
60 |
for adapter in adapters: |
|
61 |
if not hasattr(adapter, 'lookup_user'): |
|
62 |
continue |
|
63 |
user = adapter.lookup_user(idp, saml_attributes) |
|
64 |
if user: |
|
65 |
request_user = getattr(request, 'user', None) if request else None |
|
66 |
if request_user != user and request_user.is_authenticated: |
|
67 |
old_user = user |
|
68 |
user = request_user |
|
69 |
saml_identifier = old_user.saml_identifier |
|
70 |
saml_identifier.user = user |
|
71 |
saml_identifier.save(update_fields=['user']) |
|
72 |
user.saml_identifier = saml_identifier |
|
73 |
messages.warning(request, _('Your account is now linked to your eID card.')) |
|
74 |
break |
|
75 |
else: # no user found |
|
76 |
return |
|
77 |
for adapter in adapters: |
|
78 |
if not hasattr(adapter, 'provision'): |
|
79 |
continue |
|
80 |
adapter.provision(user, idp, saml_attributes) |
|
81 |
# final identifier and attributes copy before deletion of old account |
|
82 |
if old_user and old_user != user: |
|
83 |
# copy all verified attributes to newly provisionned user |
|
84 |
for key in app_settings.PROVISION_VERIFIED_ATTRIBUTES: |
|
85 |
# corner case, email not accessible through AttributesDescriptor |
|
86 |
if key == 'email': |
|
87 |
user.email = old_user.email |
|
88 |
user.email_verified = old_user.email_verified |
|
89 |
continue |
|
90 |
# fallback on AttributesDescriptor-managed attributes |
|
91 |
value = getattr(old_user.verified_attributes, key, None) |
|
92 |
if value: |
|
93 |
setattr(user.verified_attributes, key, value) |
|
94 |
else: |
|
95 |
# fallback on non-verified attributes |
|
96 |
value = getattr(old_user.attributes, key, None) |
|
97 |
if value: |
|
98 |
setattr(user.attributes, key, value) |
|
99 |
logger.debug('deleting user %s, new fedict link manually created' % old_user) |
|
100 |
old_user.delete() |
|
101 |
return user |
src/authentic2_auth_fedict/templates/authentic2_auth_fedict/profile.html | ||
---|---|---|
1 |
<p> |
|
2 |
Ce compte est relié à votre carte d'identité électronique. |
|
3 |
</p> |
|
1 |
{% load i18n static %} |
|
2 |
<div> |
|
3 |
<img src="{% static "authentic2_auth_fedict/img/beid_image_mini.png" %}" alt=""> |
|
4 |
{% if user_saml_identifiers %} |
|
5 |
<p>{% blocktrans %}This account is linked to your eID card.{% endblocktrans %}</p> |
|
6 |
<p> |
|
7 |
{% blocktrans %} |
|
8 |
You may want to unlink your account, although you will not be able to |
|
9 |
connect to your account using your eID card (without re-doing the linking |
|
10 |
procedure). |
|
11 |
{% endblocktrans %} |
|
12 |
</p> |
|
13 |
<p><a class="button" href="{% url "fedict-unlink" %}">{% trans "Unlink my account" %}</a></p> |
|
14 |
{% else %} |
|
15 |
<p> |
|
16 |
{% blocktrans %} |
|
17 |
You may want to link your existing Publik account to your eID. In order to |
|
18 |
do so, you will either need your eID card and its card reader |
|
19 |
or your list of personal tokens. |
|
20 |
{% endblocktrans %} |
|
21 |
</p> |
|
22 |
<p><a class="button" href="{% url "fedict-login" %}?next=/accounts/">{% trans "Link my account to my eID card" %}</a></p> |
|
23 |
{% endif %} |
|
24 |
</div> |
src/authentic2_auth_fedict/urls.py | ||
---|---|---|
21 | 21 |
urlpatterns = [ |
22 | 22 |
url(r'^accounts/saml/', include('mellon.urls')), |
23 | 23 |
url(r'^accounts/fedict/login/$', views.login, name='fedict-login'), |
24 |
url(r'^accounts/fedict/unlink/$', views.unlink, name='fedict-unlink'), |
|
24 | 25 |
] |
src/authentic2_auth_fedict/views.py | ||
---|---|---|
18 | 18 |
import urllib.parse |
19 | 19 | |
20 | 20 |
from django.conf import settings |
21 |
from django.contrib import messages |
|
21 | 22 |
from django.core import signing |
22 | 23 |
from django.db import transaction |
23 | 24 |
from django.http import HttpResponseRedirect |
24 |
from django.shortcuts import resolve_url |
|
25 |
from django.shortcuts import redirect, resolve_url
|
|
25 | 26 |
from django.urls import reverse |
27 |
from django.utils.translation import ugettext_lazy as _ |
|
26 | 28 |
from django.views.decorators.csrf import csrf_exempt |
27 | 29 |
from django.views.generic import View |
28 | 30 | |
... | ... | |
69 | 71 | |
70 | 72 | |
71 | 73 |
login = transaction.non_atomic_requests(csrf_exempt(LoginView.as_view())) |
74 | ||
75 | ||
76 |
def unlink(request): |
|
77 |
if not hasattr(request, 'user') or not hasattr(request.user, 'saml_identifiers'): |
|
78 |
return |
|
79 |
unlink_performed = False |
|
80 |
for saml_identifier in request.user.saml_identifiers.all(): |
|
81 |
saml_identifier.delete() |
|
82 |
unlink_performed = True |
|
83 |
if unlink_performed: |
|
84 |
messages.success(request, message=_('Unlinking complete.')) |
|
85 |
return redirect('account_management') |
tests/conftest.py | ||
---|---|---|
50 | 50 |
email_verified=True, |
51 | 51 |
) |
52 | 52 |
user.set_password('john.doe') |
53 |
user.save() |
|
53 | 54 |
return user |
54 | 55 | |
55 | 56 |
tests/metadata.xml | ||
---|---|---|
1 |
<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
<ns0:EntityDescriptor xmlns:ns0="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ns1="http://www.w3.org/2000/09/xmldsig#" entityID="https://idp.com/idp/saml2/metadata"><ns0:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><ns0:KeyDescriptor><ns1:KeyInfo><ns1:X509Data><ns1:X509Certificate>MIIDNTCCAh2gAwIBAgIUToSxnhmCYk3jdVKvMxPB8S4HvBcwDQYJKoZIhvcNAQEL |
|
3 |
BQAwKjEoMCYGA1UEAwwfc3BhcmUtYXV0aGVudGljLmRldi5wdWJsaWsubG92ZTAe |
|
4 |
Fw0yMjAxMTcxNzQ2NThaFw0zMjAxMTcxNzQ2NThaMCoxKDAmBgNVBAMMH3NwYXJl |
|
5 |
LWF1dGhlbnRpYy5kZXYucHVibGlrLmxvdmUwggEiMA0GCSqGSIb3DQEBAQUAA4IB |
|
6 |
DwAwggEKAoIBAQDlVPm7D3abG4fTh/i9VJz4s9Lplblw4OKZwJfdFj2CrZX4kehh |
|
7 |
zDEAcn8cE14gj5bzGCWcoeApw4KIFeoSzxZGpGn7dyiFTTny2IJugEuobWcT0IuC |
|
8 |
P3TcPzWrm0HXnIcZjBv6Rr/cFFa2vAKL1CjT4xgZidRjGKpQBNoT2aOnFuvTzlrz |
|
9 |
ZsGJ38+yRFZDQzoGS+5h4F3ZCHx6Y5fLNzWDW3Cu6/vv2/ev35omcHW+mSTVpds5 |
|
10 |
Oo2uzIvumYAM83oLzgWRnPLnx14lH+kWoDNaXEpUWDrmLD1A9OgNYmkeT/85qdyN |
|
11 |
jzDbe1yG421e2jUaRXez412Nv1s1ua5U/dBlAgMBAAGjUzBRMB0GA1UdDgQWBBRQ |
|
12 |
Y1+34SVZXriQAFteCRbde5jSOTAfBgNVHSMEGDAWgBRQY1+34SVZXriQAFteCRbd |
|
13 |
e5jSOTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCwVWlzEE7w |
|
14 |
0wBmngfctrz6cSyc5oAqDGo6dzVw+7CYhjRGr0Z9C1zI5rYOPWdDceAqD/lOVapw |
|
15 |
1nawFszVpcClmY8Rt0Tiot8z18NRz0NCFZzbHx9cU5TGZUigOkuXXqB+D52ppuQB |
|
16 |
6t/RREpeZakiBEp0Y8pH9CalA2DCNf/2WGcvaMXQtAL7Ko0a8vPBoOMykwgaFvCj |
|
17 |
E+ZiZXHLLg4jw2QwX1s4Fap8vKEUn062qCx0TdgILX/PrUpmCkPvZRlA/fcQLFQw |
|
18 |
1vX1rInD3cpTrxNw2T9ywGYmbXG1DmpxoF5X4ju3fcTR968QVJlX3L5JMbbWNV9X |
|
19 |
iB4feKp2tzW1</ns1:X509Certificate></ns1:X509Data></ns1:KeyInfo></ns0:KeyDescriptor><ns0:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.com/idp/saml2/artifact" index="0"/><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.com/idp/saml2/slo" ResponseLocation="https://idp.com/idp/saml2/slo_return"/><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.com/idp/saml2/slo" ResponseLocation="https://idp.com/idp/saml2/slo_return"/><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.com/idp/saml2/slo/soap"/><ns0:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.com/idp/saml2/sso"/><ns0:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.com/idp/saml2/sso"/></ns0:IDPSSODescriptor></ns0:EntityDescriptor> |
tests/saml.crt | ||
---|---|---|
1 |
-----BEGIN CERTIFICATE----- |
|
2 |
MIIDNTCCAh2gAwIBAgIUToSxnhmCYk3jdVKvMxPB8S4HvBcwDQYJKoZIhvcNAQEL |
|
3 |
BQAwKjEoMCYGA1UEAwwfc3BhcmUtYXV0aGVudGljLmRldi5wdWJsaWsubG92ZTAe |
|
4 |
Fw0yMjAxMTcxNzQ2NThaFw0zMjAxMTcxNzQ2NThaMCoxKDAmBgNVBAMMH3NwYXJl |
|
5 |
LWF1dGhlbnRpYy5kZXYucHVibGlrLmxvdmUwggEiMA0GCSqGSIb3DQEBAQUAA4IB |
|
6 |
DwAwggEKAoIBAQDlVPm7D3abG4fTh/i9VJz4s9Lplblw4OKZwJfdFj2CrZX4kehh |
|
7 |
zDEAcn8cE14gj5bzGCWcoeApw4KIFeoSzxZGpGn7dyiFTTny2IJugEuobWcT0IuC |
|
8 |
P3TcPzWrm0HXnIcZjBv6Rr/cFFa2vAKL1CjT4xgZidRjGKpQBNoT2aOnFuvTzlrz |
|
9 |
ZsGJ38+yRFZDQzoGS+5h4F3ZCHx6Y5fLNzWDW3Cu6/vv2/ev35omcHW+mSTVpds5 |
|
10 |
Oo2uzIvumYAM83oLzgWRnPLnx14lH+kWoDNaXEpUWDrmLD1A9OgNYmkeT/85qdyN |
|
11 |
jzDbe1yG421e2jUaRXez412Nv1s1ua5U/dBlAgMBAAGjUzBRMB0GA1UdDgQWBBRQ |
|
12 |
Y1+34SVZXriQAFteCRbde5jSOTAfBgNVHSMEGDAWgBRQY1+34SVZXriQAFteCRbd |
|
13 |
e5jSOTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCwVWlzEE7w |
|
14 |
0wBmngfctrz6cSyc5oAqDGo6dzVw+7CYhjRGr0Z9C1zI5rYOPWdDceAqD/lOVapw |
|
15 |
1nawFszVpcClmY8Rt0Tiot8z18NRz0NCFZzbHx9cU5TGZUigOkuXXqB+D52ppuQB |
|
16 |
6t/RREpeZakiBEp0Y8pH9CalA2DCNf/2WGcvaMXQtAL7Ko0a8vPBoOMykwgaFvCj |
|
17 |
E+ZiZXHLLg4jw2QwX1s4Fap8vKEUn062qCx0TdgILX/PrUpmCkPvZRlA/fcQLFQw |
|
18 |
1vX1rInD3cpTrxNw2T9ywGYmbXG1DmpxoF5X4ju3fcTR968QVJlX3L5JMbbWNV9X |
|
19 |
iB4feKp2tzW1 |
|
20 |
-----END CERTIFICATE----- |
tests/saml.key | ||
---|---|---|
1 |
-----BEGIN PRIVATE KEY----- |
|
2 |
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDlVPm7D3abG4fT |
|
3 |
h/i9VJz4s9Lplblw4OKZwJfdFj2CrZX4kehhzDEAcn8cE14gj5bzGCWcoeApw4KI |
|
4 |
FeoSzxZGpGn7dyiFTTny2IJugEuobWcT0IuCP3TcPzWrm0HXnIcZjBv6Rr/cFFa2 |
|
5 |
vAKL1CjT4xgZidRjGKpQBNoT2aOnFuvTzlrzZsGJ38+yRFZDQzoGS+5h4F3ZCHx6 |
|
6 |
Y5fLNzWDW3Cu6/vv2/ev35omcHW+mSTVpds5Oo2uzIvumYAM83oLzgWRnPLnx14l |
|
7 |
H+kWoDNaXEpUWDrmLD1A9OgNYmkeT/85qdyNjzDbe1yG421e2jUaRXez412Nv1s1 |
|
8 |
ua5U/dBlAgMBAAECggEAFb2xkydLIjdA8C/Sx/gujXFWzGgyVV4sfVEB1KbYG/xi |
|
9 |
3FbQxfy6pIU2Qa4gXUvfjpo6bpf2DV+Ij2gsca4KOZY6qelJASIqHTijXOByy7vb |
|
10 |
ash5gVaMuJiRePxWCJ/BOw3KVTbB15ZiBh7ayvDJEhVUYo9rgB2Ff+KF3h3i4uMe |
|
11 |
cF6Z9aA0m9tYmEx88zk8kjxZMH7BhWX3C6H5FcGCy5ARhRQ4EoljDuIWD5+kgpMg |
|
12 |
0hnr2B+pP2AfA1p+Xowf56u17NyEK3QBfdrQp9GXfJaiiSVCqWR3foOqU59LiIjY |
|
13 |
TMp/7DgQqTPmLKifLPcZQcPtjU6dvBCl2NHZuPEBgQKBgQD5bi77KT3+UwePFPwy |
|
14 |
6lXoPAQryWpsQl8JpU3iFLZLx8uMUjtngDvQdU6D7XL5lB0b0gEOlIha2oedQT7F |
|
15 |
rhTm2Qiqj5pnxc8QU4xskSqudzF4WazmXdc2ajssiPWPpZtJj5FZPlw0WF/siFCR |
|
16 |
2gGCUcXwKe9vNv1Zn9N1KVNIBQKBgQDrX0aGY25YqIc6E9z/Y2KYJaH6rjf46p2Q |
|
17 |
RL8m6UiQheuRP2ePa1JLZQrDqXKp2ibZs+IyC1r+6ST7uCEDQjZcpxr+cLTp/ELY |
|
18 |
GtL17FXHjENfZm0JreeijJfUBZLqX0R6GTsi0kT0y8SQZiClpCZw51ovhIHLXrHG |
|
19 |
hWOVpAi04QKBgEshIQ2N0pp0L+atD3nWk6Gr0iXOOTv6kd256MecLXyN5YWSj0oR |
|
20 |
mfKkIs4iC2uZbVsf2immG5wiDo8TQ/EPCkSuQqn9Lyjqr//e6oEZCJ4cUM5LVITe |
|
21 |
5yAAx2oWpsBpxWhW0hTrb6JkrB/2vy3vWF0EfHZmazQ4f/8q4Op9VBRxAoGAKOKL |
|
22 |
5Zwv9saPds8sfFBPOA6RbHIG1v4qEH1glum+6RvaJ4jT/F2wFdifXg15FXgHd5l/ |
|
23 |
mSHP1Ke6/N6nHWHK/50nWztIsbxYACHosz8yR09eBJxOJHhI3Dt/xByTwJJ72pm3 |
|
24 |
Y/0SbVNX+Z1D3oH9C2+kgsyJn0H7r3hMLBoqSQECgYBTMbQ13v2B9U7I1UFryhYR |
|
25 |
ArfaO2VLseLWmEhTO/eSg8LJu9lDNRoagLl41qSRLNabxQzL/1bZTQb0i0S9wCL5 |
|
26 |
tyx2YvLjN7wI0Pkd5WuZeT69ERroAFwASOINmzx96m83TxSe39c+OnxQbX4ujyQI |
|
27 |
iH1921Y1zSQbKsM7CeeFnw== |
|
28 |
-----END PRIVATE KEY----- |
tests/settings.py | ||
---|---|---|
1 | 1 |
import os |
2 | 2 | |
3 |
from authentic2.settings import MIDDLEWARE |
|
4 | ||
3 | 5 |
ALLOWED_HOSTS = ['localhost'] |
4 | 6 | |
5 | 7 |
DATABASES = { |
... | ... | |
15 | 17 |
DATABASES['default'][key[2:]] = os.environ[key] |
16 | 18 | |
17 | 19 |
LANGUAGE_CODE = 'en' |
18 |
A2_FC_CLIENT_ID = '' |
|
19 |
A2_FC_CLIENT_SECRET = '' |
|
20 |
A2_AUTH_SAML_ENABLE = False |
|
21 |
A2_AUTH_FEDICT_ENABLE = True |
|
22 | ||
23 |
MELLON_ADAPTER = ["authentic2_auth_fedict.adapters.AuthenticAdapter"] |
|
24 |
MELLON_LOGIN_URL = "fedict-login" |
|
25 |
MELLON_PUBLIC_KEYS = ["./tests/saml.crt"] |
|
26 |
MELLON_PRIVATE_KEY = "./tests/saml.key" |
|
27 |
MELLON_IDENTITY_PROVIDERS = [ |
|
28 |
{ |
|
29 |
"METADATA": open("./tests/metadata.xml").read(), |
|
30 |
"ENTITY_ID": "https://idp.com/", |
|
31 |
"SLUG": "idp", |
|
32 |
}, |
|
33 |
] |
|
34 | ||
35 |
MELLON_ATTRIBUTE_MAPPING = { |
|
36 |
"last_name": "{attributes[surname][0]}", |
|
37 |
"first_name": "{attri,butes[givenName][0]}", |
|
38 |
} |
|
20 | 39 | |
21 | 40 |
# test hook handlers |
22 | 41 |
A2_HOOKS_PROPAGATE_EXCEPTIONS = True |
tests/test_all.py | ||
---|---|---|
1 |
import datetime |
|
2 | ||
3 |
import lasso |
|
1 | 4 |
import pytest |
5 |
from authentic2.a2_rbac.utils import get_default_ou |
|
6 |
from authentic2.models import Attribute |
|
2 | 7 |
from django.contrib.auth import get_user_model |
8 |
from django.contrib.auth.models import AnonymousUser |
|
9 |
from django.contrib.messages.middleware import MessageMiddleware |
|
10 |
from django.contrib.sessions.middleware import SessionMiddleware |
|
11 |
from django.test.client import RequestFactory |
|
12 |
from mellon.models import Issuer, UserSAMLIdentifier |
|
3 | 13 |
from utils import login |
4 | 14 | |
15 |
from authentic2_auth_fedict.adapters import AuthenticAdapter |
|
16 |
from authentic2_auth_fedict.backends import FedictBackend |
|
17 | ||
5 | 18 |
User = get_user_model() |
6 | 19 | |
7 | 20 |
pytestmark = pytest.mark.django_db |
8 | 21 | |
22 |
factory = RequestFactory() |
|
23 | ||
24 | ||
25 |
@pytest.fixture |
|
26 |
def adapter(): |
|
27 |
return AuthenticAdapter() |
|
28 | ||
29 | ||
30 |
@pytest.fixture |
|
31 |
def idp_conf(): |
|
32 |
return { |
|
33 |
'A2_ATTRIBUTE_MAPPING': [ |
|
34 |
{ |
|
35 |
'attribute': 'email', |
|
36 |
'saml_attribute': 'mail', |
|
37 |
}, |
|
38 |
{ |
|
39 |
'attribute': 'last_name', |
|
40 |
'saml_attribute': 'surname', |
|
41 |
}, |
|
42 |
{ |
|
43 |
'attribute': 'first_name', |
|
44 |
'saml_attribute': 'givenName', |
|
45 |
}, |
|
46 |
{ |
|
47 |
'attribute': 'uuid', |
|
48 |
'saml_attribute': 'urn:be:fedict:iam:attr:fedid', |
|
49 |
}, |
|
50 |
] |
|
51 |
} |
|
52 | ||
53 | ||
54 |
@pytest.fixture |
|
55 |
def issuer(): |
|
56 |
return Issuer.objects.create(entity_id='https://idp.com/', slug='idp') |
|
57 | ||
58 | ||
59 |
@pytest.fixture |
|
60 |
def fedict_attributes(): |
|
61 |
return { |
|
62 |
'issuer': 'https://idp.com/', |
|
63 |
'name_id_content': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
64 |
'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED, |
|
65 |
'surname': ['Doe'], |
|
66 |
'givenName': ['John'], |
|
67 |
'mail': ['john.doe@example.org'], |
|
68 |
'urn:be:fedict:iam:attr:fedid': ['c54db0a8ddc24a02a2d057f857d3b102'], |
|
69 |
} |
|
70 | ||
71 | ||
72 |
def test_custom_lookup_user(adapter, idp_conf, issuer, fedict_attributes): |
|
73 |
assert User.objects.count() == 0 |
|
74 | ||
75 |
user = adapter.lookup_user(idp_conf, fedict_attributes) |
|
76 |
user.refresh_from_db() |
|
77 |
assert user.email == '' # email purposely deleted |
|
78 |
assert user.first_name == '' # not provisionned yet |
|
79 |
assert user.last_name == '' # not provisionned yet |
|
80 |
assert user.username == 'c54db0a8ddc24a02a2d057f857d3b102@saml' # <NameID>@<source> |
|
81 |
assert user.ou == get_default_ou() |
|
82 | ||
83 | ||
84 |
def test_login_fedict_authenticator_displayed(app, settings, issuer): |
|
85 |
response = app.get('/login/') |
|
86 |
assert 'Belgian eID' in response |
|
87 |
assert 'csam-login' in response |
|
88 | ||
89 | ||
90 |
def test_authenticate_eid_provision(app, settings, issuer): |
|
91 |
backend = FedictBackend() |
|
92 |
request = factory.get(path='/accounts/') |
|
93 |
request.user = AnonymousUser() |
|
94 | ||
95 |
saml_attributes = { |
|
96 |
'city': [], |
|
97 |
'zipcode': [], |
|
98 |
'givenName': ['Doe'], |
|
99 |
'surname': ['John'], |
|
100 |
'verified_attributes': [], |
|
101 |
'address': [], |
|
102 |
'last_name': ['Doe'], |
|
103 |
'first_name': ['John'], |
|
104 |
'title': [], |
|
105 |
'username': ['john.doe'], |
|
106 |
'urn:be:fedict:iam:attr:fedid': ['xyz'], # new fedict id |
|
107 |
'is_superuser': ['false'], |
|
108 |
'phone': [], |
|
109 |
'mobile': [], |
|
110 |
'issuer': 'https://idp.com/', |
|
111 |
'nonce': None, |
|
112 |
'force_authn': False, |
|
113 |
'name_id_content': 'xyz', # new fedict id |
|
114 |
'name_id_format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', |
|
115 |
'name_id_name_qualifier': 'https://idp.com/idp/saml2/metadata', |
|
116 |
'authn_instant': datetime.datetime(2022, 1, 27, 11, 26, 7, 301054), |
|
117 |
'session_not_on_or_after': datetime.datetime(2022, 1, 27, 16, 26, 7), |
|
118 |
'session_index': '_401EEEE3C700B6D203B83AA1826E3B80', |
|
119 |
'authn_context_class_ref': 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password', |
|
120 |
'name_id_content_orig': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
121 |
} |
|
122 |
credentials = {'saml_attributes': saml_attributes} |
|
123 | ||
124 |
assert len(User.objects.all()) == 0 |
|
125 |
SessionMiddleware().process_request(request) |
|
126 |
MessageMiddleware().process_request(request) |
|
127 |
backend_user = backend.authenticate(request, **credentials) |
|
128 |
assert backend_user.is_authenticated |
|
129 |
assert backend_user.username == 'xyz@saml' # <NameID>@<source> |
|
130 | ||
131 | ||
132 |
def test_authenticate_eid_link_to_existing_user(app, settings, issuer, user): |
|
133 |
backend = FedictBackend() |
|
134 |
request = factory.get(path='/accounts/') |
|
135 |
request.user = user |
|
136 | ||
137 |
UserSAMLIdentifier.objects.create( |
|
138 |
user=user, |
|
139 |
name_id='c54db0a8ddc24a02a2d057f857d3b102', |
|
140 |
issuer=Issuer.objects.first(), |
|
141 |
) |
|
142 | ||
143 |
saml_attributes = { |
|
144 |
'city': [], |
|
145 |
'zipcode': [], |
|
146 |
'givenName': ['Doe'], |
|
147 |
'surname': ['John'], |
|
148 |
'verified_attributes': [], |
|
149 |
'address': [], |
|
150 |
'last_name': ['Doe'], |
|
151 |
'first_name': ['John'], |
|
152 |
'title': [], |
|
153 |
'username': ['john.doe'], |
|
154 |
'urn:be:fedict:iam:attr:fedid': ['c54db0a8ddc24a02a2d057f857d3b102'], |
|
155 |
'is_superuser': ['false'], |
|
156 |
'phone': [], |
|
157 |
'mobile': [], |
|
158 |
'issuer': 'https://idp.com/', |
|
159 |
'nonce': None, |
|
160 |
'force_authn': False, |
|
161 |
'name_id_content': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
162 |
'name_id_format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', |
|
163 |
'name_id_name_qualifier': 'https://idp.com/idp/saml2/metadata', |
|
164 |
'authn_instant': datetime.datetime(2022, 1, 27, 11, 26, 7, 301054), |
|
165 |
'session_not_on_or_after': datetime.datetime(2022, 1, 27, 16, 26, 7), |
|
166 |
'session_index': '_401EEEE3C700B6D203B83AA1826E3B80', |
|
167 |
'authn_context_class_ref': 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password', |
|
168 |
'name_id_content_orig': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
169 |
} |
|
170 |
credentials = {'saml_attributes': saml_attributes} |
|
171 | ||
172 |
SessionMiddleware().process_request(request) |
|
173 |
MessageMiddleware().process_request(request) |
|
174 |
backend_user = backend.authenticate(request, **credentials) |
|
175 |
assert backend_user == user |
|
176 | ||
177 | ||
178 |
def test_eid_unlink(app, settings, issuer, user): |
|
179 |
assert len(UserSAMLIdentifier.objects.all()) == 0 |
|
180 | ||
181 |
# create link |
|
182 |
UserSAMLIdentifier.objects.create( |
|
183 |
user=user, |
|
184 |
name_id='abc', |
|
185 |
issuer=Issuer.objects.first(), |
|
186 |
) |
|
187 | ||
188 |
response = login(app, user, path='/accounts/', password=user.username) |
|
189 |
assert "Unlink my account" in response.text |
|
190 |
app.get('/accounts/fedict/unlink/').follow() |
|
191 | ||
192 |
response = app.get('/accounts/') |
|
193 |
assert "Link my account to my eID card" in response.text |
|
194 | ||
195 | ||
196 |
def test_provision_new_attributes_verified(app, settings, issuer, user): |
|
197 |
Attribute.objects.create(name='title', kind='title', label='Titre') |
|
198 |
# email & title verified |
|
199 |
user.email = 'john.doe@verified.publik.love' |
|
200 |
user.email_verified = True |
|
201 |
user.verified_attributes.title = 'Mr' |
|
202 |
user.first_name = 'Johnny' |
|
203 |
user.last_name = 'Smith' |
|
204 |
user.save() |
|
205 |
UserSAMLIdentifier.objects.create( |
|
206 |
user=user, |
|
207 |
name_id='c54db0a8ddc24a02a2d057f857d3b102', |
|
208 |
issuer=Issuer.objects.first(), |
|
209 |
) |
|
210 | ||
211 |
backend = FedictBackend() |
|
212 |
request = factory.get(path='/accounts/') |
|
213 |
request_user = User.objects.create( |
|
214 |
first_name='Foo', |
|
215 |
last_name='Bar', |
|
216 |
email='foo.bar@nowhere.null', |
|
217 |
) |
|
218 |
request.user = request_user |
|
219 |
saml_attributes = { |
|
220 |
'givenName': ['Doe'], |
|
221 |
'surname': ['John'], |
|
222 |
'last_name': ['Doe'], |
|
223 |
'first_name': ['John'], |
|
224 |
'username': ['john.doe'], |
|
225 |
'urn:be:fedict:iam:attr:fedid': ['c54db0a8ddc24a02a2d057f857d3b102'], |
|
226 |
'is_superuser': ['false'], |
|
227 |
'issuer': 'https://idp.com/', |
|
228 |
'name_id_content': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
229 |
'name_id_format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', |
|
230 |
'name_id_name_qualifier': 'https://idp.com/idp/saml2/metadata', |
|
231 |
'name_id_content_orig': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
232 |
} |
|
233 |
credentials = {'saml_attributes': saml_attributes} |
|
234 |
SessionMiddleware().process_request(request) |
|
235 |
MessageMiddleware().process_request(request) |
|
236 |
backend_user = backend.authenticate(request, **credentials) |
|
237 |
assert backend_user == request_user |
|
238 |
assert backend_user.email == 'john.doe@verified.publik.love' |
|
239 |
assert backend_user.email_verified == True |
|
240 |
assert backend_user.verified_attributes.title == 'Mr' |
|
241 |
assert backend_user.first_name == 'Johnny' |
|
242 |
assert backend_user.last_name == 'Smith' |
|
243 | ||
244 | ||
245 |
def test_provision_old_account_deleted(app, settings, issuer, user): |
|
246 |
backend = FedictBackend() |
|
247 |
request = factory.get(path='/accounts/') |
|
248 |
request_user = User.objects.create( |
|
249 |
first_name='foo', |
|
250 |
last_name='bar', |
|
251 |
email='foo.bar@nowhere.null', |
|
252 |
) |
|
253 |
request.user = request_user |
|
254 |
count = User.objects.count() |
|
255 |
user_uuid = user.uuid |
|
256 |
UserSAMLIdentifier.objects.create( |
|
257 |
user=user, |
|
258 |
name_id='c54db0a8ddc24a02a2d057f857d3b102', |
|
259 |
issuer=Issuer.objects.first(), |
|
260 |
) |
|
261 |
saml_attributes = { |
|
262 |
'givenName': ['Doe'], |
|
263 |
'surname': ['John'], |
|
264 |
'last_name': ['Doe'], |
|
265 |
'first_name': ['John'], |
|
266 |
'username': ['john.doe'], |
|
267 |
'urn:be:fedict:iam:attr:fedid': ['c54db0a8ddc24a02a2d057f857d3b102'], |
|
268 |
'is_superuser': ['false'], |
|
269 |
'issuer': 'https://idp.com/', |
|
270 |
'name_id_content': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
271 |
'name_id_format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', |
|
272 |
'name_id_name_qualifier': 'https://idp.com/idp/saml2/metadata', |
|
273 |
'name_id_content_orig': 'c54db0a8ddc24a02a2d057f857d3b102', |
|
274 |
} |
|
275 |
credentials = {'saml_attributes': saml_attributes} |
|
9 | 276 | |
10 |
def test_dummy(app): |
|
11 |
assert 1 |
|
277 |
SessionMiddleware().process_request(request) |
|
278 |
MessageMiddleware().process_request(request) |
|
279 |
backend_user = backend.authenticate(request, **credentials) |
|
280 |
# link has been created on currently logged-in user |
|
281 |
assert backend_user == request_user |
|
282 |
# previous user as been deactivated |
|
283 |
assert User.objects.count() == count - 1 |
|
284 |
assert not User.objects.filter(uuid=user_uuid) |
tests/utils.py | ||
---|---|---|
9 | 9 |
else: |
10 | 10 |
login_page = app.get(reverse('auth_login')) |
11 | 11 |
assert login_page.request.path == reverse('auth_login') |
12 |
form = login_page.form |
|
12 |
form = login_page.forms[0]
|
|
13 | 13 |
form.set('username', user.username if hasattr(user, 'username') else user) |
14 | 14 |
# password is supposed to be the same as username |
15 | 15 |
form.set('password', password or user.username) |
16 |
- |