mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
+29
-5
@@ -240,14 +240,14 @@ This parameter is optional but you must ensure it is set to ``False``
|
||||
The default configuration file provided by the ``modoboa-admin.py``
|
||||
command is properly configured.
|
||||
|
||||
****
|
||||
LDAP
|
||||
****
|
||||
***********************
|
||||
External authentication
|
||||
***********************
|
||||
|
||||
.. _ldap_auth:
|
||||
|
||||
Authentication
|
||||
==============
|
||||
LDAP
|
||||
====
|
||||
|
||||
Modoboa supports external LDAP authentication using the following extra components:
|
||||
|
||||
@@ -381,6 +381,30 @@ Modoboa.
|
||||
to manually delete the Modoboa record, it will be recreated on the
|
||||
next user login.
|
||||
|
||||
.. _smtp_auth:
|
||||
|
||||
SMTP
|
||||
====
|
||||
|
||||
It is possible to use an existing SMTP server as an authentication
|
||||
source. To enable this feature, edit the :file:`settings.py` file and
|
||||
change the following setting:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'modoboa.lib.authbackends.SMTPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
SMTP server location can be customized using the following settings:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
AUTH_SMTP_SERVER_ADDRESS = 'localhost'
|
||||
AUTH_SMTP_SERVER_PORT = 25
|
||||
AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
|
||||
|
||||
********************
|
||||
Database maintenance
|
||||
********************
|
||||
|
||||
@@ -84,6 +84,20 @@ documentation.
|
||||
Specific instructions
|
||||
*********************
|
||||
|
||||
1.7.0
|
||||
=====
|
||||
|
||||
Modoboa used to provide a custom authentication backend
|
||||
(``modoboa.lib.authbackends.SimpleBackend``) but it has been
|
||||
removed. Please modify you :file:`settings.py` file as follows:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
# Other backends before...
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
1.6.1
|
||||
=====
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class AccountTestCase(ModoTestCase):
|
||||
@skipIf(NO_LDAP, "No ldap module installed")
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
'modoboa.lib.authbackends.SimpleBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
))
|
||||
class LDAPAccountTestCase(test_core.LDAPTestCaseMixin, ModoTestCase):
|
||||
"""Check LDAP related code."""
|
||||
|
||||
@@ -89,9 +89,17 @@ MIDDLEWARE_CLASSES = (
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'modoboa.lib.authbackends.SimpleBackend',
|
||||
# 'modoboa.lib.authbackends.LDAPBackend',
|
||||
# 'modoboa.lib.authbackends.SMTPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
# SMTP authentication
|
||||
# AUTH_SMTP_SERVER_ADDRESS = 'localhost'
|
||||
# AUTH_SMTP_SERVER_PORT = 25
|
||||
# AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted
|
||||
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Tests for core application."""
|
||||
|
||||
import smtplib
|
||||
|
||||
from mock import patch
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import override_settings
|
||||
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
|
||||
from .. import factories
|
||||
from .. import models
|
||||
|
||||
|
||||
class AuthenticationTestCase(ModoTestCase):
|
||||
"""Validate authentication scenarios."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create test data."""
|
||||
super(AuthenticationTestCase, cls).setUpTestData()
|
||||
cls.account = factories.UserFactory(
|
||||
username="user@test.com", groups=('SimpleUsers',)
|
||||
)
|
||||
|
||||
def test_authentication(self):
|
||||
"""Validate simple case."""
|
||||
self.client.logout()
|
||||
data = {"username": "user@test.com", "password": "toto"}
|
||||
response = self.client.post(reverse("core:login"), data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(reverse("core:user_index")))
|
||||
|
||||
response = self.client.post(reverse("core:logout"), {})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
data = {"username": "admin", "password": "password"}
|
||||
response = self.client.post(reverse("core:login"), data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(reverse("core:dashboard")))
|
||||
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
"modoboa.lib.authbackends.SMTPBackend",
|
||||
"django.contrib.auth.backends.ModelBackend"
|
||||
))
|
||||
class SMTPAuthenticationTestCase(ModoTestCase):
|
||||
"""Validate SMTP authentication scenarios."""
|
||||
|
||||
def _test_smtp_authentication(self, mock_smtp):
|
||||
"""Common code to check authentication"""
|
||||
self.client.logout()
|
||||
username = "user@unknown.test"
|
||||
password = "toto"
|
||||
data = {"username": username, "password": password}
|
||||
response = self.client.post(reverse("core:login"), data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(reverse("core:user_index")))
|
||||
mock_smtp.return_value.login.assert_called_once_with(
|
||||
username, password)
|
||||
self.assertTrue(
|
||||
models.User.objects.filter(username=username).exists())
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_smtp_authentication(self, mock_smtp):
|
||||
"""Check simple SMTP authentication."""
|
||||
self._test_smtp_authentication(mock_smtp)
|
||||
|
||||
@patch("smtplib.SMTP_SSL")
|
||||
@override_settings(AUTH_SMTP_SECURED_MODE="ssl")
|
||||
def test_smtp_authentication_over_ssl(self, mock_smtp):
|
||||
"""Check SMTP authentication over SSL."""
|
||||
self._test_smtp_authentication(mock_smtp)
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
@override_settings(AUTH_SMTP_SECURED_MODE="starttls")
|
||||
def test_smtp_authentication_over_starttls(self, mock_smtp):
|
||||
"""Check SMTP authentication over STARTTLS."""
|
||||
self._test_smtp_authentication(mock_smtp)
|
||||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_smtp_authentication_failure(self, mock_smtp):
|
||||
"""Check SMTP authentication failure."""
|
||||
instance = mock_smtp.return_value
|
||||
instance.login.side_effect = smtplib.SMTPAuthenticationError(
|
||||
450, "User not found")
|
||||
self.client.logout()
|
||||
username = "user@unknown.test"
|
||||
password = "toto"
|
||||
data = {"username": username, "password": password}
|
||||
response = self.client.post(reverse("core:login"), data)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
mock_smtp.return_value.login.assert_called_once_with(
|
||||
username, password)
|
||||
self.assertFalse(
|
||||
models.User.objects.filter(username=username).exists())
|
||||
@@ -1,17 +1,12 @@
|
||||
"""Tests for core application."""
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
import httmock
|
||||
|
||||
from django.core import management
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import override_settings, TestCase
|
||||
from django.utils.functional import cached_property
|
||||
from django.test import TestCase
|
||||
|
||||
from modoboa.lib import exceptions
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.lib.tests import NO_LDAP
|
||||
|
||||
from .. import factories
|
||||
from .. import mocks
|
||||
@@ -58,120 +53,8 @@ class ChangeDefaultAdminTestCase(TestCase):
|
||||
self.client.login(username="modoadmin", password="password"))
|
||||
|
||||
|
||||
@skipIf(NO_LDAP, 'No ldap module installed')
|
||||
class LDAPTestCaseMixin(object):
|
||||
"""Set of methods used to test LDAP features."""
|
||||
|
||||
@cached_property
|
||||
def ldapauthbackend(self):
|
||||
"""Return LDAPAuthBackend instance."""
|
||||
from modoboa.lib.ldap_utils import LDAPAuthBackend
|
||||
return LDAPAuthBackend()
|
||||
|
||||
def activate_ldap_authentication(self):
|
||||
"""Modify settings."""
|
||||
self.set_global_parameters({
|
||||
"authentication_type": "ldap",
|
||||
"ldap_server_port": 3389
|
||||
})
|
||||
|
||||
def restore_user_password(self, username, new_password):
|
||||
"""Restore user password to its initial state."""
|
||||
for password in ["Toto1234", "test"]:
|
||||
try:
|
||||
self.ldapauthbackend.update_user_password(
|
||||
username, password, new_password)
|
||||
except exceptions.InternalError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
raise RuntimeError("Can't restore user password.")
|
||||
|
||||
def authenticate(self, user, password, restore_before=True):
|
||||
"""Restore password and authenticate user."""
|
||||
self.client.logout()
|
||||
if restore_before:
|
||||
self.restore_user_password(user, password)
|
||||
self.assertTrue(
|
||||
self.client.login(username=user, password=password))
|
||||
|
||||
def searchbind_mode(self):
|
||||
"""Apply settings required by the searchbind mode."""
|
||||
self.set_global_parameters({
|
||||
"ldap_auth_method": "searchbind",
|
||||
"ldap_bind_dn": "cn=admin,dc=example,dc=com",
|
||||
"ldap_bind_password": "test",
|
||||
"ldap_search_base": "ou=users,dc=example,dc=com"
|
||||
})
|
||||
|
||||
def directbind_mode(self):
|
||||
"""Apply settings required by the directbind mode."""
|
||||
self.set_global_parameters({
|
||||
"ldap_auth_method": "directbind",
|
||||
"ldap_user_dn_template": "cn=%(user)s,ou=users,dc=example,dc=com"
|
||||
})
|
||||
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
'modoboa.lib.authbackends.SimpleBackend',
|
||||
))
|
||||
class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase):
|
||||
"""Validate LDAP authentication scenarios."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create test data."""
|
||||
super(LDAPAuthenticationTestCase, self).setUp()
|
||||
self.activate_ldap_authentication()
|
||||
|
||||
def check_created_user(self, username, group="SimpleUsers", with_mb=True):
|
||||
"""Check that created user is valid."""
|
||||
user = models.User.objects.get(username=username)
|
||||
self.assertEqual(user.role, group)
|
||||
if with_mb:
|
||||
self.assertEqual(user.email, username)
|
||||
self.assertEqual(user.mailbox.domain.name, "example.com")
|
||||
self.assertEqual(user.mailbox.full_address, username)
|
||||
|
||||
@override_settings(AUTH_LDAP_USER_DN_TEMPLATE=None)
|
||||
def test_searchbind_authentication(self):
|
||||
"""Test the bind&search method."""
|
||||
self.searchbind_mode()
|
||||
username = "testuser@example.com"
|
||||
self.authenticate(username, "test")
|
||||
self.check_created_user(username)
|
||||
|
||||
self.set_global_parameters({
|
||||
"ldap_admin_groups": "admins",
|
||||
"ldap_groups_search_base": "ou=groups,dc=example,dc=com"
|
||||
})
|
||||
username = "mailadmin@example.com"
|
||||
self.authenticate(username, "test", False)
|
||||
self.check_created_user(username, "DomainAdmins")
|
||||
|
||||
def test_directbind_authentication(self):
|
||||
"""Test the directbind method."""
|
||||
self.client.logout()
|
||||
self.directbind_mode()
|
||||
|
||||
# 1: must fail because usernames of simple users must be email
|
||||
# addresses
|
||||
username = "testuser"
|
||||
with self.assertRaises(TypeError):
|
||||
self.client.login(username=username, password="test")
|
||||
|
||||
# 1: must work because usernames of domain admins are not
|
||||
# always email addresses
|
||||
self.set_global_parameters({
|
||||
"ldap_admin_groups": "admins",
|
||||
"ldap_groups_search_base": "ou=groups,dc=example,dc=com"
|
||||
})
|
||||
username = "mailadmin"
|
||||
self.authenticate(username, "test", False)
|
||||
self.check_created_user(username, "DomainAdmins", False)
|
||||
|
||||
|
||||
class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase):
|
||||
class ProfileTestCase(ModoTestCase):
|
||||
"""Profile related tests."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -217,24 +100,6 @@ class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase):
|
||||
self.client.login(username="user@test.com", password="Toto1234")
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
'modoboa.lib.authbackends.SimpleBackend',
|
||||
))
|
||||
def test_update_password_ldap(self):
|
||||
"""Update password for an LDAP user."""
|
||||
self.activate_ldap_authentication()
|
||||
self.searchbind_mode()
|
||||
|
||||
username = "testuser@example.com"
|
||||
self.authenticate(username, "test")
|
||||
self.ajax_post(
|
||||
reverse("core:user_profile"),
|
||||
{"language": "en", "oldpassword": "test",
|
||||
"newpassword": "Toto1234", "confirmation": "Toto1234"}
|
||||
)
|
||||
self.authenticate(username, "Toto1234", False)
|
||||
|
||||
|
||||
class APIAccessFormTestCase(ModoTestCase):
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Tests for core application."""
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import override_settings
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from modoboa.lib import exceptions
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.lib.tests import NO_LDAP
|
||||
|
||||
from .. import factories
|
||||
from .. import models
|
||||
|
||||
|
||||
@skipIf(NO_LDAP, 'No ldap module installed')
|
||||
class LDAPTestCaseMixin(object):
|
||||
"""Set of methods used to test LDAP features."""
|
||||
|
||||
@cached_property
|
||||
def ldapauthbackend(self):
|
||||
"""Return LDAPAuthBackend instance."""
|
||||
from modoboa.lib.ldap_utils import LDAPAuthBackend
|
||||
return LDAPAuthBackend()
|
||||
|
||||
def activate_ldap_authentication(self):
|
||||
"""Modify settings."""
|
||||
self.set_global_parameters({
|
||||
"authentication_type": "ldap",
|
||||
"ldap_server_port": 3389
|
||||
})
|
||||
|
||||
def restore_user_password(self, username, new_password):
|
||||
"""Restore user password to its initial state."""
|
||||
for password in ["Toto1234", "test"]:
|
||||
try:
|
||||
self.ldapauthbackend.update_user_password(
|
||||
username, password, new_password)
|
||||
except exceptions.InternalError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
raise RuntimeError("Can't restore user password.")
|
||||
|
||||
def authenticate(self, user, password, restore_before=True):
|
||||
"""Restore password and authenticate user."""
|
||||
self.client.logout()
|
||||
if restore_before:
|
||||
self.restore_user_password(user, password)
|
||||
self.assertTrue(
|
||||
self.client.login(username=user, password=password))
|
||||
|
||||
def searchbind_mode(self):
|
||||
"""Apply settings required by the searchbind mode."""
|
||||
self.set_global_parameters({
|
||||
"ldap_auth_method": "searchbind",
|
||||
"ldap_bind_dn": "cn=admin,dc=example,dc=com",
|
||||
"ldap_bind_password": "test",
|
||||
"ldap_search_base": "ou=users,dc=example,dc=com"
|
||||
})
|
||||
|
||||
def directbind_mode(self):
|
||||
"""Apply settings required by the directbind mode."""
|
||||
self.set_global_parameters({
|
||||
"ldap_auth_method": "directbind",
|
||||
"ldap_user_dn_template": "cn=%(user)s,ou=users,dc=example,dc=com"
|
||||
})
|
||||
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend'
|
||||
))
|
||||
class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase):
|
||||
"""Validate LDAP authentication scenarios."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create test data."""
|
||||
super(LDAPAuthenticationTestCase, self).setUp()
|
||||
self.activate_ldap_authentication()
|
||||
|
||||
def check_created_user(self, username, group="SimpleUsers", with_mb=True):
|
||||
"""Check that created user is valid."""
|
||||
user = models.User.objects.get(username=username)
|
||||
self.assertEqual(user.role, group)
|
||||
if with_mb:
|
||||
self.assertEqual(user.email, username)
|
||||
self.assertEqual(user.mailbox.domain.name, "example.com")
|
||||
self.assertEqual(user.mailbox.full_address, username)
|
||||
|
||||
@override_settings(AUTH_LDAP_USER_DN_TEMPLATE=None)
|
||||
def test_searchbind_authentication(self):
|
||||
"""Test the bind&search method."""
|
||||
self.searchbind_mode()
|
||||
username = "testuser@example.com"
|
||||
self.authenticate(username, "test")
|
||||
self.check_created_user(username)
|
||||
|
||||
self.set_global_parameters({
|
||||
"ldap_admin_groups": "admins",
|
||||
"ldap_groups_search_base": "ou=groups,dc=example,dc=com"
|
||||
})
|
||||
username = "mailadmin@example.com"
|
||||
self.authenticate(username, "test", False)
|
||||
self.check_created_user(username, "DomainAdmins")
|
||||
|
||||
def test_directbind_authentication(self):
|
||||
"""Test the directbind method."""
|
||||
self.client.logout()
|
||||
self.directbind_mode()
|
||||
|
||||
# 1: must fail because usernames of simple users must be email
|
||||
# addresses
|
||||
username = "testuser"
|
||||
with self.assertRaises(TypeError):
|
||||
self.client.login(username=username, password="test")
|
||||
|
||||
# 1: must work because usernames of domain admins are not
|
||||
# always email addresses
|
||||
self.set_global_parameters({
|
||||
"ldap_admin_groups": "admins",
|
||||
"ldap_groups_search_base": "ou=groups,dc=example,dc=com"
|
||||
})
|
||||
username = "mailadmin"
|
||||
self.authenticate(username, "test", False)
|
||||
self.check_created_user(username, "DomainAdmins", False)
|
||||
|
||||
|
||||
class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase):
|
||||
"""Profile related tests."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create test data."""
|
||||
super(ProfileTestCase, cls).setUpTestData()
|
||||
cls.account = factories.UserFactory(
|
||||
username="user@test.com", groups=('SimpleUsers',)
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(
|
||||
'modoboa.lib.authbackends.LDAPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend'
|
||||
))
|
||||
def test_update_password_ldap(self):
|
||||
"""Update password for an LDAP user."""
|
||||
self.activate_ldap_authentication()
|
||||
self.searchbind_mode()
|
||||
|
||||
username = "testuser@example.com"
|
||||
self.authenticate(username, "test")
|
||||
self.ajax_post(
|
||||
reverse("core:user_profile"),
|
||||
{"language": "en", "oldpassword": "test",
|
||||
"newpassword": "Toto1234", "confirmation": "Toto1234"}
|
||||
)
|
||||
self.authenticate(username, "Toto1234", False)
|
||||
+45
-10
@@ -1,29 +1,64 @@
|
||||
"""Custom authentication backends."""
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
import smtplib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
|
||||
class SimpleBackend(ModelBackend):
|
||||
"""Simple authentication backend."""
|
||||
class SMTPBackend(object):
|
||||
"""A backend to authenticate against an SMTP server."""
|
||||
|
||||
def authenticate(self, username=None, password=None):
|
||||
"""Check the username/password and return a User."""
|
||||
if type(username) is unicode:
|
||||
username = username.encode("utf-8")
|
||||
if type(password) is unicode:
|
||||
password = password.encode("utf-8")
|
||||
|
||||
host = getattr(settings, "AUTH_SMTP_SERVER_ADDRESS", "localhost")
|
||||
port = getattr(settings, "AUTH_SMTP_SERVER_PORT", 25)
|
||||
secured_mode = getattr(settings, "AUTH_SMTP_SECURED_MODE", None)
|
||||
if secured_mode == "ssl":
|
||||
smtp = smtplib.SMTP_SSL(host, port)
|
||||
else:
|
||||
smtp = smtplib.SMTP(host, port)
|
||||
if secured_mode == "starttls":
|
||||
smtp.starttls()
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
if not user.check_password(password):
|
||||
smtp.login(username, password)
|
||||
except smtplib.SMTPException:
|
||||
return None
|
||||
return self.get_or_create_user(username)
|
||||
|
||||
def get_or_create_user(self, username):
|
||||
"""Get a user or create it the first time.
|
||||
|
||||
.. note::
|
||||
|
||||
We assume the username is a valid email address.
|
||||
"""
|
||||
user, created = User.objects.get_or_create(
|
||||
username__iexact=username, defaults={
|
||||
"username": username.lower(), "email": username.lower()
|
||||
}
|
||||
)
|
||||
if created:
|
||||
populate_callback(user)
|
||||
return user
|
||||
|
||||
def get_user(self, user_id):
|
||||
def get_user(self, user_pk):
|
||||
"""Retrieve a User instance."""
|
||||
user = None
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
user = User.objects.get(pk=user_pk)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
pass
|
||||
return user
|
||||
|
||||
|
||||
try:
|
||||
from django_auth_ldap.backend import (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
factory-boy>=2.4
|
||||
testfixtures==4.7.0
|
||||
mock==2.0.0
|
||||
httmock==1.2.5
|
||||
testfixtures==4.7.0
|
||||
tox
|
||||
|
||||
Reference in New Issue
Block a user