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/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 diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index a55ceb655..8b6b6d009 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -22,12 +22,15 @@ 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 import ldap from radicale import auth, config from radicale.log import logger + class Auth(auth.BaseAuth): _ldap_uri: str _ldap_base: str @@ -38,12 +41,19 @@ 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") + + # Load LDAP reader details via env first if available + 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_secret = configuration.get("auth", "ldap_secret") - self._ldap_filter = configuration.get("auth", "ldap_filter") + 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. @@ -59,30 +69,38 @@ def login(self, login: str, password: str) -> str: 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..b43d97abe 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": None, + "help": "ldap server certificate", + "type": filepath}), + ])), ("rights", OrderedDict([ ("type", { "value": "owner_only", diff --git a/setup.py b/setup.py index 17cd74373..3807eef1a 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.3,<3.5.0"] 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=[