Implement authentication with LDAP

Enable an additional lookup against an LDAP directory
during login and user creation to ensure that only
specific users can login and register on the EteBase
server instance.
This commit is contained in:
PapaTutuWawa 2020-11-06 15:22:26 +01:00
parent 801826b8b6
commit 1fef1e2b7a
5 changed files with 98 additions and 1 deletions

View File

@ -78,6 +78,34 @@ class AppSettings:
@cached_property @cached_property
def CHALLENGE_VALID_SECONDS(self): # pylint: disable=invalid-name def CHALLENGE_VALID_SECONDS(self): # pylint: disable=invalid-name
return self._setting("CHALLENGE_VALID_SECONDS", 60) return self._setting("CHALLENGE_VALID_SECONDS", 60)
@cached_property
def USE_LDAP(self): # pylint: disable=invalid-name
return self._setting("USE_LDAP", False)
@cached_property
def LDAP_FILTER(self): # pylint: disable=invalid-name
return self._setting("LDAP_FILTER", "")
@cached_property
def LDAP_SEARCH_BASE(self): # pylint: disable=invalid-name
return self._setting("LDAP_SEARCH_BASE", "")
@cached_property
def LDAP_SERVER(self): # pylint: disable=invalid-name
return self._setting("LDAP_SERVER", "")
@cached_property
def LDAP_BIND_DN(self): # pylint: disable=invalid-name
return self._setting("LDAP_BIND_DN", "")
@cached_property
def LDAP_BIND_PASSWORD(self): # pylint: disable=invalid-name
return self._setting("LDAP_BIND_PW", "")
@cached_property
def LDAP_SEARCH_BASE(self): # pylint: disable=invalid-name
return self._setting("LDAP_SEARCH_BASE", "")
app_settings = AppSettings('ETEBASE_') app_settings = AppSettings('ETEBASE_')

45
django_etebase/ldap.py Normal file
View File

@ -0,0 +1,45 @@
import logging
from . import app_settings
import ldap
class LDAPConnection:
__instance__ = None
@staticmethod
def get_instance():
'''To get a Singleton'''
if not LDAPConnection.__instance__:
return LDAPConnection()
else:
return LDAPConnection.__instance__
def __init__(self):
# Pull settings from django.conf.settings
# NOTE: We assume that settings.USE_LDAP is True
self.__ldap_connection = ldap.initialize(app_settings.LDAP_SERVER)
try:
self.__ldap_connection.simple_bind_s(app_settings.LDAP_BIND_DN,
app_settings.LDAP_BIND_PASSWORD)
except ldap.LDAPError as err:
logging.error(f'LDAP Error occuring during initialization: {err.desc}')
def has_user(self, username):
'''
Since we don't care about the password and so authentication
another way, all we care about is whether the user exists.
'''
filterstr = app_settings.LDAP_FILTER.replace('%s', username)
try:
result = self.__ldap_connection.search_s(app_settings.LDAP_SEARCH_BASE,
ldap.SCOPE_SUBTREE,
filterstr=filterstr)
except ldap.NO_RESULTS_RETURNED:
# We handle the specific error first and the the generic error, as
# we may expect ldap.NO_RESULTS_RETURNED, but not any other error
return False
except ldap.LDAPError as err:
logging.error(f'Error occured while performing an LDAP query: {err.desc}')
return False
return len(result) == 1

View File

@ -1,16 +1,19 @@
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .. import app_settings
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication as DRFTokenAuthentication from rest_framework.authentication import TokenAuthentication as DRFTokenAuthentication
from .models import AuthToken, get_default_expiry from .models import AuthToken, get_default_expiry
if app_settings.USE_LDAP:
from ..ldap import LDAPConnection
AUTO_REFRESH = True AUTO_REFRESH = True
MIN_REFRESH_INTERVAL = 60 MIN_REFRESH_INTERVAL = 60
class TokenAuthentication(DRFTokenAuthentication): class TokenAuthentication(DRFTokenAuthentication):
keyword = 'Token' keyword = 'Token'
model = AuthToken model = AuthToken
@ -23,6 +26,10 @@ class TokenAuthentication(DRFTokenAuthentication):
except model.DoesNotExist: except model.DoesNotExist:
raise exceptions.AuthenticationFailed(msg) raise exceptions.AuthenticationFailed(msg)
if app_settings.USE_LDAP:
if not LDAPConnection.get_instance().has_user(token.user.username):
raise exceptions.AuthenticationFailed('User is not listed in the LDAP registry.')
if not token.user.is_active: if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

View File

@ -2,6 +2,7 @@ from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from . import app_settings from . import app_settings
from .ldap import LDAPConnection
User = get_user_model() User = get_user_model()
@ -15,6 +16,11 @@ def get_user_queryset(queryset, view):
def create_user(*args, **kwargs): def create_user(*args, **kwargs):
# Check if the LDAP query returns exactly one user
if app_settings.USE_LDAP:
if not LDAPConnection.get_instance().has_user(kwargs['username']):
raise PermissionDenied('User is not listed in the LDAP registry.')
custom_func = app_settings.CREATE_USER_FUNC custom_func = app_settings.CREATE_USER_FUNC
if custom_func is not None: if custom_func is not None:
return custom_func(*args, **kwargs) return custom_func(*args, **kwargs)

View File

@ -164,6 +164,17 @@ if any(os.path.isfile(x) for x in config_locations):
if 'database' in config: if 'database' in config:
DATABASES = { 'default': { x.upper(): y for x, y in config.items('database') } } DATABASES = { 'default': { x.upper(): y for x, y in config.items('database') } }
if 'ldap' in config:
ldap = config['ldap']
ETEBASE_USE_LDAP = True
ETEBASE_LDAP_SERVER = ldap.get('server', '')
ETEBASE_LDAP_SEARCH_BASE = ldap.get('search_base', '')
ETEBASE_LDAP_FILTER = ldap.get('filter', '')
ETEBASE_LDAP_BIND_DN = ldap.get('bind_dn', '')
ETEBASE_LDAP_BIND_PW = ldap.get('bind_pw', '')
else:
ETEBASE_USE_LDAP = False
ETEBASE_API_PERMISSIONS = ('rest_framework.permissions.IsAuthenticated', ) ETEBASE_API_PERMISSIONS = ('rest_framework.permissions.IsAuthenticated', )
ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAuthentication', ETEBASE_API_AUTHENTICATORS = ('django_etebase.token_auth.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication') 'rest_framework.authentication.SessionAuthentication')