_auth_management.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. # Copyright (c) "Neo4j"
  2. # Neo4j Sweden AB [https://neo4j.com]
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # https://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from __future__ import annotations
  16. import abc
  17. import time
  18. import typing as t
  19. from dataclasses import dataclass
  20. from .api import Auth
  21. from .exceptions import AuthError
  22. if t.TYPE_CHECKING:
  23. from os import PathLike
  24. from typing_extensions import Protocol as _Protocol
  25. from .api import _TAuth
  26. from .exceptions import Neo4jError
  27. else:
  28. _Protocol = object
  29. @dataclass
  30. class ExpiringAuth:
  31. """
  32. Represents potentially expiring authentication information.
  33. This class is used with :meth:`.AuthManagers.bearer` and
  34. :meth:`.AsyncAuthManagers.bearer`.
  35. :param auth: The authentication information.
  36. :param expires_at:
  37. Unix timestamp (seconds since 1970-01-01 00:00:00 UTC)
  38. indicating when the authentication information expires.
  39. If :data:`None`, the authentication information is considered to not
  40. expire until the server explicitly indicates so.
  41. .. seealso::
  42. :meth:`.AuthManagers.bearer`,
  43. :meth:`.AsyncAuthManagers.bearer`
  44. .. versionadded:: 5.8
  45. .. versionchanged:: 5.9
  46. * Removed parameter and attribute ``expires_in`` (relative expiration
  47. time). Replaced with ``expires_at`` (absolute expiration time).
  48. * :meth:`.expires_in` can be used to create an :class:`.ExpiringAuth`
  49. with a relative expiration time.
  50. .. versionchanged:: 5.14 Stabilized from preview.
  51. """
  52. auth: _TAuth
  53. expires_at: float | None = None
  54. def expires_in(self, seconds: float) -> ExpiringAuth:
  55. """
  56. Return a (flat) copy of this object with a new expiration time.
  57. This is a convenience method for creating an :class:`.ExpiringAuth`
  58. for a relative expiration time ("expires in" instead of "expires at").
  59. >>> import time, freezegun
  60. >>> with freezegun.freeze_time("1970-01-01 00:00:40"):
  61. ... ExpiringAuth(("user", "pass")).expires_in(2)
  62. ExpiringAuth(auth=('user', 'pass'), expires_at=42.0)
  63. >>> with freezegun.freeze_time("1970-01-01 00:00:40"):
  64. ... ExpiringAuth(("user", "pass"), time.time() + 2)
  65. ExpiringAuth(auth=('user', 'pass'), expires_at=42.0)
  66. :param seconds:
  67. The number of seconds from now until the authentication information
  68. expires.
  69. .. versionadded:: 5.9
  70. """
  71. return ExpiringAuth(self.auth, time.time() + seconds)
  72. def expiring_auth_has_expired(auth: ExpiringAuth) -> bool:
  73. expires_at = auth.expires_at
  74. return expires_at is not None and expires_at < time.time()
  75. class AuthManager(metaclass=abc.ABCMeta):
  76. """
  77. Abstract base class for authentication information managers.
  78. The driver provides some default implementations of this class in
  79. :class:`.AuthManagers` for convenience.
  80. Custom implementations of this class can be used to provide more complex
  81. authentication refresh functionality.
  82. .. warning::
  83. The manager **must not** interact with the driver in any way as this
  84. can cause deadlocks and undefined behaviour.
  85. Furthermore, the manager is expected to be thread-safe.
  86. The token returned must always belong to the same identity.
  87. Switching identities using the `AuthManager` is undefined behavior.
  88. You may use :ref:`session-level authentication<session-auth-ref>`
  89. for such use-cases.
  90. .. seealso:: :class:`.AuthManagers`
  91. .. versionadded:: 5.8
  92. .. versionchanged:: 5.12
  93. ``on_auth_expired`` was removed from the interface and replaced by
  94. :meth:`handle_security_exception`. The new method is called when the
  95. server returns any `Neo.ClientError.Security.*` error. Its signature
  96. differs in that it additionally receives the error returned by the
  97. server and returns a boolean indicating whether the error was handled.
  98. .. versionchanged:: 5.14 Stabilized from preview.
  99. """
  100. @abc.abstractmethod
  101. def get_auth(self) -> _TAuth:
  102. """
  103. Return the current authentication information.
  104. The driver will call this method very frequently. It is recommended
  105. to implement some form of caching to avoid unnecessary overhead.
  106. .. warning::
  107. The method must only ever return auth information belonging to the
  108. same identity.
  109. Switching identities using the `AuthManager` is undefined behavior.
  110. You may use :ref:`session-level authentication<session-auth-ref>`
  111. for such use-cases.
  112. """
  113. ...
  114. @abc.abstractmethod
  115. def handle_security_exception(
  116. self, auth: _TAuth, error: Neo4jError
  117. ) -> bool:
  118. """
  119. Handle the server indicating authentication failure.
  120. The driver will call this method when the server returns any
  121. `Neo.ClientError.Security.*` error. The error will then be processed
  122. further as usual.
  123. :param auth:
  124. The authentication information that was used when the server
  125. returned the error.
  126. :param error:
  127. The error returned by the server.
  128. :returns:
  129. Whether the error was handled (:data:`True`), in which case the
  130. driver will mark the error as retryable
  131. (see :meth:`.Neo4jError.is_retryable`).
  132. .. versionadded:: 5.12
  133. """
  134. ...
  135. class AsyncAuthManager(_Protocol, metaclass=abc.ABCMeta):
  136. """
  137. Async version of :class:`.AuthManager`.
  138. .. seealso:: :class:`.AuthManager`
  139. .. versionadded:: 5.8
  140. .. versionchanged:: 5.12
  141. ``on_auth_expired`` was removed from the interface and replaced by
  142. :meth:`handle_security_exception`. See :class:`.AuthManager`.
  143. .. versionchanged:: 5.14 Stabilized from preview.
  144. """
  145. @abc.abstractmethod
  146. async def get_auth(self) -> _TAuth:
  147. """
  148. Async version of :meth:`.AuthManager.get_auth`.
  149. .. seealso:: :meth:`.AuthManager.get_auth`
  150. """
  151. ...
  152. @abc.abstractmethod
  153. async def handle_security_exception(
  154. self, auth: _TAuth, error: Neo4jError
  155. ) -> bool:
  156. """
  157. Async version of :meth:`.AuthManager.handle_security_exception`.
  158. .. seealso:: :meth:`.AuthManager.handle_security_exception`
  159. """
  160. ...
  161. @dataclass
  162. class ClientCertificate:
  163. """
  164. Simple data class to hold client certificate information.
  165. The attributes are the same as the arguments to
  166. :meth:`ssl.SSLContext.load_cert_chain()`.
  167. .. versionadded:: 5.19
  168. .. versionchanged:: 5.27 Stabilized from preview.
  169. """
  170. certfile: str | bytes | PathLike[str] | PathLike[bytes]
  171. keyfile: str | bytes | PathLike[str] | PathLike[bytes] | None = None
  172. password: t.Callable[[], str | bytes] | str | bytes | None = None
  173. class ClientCertificateProvider(_Protocol, metaclass=abc.ABCMeta):
  174. """
  175. Interface for providing a client certificate to the driver for mutual TLS.
  176. This is an abstract base class (:class:`abc.ABC`) as well as a protocol
  177. (:class:`typing.Protocol`). Meaning you can either inherit from it or just
  178. implement all required method on a class to satisfy the type constraints.
  179. The package provides some default implementations of this class in
  180. :class:`.ClientCertificateProviders` for convenience.
  181. The driver will call :meth:`.get_certificate` to check if the client wants
  182. the driver to use as new certificate for mutual TLS.
  183. The certificate is only used as a second factor for authenticating the
  184. client.
  185. The DBMS user still needs to authenticate with an authentication token.
  186. Note that the work done in the methods of this interface count towards the
  187. connection acquisition affected by the respective timeout setting
  188. :ref:`connection-acquisition-timeout-ref`.
  189. Should fetching the certificate be particularly slow, it might be necessary
  190. to increase the timeout.
  191. .. warning::
  192. The provider **must not** interact with the driver in any way as this
  193. can cause deadlocks and undefined behaviour.
  194. .. versionadded:: 5.19
  195. .. versionchanged:: 5.27 Stabilized from preview.
  196. """
  197. @abc.abstractmethod
  198. def get_certificate(self) -> ClientCertificate | None:
  199. """
  200. Return the new certificate (if present) to use for new connections.
  201. If no new certificate is available, return :data:`None`.
  202. This will make the driver continue using the current certificate.
  203. Note that a new certificate will only be used for new connections.
  204. Already established connections will continue using the old
  205. certificate as TLS is established during connection setup.
  206. :returns: The new certificate to use for new connections.
  207. """
  208. ...
  209. class AsyncClientCertificateProvider(_Protocol, metaclass=abc.ABCMeta):
  210. """
  211. Async version of :class:`.ClientCertificateProvider`.
  212. The package provides some default implementations of this class in
  213. :class:`.AsyncClientCertificateProviders` for convenience.
  214. .. seealso::
  215. :class:`.ClientCertificateProvider`,
  216. :class:`.AsyncClientCertificateProviders`
  217. .. versionadded:: 5.19
  218. .. versionchanged:: 5.27 Stabilized from preview.
  219. """
  220. @abc.abstractmethod
  221. async def get_certificate(self) -> ClientCertificate | None:
  222. """
  223. Return the new certificate (if present) to use for new connections.
  224. .. seealso:: :meth:`.ClientCertificateProvider.get_certificate`
  225. """
  226. ...
  227. def to_auth_dict(auth: _TAuth) -> dict[str, t.Any]:
  228. # Determine auth details
  229. if not auth:
  230. return {}
  231. elif isinstance(auth, tuple) and 2 <= len(auth) <= 3:
  232. return vars(Auth("basic", *auth))
  233. else:
  234. try:
  235. return vars(auth)
  236. except (KeyError, TypeError) as e:
  237. # TODO: 6.0 - change this to be a DriverError (or subclass)
  238. raise AuthError(
  239. f"Cannot determine auth details from {auth!r}"
  240. ) from e