mirror of https://github.com/zulip/zulip.git
ldap: Use email search in django_to_ldap_username.
With this, django_to_ldap_username can take an email and find the ldap username of the ldap user who has this email - if email search is configured. This allows successful authenticate() with ldap email and ldap password, instead of ldap username. This is especially useful because when a user wants to fetch their api key, the server attempts authenticate with user_profile.email - and this used to fail if the user was an ldap user (because the ldap username was required to authenticate succesfully). See issue #9277.
This commit is contained in:
parent
fea4d0b2be
commit
3699fe28f8
|
@ -3,7 +3,7 @@ from django.conf import settings
|
|||
from django.core import mail
|
||||
from django.http import HttpResponse
|
||||
from django.test import override_settings
|
||||
from django_auth_ldap.backend import LDAPBackend, LDAPSearch, _LDAPUser
|
||||
from django_auth_ldap.backend import LDAPBackend, LDAPSearch
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.timezone import now as timezone_now
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
|
@ -265,42 +265,35 @@ class AuthBackendTest(ZulipTestCase):
|
|||
result = self.client_get('/register/')
|
||||
self.assert_not_in_success_response(["No authentication backends are enabled"], result)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
||||
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',),
|
||||
LDAP_EMAIL_ATTR="mail")
|
||||
def test_ldap_backend(self) -> None:
|
||||
self.init_default_ldap_database()
|
||||
user_profile = self.example_user('hamlet')
|
||||
email = user_profile.email
|
||||
password = "test_password"
|
||||
password = "testing"
|
||||
self.setup_subdomain(user_profile)
|
||||
|
||||
username = self.get_username()
|
||||
backend = ZulipLDAPAuthBackend()
|
||||
|
||||
# Test LDAP auth fails when LDAP server rejects password
|
||||
with mock.patch('django_auth_ldap.backend._LDAPUser._authenticate_user_dn',
|
||||
side_effect=_LDAPUser.AuthenticationFailed("Failed")), (
|
||||
mock.patch('django_auth_ldap.backend._LDAPUser._check_requirements')), (
|
||||
mock.patch('django_auth_ldap.backend._LDAPUser.attrs',
|
||||
return_value=dict(full_name=['Hamlet']))):
|
||||
self.assertIsNone(backend.authenticate(username=email, password=password, realm=get_realm("zulip")))
|
||||
self.assertIsNone(backend.authenticate(username=email, password="wrongpass", realm=get_realm("zulip")))
|
||||
|
||||
with mock.patch('django_auth_ldap.backend._LDAPUser._authenticate_user_dn'), (
|
||||
mock.patch('django_auth_ldap.backend._LDAPUser._check_requirements')), (
|
||||
mock.patch('django_auth_ldap.backend._LDAPUser.attrs',
|
||||
return_value=dict(full_name=['Hamlet']))):
|
||||
self.verify_backend(backend,
|
||||
bad_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zephyr')),
|
||||
good_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zulip')))
|
||||
self.verify_backend(backend,
|
||||
bad_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zephyr')),
|
||||
good_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zulip')))
|
||||
self.verify_backend(backend,
|
||||
bad_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zephyr')),
|
||||
good_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zulip')))
|
||||
self.verify_backend(backend,
|
||||
bad_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zephyr')),
|
||||
good_kwargs=dict(username=username,
|
||||
password=password,
|
||||
realm=get_realm('zulip')))
|
||||
|
||||
def test_devauth_backend(self) -> None:
|
||||
self.verify_backend(DevAuthBackend(),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import ldap
|
||||
import random
|
||||
import re
|
||||
import ujson
|
||||
|
@ -5,6 +6,7 @@ import ujson
|
|||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.test import override_settings
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
from email.utils import formataddr
|
||||
from mock import patch, MagicMock
|
||||
from typing import List, Optional
|
||||
|
@ -47,7 +49,11 @@ class TestFollowupEmails(ZulipTestCase):
|
|||
# See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#ldap-including-active-directory
|
||||
# for case details.
|
||||
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',
|
||||
'zproject.backends.ZulipDummyBackend'))
|
||||
'zproject.backends.ZulipDummyBackend'),
|
||||
# configure email search for email address in the uid attribute:
|
||||
AUTH_LDAP_REVERSE_EMAIL_SEARCH=LDAPSearch("ou=users,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(uid=%(email)s)"))
|
||||
def test_day1_email_ldap_case_a_login_credentials(self) -> None:
|
||||
self.init_default_ldap_database()
|
||||
ldap_user_attr_map = {'full_name': 'cn', 'short_name': 'sn'}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import authenticate
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.models import get_realm
|
||||
from zproject.backends import ZulipLDAPAuthBackend, ZulipLDAPExceptionOutsideDomain
|
||||
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
|
||||
import ldap
|
||||
import mock
|
||||
|
||||
"""
|
||||
This is a file for additional LDAP tests, that don't belong
|
||||
anywhere else.
|
||||
"""
|
||||
|
||||
class DjangoToLDAPUsernameTests(ZulipTestCase):
|
||||
def setUp(self) -> None:
|
||||
self.init_default_ldap_database()
|
||||
self.backend = ZulipLDAPAuthBackend()
|
||||
|
||||
def test_django_to_ldap_username_with_append_domain(self) -> None:
|
||||
with self.settings(LDAP_APPEND_DOMAIN="zulip.com"):
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet"), "hamlet")
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet@zulip.com"), "hamlet")
|
||||
with self.assertRaises(ZulipLDAPExceptionOutsideDomain):
|
||||
self.backend.django_to_ldap_username("hamlet@example.com")
|
||||
|
||||
def test_django_to_ldap_username_without_email_search(self) -> None:
|
||||
with self.settings(AUTH_LDAP_REVERSE_EMAIL_SEARCH=None):
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet"), "hamlet")
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet@zulip.com"), "hamlet@zulip.com")
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet@example.com"), "hamlet@example.com")
|
||||
|
||||
def test_django_to_ldap_username_with_email_search(self) -> None:
|
||||
with self.settings():
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet"), "hamlet")
|
||||
self.assertEqual(self.backend.django_to_ldap_username("hamlet@zulip.com"), "hamlet")
|
||||
# If there are no matches through the email search, return the email unchanged:
|
||||
self.assertEqual(self.backend.django_to_ldap_username("no_such_email@example.com"),
|
||||
"no_such_email@example.com")
|
||||
# aaron has uid=letham in our test directory:
|
||||
self.assertEqual(self.backend.django_to_ldap_username("aaron@zulip.com"), "letham")
|
||||
|
||||
with mock.patch("zproject.backends.logging.warning") as mock_warn:
|
||||
self.assertEqual(
|
||||
self.backend.django_to_ldap_username("shared_email@zulip.com"),
|
||||
"shared_email@zulip.com"
|
||||
)
|
||||
mock_warn.assert_called_with("Multiple users with email shared_email@zulip.com found in LDAP.")
|
||||
|
||||
# Configure email search for emails in the uid attribute:
|
||||
with self.settings(AUTH_LDAP_REVERSE_EMAIL_SEARCH=LDAPSearch("ou=users,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(uid=%(email)s)")):
|
||||
self.assertEqual(self.backend.django_to_ldap_username("newuser_email_as_uid@zulip.com"),
|
||||
"newuser_email_as_uid@zulip.com")
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
|
||||
'zproject.backends.ZulipLDAPAuthBackend',))
|
||||
def test_authenticate_to_ldap_via_email(self) -> None:
|
||||
"""
|
||||
With AUTH_LDAP_REVERSE_EMAIL_SEARCH configured, django_to_ldap_username
|
||||
should be able to translate an email to ldap username,
|
||||
and thus it should be possible to authenticate through user_profile.email.
|
||||
"""
|
||||
realm = get_realm("zulip")
|
||||
user_profile = self.example_user("hamlet")
|
||||
password = "testpassword"
|
||||
user_profile.set_password(password)
|
||||
user_profile.save()
|
||||
|
||||
# Without email search, can't login via ldap:
|
||||
with self.settings(AUTH_LDAP_REVERSE_EMAIL_SEARCH=None, LDAP_EMAIL_ATTR='mail'):
|
||||
# Using hamlet's ldap password fails without email search:
|
||||
self.assertEqual(authenticate(username=user_profile.email, password="testing", realm=realm),
|
||||
None)
|
||||
# Need hamlet's zulip password to login (via email backend)
|
||||
self.assertEqual(authenticate(username=user_profile.email, password="testpassword", realm=realm),
|
||||
user_profile)
|
||||
# To login via ldap, username needs to be the ldap username, not email:
|
||||
self.assertEqual(authenticate(username="hamlet", password="testing", realm=realm),
|
||||
user_profile)
|
||||
|
||||
# With email search:
|
||||
with self.settings(LDAP_EMAIL_ATTR='mail'):
|
||||
# Ldap password works now:
|
||||
self.assertEqual(authenticate(username=user_profile.email, password="testing", realm=realm),
|
||||
user_profile)
|
|
@ -328,6 +328,24 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend):
|
|||
raise ZulipLDAPExceptionOutsideDomain("Email %s does not match LDAP domain %s." % (
|
||||
username, settings.LDAP_APPEND_DOMAIN))
|
||||
return email_to_username(username)
|
||||
else:
|
||||
return username
|
||||
|
||||
if settings.AUTH_LDAP_USERNAME_ATTR and settings.AUTH_LDAP_REVERSE_EMAIL_SEARCH:
|
||||
# We can use find_ldap_users_by_email
|
||||
if is_valid_email(username):
|
||||
result = find_ldap_users_by_email(username)
|
||||
if result is None:
|
||||
return username
|
||||
if len(result) == 1:
|
||||
return result[0]._username
|
||||
if len(result) > 1:
|
||||
# This is possible, but strange, so worth logging a warning about.
|
||||
# We can't translate the email to a unique username,
|
||||
# so we don't do anything else here.
|
||||
logging.warning("Multiple users with email {} found in LDAP.".format(username))
|
||||
return username
|
||||
|
||||
return username
|
||||
|
||||
def ldap_to_django_username(self, username: str) -> str:
|
||||
|
@ -492,6 +510,12 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase):
|
|||
return None
|
||||
|
||||
try:
|
||||
# We want to apss the user's LDAP username into
|
||||
# authenticate() below. If an email address was entered
|
||||
# in the login form, we need to use
|
||||
# django_to_ldap_username to translate the email address
|
||||
# to the user's LDAP username before calling the
|
||||
# django-auth-ldap authenticate().
|
||||
username = self.django_to_ldap_username(username)
|
||||
except ZulipLDAPExceptionOutsideDomain:
|
||||
if return_data is not None:
|
||||
|
|
|
@ -71,6 +71,10 @@ GOOGLE_OAUTH2_CLIENT_ID = "test_client_id"
|
|||
AUTH_LDAP_ALWAYS_UPDATE_USER = False
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL, "(uid=%(user)s)")
|
||||
AUTH_LDAP_USERNAME_ATTR = "uid"
|
||||
AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch("ou=users,dc=zulip,dc=com",
|
||||
ldap.SCOPE_ONELEVEL,
|
||||
"(mail=%(email)s)")
|
||||
|
||||
TEST_SUITE = True
|
||||
RATE_LIMITING = False
|
||||
|
|
Loading…
Reference in New Issue