Projet

Général

Profil

0001-backends-add-SAML-LDAP-authentication-backend-30125.patch

Serghei Mihai (congés, retour 15/05), 30 janvier 2019 17:25

Télécharger (10,8 ko)

Voir les différences:

Subject: [PATCH] backends: add SAML LDAP authentication backend (#30125)

 src/authentic2/backends/ldap_backend.py       | 32 +++++----
 src/authentic2_auth_saml/__init__.py          |  3 +-
 .../app_ldap_saml_settings.py                 | 31 +++++++++
 src/authentic2_auth_saml/app_settings.py      |  4 ++
 src/authentic2_auth_saml/backends.py          | 40 +++++++++++
 tests/test_auth_saml.py                       | 66 +++++++++++++++++++
 6 files changed, 162 insertions(+), 14 deletions(-)
 create mode 100644 src/authentic2_auth_saml/app_ldap_saml_settings.py
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
-