0001-backends-add-SAML-LDAP-authentication-backend-30125.patch
src/authentic2/backends/ldap_backend.py | ||
---|---|---|
316 | 316 |
'connect_with_user_credentials': True, |
317 | 317 |
# can reset password |
318 | 318 |
'can_reset_password': False, |
319 |
# entity id of the IDP based on the same LDAP |
|
320 |
'entity_id': '' |
|
319 | 321 |
} |
320 | 322 |
_REQUIRED = ('url', 'basedn') |
321 | 323 |
_TO_ITERABLE = ('url', 'groupsu', 'groupstaff', 'groupactive') |
... | ... | |
947 | 949 |
yield user_dn, user |
948 | 950 | |
949 | 951 |
@classmethod |
950 |
def get_users(cls):
|
|
952 |
def get_block_users(cls, block, user_filter=None):
|
|
951 | 953 |
logger = logging.getLogger(__name__) |
954 |
conn = cls.get_connection(block) |
|
955 |
if conn is None: |
|
956 |
logger.warning(u'unable to synchronize with LDAP servers %r', block['url']) |
|
957 |
return |
|
958 |
user_basedn = block.get('user_basedn') or block['basedn'] |
|
959 |
user_filter = user_filter or block['sync_ldap_users_filter'] or block['user_filter'] |
|
960 |
user_filter = user_filter.replace('%s', '*') |
|
961 |
attrs = cls.get_ldap_attributes_names(block) |
|
962 |
return cls.paged_search(conn, user_basedn, ldap.SCOPE_SUBTREE, user_filter, |
|
963 |
attrlist=attrs) |
|
964 | ||
965 |
@classmethod |
|
966 |
def get_users(cls): |
|
952 | 967 |
for block in cls.get_config(): |
953 |
conn = cls.get_connection(block) |
|
954 |
if conn is None: |
|
955 |
logger.warning(u'unable to synchronize with LDAP servers %r', block['url']) |
|
956 |
continue |
|
957 |
user_basedn = block.get('user_basedn') or block['basedn'] |
|
958 |
user_filter = block['sync_ldap_users_filter'] or block['user_filter'] |
|
959 |
user_filter = user_filter.replace('%s', '*') |
|
960 |
attrs = cls.get_ldap_attributes_names(block) |
|
961 |
users = cls.paged_search(conn, user_basedn, ldap.SCOPE_SUBTREE, user_filter, |
|
962 |
attrlist=attrs) |
|
963 |
backend = cls() |
|
964 |
for user_dn, data in users: |
|
968 |
for user_dn, data in cls.get_block_users(block): |
|
965 | 969 |
# ignore referrals |
966 | 970 |
if not user_dn: |
967 | 971 |
continue |
968 | 972 |
data = cls.normalize_ldap_results(data) |
969 | 973 |
data['dn'] = user_dn |
974 |
conn = cls.get_connection(block) |
|
975 |
backend = cls() |
|
970 | 976 |
yield backend._return_user(user_dn, None, conn, block, data) |
971 | 977 | |
972 | 978 |
@classmethod |
src/authentic2_auth_saml/__init__.py | ||
---|---|---|
7 | 7 |
return ['mellon', __name__] |
8 | 8 | |
9 | 9 |
def get_authentication_backends(self): |
10 |
return ['authentic2_auth_saml.backends.SAMLBackend'] |
|
10 |
return ['authentic2_auth_saml.backends.SAMLLdapBackend', |
|
11 |
'authentic2_auth_saml.backends.SAMLBackend'] |
|
11 | 12 | |
12 | 13 |
def get_auth_frontends(self): |
13 | 14 |
return ['authentic2_auth_saml.auth_frontends.SAMLFrontend'] |
src/authentic2_auth_saml/app_ldap_saml_settings.py | ||
---|---|---|
1 | ||
2 |
class AppSettings(object): |
|
3 |
'''Thanks django-allauth''' |
|
4 |
__SENTINEL = object() |
|
5 | ||
6 |
def __init__(self, prefix): |
|
7 |
self.prefix = prefix |
|
8 | ||
9 |
def _setting(self, name, dflt=__SENTINEL): |
|
10 |
from django.conf import settings |
|
11 |
from django.core.exceptions import ImproperlyConfigured |
|
12 | ||
13 |
v = getattr(settings, self.prefix + name, dflt) |
|
14 |
if v is self.__SENTINEL: |
|
15 |
raise ImproperlyConfigured('Missing setting %r' % (self.prefix + name)) |
|
16 |
return v |
|
17 | ||
18 |
@property |
|
19 |
def enable(self): |
|
20 |
return self._setting('ENABLE', False) |
|
21 | ||
22 |
@property |
|
23 |
def enable_condition(self): |
|
24 |
return self._setting('ENABLE_CONDITION', None) |
|
25 | ||
26 | ||
27 |
import sys |
|
28 | ||
29 |
app_settings = AppSettings('A2_AUTH_SAML_LDAP') |
|
30 |
app_settings.__name__ = __name__ |
|
31 |
sys.modules[__name__] = app_settings |
src/authentic2_auth_saml/app_settings.py | ||
---|---|---|
19 | 19 |
def enable(self): |
20 | 20 |
return self._setting('ENABLE', False) |
21 | 21 | |
22 |
@property |
|
23 |
def ldap_enable(self): |
|
24 |
return self._setting('LDAP_ENABLE', False) |
|
25 | ||
22 | 26 | |
23 | 27 |
import sys |
24 | 28 |
src/authentic2_auth_saml/backends.py | ||
---|---|---|
1 |
import re |
|
2 | ||
3 |
from django.conf import settings |
|
4 | ||
1 | 5 |
from mellon.backends import SAMLBackend |
2 | 6 | |
3 | 7 |
from authentic2.middleware import StoreRequestMiddleware |
8 |
from authentic2.backends.ldap_backend import LDAPBackend |
|
4 | 9 | |
5 | 10 |
from . import app_settings |
6 | 11 | |
... | ... | |
22 | 27 | |
23 | 28 |
import lasso |
24 | 29 |
return lasso.SAML2_AUTHN_CONTEXT_PREVIOUS_SESSION |
30 | ||
31 | ||
32 |
class SAMLLdapBackend(SAMLBackend): |
|
33 | ||
34 |
def authenticate(self, saml_attributes, **credentials): |
|
35 |
if not app_settings.ldap_enable: |
|
36 |
return None |
|
37 |
if not getattr(settings, 'LDAP_AUTH_SETTINGS', []): |
|
38 |
return None |
|
39 |
no_ldap_for_entity_id = True |
|
40 |
for block in settings.LDAP_AUTH_SETTINGS: |
|
41 |
if block.get('entity_id', '') == saml_attributes['issuer']: |
|
42 |
no_ldap_for_entity_id = False |
|
43 |
break |
|
44 |
if no_ldap_for_entity_id: |
|
45 |
return None |
|
46 | ||
47 |
user_filter = block['sync_ldap_users_filter'] or block['user_filter'] |
|
48 |
args = [] |
|
49 |
# get user filter attribute names |
|
50 |
for group in re.findall('\w+=%\w+', user_filter): |
|
51 |
filter_name, filter_value = group.split('=') |
|
52 |
args.append(saml_attributes[filter_name][0]) |
|
53 |
user_filter = user_filter % tuple(args) |
|
54 | ||
55 |
users = list(LDAPBackend.get_block_users(block, user_filter)) |
|
56 |
if users: |
|
57 |
user_dn, data = users[0] |
|
58 |
conn = LDAPBackend.get_connection(block) |
|
59 |
backend = LDAPBackend() |
|
60 |
data = backend.normalize_ldap_results(data) |
|
61 |
data['dn'] = user_dn |
|
62 |
return backend._return_user(user_dn, None, conn, block, data) |
|
63 |
else: |
|
64 |
return None |
tests/test_auth_saml.py | ||
---|---|---|
1 | 1 |
import pytest |
2 |
import mock |
|
3 |
from pprint import pprint |
|
2 | 4 | |
3 | 5 |
from django.contrib.auth import get_user_model |
6 |
from django.utils.timezone import datetime |
|
7 |
from django.test.utils import override_settings |
|
8 | ||
4 | 9 |
from authentic2.models import Attribute |
5 | 10 | |
6 | 11 |
pytestmark = pytest.mark.django_db |
... | ... | |
38 | 43 |
del saml_attributes['mail'] |
39 | 44 |
with pytest.raises(ValueError): |
40 | 45 |
adapter.finish_create_user(idp, saml_attributes, user) |
46 | ||
47 | ||
48 |
@mock.patch('authentic2.backends.ldap_backend.LDAPBackend.get_block_users') |
|
49 |
@mock.patch('authentic2.backends.ldap_backend.LDAPBackend.get_connection') |
|
50 |
@mock.patch('authentic2.backends.ldap_backend.LDAPBackend._return_user') |
|
51 |
def test_disabled_saml_ldap_backend(ldap_return_user, ldap_connection, ldap_block_users): |
|
52 |
from authentic2_auth_saml.backends import SAMLLdapBackend |
|
53 |
backend = SAMLLdapBackend() |
|
54 |
saml_attributes = {} |
|
55 |
assert backend.authenticate(saml_attributes) == None |
|
56 |
with override_settings(A2_AUTH_SAML_LDAP_ENABLE=True, LDAP_AUTH_SETTINGS=[]): |
|
57 |
assert backend.authenticate(saml_attributes) == None |
|
58 | ||
59 | ||
60 |
LDAP_SETTINGS = { |
|
61 |
"realm": "example.com", |
|
62 |
"url": "ldaps://ldap.example.com/", |
|
63 |
"basedn": "ou=people,o=example,o=com", |
|
64 |
"user_filter": "(|(mail=%s)(uid=%s))", |
|
65 |
"sync_ldap_users_filter": "", |
|
66 |
"username_template": "{uid[0]}", |
|
67 |
"attributes": [ "uid" ], |
|
68 |
"set_mandatory_groups": ["LDAP Entrouvert"], |
|
69 |
"timeout": 3, |
|
70 |
"use_tls": False, |
|
71 |
'entity_id': 'https://some-idp.com/idp/saml2/metadata', |
|
72 |
'global_ldap_options': {}, |
|
73 |
} |
|
74 | ||
75 |
saml_attributes = {'username': [u'foo'], 'first_name': [u'Foo'], |
|
76 |
'last_name': [u'Bar'], 'uid': [u'foobar'], |
|
77 |
'name_id_name_qualifier': u'https://some-idp.com/idp/saml2/metadata', |
|
78 |
'authn_instant': datetime(2019, 1, 30, 11, 12, 40), |
|
79 |
'name_id_format': u'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', |
|
80 |
'name_id_content': u'9f160fa0eb6045e5a5257a34fb4651e0', |
|
81 |
'mail': [u'foo@example.com'], |
|
82 |
'issuer': 'https://some-idp.com/idp/saml2/metadata' |
|
83 |
} |
|
84 | ||
85 |
with override_settings(A2_AUTH_SAML_LDAP_ENABLE=True, LDAP_AUTH_SETTINGS=[LDAP_SETTINGS]): |
|
86 |
ldap_block_users.return_value = [] |
|
87 |
backend.authenticate(saml_attributes) |
|
88 |
assert ldap_block_users.call_args[0][1] == '(|(mail=foo@example.com)(uid=foobar))' |
|
89 |
ldap_connection.assert_not_called() |
|
90 |
user_dn = 'uid=foobar,ou=people,o=example,o=com' |
|
91 |
user_data = {'mail': ['foo@example.com'], 'givenName': ['Foo'], |
|
92 |
'uid': ['foobar'], 'sn': ['Bar']} |
|
93 |
ldap_block_users.return_value = [(user_dn, user_data)] |
|
94 |
ldap_return_user.return_value = None |
|
95 | ||
96 |
backend.authenticate(saml_attributes) |
|
97 |
assert ldap_block_users.call_args[0][1] == '(|(mail=foo@example.com)(uid=foobar))' |
|
98 |
assert ldap_connection.call_args[0][0] == LDAP_SETTINGS |
|
99 |
assert ldap_return_user.call_args[0][0] == user_dn |
|
100 |
assert ldap_return_user.call_args[0][3] == LDAP_SETTINGS |
|
101 |
data = ldap_return_user.call_args[0][4] |
|
102 |
assert data['dn'] == user_dn |
|
103 |
assert data['mail'] == user_data['mail'] |
|
104 |
assert data['givenname'] == user_data['givenName'] |
|
105 |
assert data['uid'] == user_data['uid'] |
|
106 |
assert data['sn'] == user_data['sn'] |
|
41 |
- |