From 048ababd6fadd550ec36e18e2c2c901766fc7597 Mon Sep 17 00:00:00 2001 From: Johannes Zellner Date: Mon, 22 Jun 2020 22:17:37 +0200 Subject: Initial support for Radicale 3 Fixes #11 --- README.md | 4 +- radicale_auth_ldap/__init__.py | 182 ++++++++++++++++++++++++++--------------- setup.py | 8 +- 3 files changed, 122 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 7d4a4be..c03e0be 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 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. + +Use the `2-final` git tag for Radicale 2 support. # How to configure You will need to set a few options inside your radicale config file. Example: diff --git a/radicale_auth_ldap/__init__.py b/radicale_auth_ldap/__init__.py index 55ed3d5..0adb19b 100644 --- a/radicale_auth_ldap/__init__.py +++ b/radicale_auth_ldap/__init__.py @@ -5,6 +5,7 @@ # Copyright © 2011-2013 Guillaume Ayoub # Copyright © 2015 Raoul Thill # Copyright © 2017 Marco Huenseler +# Copyright © 2020 Johannes Zellner # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,79 +26,126 @@ Authentication based on the ``ldap3`` module (https://github.com/cannatag/ldap3/). """ - import ldap3 -import ldap3.core.exceptions from radicale.auth import BaseAuth +from radicale.log import logger -import radicale_auth_ldap.ldap3imports +PLUGIN_CONFIG_SCHEMA = { + "auth": { + "password": { + "value": "", + "type": str + }, + "ldap_url": { + "value": "ldap://localhost:389", + "help": "LDAP server URL, with protocol and port", + "type": str + }, + "ldap_base": { + "value": "ou=users,dc=example", + "help": "LDAP base DN for users", + "type": str + }, + "ldap_filter": { + "value": "(&(objectclass=user)(username=%username))", + "help": "LDAP search filter to find login user", + "type": str + }, + "ldap_attribute": { + "value": "username", + "help": "LDAP attribute to uniquely identify the user", + "type": str + }, + "ldap_binddn": { + "value": "", + "help": "LDAP dn used if server does not allow anonymous search", + "type": str + }, + "ldap_password": { + "value": "", + "help": "LDAP password used with ldap_binddn", + "type": str + } + } +} class Auth(BaseAuth): - def is_authenticated(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") - ATTRIBUTE = self.configuration.get("auth", "ldap_attribute") - FILTER = self.configuration.get("auth", "ldap_filter") - 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) - - if BINDDN and PASSWORD: - conn = ldap3.Connection(SERVER, BINDDN, PASSWORD) - else: - conn = ldap3.Connection(SERVER) + ldap_url = "" + ldap_base = "" + ldap_filter = "" + ldap_attribute = "" + ldap_binddn = "" + ldap_password = "" + + def __init__(self, configuration): + super().__init__(configuration.copy(PLUGIN_CONFIG_SCHEMA)) + + options = configuration.options("auth") + + if "ldap_url" not in options: raise RuntimeError("The ldap_url configuration for ldap auth is required.") + if "ldap_base" not in options: raise RuntimeError("The ldap_base configuration for ldap auth is required.") + if "ldap_filter" not in options: raise RuntimeError("The ldap_filter configuration for ldap auth is required.") + if "ldap_attribute" not in options: raise RuntimeError("The ldap_attribute configuration for ldap auth is required.") + if "ldap_binddn" not in options: raise RuntimeError("The ldap_binddn configuration for ldap auth is required.") + if "ldap_password" not in options: raise RuntimeError("The ldap_password configuration for ldap auth is required.") + + # also get rid of trailing slashes which are typical for uris + self.ldap_url = configuration.get("auth", "ldap_url").rstrip("/") + self.ldap_base = configuration.get("auth", "ldap_base") + self.ldap_filter = configuration.get("auth", "ldap_filter") + self.ldap_attribute = configuration.get("auth", "ldap_attribute") + self.ldap_binddn = configuration.get("auth", "ldap_binddn") + self.ldap_password = configuration.get("auth", "ldap_password") + + logger.info("LDAP auth configuration:") + logger.info(" %r is %r", "ldap_url", self.ldap_url) + logger.info(" %r is %r", "ldap_base", self.ldap_base) + logger.info(" %r is %r", "ldap_filter", self.ldap_filter) + logger.info(" %r is %r", "ldap_attribute", self.ldap_attribute) + logger.info(" %r is %r", "ldap_binddn", self.ldap_binddn) + logger.info(" %r is %r", "ldap_password", self.ldap_password) + + def login(self, login, password): + if login == "" or password == "": + return "" + + server = ldap3.Server(self.ldap_url, get_info=ldap3.ALL) + conn = ldap3.Connection(server=server, user=self.ldap_binddn, + password=self.ldap_password, check_names=True, + lazy=False, raise_exceptions=False) + conn.open() conn.bind() - try: - self.logger.debug("LDAP whoami: %s" % conn.extend.standard.who_am_i()) - except Exception as err: - self.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)) - - if FILTER: - filter_string = "(&(%s)%s)" % (distinguished_name, FILTER) - else: - filter_string = distinguished_name - self.logger.debug("LDAP filter: %s" % filter_string) - - conn.search(search_base=BASE, - search_scope=SCOPE, - search_filter=filter_string, - attributes=[ATTRIBUTE]) - - users = conn.response - - if users: - user_dn = users[0]['dn'] - uid = users[0]['attributes'][ATTRIBUTE] - self.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) - if SUPPORT_EXTENDED: - whoami = conn.extend.standard.who_am_i() - self.logger.debug("LDAP whoami: %s" % whoami) - else: - self.logger.debug("LDAP skip extended: call whoami") - whoami = conn.result['result'] == 0 - if whoami: - self.logger.debug("LDAP bind OK") - return True - else: - self.logger.debug("LDAP bind failed") - return False - except ldap3.core.exceptions.LDAPInvalidCredentialsResult: - self.logger.debug("LDAP invalid credentials") - except Exception as err: - self.logger.debug("LDAP error %s" % err) - return False - else: - self.logger.debug("LDAP user %s not found" % user) - return False + if conn.result["result"] != 0: + logger.error(conn.result) + return "" + + final_search_filter = self.ldap_filter.replace("%username", login) + conn.search(search_base=self.ldap_base, + search_filter=final_search_filter, + attributes=ldap3.ALL_ATTRIBUTES) + + if conn.result["result"] != 0: + logger.error(conn.result) + return "" + + if len(conn.response) == 0: + return "" + + final_user_dn = conn.response[0]["dn"] + conn.unbind() + + # new connection to check the password as we cannot rebind here + conn = ldap3.Connection(server=server, user=final_user_dn, + password=password, check_names=True, + lazy=False, raise_exceptions=False) + conn.open() + conn.bind() + + if conn.result["result"] != 0: + logger.error(conn.result) + return "" + + return login diff --git a/setup.py b/setup.py index eb6e257..965e73c 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -from setuptools import setup +from distutils.core import setup setup( name="radicale-auth-ldap", - version="0.1", - description="LDAP Authentication Plugin for Radicale 2", + version="0.2", + description="LDAP Authentication Plugin for Radicale 3", author="Raoul Thill", license="GNU GPL v3", - install_requires=["radicale >= 2.0", "ldap3 >= 2.3"], + install_requires=["radicale >= 3.0", "ldap3 >= 2.3"], packages=["radicale_auth_ldap"]) -- cgit