api.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  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. """Base classes and helpers."""
  16. from __future__ import annotations
  17. import abc
  18. import typing as t
  19. from urllib.parse import (
  20. parse_qs,
  21. urlparse,
  22. )
  23. if t.TYPE_CHECKING:
  24. from typing_extensions import deprecated
  25. else:
  26. from ._meta import deprecated
  27. from .exceptions import ConfigurationError
  28. if t.TYPE_CHECKING:
  29. import typing_extensions as te
  30. from typing_extensions import Protocol as _Protocol
  31. from .addressing import Address
  32. else:
  33. _Protocol = object
  34. __all__ = [
  35. "DEFAULT_DATABASE",
  36. "DRIVER_BOLT",
  37. "DRIVER_NEO4J",
  38. "READ_ACCESS",
  39. "SECURITY_TYPE_NOT_SECURE",
  40. "SECURITY_TYPE_SECURE",
  41. "SECURITY_TYPE_SELF_SIGNED_CERTIFICATE",
  42. "SYSTEM_DATABASE",
  43. "TRUST_ALL_CERTIFICATES",
  44. "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES",
  45. "URI_SCHEME_BOLT",
  46. "URI_SCHEME_BOLT_ROUTING",
  47. "URI_SCHEME_BOLT_SECURE",
  48. "URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE",
  49. "URI_SCHEME_NEO4J",
  50. "URI_SCHEME_NEO4J_SECURE",
  51. "URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE",
  52. "WRITE_ACCESS",
  53. "AsyncBookmarkManager",
  54. "Auth",
  55. "AuthToken",
  56. "Bookmark",
  57. "BookmarkManager",
  58. "Bookmarks",
  59. "ServerInfo",
  60. "Version",
  61. "basic_auth",
  62. "bearer_auth",
  63. "check_access_mode",
  64. "custom_auth",
  65. "kerberos_auth",
  66. "parse_neo4j_uri",
  67. "parse_routing_context",
  68. ]
  69. READ_ACCESS: te.Final[str] = "READ"
  70. WRITE_ACCESS: te.Final[str] = "WRITE"
  71. # TODO: 6.0 - make these 2 constants private
  72. DRIVER_BOLT: te.Final[str] = "DRIVER_BOLT"
  73. DRIVER_NEO4J: te.Final[str] = "DRIVER_NEO4J"
  74. # TODO: 6.0 - make these 3 constants private
  75. SECURITY_TYPE_NOT_SECURE: te.Final[str] = "SECURITY_TYPE_NOT_SECURE"
  76. SECURITY_TYPE_SELF_SIGNED_CERTIFICATE: te.Final[str] = (
  77. "SECURITY_TYPE_SELF_SIGNED_CERTIFICATE"
  78. )
  79. SECURITY_TYPE_SECURE: te.Final[str] = "SECURITY_TYPE_SECURE"
  80. URI_SCHEME_BOLT: te.Final[str] = "bolt"
  81. URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE: te.Final[str] = "bolt+ssc"
  82. URI_SCHEME_BOLT_SECURE: te.Final[str] = "bolt+s"
  83. URI_SCHEME_NEO4J: te.Final[str] = "neo4j"
  84. URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE: te.Final[str] = "neo4j+ssc"
  85. URI_SCHEME_NEO4J_SECURE: te.Final[str] = "neo4j+s"
  86. URI_SCHEME_BOLT_ROUTING: te.Final[str] = "bolt+routing"
  87. # TODO: 6.0 - remove TRUST constants
  88. TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: te.Final[str] = (
  89. "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES" # Default
  90. )
  91. TRUST_ALL_CERTIFICATES: te.Final[str] = "TRUST_ALL_CERTIFICATES"
  92. SYSTEM_DATABASE: te.Final[str] = "system"
  93. DEFAULT_DATABASE: te.Final[None] = None # Must be a non string hashable value
  94. # TODO: This class is not tested
  95. class Auth:
  96. """
  97. Container for auth details.
  98. :param scheme: specifies the type of authentication, examples: "basic",
  99. "kerberos"
  100. :type scheme: str | None
  101. :param principal: specifies who is being authenticated
  102. :type principal: str | None
  103. :param credentials: authenticates the principal
  104. :type credentials: str | None
  105. :param realm: specifies the authentication provider
  106. :type realm: str | None
  107. :param parameters: extra key word parameters passed along to the
  108. authentication provider
  109. :type parameters: typing.Any
  110. """
  111. def __init__(
  112. self,
  113. scheme: str | None,
  114. principal: str | None,
  115. credentials: str | None,
  116. realm: str | None = None,
  117. **parameters: t.Any,
  118. ) -> None:
  119. self.scheme = scheme
  120. # Neo4j servers pre 4.4 require the principal field to always be
  121. # present. Therefore, we transmit it even if it's an empty sting.
  122. if principal is not None:
  123. self.principal = principal
  124. if credentials:
  125. self.credentials = credentials
  126. if realm:
  127. self.realm = realm
  128. if parameters:
  129. self.parameters = parameters
  130. def __eq__(self, other: t.Any) -> bool:
  131. if not isinstance(other, Auth):
  132. return NotImplemented
  133. return vars(self) == vars(other)
  134. # For backwards compatibility
  135. AuthToken = Auth
  136. if t.TYPE_CHECKING:
  137. _TAuth = t.Union[t.Tuple[str, str], Auth, None]
  138. def basic_auth(user: str, password: str, realm: str | None = None) -> Auth:
  139. """
  140. Generate a basic auth token for a given user and password.
  141. This will set the scheme to "basic" for the auth token.
  142. :param user: user name, this will set the principal
  143. :param password: current password, this will set the credentials
  144. :param realm: specifies the authentication provider
  145. :returns: auth token for use with :meth:`GraphDatabase.driver` or
  146. :meth:`AsyncGraphDatabase.driver`
  147. """
  148. return Auth("basic", user, password, realm)
  149. def kerberos_auth(base64_encoded_ticket: str) -> Auth:
  150. """
  151. Generate a kerberos auth token with the base64 encoded ticket.
  152. This will set the scheme to "kerberos" for the auth token.
  153. :param base64_encoded_ticket: a base64 encoded service ticket, this will
  154. set the credentials
  155. :returns: auth token for use with :meth:`GraphDatabase.driver` or
  156. :meth:`AsyncGraphDatabase.driver`
  157. """
  158. return Auth("kerberos", "", base64_encoded_ticket)
  159. def bearer_auth(base64_encoded_token: str) -> Auth:
  160. """
  161. Generate an auth token for Single-Sign-On providers.
  162. This will set the scheme to "bearer" for the auth token.
  163. :param base64_encoded_token: a base64 encoded authentication token
  164. generated by a Single-Sign-On provider.
  165. :returns: auth token for use with :meth:`GraphDatabase.driver` or
  166. :meth:`AsyncGraphDatabase.driver`
  167. """
  168. return Auth("bearer", None, base64_encoded_token)
  169. def custom_auth(
  170. principal: str | None,
  171. credentials: str | None,
  172. realm: str | None,
  173. scheme: str | None,
  174. **parameters: t.Any,
  175. ) -> Auth:
  176. """
  177. Generate a custom auth token.
  178. :param principal: specifies who is being authenticated
  179. :param credentials: authenticates the principal
  180. :param realm: specifies the authentication provider
  181. :param scheme: specifies the type of authentication
  182. :param parameters: extra key word parameters passed along to the
  183. authentication provider
  184. :returns: auth token for use with :meth:`GraphDatabase.driver` or
  185. :meth:`AsyncGraphDatabase.driver`
  186. """
  187. return Auth(scheme, principal, credentials, realm, **parameters)
  188. # TODO: 6.0 - remove this class
  189. @deprecated("Use the `Bookmarks` class instead.")
  190. class Bookmark:
  191. """
  192. A Bookmark object contains an immutable list of bookmark string values.
  193. :param values: ASCII string values
  194. .. deprecated:: 5.0
  195. `Bookmark` will be removed in version 6.0.
  196. Use :class:`Bookmarks` instead.
  197. """
  198. def __init__(self, *values: str) -> None:
  199. if values:
  200. bookmarks = []
  201. for ix in values:
  202. try:
  203. if ix:
  204. ix.encode("ascii")
  205. bookmarks.append(ix)
  206. except UnicodeEncodeError as e:
  207. raise ValueError(f"The value {ix} is not ASCII") from e
  208. self._values = frozenset(bookmarks)
  209. else:
  210. self._values = frozenset()
  211. def __repr__(self) -> str:
  212. """
  213. Represent the container as str.
  214. :returns: repr string with sorted values
  215. """
  216. values = ", ".join([f"'{ix}'" for ix in sorted(self._values)])
  217. return f"<Bookmark values={{{values}}}>"
  218. def __bool__(self) -> bool:
  219. return bool(self._values)
  220. @property
  221. def values(self) -> frozenset:
  222. """:returns: immutable list of bookmark string values"""
  223. return self._values
  224. class Bookmarks:
  225. """
  226. Container for an immutable set of bookmark string values.
  227. Bookmarks are used to causally chain sessions.
  228. See :meth:`Session.last_bookmarks` or :meth:`AsyncSession.last_bookmarks`
  229. for more information.
  230. Use addition to combine multiple Bookmarks objects::
  231. bookmarks3 = bookmarks1 + bookmarks2
  232. """
  233. def __init__(self):
  234. self._raw_values = frozenset()
  235. def __repr__(self) -> str:
  236. """
  237. Represent the container as str.
  238. :returns: repr string with sorted values
  239. """
  240. return "<Bookmarks values={{{}}}>".format(
  241. ", ".join(map(repr, sorted(self._raw_values)))
  242. )
  243. def __bool__(self) -> bool:
  244. """Indicate whether there are bookmarks in the container."""
  245. return bool(self._raw_values)
  246. def __add__(self, other: Bookmarks) -> Bookmarks:
  247. """Add multiple containers together."""
  248. if isinstance(other, Bookmarks):
  249. if not other:
  250. return self
  251. ret = self.__class__()
  252. ret._raw_values = self._raw_values | other._raw_values
  253. return ret
  254. return NotImplemented
  255. @property
  256. def raw_values(self) -> frozenset[str]:
  257. """
  258. The raw bookmark values.
  259. You should not need to access them unless you want to serialize
  260. bookmarks.
  261. :returns: immutable list of bookmark string values
  262. :rtype: frozenset[str]
  263. """
  264. return self._raw_values
  265. @classmethod
  266. def from_raw_values(cls, values: t.Iterable[str]) -> Bookmarks:
  267. """
  268. Create a Bookmarks object from a list of raw bookmark string values.
  269. You should not need to use this method unless you want to deserialize
  270. bookmarks.
  271. :param values: ASCII string values (raw bookmarks)
  272. """
  273. if isinstance(values, str):
  274. # Unfortunately, str itself is an iterable of str, iterating
  275. # over characters. Type checkers will not catch this, so we help
  276. # the user out.
  277. values = (values,)
  278. obj = cls()
  279. bookmarks = []
  280. for value in values:
  281. if not isinstance(value, str):
  282. raise TypeError(
  283. "Raw bookmark values must be str. " f"Found {type(value)}"
  284. )
  285. try:
  286. value.encode("ascii")
  287. except UnicodeEncodeError as e:
  288. raise ValueError(f"The value {value} is not ASCII") from e
  289. bookmarks.append(value)
  290. obj._raw_values = frozenset(bookmarks)
  291. return obj
  292. class ServerInfo:
  293. """Represents a package of information relating to a Neo4j server."""
  294. def __init__(self, address: Address, protocol_version: Version):
  295. self._address = address
  296. self._protocol_version = protocol_version
  297. self._metadata: dict = {}
  298. @property
  299. def address(self) -> Address:
  300. """Network address of the remote server."""
  301. return self._address
  302. @property
  303. def protocol_version(self) -> tuple[int, int]:
  304. """
  305. Bolt protocol version with which the remote server communicates.
  306. This is returned as a 2-tuple:class:`tuple` (subclass) of
  307. ``(major, minor)`` integers.
  308. """
  309. # TODO: 6.0 - remove cast when support for Python 3.7 is dropped
  310. return t.cast(t.Tuple[int, int], self._protocol_version)
  311. @property
  312. def agent(self) -> str:
  313. """Server agent string by which the remote server identifies itself."""
  314. return str(self._metadata.get("server"))
  315. @property # type: ignore
  316. @deprecated(
  317. "The connection id is considered internal information "
  318. "and will no longer be exposed in future versions."
  319. )
  320. def connection_id(self):
  321. """Unique identifier for the remote server connection."""
  322. return self._metadata.get("connection_id")
  323. def update(self, metadata: dict) -> None:
  324. """
  325. Update server information with extra metadata.
  326. This is typically drawn from the metadata received after successful
  327. connection initialisation.
  328. """
  329. self._metadata.update(metadata)
  330. # TODO: 6.0 - this class should not be public.
  331. # As far the user is concerned, protocol versions should simply be a
  332. # tuple[int, int].
  333. if t.TYPE_CHECKING:
  334. _version_base = t.Tuple[int, int]
  335. else:
  336. _version_base = tuple
  337. class Version(_version_base):
  338. def __new__(cls, *v):
  339. return super().__new__(cls, v)
  340. def __repr__(self):
  341. return f"{self.__class__.__name__}{super().__repr__()}"
  342. def __str__(self):
  343. return ".".join(map(str, self))
  344. def to_bytes(self) -> bytes:
  345. b = bytearray(4)
  346. for i, v in enumerate(self):
  347. if not 0 <= i < 2:
  348. raise ValueError("Too many version components")
  349. if isinstance(v, list):
  350. b[-i - 1] = int(v[0] % 0x100)
  351. b[-i - 2] = int((v[0] - v[-1]) % 0x100)
  352. else:
  353. b[-i - 1] = int(v % 0x100)
  354. return bytes(b)
  355. @classmethod
  356. def from_bytes(cls, b: bytes) -> Version:
  357. b = bytearray(b)
  358. if len(b) != 4:
  359. raise ValueError("Byte representation must be exactly four bytes")
  360. if b[0] != 0 or b[1] != 0:
  361. raise ValueError("First two bytes must contain zero")
  362. return Version(b[-1], b[-2])
  363. class BookmarkManager(_Protocol, metaclass=abc.ABCMeta):
  364. """
  365. Class to manage bookmarks throughout the driver's lifetime.
  366. Neo4j clusters are eventually consistent, meaning that there is no
  367. guarantee a query will be able to read changes made by a previous query.
  368. For cases where such a guarantee is necessary, the server provides
  369. bookmarks to the client. A bookmark is an abstract token that represents
  370. some state of the database. By passing one or multiple bookmarks along
  371. with a query, the server will make sure that the query will not get
  372. executed before the represented state(s) (or a later state) have been
  373. established.
  374. The bookmark manager is an interface used by the driver for keeping
  375. track of the bookmarks and this way keeping sessions automatically
  376. consistent. Configure the driver to use a specific bookmark manager with
  377. :ref:`bookmark-manager-ref`.
  378. This class is just an abstract base class that defines the required
  379. interface. Create a child class to implement a specific bookmark manager
  380. or make use of the default implementation provided by the driver through
  381. :meth:`.GraphDatabase.bookmark_manager`.
  382. .. note::
  383. All methods must be concurrency safe.
  384. .. versionadded:: 5.0
  385. .. versionchanged:: 5.3
  386. The bookmark manager no longer tracks bookmarks per database.
  387. This effectively changes the signature of almost all bookmark
  388. manager related methods:
  389. * :meth:`.update_bookmarks` has no longer a ``database`` argument.
  390. * :meth:`.get_bookmarks` has no longer a ``database`` argument.
  391. * The ``get_all_bookmarks`` method was removed.
  392. * The ``forget`` method was removed.
  393. .. versionchanged:: 5.8 Stabilized from experimental.
  394. """
  395. @abc.abstractmethod
  396. def update_bookmarks(
  397. self,
  398. previous_bookmarks: t.Collection[str],
  399. new_bookmarks: t.Collection[str],
  400. ) -> None:
  401. """
  402. Handle bookmark updates.
  403. :param previous_bookmarks:
  404. The bookmarks used at the start of a transaction
  405. :param new_bookmarks:
  406. The new bookmarks retrieved at the end of a transaction
  407. """
  408. ...
  409. @abc.abstractmethod
  410. def get_bookmarks(self) -> t.Collection[str]:
  411. """
  412. Return the bookmarks stored in the bookmark manager.
  413. :returns: The bookmarks for the given database
  414. """
  415. ...
  416. class AsyncBookmarkManager(_Protocol, metaclass=abc.ABCMeta):
  417. """
  418. Same as :class:`.BookmarkManager` but with async methods.
  419. The driver comes with a default implementation of the async bookmark
  420. manager accessible through :attr:`.AsyncGraphDatabase.bookmark_manager()`.
  421. .. versionadded:: 5.0
  422. .. versionchanged:: 5.3
  423. See :class:`.BookmarkManager` for changes.
  424. .. versionchanged:: 5.8 Stabilized from experimental.
  425. """
  426. @abc.abstractmethod
  427. async def update_bookmarks(
  428. self,
  429. previous_bookmarks: t.Collection[str],
  430. new_bookmarks: t.Collection[str],
  431. ) -> None: ...
  432. update_bookmarks.__doc__ = BookmarkManager.update_bookmarks.__doc__
  433. @abc.abstractmethod
  434. async def get_bookmarks(self) -> t.Collection[str]: ...
  435. get_bookmarks.__doc__ = BookmarkManager.get_bookmarks.__doc__
  436. # TODO: 6.0 - make this function private
  437. def parse_neo4j_uri(uri):
  438. parsed = urlparse(uri)
  439. if parsed.username:
  440. raise ConfigurationError("Username is not supported in the URI")
  441. if parsed.password:
  442. raise ConfigurationError("Password is not supported in the URI")
  443. if parsed.scheme == URI_SCHEME_BOLT_ROUTING:
  444. raise ConfigurationError(
  445. f"Uri scheme {parsed.scheme!r} has been renamed. "
  446. f"Use {URI_SCHEME_NEO4J!r}"
  447. )
  448. elif parsed.scheme == URI_SCHEME_BOLT:
  449. driver_type = DRIVER_BOLT
  450. security_type = SECURITY_TYPE_NOT_SECURE
  451. elif parsed.scheme == URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE:
  452. driver_type = DRIVER_BOLT
  453. security_type = SECURITY_TYPE_SELF_SIGNED_CERTIFICATE
  454. elif parsed.scheme == URI_SCHEME_BOLT_SECURE:
  455. driver_type = DRIVER_BOLT
  456. security_type = SECURITY_TYPE_SECURE
  457. elif parsed.scheme == URI_SCHEME_NEO4J:
  458. driver_type = DRIVER_NEO4J
  459. security_type = SECURITY_TYPE_NOT_SECURE
  460. elif parsed.scheme == URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE:
  461. driver_type = DRIVER_NEO4J
  462. security_type = SECURITY_TYPE_SELF_SIGNED_CERTIFICATE
  463. elif parsed.scheme == URI_SCHEME_NEO4J_SECURE:
  464. driver_type = DRIVER_NEO4J
  465. security_type = SECURITY_TYPE_SECURE
  466. else:
  467. supported_schemes = [
  468. URI_SCHEME_BOLT,
  469. URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
  470. URI_SCHEME_BOLT_SECURE,
  471. URI_SCHEME_NEO4J,
  472. URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
  473. URI_SCHEME_NEO4J_SECURE,
  474. ]
  475. raise ConfigurationError(
  476. f"URI scheme {parsed.scheme!r} is not supported. "
  477. f"Supported URI schemes are {supported_schemes}. "
  478. "Examples: bolt://host[:port] or "
  479. "neo4j://host[:port][?routing_context]"
  480. )
  481. return driver_type, security_type, parsed
  482. # TODO: 6.0 - make this function private
  483. def check_access_mode(access_mode):
  484. if access_mode is None:
  485. return WRITE_ACCESS
  486. if access_mode not in {READ_ACCESS, WRITE_ACCESS}:
  487. msg = f"Unsupported access mode {access_mode}"
  488. raise ConfigurationError(msg)
  489. return access_mode
  490. # TODO: 6.0 - make this function private
  491. def parse_routing_context(query):
  492. """
  493. Parse the query portion of a URI.
  494. Generates a routing context dictionary.
  495. """
  496. if not query:
  497. return {}
  498. context = {}
  499. parameters = parse_qs(query, True)
  500. for key in parameters:
  501. value_list = parameters[key]
  502. if len(value_list) != 1:
  503. raise ConfigurationError(
  504. f"Duplicated query parameters with key '{key}', value "
  505. f"'{value_list}' found in query string '{query}'"
  506. )
  507. value = value_list[0]
  508. if not value:
  509. raise ConfigurationError(
  510. f"Invalid parameters:'{key}={value}' in query string "
  511. f"'{query}'."
  512. )
  513. context[key] = value
  514. return context