ldap.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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_secret_file The path of the file containing the password of the ldap_reader_dn
  24. ldap_filter The search filter to find the user to authenticate by the username
  25. ldap_load_groups If the groups of the authenticated users need to be loaded
  26. Following parameters controls SSL connections:
  27. ldap_use_ssl If the connection
  28. ldap_ssl_verify_mode The certificate verification mode. NONE, OPTIONAL, default is REQUIRED
  29. ldap_ssl_ca_file
  30. """
  31. import ssl
  32. from radicale import auth, config
  33. from radicale.log import logger
  34. class Auth(auth.BaseAuth):
  35. _ldap_uri: str
  36. _ldap_base: str
  37. _ldap_reader_dn: str
  38. _ldap_secret: str
  39. _ldap_filter: str
  40. _ldap_load_groups: bool
  41. _ldap_version: int = 3
  42. _ldap_use_ssl: bool = False
  43. _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED
  44. _ldap_ssl_ca_file: str = ""
  45. def __init__(self, configuration: config.Configuration) -> None:
  46. super().__init__(configuration)
  47. try:
  48. import ldap3
  49. self.ldap3 = ldap3
  50. except ImportError:
  51. try:
  52. import ldap
  53. self._ldap_version = 2
  54. self.ldap = ldap
  55. except ImportError as e:
  56. raise RuntimeError("LDAP authentication requires the ldap3 module") from e
  57. self._ldap_uri = configuration.get("auth", "ldap_uri")
  58. self._ldap_base = configuration.get("auth", "ldap_base")
  59. self._ldap_reader_dn = configuration.get("auth", "ldap_reader_dn")
  60. self._ldap_load_groups = configuration.get("auth", "ldap_load_groups")
  61. self._ldap_secret = configuration.get("auth", "ldap_secret")
  62. self._ldap_filter = configuration.get("auth", "ldap_filter")
  63. ldap_secret_file_path = configuration.get("auth", "ldap_secret_file")
  64. if ldap_secret_file_path:
  65. with open(ldap_secret_file_path, 'r') as file:
  66. self._ldap_secret = file.read().rstrip('\n')
  67. if self._ldap_version == 3:
  68. self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl")
  69. if self._ldap_use_ssl:
  70. self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file")
  71. tmp = configuration.get("auth", "ldap_ssl_verify_mode")
  72. if tmp == "NONE":
  73. self._ldap_ssl_verify_mode = ssl.CERT_NONE
  74. elif tmp == "OPTIONAL":
  75. self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL
  76. def _login2(self, login: str, password: str) -> str:
  77. try:
  78. """Bind as reader dn"""
  79. logger.debug(f"_login2 {self._ldap_uri}, {self._ldap_reader_dn}")
  80. conn = self.ldap.initialize(self._ldap_uri)
  81. conn.protocol_version = 3
  82. conn.set_option(self.ldap.OPT_REFERRALS, 0)
  83. conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret)
  84. """Search for the dn of user to authenticate"""
  85. res = conn.search_s(self._ldap_base, self.ldap.SCOPE_SUBTREE, filterstr=self._ldap_filter.format(login), attrlist=['memberOf'])
  86. if len(res) == 0:
  87. """User could not be find"""
  88. return ""
  89. user_dn = res[0][0]
  90. logger.debug("LDAP Auth user: %s", user_dn)
  91. """Close ldap connection"""
  92. conn.unbind()
  93. except Exception as e:
  94. raise RuntimeError(f"Invalid ldap configuration:{e}")
  95. try:
  96. """Bind as user to authenticate"""
  97. conn = self.ldap.initialize(self._ldap_uri)
  98. conn.protocol_version = 3
  99. conn.set_option(self.ldap.OPT_REFERRALS, 0)
  100. conn.simple_bind_s(user_dn, password)
  101. tmp: list[str] = []
  102. if self._ldap_load_groups:
  103. tmp = []
  104. for t in res[0][1]['memberOf']:
  105. tmp.append(t.decode('utf-8').split(',')[0][3:])
  106. self._ldap_groups = set(tmp)
  107. logger.debug("LDAP Auth groups of user: %s", ",".join(self._ldap_groups))
  108. conn.unbind()
  109. return login
  110. except self.ldap.INVALID_CREDENTIALS:
  111. return ""
  112. def _login3(self, login: str, password: str) -> str:
  113. """Connect the server"""
  114. try:
  115. logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}")
  116. if self._ldap_use_ssl:
  117. tls = self.ldap3.Tls(validate=self._ldap_ssl_verify_mode)
  118. if self._ldap_ssl_ca_file != "":
  119. tls = self.ldap3.Tls(
  120. validate=self._ldap_ssl_verify_mode,
  121. ca_certs_file=self._ldap_ssl_ca_file
  122. )
  123. server = self.ldap3.Server(self._ldap_uri, use_ssl=True, tls=tls)
  124. else:
  125. server = self.ldap3.Server(self._ldap_uri)
  126. conn = self.ldap3.Connection(server, self._ldap_reader_dn, password=self._ldap_secret)
  127. except self.ldap3.core.exceptions.LDAPSocketOpenError:
  128. raise RuntimeError("Unable to reach ldap server")
  129. except Exception as e:
  130. logger.debug(f"_login3 error 1 {e}")
  131. pass
  132. if not conn.bind():
  133. logger.debug("_login3 can not bind")
  134. raise RuntimeError("Unable to read from ldap server")
  135. logger.debug(f"_login3 bind as {self._ldap_reader_dn}")
  136. """Search the user dn"""
  137. conn.search(
  138. search_base=self._ldap_base,
  139. search_filter=self._ldap_filter.format(login),
  140. search_scope=self.ldap3.SUBTREE,
  141. attributes=['memberOf']
  142. )
  143. if len(conn.entries) == 0:
  144. logger.debug(f"_login3 user '{login}' can not be find")
  145. """User could not be find"""
  146. return ""
  147. user_entry = conn.response[0]
  148. conn.unbind()
  149. user_dn = user_entry['dn']
  150. logger.debug(f"_login3 found user_dn {user_dn}")
  151. try:
  152. """Try to bind as the user itself"""
  153. conn = self.ldap3.Connection(server, user_dn, password=password)
  154. if not conn.bind():
  155. logger.debug(f"_login3 user '{login}' can not be find")
  156. return ""
  157. if self._ldap_load_groups:
  158. tmp = []
  159. for g in user_entry['attributes']['memberOf']:
  160. tmp.append(g.split(',')[0][3:])
  161. self._ldap_groups = set(tmp)
  162. conn.unbind()
  163. logger.debug(f"_login3 {login} successfully authorized")
  164. return login
  165. except Exception as e:
  166. logger.debug(f"_login3 error 2 {e}")
  167. pass
  168. return ""
  169. def login(self, login: str, password: str) -> str:
  170. """Validate credentials.
  171. In first step we make a connection to the ldap server with the ldap_reader_dn credential.
  172. In next step the DN of the user to authenticate will be searched.
  173. In the last step the authentication of the user will be proceeded.
  174. """
  175. if self._ldap_version == 2:
  176. return self._login2(login, password)
  177. return self._login3(login, password)