ldap.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright 2022 Peter Varkoly
  3. #
  4. # This library is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Authentication backend that checks credentials with a ldap server.
  18. Following parameters are needed in the configuration:
  19. ldap_uri The ldap url to the server like ldap://localhost
  20. ldap_base The baseDN of the ldap server
  21. ldap_reader_dn The DN of a ldap user with read access to get the user accounts
  22. ldap_secret The password of the ldap_reader_dn
  23. ldap_filter The search filter to find the user to authenticate by the username
  24. ldap_load_groups If the groups of the authenticated users need to be loaded
  25. Following parameters controls SSL connections:
  26. ldap_use_ssl If the connection
  27. ldap_ssl_verify_mode The certifikat verification mode. NONE, OPTIONAL, default is REQUIRED
  28. ldap_ssl_ca_file
  29. """
  30. import ssl
  31. from radicale import auth, config
  32. from radicale.log import logger
  33. class Auth(auth.BaseAuth):
  34. _ldap_uri: str
  35. _ldap_base: str
  36. _ldap_reader_dn: str
  37. _ldap_secret: str
  38. _ldap_filter: str
  39. _ldap_load_groups: bool
  40. _ldap_version: int = 3
  41. _ldap_use_ssl: bool = False
  42. _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED
  43. _ldap_ssl_ca_file: str = ""
  44. def __init__(self, configuration: config.Configuration) -> None:
  45. super().__init__(configuration)
  46. try:
  47. import ldap3
  48. self.ldap3 = ldap3
  49. except ImportError:
  50. try:
  51. import ldap
  52. self._ldap_version = 2
  53. self.ldap = ldap
  54. except ImportError as e:
  55. raise RuntimeError("LDAP authentication requires the ldap3 module") from e
  56. self._ldap_uri = configuration.get("auth", "ldap_uri")
  57. self._ldap_base = configuration.get("auth", "ldap_base")
  58. self._ldap_reader_dn = configuration.get("auth", "ldap_reader_dn")
  59. self._ldap_load_groups = configuration.get("auth", "ldap_load_groups")
  60. self._ldap_secret = configuration.get("auth", "ldap_secret")
  61. self._ldap_filter = configuration.get("auth", "ldap_filter")
  62. if self._ldap_version == 3:
  63. self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl")
  64. if self._ldap_use_ssl:
  65. self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file")
  66. tmp = configuration.get("auth", "ldap_ssl_verify_mode")
  67. if tmp == "NONE":
  68. self._ldap_ssl_verify_mode = ssl.CERT_NONE
  69. elif tmp == "OPTIONAL":
  70. self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL
  71. def _login2(self, login: str, password: str) -> str:
  72. try:
  73. """Bind as reader dn"""
  74. logger.debug(f"_login2 {self._ldap_uri}, {self._ldap_reader_dn}")
  75. conn = self.ldap.initialize(self._ldap_uri)
  76. conn.protocol_version = 3
  77. conn.set_option(self.ldap.OPT_REFERRALS, 0)
  78. conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret)
  79. """Search for the dn of user to authenticate"""
  80. res = conn.search_s(self._ldap_base, self.ldap.SCOPE_SUBTREE, filterstr=self._ldap_filter.format(login), attrlist=['memberOf'])
  81. if len(res) == 0:
  82. """User could not be find"""
  83. return ""
  84. user_dn = res[0][0]
  85. logger.debug("LDAP Auth user: %s", user_dn)
  86. """Close ldap connection"""
  87. conn.unbind()
  88. except Exception as e:
  89. raise RuntimeError(f"Invalid ldap configuration:{e}")
  90. try:
  91. """Bind as user to authenticate"""
  92. conn = self.ldap.initialize(self._ldap_uri)
  93. conn.protocol_version = 3
  94. conn.set_option(self.ldap.OPT_REFERRALS, 0)
  95. conn.simple_bind_s(user_dn, password)
  96. tmp: list[str] = []
  97. if self._ldap_load_groups:
  98. tmp = []
  99. for t in res[0][1]['memberOf']:
  100. tmp.append(t.decode('utf-8').split(',')[0][3:])
  101. self._ldap_groups = set(tmp)
  102. logger.debug("LDAP Auth groups of user: %s", ",".join(self._ldap_groups))
  103. conn.unbind()
  104. return login
  105. except self.ldap.INVALID_CREDENTIALS:
  106. return ""
  107. def _login3(self, login: str, password: str) -> str:
  108. """Connect the server"""
  109. try:
  110. logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}")
  111. if self._ldap_use_ssl:
  112. tls = self.ldap3.Tls(validate=self._ldap_ssl_verify_mode)
  113. if self._ldap_ssl_ca_file != "":
  114. tls = self.ldap3.Tls(
  115. validate=self._ldap_ssl_verify_mode,
  116. ca_certs_file=self._ldap_ssl_ca_file
  117. )
  118. server = self.ldap3.Server(self._ldap_uri, use_ssl=True, tls=tls)
  119. else:
  120. server = self.ldap3.Server(self._ldap_uri)
  121. conn = self.ldap3.Connection(server, self._ldap_reader_dn, password=self._ldap_secret)
  122. except self.ldap3.core.exceptions.LDAPSocketOpenError:
  123. raise RuntimeError("Unable to reach ldap server")
  124. except Exception as e:
  125. logger.debug(f"_login3 error 1 {e}")
  126. pass
  127. if not conn.bind():
  128. logger.debug("_login3 can not bind")
  129. raise RuntimeError("Unable to read from ldap server")
  130. logger.debug(f"_login3 bind as {self._ldap_reader_dn}")
  131. """Search the user dn"""
  132. conn.search(
  133. search_base=self._ldap_base,
  134. search_filter=self._ldap_filter.format(login),
  135. search_scope=self.ldap3.SUBTREE,
  136. attributes=['memberOf']
  137. )
  138. if len(conn.entries) == 0:
  139. logger.debug(f"_login3 user '{login}' can not be find")
  140. """User could not be find"""
  141. return ""
  142. user_entry = conn.response[0]
  143. conn.unbind()
  144. user_dn = user_entry['dn']
  145. logger.debug(f"_login3 found user_dn {user_dn}")
  146. try:
  147. """Try to bind as the user itself"""
  148. conn = self.ldap3.Connection(server, user_dn, password=password)
  149. if not conn.bind():
  150. logger.debug(f"_login3 user '{login}' can not be find")
  151. return ""
  152. if self._ldap_load_groups:
  153. tmp = []
  154. for g in user_entry['attributes']['memberOf']:
  155. tmp.append(g.split(',')[0][3:])
  156. self._ldap_groups = set(tmp)
  157. conn.unbind()
  158. logger.debug(f"_login3 {login} successfully authorized")
  159. return login
  160. except Exception as e:
  161. logger.debug(f"_login3 error 2 {e}")
  162. pass
  163. return ""
  164. def login(self, login: str, password: str) -> str:
  165. """Validate credentials.
  166. In first step we make a connection to the ldap server with the ldap_reader_dn credential.
  167. In next step the DN of the user to authenticate will be searched.
  168. In the last step the authentication of the user will be proceeded.
  169. """
  170. if self._ldap_version == 2:
  171. return self._login2(login, password)
  172. return self._login3(login, password)