From 5e3dc9ec3dd3a2cc48812c8c9ed35536ba9292f1 Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Sat, 24 Sep 2022 23:20:15 +0200 Subject: [PATCH 1/9] Update LDAP with certificate --- radicale/auth/ldap.py | 33 +++++++++++++++++++++------------ radicale/config.py | 6 +++++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index a55ceb655..c24f75ba6 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -28,6 +28,7 @@ from radicale import auth, config from radicale.log import logger + class Auth(auth.BaseAuth): _ldap_uri: str _ldap_base: str @@ -38,12 +39,13 @@ class Auth(auth.BaseAuth): def __init__(self, configuration: config.Configuration) -> None: super().__init__(configuration) - self._ldap_uri = configuration.get("auth", "ldap_uri") + self._ldap_uri = configuration.get("auth", "ldap_uri") self._ldap_base = configuration.get("auth", "ldap_base") self._ldap_reader_dn = configuration.get("auth", "ldap_reader_dn") self._ldap_load_groups = configuration.get("auth", "ldap_load_groups") - self._ldap_secret = configuration.get("auth", "ldap_secret") - self._ldap_filter = configuration.get("auth", "ldap_filter") + self._ldap_secret = configuration.get("auth", "ldap_secret") + self._ldap_filter = configuration.get("auth", "ldap_filter") + self._ldaps_certificate = configuration.get("auth", "ldaps_certificate") def login(self, login: str, password: str) -> str: """Validate credentials. @@ -54,35 +56,42 @@ def login(self, login: str, password: str) -> str: """ try: """Bind as reader dn""" - conn = ldap.initialize(self._ldap_uri) conn.protocol_version = 3 conn.set_option(ldap.OPT_REFERRALS, 0) conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret) """Search for the dn of user to authenticate""" - res = conn.search_s(self._ldap_base, ldap.SCOPE_SUBTREE, filterstr=self._ldap_filter.format(login), attrlist=['memberOf']) + res = conn.search_s( + self._ldap_base, + ldap.SCOPE_SUBTREE, + filterstr=self._ldap_filter.format(login), + attrlist=["memberOf"], + ) if len(res) == 0: - """User could not be find""" + """User could not be found""" + logger.debug("LDAP search returned no results.") return "" user_dn = res[0][0] - logger.debug("LDAP Auth user: %s",user_dn) + logger.debug("LDAP Auth user: %s", user_dn) """Close ldap connection""" conn.unbind() except Exception: - raise RuntimeError("Invalide ldap configuration") + raise RuntimeError("Invalid ldap configuration") try: """Bind as user to authenticate""" conn = ldap.initialize(self._ldap_uri) conn.protocol_version = 3 conn.set_option(ldap.OPT_REFERRALS, 0) - conn.simple_bind_s(user_dn,password) + conn.simple_bind_s(user_dn, password) tmp = [] if self._ldap_load_groups: tmp = [] - for t in res[0][1]['memberOf']: - tmp.append(t.decode('utf-8').split(',')[0][3:]) + for t in res[0][1]["memberOf"]: + tmp.append(t.decode("utf-8").split(",")[0][3:]) self._ldap_groups = set(tmp) - logger.debug("LDAP Auth groups of user: %s",",".join(self._ldap_groups)) + logger.debug( + "LDAP Auth groups of user: %s", ",".join(self._ldap_groups) + ) conn.unbind() return login except ldap.INVALID_CREDENTIALS: diff --git a/radicale/config.py b/radicale/config.py index 238bd3b6b..f677343fe 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -202,7 +202,11 @@ def _convert_to_bool(value: Any) -> bool: "value": "False", "help": "load the ldap groups of the authenticated user", "type": bool}), - ])), + ( "ldaps_certificate", { + "value": "/etc/radicale/ldap_certificate.pem", + "help": "ldap server certificate", + "type": filepath}), + ])), ("rights", OrderedDict([ ("type", { "value": "owner_only", From 8094294be780745db7267d2b0e42d5dc34370c9f Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Sat, 24 Sep 2022 23:51:23 +0200 Subject: [PATCH 2/9] Complete LDAPS configuration --- radicale/auth/ldap.py | 3 +++ radicale/config.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index c24f75ba6..b3c0536cc 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -46,6 +46,9 @@ def __init__(self, configuration: config.Configuration) -> None: self._ldap_secret = configuration.get("auth", "ldap_secret") self._ldap_filter = configuration.get("auth", "ldap_filter") self._ldaps_certificate = configuration.get("auth", "ldaps_certificate") + # If a ldaps_certificate is set, configure ldap to use it + if self._ldaps_certificate: + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self._ldaps_certificate) def login(self, login: str, password: str) -> str: """Validate credentials. diff --git a/radicale/config.py b/radicale/config.py index f677343fe..b43d97abe 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -203,7 +203,7 @@ def _convert_to_bool(value: Any) -> bool: "help": "load the ldap groups of the authenticated user", "type": bool}), ( "ldaps_certificate", { - "value": "/etc/radicale/ldap_certificate.pem", + "value": None, "help": "ldap server certificate", "type": filepath}), ])), From 498117da981fe62b329411d54a03ec2348a821f2 Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Mon, 26 Sep 2022 12:27:56 +0200 Subject: [PATCH 3/9] Add missing extra requirements for ldap --- setup.py | 61 +++++++++++++++----------------------------------------- 1 file changed, 16 insertions(+), 45 deletions(-) diff --git a/setup.py b/setup.py index 17cd74373..0114f2581 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # This file is part of Radicale - CalDAV and CardDAV server # Copyright © 2009-2017 Guillaume Ayoub # Copyright © 2017-2018 Unrud @@ -17,71 +15,44 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . -""" -Radicale CalDAV and CardDAV server -================================== - -The Radicale Project is a CalDAV (calendar) and CardDAV (contact) server. It -aims to be a light solution, easy to use, easy to install, easy to configure. -As a consequence, it requires few software dependances and is pre-configured to -work out-of-the-box. - -The Radicale Project runs on most of the UNIX-like platforms (Linux, BSD, -MacOS X) and Windows. It is known to work with Evolution, Lightning, iPhone -and Android clients. It is free and open-source software, released under GPL -version 3. - -For further information, please visit the `Radicale Website -`_. - -""" - -import os -import sys - from setuptools import find_packages, setup # When the version is updated, a new section in the CHANGELOG.md file must be # added too. VERSION = "master" -WEB_FILES = ["web/internal_data/css/icon.png", + +with open("README.md", encoding="utf-8") as f: + long_description = f.read() +web_files = ["web/internal_data/css/icon.png", "web/internal_data/css/main.css", "web/internal_data/fn.js", "web/internal_data/index.html"] -setup_requires = [] -if {"pytest", "test", "ptr"}.intersection(sys.argv): - setup_requires.append("pytest-runner") -tests_require = ["pytest-runner", "pytest<7", "pytest-cov", "pytest-flake8", - "pytest-isort", "typeguard", "waitress"] -os.environ["PYTEST_ADDOPTS"] = os.environ.get("PYTEST_ADDOPTS", "") -# Mypy only supports CPython -if sys.implementation.name == "cpython": - tests_require.extend(["pytest-mypy", "types-setuptools"]) - os.environ["PYTEST_ADDOPTS"] += " --mypy" +install_requires = ["defusedxml", "passlib", "vobject>=0.9.6", + "python-dateutil>=2.7.3", + "setuptools; python_version<'3.9'"] +bcrypt_requires = ["passlib[bcrypt]", "bcrypt"] +# typeguard requires pytest<7 +test_requires = ["pytest<7", "typeguard", "waitress", *bcrypt_requires] +ldap_requires = ["python-ldap<=3.4.2"] setup( name="Radicale", version=VERSION, description="CalDAV and CardDAV Server", - long_description=__doc__, + long_description=long_description, + long_description_content_type="text/markdown", author="Guillaume Ayoub", author_email="guillaume.ayoub@kozea.fr", url="https://radicale.org/", - download_url=("https://pypi.python.org/packages/source/R/Radicale/" - "Radicale-%s.tar.gz" % VERSION), license="GNU GPL v3", platforms="Any", packages=find_packages( exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), - package_data={"radicale": [*WEB_FILES, "py.typed"]}, + package_data={"radicale": [*web_files, "py.typed"]}, entry_points={"console_scripts": ["radicale = radicale.__main__:run"]}, - install_requires=["defusedxml", "passlib", "vobject>=0.9.6", - "python-dateutil>=2.7.3", "setuptools"], - setup_requires=setup_requires, - tests_require=tests_require, - extras_require={"test": tests_require, - "bcrypt": ["passlib[bcrypt]", "bcrypt"]}, + install_requires=install_requires, + extras_require={"test": test_requires, "bcrypt": bcrypt_requires, "ldap": ldap_requires}, keywords=["calendar", "addressbook", "CalDAV", "CardDAV"], python_requires=">=3.6.0", classifiers=[ From 3ea2cda749181e82f6df2164991568dc48ac84af Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Mon, 26 Sep 2022 12:34:49 +0200 Subject: [PATCH 4/9] Add openldap-dev to Dockerfile --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1bfc82aca..e87e8aa6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,6 @@ EXPOSE 5232 CMD ["radicale", "--hosts", "0.0.0.0:5232"] RUN apk add --no-cache ca-certificates openssl \ - && apk add --no-cache --virtual .build-deps gcc libffi-dev musl-dev \ - && pip install --no-cache-dir "Radicale[bcrypt] @ https://github.com/Kozea/Radicale/archive/${VERSION}.tar.gz" \ - && apk del .build-deps + && apk add --no-cache --virtual .build-deps gcc libffi-dev musl-dev openldap-dev \ + && pip install --no-cache-dir "Radicale[bcrypt] @ https://github.com/Kozea/Radicale/archive/${VERSION}.tar.gz" \ + && apk del .build-deps From 43f5a0a11d224ff6ab8de2e8b541a75821eff5f7 Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Mon, 26 Sep 2022 12:54:13 +0200 Subject: [PATCH 5/9] Update ldap requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0114f2581..3807eef1a 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ bcrypt_requires = ["passlib[bcrypt]", "bcrypt"] # typeguard requires pytest<7 test_requires = ["pytest<7", "typeguard", "waitress", *bcrypt_requires] -ldap_requires = ["python-ldap<=3.4.2"] +ldap_requires = ["python-ldap>=3.4.3,<3.5.0"] setup( name="Radicale", From 025918e039a6f14073767f4c186b490d2d212b08 Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Mon, 26 Sep 2022 14:51:37 +0200 Subject: [PATCH 6/9] Restore deleted line --- radicale/auth/ldap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index b3c0536cc..2fc4473ee 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -59,6 +59,7 @@ def login(self, login: str, password: str) -> str: """ try: """Bind as reader dn""" + conn = ldap.initialize(self._ldap_uri) conn.protocol_version = 3 conn.set_option(ldap.OPT_REFERRALS, 0) conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret) From ca395045b41ece1c84ab82ef4e5c62bcb6a4dd3e Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Tue, 27 Sep 2022 10:11:57 +0200 Subject: [PATCH 7/9] Allow loading reader DN and secret through env var --- radicale/auth/ldap.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 2fc4473ee..3c5800020 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -24,6 +24,7 @@ ldap_load_groups If the groups of the authenticated users need to be loaded """ +import os import ldap from radicale import auth, config from radicale.log import logger @@ -41,9 +42,12 @@ def __init__(self, configuration: config.Configuration) -> None: super().__init__(configuration) self._ldap_uri = configuration.get("auth", "ldap_uri") self._ldap_base = configuration.get("auth", "ldap_base") - self._ldap_reader_dn = configuration.get("auth", "ldap_reader_dn") + + # Load LDAP reader details via env first if available + self._ldap_reader_dn = os.environ.get("AUTH_LDAP_READER_DN") if os.environ.get("AUTH_LDAP_READER_DN", False) else configuration.get("auth", "ldap_reader_dn") + self._ldap_secret = os.environ.get("AUTH_LDAP_SECRET") if os.environ.get("AUTH_LDAP_SECRET", False) else configuration.get("auth", "ldap_secret") + self._ldap_load_groups = configuration.get("auth", "ldap_load_groups") - self._ldap_secret = configuration.get("auth", "ldap_secret") self._ldap_filter = configuration.get("auth", "ldap_filter") self._ldaps_certificate = configuration.get("auth", "ldaps_certificate") # If a ldaps_certificate is set, configure ldap to use it From 052b381ebe215e2e03c7733bd9c68641b67ae6b1 Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Tue, 27 Sep 2022 10:13:45 +0200 Subject: [PATCH 8/9] Update documentation --- DOCUMENTATION.md | 6 ++++++ radicale/auth/ldap.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c9efc6d40..f6d369586 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -752,6 +752,12 @@ Load the ldap groups of the authenticated user. These groups can be used later o Default: False +##### ldaps_certificate + +Path to a certificate to authenticate against LDAPS server, e.g. for self-signed certificate or CAs. + +Default: None + #### rights ##### type diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 3c5800020..839aa7699 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -22,6 +22,7 @@ ldap_secret The password of the ldap_reader_dn ldap_filter The search filter to find the user to authenticate by the username ldap_load_groups If the groups of the authenticated users need to be loaded + ldaps_certificate The path to a certificate to validate ldaps with """ import os @@ -42,7 +43,7 @@ def __init__(self, configuration: config.Configuration) -> None: super().__init__(configuration) self._ldap_uri = configuration.get("auth", "ldap_uri") self._ldap_base = configuration.get("auth", "ldap_base") - + # Load LDAP reader details via env first if available self._ldap_reader_dn = os.environ.get("AUTH_LDAP_READER_DN") if os.environ.get("AUTH_LDAP_READER_DN", False) else configuration.get("auth", "ldap_reader_dn") self._ldap_secret = os.environ.get("AUTH_LDAP_SECRET") if os.environ.get("AUTH_LDAP_SECRET", False) else configuration.get("auth", "ldap_secret") From 266c3cedf39994fb56284bc50d274c463f4b0693 Mon Sep 17 00:00:00 2001 From: Eugene Davis Date: Tue, 27 Sep 2022 11:04:33 +0200 Subject: [PATCH 9/9] Get environment vars the smarter way --- radicale/auth/ldap.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 839aa7699..8b6b6d009 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -45,8 +45,8 @@ def __init__(self, configuration: config.Configuration) -> None: self._ldap_base = configuration.get("auth", "ldap_base") # Load LDAP reader details via env first if available - self._ldap_reader_dn = os.environ.get("AUTH_LDAP_READER_DN") if os.environ.get("AUTH_LDAP_READER_DN", False) else configuration.get("auth", "ldap_reader_dn") - self._ldap_secret = os.environ.get("AUTH_LDAP_SECRET") if os.environ.get("AUTH_LDAP_SECRET", False) else configuration.get("auth", "ldap_secret") + self._ldap_reader_dn = os.environ.get("AUTH__LDAP_READER_DN", configuration.get("auth", "ldap_reader_dn")) + self._ldap_secret = os.environ.get("AUTH__LDAP_SECRET", configuration.get("auth", "ldap_secret")) self._ldap_load_groups = configuration.get("auth", "ldap_load_groups") self._ldap_filter = configuration.get("auth", "ldap_filter")