From 4c26df8f3dc344cf243ac366524f794ccb29384b Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 14 Feb 2019 12:17:10 +0100 Subject: [PATCH 6/6] support ou selector in backends and forms (fixes #30252) --- src/authentic2/backends/ldap_backend.py | 8 +++- src/authentic2/backends/models_backend.py | 11 +++-- src/authentic2/forms/__init__.py | 23 ++++++++- tests/test_ldap.py | 57 +++++++++++++++++++++++ tests/test_login.py | 14 +++++- 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/authentic2/backends/ldap_backend.py b/src/authentic2/backends/ldap_backend.py index 91852a1a..0410fef0 100644 --- a/src/authentic2/backends/ldap_backend.py +++ b/src/authentic2/backends/ldap_backend.py @@ -346,7 +346,7 @@ class LDAPBackend(object): log.debug('got config %r', blocks) return blocks - def authenticate(self, username=None, password=None, realm=None): + def authenticate(self, username=None, password=None, realm=None, ou=None, request=None): if username is None or password is None: return None @@ -357,9 +357,15 @@ class LDAPBackend(object): if not ldap: raise ImproperlyConfigured('ldap is not available') + default_ou_slug = get_default_ou().slug + # Now we can try to authenticate for block in config: uid = username + # if ou is provided, ignore LDAP server for other OU + if ou: + if ou.slug != (block.get('ou_slug') or default_ou_slug): + continue if block['limit_to_realm']: if realm is None and '@' in username: uid, realm = username.rsplit('@', 1) diff --git a/src/authentic2/backends/models_backend.py b/src/authentic2/backends/models_backend.py index 1995e59c..2d0d417c 100644 --- a/src/authentic2/backends/models_backend.py +++ b/src/authentic2/backends/models_backend.py @@ -39,7 +39,7 @@ class ModelBackend(ModelBackend): Authenticates against settings.AUTH_USER_MODEL. """ - def get_query(self, username, realm): + def get_query(self, username, realm=None, ou=None): UserModel = get_user_model() username_field = 'username' queries = [] @@ -59,19 +59,22 @@ class ModelBackend(ModelBackend): **{username_field: upn(username, realm)})) else: queries.append(models.Q(**{username_field: upn(username, realm)})) - return six.moves.reduce(models.Q.__or__, queries) + queries = six.moves.reduce(models.Q.__or__, queries) + if ou: + queries &= models.Q(ou=ou) + return queries def must_reset_password(self, user): from .. import models return bool(models.PasswordReset.filter(user=user).count()) - def authenticate(self, username=None, password=None, realm=None, **kwargs): + def authenticate(self, username=None, password=None, realm=None, ou=None, **kwargs): UserModel = get_user_model() if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) if not username: return - query = self.get_query(username, realm) + query = self.get_query(username=username, realm=realm, ou=ou) users = get_user_queryset().filter(query) # order by username to make username without realm come before usernames with realms # i.e. "toto" should come before "toto@example.com" diff --git a/src/authentic2/forms/__init__.py b/src/authentic2/forms/__init__.py index 08157373..459e4181 100644 --- a/src/authentic2/forms/__init__.py +++ b/src/authentic2/forms/__init__.py @@ -22,6 +22,8 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import REDIRECT_FIELD_NAME, forms as auth_forms from django.utils import html +from django.contrib.auth import authenticate + from django_rbac.utils import get_ou_model from authentic2.utils import lazy_label @@ -230,7 +232,7 @@ class AuthenticationForm(auth_forms.AuthenticationForm): raise forms.ValidationError(msg) try: - super(AuthenticationForm, self).clean() + self.clean_authenticate() except Exception: if keys: self.exponential_backoff.failure(*keys) @@ -240,6 +242,25 @@ class AuthenticationForm(auth_forms.AuthenticationForm): self.exponential_backoff.success(*keys) return self.cleaned_data + def clean_authenticate(self): + # copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector + username = self.cleaned_data.get('username') + password = self.cleaned_data.get('password') + ou = self.cleaned_data.get('ou') + + if username is not None and password: + self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request) + if self.user_cache is None: + raise forms.ValidationError( + self.error_messages['invalid_login'], + code='invalid_login', + params={'username': self.username_field.verbose_name}, + ) + else: + self.confirm_login_allowed(self.user_cache) + + return self.cleaned_data + @property def media(self): media = super(AuthenticationForm, self).media diff --git a/tests/test_ldap.py b/tests/test_ldap.py index 738bc26c..fe460093 100644 --- a/tests/test_ldap.py +++ b/tests/test_ldap.py @@ -700,3 +700,60 @@ def test_user_attributes(slapd, settings, client, db): user = User.objects.get(username=username) assert user.attributes.locality == u'locality%s' % i client.session.flush() + + +def test_ou_selector(slapd, settings, app, ou1): + settings.LDAP_AUTH_SETTINGS = [{ + 'url': [slapd.ldap_url], + 'binddn': force_text(DN), + 'bindpw': PASS, + 'basedn': u'o=ôrga', + 'ou_slug': ou1.slug, + 'use_tls': False, + }] + settings.A2_LOGIN_FORM_OU_SELECTOR = True + + # Check login to the wrong ou does not work + response = app.get('/login/') + response.form.set('username', USERNAME) + response.form.set('password', PASS) + response.form.set('ou', str(get_default_ou().pk)) + response = response.form.submit(name='login-password-submit') + assert response.pyquery('.errorlist.nonfield') + assert '_auth_user_id' not in app.session + + # Check login to the proper ou works + response = app.get('/login/') + response.form.set('username', USERNAME) + response.form.set('password', PASS) + response.form.set('ou', str(ou1.pk)) + response = response.form.submit(name='login-password-submit').follow() + assert '_auth_user_id' in app.session + + +def test_ou_selector_default_ou(slapd, settings, app, ou1): + settings.LDAP_AUTH_SETTINGS = [{ + 'url': [slapd.ldap_url], + 'binddn': force_text(DN), + 'bindpw': PASS, + 'basedn': u'o=ôrga', + 'use_tls': False, + }] + settings.A2_LOGIN_FORM_OU_SELECTOR = True + + # Check login to the wrong ou does not work + response = app.get('/login/') + response.form.set('username', USERNAME) + response.form.set('password', PASS) + response.form.set('ou', str(ou1.pk)) + response = response.form.submit(name='login-password-submit') + assert response.pyquery('.errorlist.nonfield') + assert '_auth_user_id' not in app.session + + # Check login to the proper ou works + response = app.get('/login/') + response.form.set('username', USERNAME) + response.form.set('password', PASS) + response.form.set('ou', str(get_default_ou().pk)) + response = response.form.submit(name='login-password-submit').follow() + assert '_auth_user_id' in app.session diff --git a/tests/test_login.py b/tests/test_login.py index 3d40b7a9..c5a22e5e 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -135,7 +135,7 @@ def test_session_remember_me_nok(app, settings, simple_user, freezer): assert simple_user.first_name not in response -def test_ou_selector(app, settings, simple_user): +def test_ou_selector(app, settings, simple_user, ou1): settings.A2_LOGIN_FORM_OU_SELECTOR = True response = app.get('/login/') # Check selector is here and there are no errors @@ -148,3 +148,15 @@ def test_ou_selector(app, settings, simple_user): response.form.set('password', simple_user.username) response = response.form.submit(name='login-password-submit') assert response.pyquery('.errorlist') + # Check login to the wrong ou do not work + response.form.set('password', simple_user.username) + response.form.set('ou', str(ou1.pk)) + response = response.form.submit(name='login-password-submit') + assert not response.pyquery('.errorlist:not(.nonfield)') + assert response.pyquery('.errorlist.nonfield') + assert '_auth_user_id' not in app.session + # Check login to the proper ou works + response.form.set('password', simple_user.username) + response.form.set('ou', str(simple_user.ou.pk)) + response = response.form.submit(name='login-password-submit').follow() + assert '_auth_user_id' in app.session -- 2.20.1