Browse Source

Initial version of ldap authentication backend.

Peter Varkoly 4 years ago
parent
commit
2dc0fd29dc
4 changed files with 156 additions and 2 deletions
  1. 39 0
      DOCUMENTATION.md
  2. 1 1
      radicale/auth/__init__.py
  3. 90 0
      radicale/auth/ldap.py
  4. 26 1
      radicale/config.py

+ 39 - 0
DOCUMENTATION.md

@@ -667,6 +667,9 @@ Available backends:
   authentication. This can be used to provide the username from a reverse
   proxy.
 
+`ldap`
+: Use a LDAP or AD server to authenticate users.
+
 Default: `none`
 
 ##### htpasswd_filename
@@ -713,6 +716,42 @@ Message displayed in the client when a password is needed.
 
 Default: `Radicale - Password Required`
 
+##### ldap_uri
+
+The URI to the ldap server
+
+Default: `ldap://localhost`
+
+##### ldap_base
+
+LDAP base DN of the ldap server. This parameter must be provided if auth type is ldap.
+
+Default:
+
+##### ldap_reader_dn
+
+The DN of a ldap user with read access to get the user accounts. This parameter must be provided if auth type is ldap.
+
+Default:
+
+##### ldap_secret
+
+The password of the ldap_reader_dn. This parameter must be provided if auth type is ldap.
+
+Default:
+
+##### ldap_filter
+
+The search filter to find the user DN to authenticate by the username. User '{0}' as placeholder for the user name.
+
+Default: `(cn={0})`
+
+##### ldap_load_groups
+
+Load the ldap groups of the authenticated user. These groups can be used later on to define rights.
+
+Default: False
+
 #### rights
 
 ##### type

+ 1 - 1
radicale/auth/__init__.py

@@ -33,7 +33,7 @@ from typing import Sequence, Tuple, Union
 from radicale import config, types, utils
 
 INTERNAL_TYPES: Sequence[str] = ("none", "remote_user", "http_x_remote_user",
-                                 "htpasswd")
+                                 "htpasswd", "ldap")
 
 
 def load(configuration: "config.Configuration") -> "BaseAuth":

+ 90 - 0
radicale/auth/ldap.py

@@ -0,0 +1,90 @@
+# This file is part of Radicale - CalDAV and CardDAV server
+# Copyright 2022 Peter Varkoly
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
+"""
+Authentication backend that checks credentials with a ldap server.
+Following parameters are needed in the configuration
+   ldap_uri       The ldap url to the server like ldap://localhost
+   ldap_base      The baseDN of the ldap server
+   ldap_reader_dn The DN of a ldap user with read access to get the user accounts
+   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
+"""
+
+import ldap
+from radicale import auth, config
+from radicale.log import logger
+
+class Auth(auth.BaseAuth):
+    _ldap_uri: str
+    _ldap_base: str
+    _ldap_reader_dn: str
+    _ldap_secret: str
+    _ldap_filter: str
+    _ldap_load_groups: bool
+    _ldap_groups = []
+
+    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")
+        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")
+
+    def login(self, login: str, password: str) -> str:
+        """Validate credentials.
+        In first step we make a connection to the ldap server with the ldap_reader_dn credential.
+        In next step the DN of the user to authenticate will be searched.
+        In the last step the authentication of the user will be proceeded.
+
+        """
+        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'])
+            if len(res) == 0:
+                """User could not be find"""
+                return ""
+            user_dn = res[0][0]
+            logger.debug("LDAP Auth user: %s",user_dn)
+            """Close ldap connection"""
+            conn.unbind()
+        except Exception:
+            raise RuntimeError("Invalide 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)
+            if self._ldap_load_groups:
+                self._ldap_groups = []
+                for t in res[0][1]['memberOf']:
+                    self._ldap_groups.append(t.decode('utf-8').split(',')[0][3:])
+                logger.debug("LDAP Auth groups of user: %s",",".join(self._ldap_groups))
+            conn.unbind()
+            return login
+        except ldap.INVALID_CREDENTIALS:
+            return ""
+
+

+ 26 - 1
radicale/config.py

@@ -177,7 +177,32 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
         ("delay", {
             "value": "1",
             "help": "incorrect authentication delay",
-            "type": positive_float})])),
+            "type": positive_float}),
+        ("ldap_uri", {
+            "value": "ldap://localhost",
+            "help": "URI to the ldap server",
+            "type": str}),
+        ("ldap_base", {
+            "value": "none",
+            "help": "LDAP base DN of the ldap server",
+            "type": str}),
+        ("ldap_reader_dn", {
+            "value": "none",
+            "help": "the DN of a ldap user with read access to get the user accounts",
+            "type": str}),
+        ("ldap_secret", {
+            "value": "none",
+            "help": "the password of the ldap_reader_dn",
+            "type": str}),
+        ("ldap_filter", {
+            "value": "(cn={0})",
+            "help": "the search filter to find the user DN to authenticate by the username",
+            "type": str}),
+        ("ldap_load_groups", {
+            "value": "False",
+            "help": "load the ldap groups of the authenticated user",
+            "type": bool}),
+        ])),
     ("rights", OrderedDict([
         ("type", {
             "value": "owner_only",