Compare commits

...

2 Commits

Author SHA1 Message Date
Julien Dupouy
5ca21d4f46 Rename to radicale3_auth_ldap 2021-04-07 01:09:06 -03:00
Julien Dupouy
0e64ecc229 Update to support Radicale 3.x 2021-04-07 00:52:54 -03:00
8 changed files with 66 additions and 35 deletions

View File

@ -1,12 +1,12 @@
# What is this?
This is an authentication plugin for Radicale 2. It adds an LDAP authentication backend which can be used for authenticating users against an LDAP server.
This is an authentication plugin for Radicale 3. It adds an LDAP authentication backend which can be used for authenticating users against an LDAP server.
# How to configure
You will need to set a few options inside your radicale config file. Example:
```
[auth]
type = radicale_auth_ldap
type = radicale3_auth_ldap
# LDAP server URL, with protocol and port
ldap_url = ldap://ldap:389

View File

@ -30,12 +30,41 @@ import ldap3
import ldap3.core.exceptions
from radicale.auth import BaseAuth
from radicale.log import logger
import radicale_auth_ldap.ldap3imports
import radicale3_auth_ldap.ldap3imports
PLUGIN_CONFIG_SCHEMA = {"auth": {
"ldap_url": {
"value": "ldap://localhost:389",
"type": str},
"ldap_base": {
"value": "ou=Users",
"type": str},
"ldap_attribute": {
"value": "uid",
"type": str},
"ldap_filter": {
"value": "(objectClass=*)",
"type": str},
"ldap_binddn": {
"value": None,
"type": str},
"ldap_password": {
"value": None,
"type": str},
"ldap_scope": {
"value": "SUBTREE",
"type": str},
"ldap_support_extended": {
"value": True,
"type": bool}}}
class Auth(BaseAuth):
def is_authenticated(self, user, password):
def __init__(self, configuration):
super().__init__(configuration.copy(PLUGIN_CONFIG_SCHEMA))
def login(self, user, password):
"""Check if ``user``/``password`` couple is valid."""
SERVER = ldap3.Server(self.configuration.get("auth", "ldap_url"))
BASE = self.configuration.get("auth", "ldap_base")
@ -44,7 +73,7 @@ class Auth(BaseAuth):
BINDDN = self.configuration.get("auth", "ldap_binddn")
PASSWORD = self.configuration.get("auth", "ldap_password")
SCOPE = self.configuration.get("auth", "ldap_scope")
SUPPORT_EXTENDED = self.configuration.getboolean("auth", "ldap_support_extended", fallback=True)
SUPPORT_EXTENDED = self.configuration.get("auth", "ldap_support_extended")
if BINDDN and PASSWORD:
conn = ldap3.Connection(SERVER, BINDDN, PASSWORD)
@ -53,18 +82,18 @@ class Auth(BaseAuth):
conn.bind()
try:
self.logger.debug("LDAP whoami: %s" % conn.extend.standard.who_am_i())
logger.debug("LDAP whoami: %s" % conn.extend.standard.who_am_i())
except Exception as err:
self.logger.debug("LDAP error: %s" % err)
logger.debug("LDAP error: %s" % err)
distinguished_name = "%s=%s" % (ATTRIBUTE, ldap3imports.escape_attribute_value(user))
self.logger.debug("LDAP bind for %s in base %s" % (distinguished_name, BASE))
logger.debug("LDAP bind for %s in base %s" % (distinguished_name, BASE))
if FILTER:
filter_string = "(&(%s)%s)" % (distinguished_name, FILTER)
else:
filter_string = distinguished_name
self.logger.debug("LDAP filter: %s" % filter_string)
logger.debug("LDAP filter: %s" % filter_string)
conn.search(search_base=BASE,
search_scope=SCOPE,
@ -76,28 +105,28 @@ class Auth(BaseAuth):
if users:
user_dn = users[0]['dn']
uid = users[0]['attributes'][ATTRIBUTE]
self.logger.debug("LDAP user %s (%s) found" % (uid, user_dn))
logger.debug("LDAP user %s (%s) found" % (uid, user_dn))
try:
conn = ldap3.Connection(SERVER, user_dn, password)
conn.bind()
self.logger.debug(conn.result)
logger.debug(conn.result)
if SUPPORT_EXTENDED:
whoami = conn.extend.standard.who_am_i()
self.logger.debug("LDAP whoami: %s" % whoami)
logger.debug("LDAP whoami: %s" % whoami)
else:
self.logger.debug("LDAP skip extended: call whoami")
logger.debug("LDAP skip extended: call whoami")
whoami = conn.result['result'] == 0
if whoami:
self.logger.debug("LDAP bind OK")
return True
logger.debug("LDAP bind OK")
return uid[0]
else:
self.logger.debug("LDAP bind failed")
return False
logger.debug("LDAP bind failed")
return ""
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
self.logger.debug("LDAP invalid credentials")
logger.debug("LDAP invalid credentials")
except Exception as err:
self.logger.debug("LDAP error %s" % err)
return False
logger.debug("LDAP error %s" % err)
return ""
else:
self.logger.debug("LDAP user %s not found" % user)
return False
logger.debug("LDAP user %s not found" % user)
return ""

View File

@ -3,10 +3,10 @@
from setuptools import setup
setup(
name="radicale-auth-ldap",
version="0.1",
description="LDAP Authentication Plugin for Radicale 2",
name="radicale3-auth-ldap",
version="3.0",
description="LDAP Authentication Plugin for Radicale 3",
author="Raoul Thill",
license="GNU GPL v3",
install_requires=["radicale >= 2.0", "ldap3 >= 2.3"],
packages=["radicale_auth_ldap"])
install_requires=["radicale >= 3.0", "ldap3 >= 2.3"],
packages=["radicale3_auth_ldap"])

View File

@ -2,7 +2,7 @@
import unittest
import radicale_auth_ldap
import radicale3_auth_ldap
class References(unittest.TestCase):
@ -18,4 +18,4 @@ class References(unittest.TestCase):
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
successful_test = True
self.assertTrue(successful_test)
self.assertTrue(successful_test)

View File

@ -8,7 +8,8 @@ TEST_CONFIGURATION = {
'ldap_filter': '(objectClass=person)',
'ldap_binddn': 'cn=xxx,dc=xxx,dc=xx',
'ldap_password': '',
'ldap_scope': 'LEVEL'
'ldap_scope': 'LEVEL',
'ldap_support_extended': True
}
}

View File

@ -3,23 +3,21 @@
import logging
import unittest
import radicale_auth_ldap
import radicale3_auth_ldap
from test.configuration import TEST_CONFIGURATION, VALID_USER, VALID_PASS
from test.util import ConfigMock
class Authentication(unittest.TestCase):
configuration = None
logger = None
@classmethod
def setUpClass(cls):
cls.configuration = ConfigMock(TEST_CONFIGURATION)
cls.logger = logging.getLogger(__name__)
def test_authentication_works(self):
auth = radicale_auth_ldap.Auth(self.__class__.configuration, self.__class__.logger)
self.assertTrue(auth.is_authenticated(VALID_USER, VALID_PASS))
auth = radicale3_auth_ldap.Auth(self.__class__.configuration)
self.assertTrue(auth.login(VALID_USER, VALID_PASS))
if __name__ == '__main__':

View File

@ -11,3 +11,6 @@ class ConfigMock:
def get(self, a, b):
return self.configuration[a][b]
def copy(self, c):
return self