ldap.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2022-2024 Peter Varkoly
  3. # Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
  4. #
  5. # This library is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  17. """
  18. Authentication backend that checks credentials with a LDAP server.
  19. Following parameters are needed in the configuration:
  20. ldap_uri The LDAP URL to the server like ldap://localhost
  21. ldap_base The baseDN of the LDAP server
  22. ldap_reader_dn The DN of a LDAP user with read access to get the user accounts
  23. ldap_secret The password of the ldap_reader_dn
  24. ldap_secret_file The path of the file containing the password of the ldap_reader_dn
  25. ldap_filter The search filter to find the user to authenticate by the username
  26. ldap_user_attribute The attribute to be used as username after authentication
  27. ldap_groups_attribute The attribute containing group memberships in the LDAP user entry
  28. Following parameters controls SSL connections:
  29. ldap_use_ssl If ssl encryption should be used (to be deprecated)
  30. ldap_security The encryption mode to be used: *none*|tls|starttls
  31. ldap_ssl_verify_mode The certificate verification mode. Works for tls and starttls. NONE, OPTIONAL, default is REQUIRED
  32. ldap_ssl_ca_file
  33. """
  34. import ssl
  35. from radicale import auth, config
  36. from radicale.log import logger
  37. class Auth(auth.BaseAuth):
  38. _ldap_uri: str
  39. _ldap_base: str
  40. _ldap_reader_dn: str
  41. _ldap_secret: str
  42. _ldap_filter: str
  43. _ldap_attributes: list[str] = []
  44. _ldap_user_attr: str
  45. _ldap_groups_attr: str
  46. _ldap_module_version: int = 3
  47. _ldap_use_ssl: bool = False
  48. _ldap_security: str = "none"
  49. _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED
  50. _ldap_ssl_ca_file: str = ""
  51. def __init__(self, configuration: config.Configuration) -> None:
  52. super().__init__(configuration)
  53. try:
  54. import ldap3
  55. self.ldap3 = ldap3
  56. except ImportError:
  57. try:
  58. import ldap
  59. self._ldap_module_version = 2
  60. self.ldap = ldap
  61. except ImportError as e:
  62. raise RuntimeError("LDAP authentication requires the ldap3 module") from e
  63. self._ldap_ignore_attribute_create_modify_timestamp = configuration.get("auth", "ldap_ignore_attribute_create_modify_timestamp")
  64. if self._ldap_ignore_attribute_create_modify_timestamp:
  65. self.ldap3.utils.config._ATTRIBUTES_EXCLUDED_FROM_CHECK.extend(['createTimestamp', 'modifyTimestamp'])
  66. logger.info("auth.ldap_ignore_attribute_create_modify_timestamp applied")
  67. self._ldap_uri = configuration.get("auth", "ldap_uri")
  68. self._ldap_base = configuration.get("auth", "ldap_base")
  69. self._ldap_reader_dn = configuration.get("auth", "ldap_reader_dn")
  70. self._ldap_secret = configuration.get("auth", "ldap_secret")
  71. self._ldap_filter = configuration.get("auth", "ldap_filter")
  72. self._ldap_user_attr = configuration.get("auth", "ldap_user_attribute")
  73. self._ldap_groups_attr = configuration.get("auth", "ldap_groups_attribute")
  74. ldap_secret_file_path = configuration.get("auth", "ldap_secret_file")
  75. if ldap_secret_file_path:
  76. with open(ldap_secret_file_path, 'r') as file:
  77. self._ldap_secret = file.read().rstrip('\n')
  78. if self._ldap_module_version == 3:
  79. self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl")
  80. self._ldap_security = configuration.get("auth", "ldap_security")
  81. self._use_encryption = self._ldap_use_ssl or self._ldap_security in ("tls", "starttls")
  82. if self._ldap_use_ssl and self._ldap_security == "starttls":
  83. raise RuntimeError("Cannot set both 'ldap_use_ssl = True' and 'ldap_security' = 'starttls'")
  84. if self._ldap_use_ssl:
  85. logger.warning("Configuration uses soon to be deprecated 'ldap_use_ssl', use 'ldap_security' ('none', 'tls', 'starttls') instead.")
  86. if self._use_encryption:
  87. self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file")
  88. tmp = configuration.get("auth", "ldap_ssl_verify_mode")
  89. if tmp == "NONE":
  90. self._ldap_ssl_verify_mode = ssl.CERT_NONE
  91. elif tmp == "OPTIONAL":
  92. self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL
  93. logger.info("auth.ldap_uri : %r" % self._ldap_uri)
  94. logger.info("auth.ldap_base : %r" % self._ldap_base)
  95. logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn)
  96. logger.info("auth.ldap_filter : %r" % self._ldap_filter)
  97. if self._ldap_user_attr:
  98. logger.info("auth.ldap_user_attribute : %r" % self._ldap_user_attr)
  99. else:
  100. logger.info("auth.ldap_user_attribute : (not provided)")
  101. if self._ldap_groups_attr:
  102. logger.info("auth.ldap_groups_attribute: %r" % self._ldap_groups_attr)
  103. else:
  104. logger.info("auth.ldap_groups_attribute: (not provided)")
  105. if ldap_secret_file_path:
  106. logger.info("auth.ldap_secret_file_path: %r" % ldap_secret_file_path)
  107. if self._ldap_secret:
  108. logger.info("auth.ldap_secret : (from file)")
  109. else:
  110. logger.info("auth.ldap_secret_file_path: (not provided)")
  111. if self._ldap_secret:
  112. logger.info("auth.ldap_secret : (from config)")
  113. if self._ldap_reader_dn and not self._ldap_secret:
  114. logger.error("auth.ldap_secret : (not provided)")
  115. raise RuntimeError("LDAP authentication requires ldap_secret for ldap_reader_dn")
  116. logger.info("auth.ldap_use_ssl : %s" % self._ldap_use_ssl)
  117. logger.info("auth.ldap_security : %s" % self._ldap_security)
  118. if self._use_encryption:
  119. logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode)
  120. if self._ldap_ssl_ca_file:
  121. logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file)
  122. else:
  123. logger.info("auth.ldap_ssl_ca_file : (not provided)")
  124. """Extend attributes to to be returned in the user query"""
  125. if self._ldap_groups_attr:
  126. self._ldap_attributes.append(self._ldap_groups_attr)
  127. if self._ldap_user_attr:
  128. self._ldap_attributes.append(self._ldap_user_attr)
  129. logger.info("ldap_attributes : %r" % self._ldap_attributes)
  130. def _login2(self, login: str, password: str) -> str:
  131. try:
  132. """Bind as reader dn"""
  133. logger.debug(f"_login2 {self._ldap_uri}, {self._ldap_reader_dn}")
  134. conn = self.ldap.initialize(self._ldap_uri)
  135. conn.protocol_version = 3
  136. conn.set_option(self.ldap.OPT_REFERRALS, 0)
  137. conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret)
  138. """Search for the dn of user to authenticate"""
  139. escaped_login = self.ldap.filter.escape_filter_chars(login)
  140. logger.debug(f"_login2 login escaped for LDAP filters: {escaped_login}")
  141. res = conn.search_s(
  142. self._ldap_base,
  143. self.ldap.SCOPE_SUBTREE,
  144. filterstr=self._ldap_filter.format(escaped_login),
  145. attrlist=self._ldap_attributes
  146. )
  147. if len(res) != 1:
  148. """User could not be found unambiguously"""
  149. logger.debug(f"_login2 no unique DN found for '{login}'")
  150. return ""
  151. user_entry = res[0]
  152. user_dn = user_entry[0]
  153. logger.debug(f"_login2 found LDAP user DN {user_dn}")
  154. """Close LDAP connection"""
  155. conn.unbind()
  156. except Exception as e:
  157. raise RuntimeError(f"Invalid LDAP configuration:{e}")
  158. try:
  159. """Bind as user to authenticate"""
  160. conn = self.ldap.initialize(self._ldap_uri)
  161. conn.protocol_version = 3
  162. conn.set_option(self.ldap.OPT_REFERRALS, 0)
  163. conn.simple_bind_s(user_dn, password)
  164. tmp: list[str] = []
  165. if self._ldap_groups_attr:
  166. tmp = []
  167. for g in user_entry[1][self._ldap_groups_attr]:
  168. """Get group g's RDN's attribute value"""
  169. try:
  170. rdns = self.ldap.dn.explode_dn(g, notypes=True)
  171. tmp.append(rdns[0])
  172. except Exception:
  173. tmp.append(g.decode('utf8'))
  174. self._ldap_groups = set(tmp)
  175. logger.debug("_login2 LDAP groups of user: %s", ",".join(self._ldap_groups))
  176. if self._ldap_user_attr:
  177. if user_entry[1][self._ldap_user_attr]:
  178. tmplogin = user_entry[1][self._ldap_user_attr][0]
  179. login = tmplogin.decode('utf-8')
  180. logger.debug(f"_login2 user set to: '{login}'")
  181. conn.unbind()
  182. logger.debug(f"_login2 {login} successfully authenticated")
  183. return login
  184. except self.ldap.INVALID_CREDENTIALS:
  185. return ""
  186. def _login3(self, login: str, password: str) -> str:
  187. """Connect the server"""
  188. try:
  189. logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}")
  190. if self._use_encryption:
  191. logger.debug("_login3 using encryption (reader)")
  192. tls = self.ldap3.Tls(validate=self._ldap_ssl_verify_mode)
  193. if self._ldap_ssl_ca_file != "":
  194. tls = self.ldap3.Tls(
  195. validate=self._ldap_ssl_verify_mode,
  196. ca_certs_file=self._ldap_ssl_ca_file
  197. )
  198. if self._ldap_use_ssl or self._ldap_security == "tls":
  199. logger.debug("_login3 using ssl (reader)")
  200. server = self.ldap3.Server(self._ldap_uri, use_ssl=True, tls=tls)
  201. else:
  202. server = self.ldap3.Server(self._ldap_uri, use_ssl=False, tls=tls)
  203. else:
  204. logger.debug("_login3 not using encryption (reader)")
  205. server = self.ldap3.Server(self._ldap_uri)
  206. try:
  207. conn = self.ldap3.Connection(server, self._ldap_reader_dn, password=self._ldap_secret, auto_bind=False, raise_exceptions=True)
  208. if self._ldap_security == "starttls":
  209. logger.debug("_login3 using starttls (reader)")
  210. conn.start_tls()
  211. except self.ldap3.core.exceptions.LDAPStartTLSError as e:
  212. raise RuntimeError(f"_login3 StartTLS Error: {e}")
  213. except self.ldap3.core.exceptions.LDAPSocketOpenError:
  214. raise RuntimeError("Unable to reach LDAP server")
  215. except Exception as e:
  216. logger.debug(f"_login3 error 1 {e} (reader)")
  217. pass
  218. if not conn.bind(read_server_info=False):
  219. logger.debug("_login3 cannot bind (reader)")
  220. raise RuntimeError("Unable to read from LDAP server")
  221. logger.debug(f"_login3 bind as {self._ldap_reader_dn}")
  222. """Search the user dn"""
  223. escaped_login = self.ldap3.utils.conv.escape_filter_chars(login)
  224. logger.debug(f"_login3 login escaped for LDAP filters: {escaped_login}")
  225. conn.search(
  226. search_base=self._ldap_base,
  227. search_filter=self._ldap_filter.format(escaped_login),
  228. search_scope=self.ldap3.SUBTREE,
  229. attributes=self._ldap_attributes
  230. )
  231. if len(conn.entries) != 1:
  232. """User could not be found unambiguously"""
  233. logger.debug(f"_login3 no unique DN found for '{login}'")
  234. return ""
  235. user_entry = conn.response[0]
  236. conn.unbind()
  237. user_dn = user_entry['dn']
  238. logger.debug(f"_login3 found LDAP user DN {user_dn}")
  239. try:
  240. """Try to bind as the user itself"""
  241. try:
  242. conn = self.ldap3.Connection(server, user_dn, password=password, auto_bind=False)
  243. if self._ldap_security == "starttls":
  244. logger.debug("_login3 using starttls (user)")
  245. conn.start_tls()
  246. except self.ldap3.core.exceptions.LDAPStartTLSError as e:
  247. raise RuntimeError(f"_login3 StartTLS Error: {e}")
  248. if not conn.bind(read_server_info=False):
  249. logger.debug(f"_login3 user '{login}' cannot be found")
  250. return ""
  251. tmp: list[str] = []
  252. if self._ldap_groups_attr:
  253. tmp = []
  254. for g in user_entry['attributes'][self._ldap_groups_attr]:
  255. """Get group g's RDN's attribute value"""
  256. try:
  257. rdns = self.ldap3.utils.dn.parse_dn(g)
  258. tmp.append(rdns[0][1])
  259. except Exception:
  260. tmp.append(g)
  261. self._ldap_groups = set(tmp)
  262. logger.debug("_login3 LDAP groups of user: %s", ",".join(self._ldap_groups))
  263. if self._ldap_user_attr:
  264. if user_entry['attributes'][self._ldap_user_attr]:
  265. if isinstance(user_entry['attributes'][self._ldap_user_attr], list):
  266. login = user_entry['attributes'][self._ldap_user_attr][0]
  267. else:
  268. login = user_entry['attributes'][self._ldap_user_attr]
  269. logger.debug(f"_login3 user set to: '{login}'")
  270. conn.unbind()
  271. logger.debug(f"_login3 {login} successfully authenticated")
  272. return login
  273. except Exception as e:
  274. logger.debug(f"_login3 error 2 {e}")
  275. pass
  276. return ""
  277. def _login(self, login: str, password: str) -> str:
  278. """Validate credentials.
  279. In first step we make a connection to the LDAP server with the ldap_reader_dn credential.
  280. In next step the DN of the user to authenticate will be searched.
  281. In the last step the authentication of the user will be proceeded.
  282. """
  283. if self._ldap_module_version == 2:
  284. return self._login2(login, password)
  285. return self._login3(login, password)