Skip to content

Refine TLS certificate handling #1079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.

## NEXT RELEASE
- No breaking or major changes.
- Made `neo4j.auth_management.RotatingClientCertificateProvider` and
`...AsyncRotatingClientCertificateProvider` (in preview)
abstract classes, meaning they can no longer be instantiated directly.
Please use the provided factory methods instead:
`neo4j.auth_management.RotatingClientCertificateProvider.rotating` and
`....AsyncRotatingClientCertificateProvider.rotating()` respectively.


## Version 5.23
Expand Down
9 changes: 9 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,17 @@ https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
.. versionadded:: 5.19

.. autoclass:: neo4j.auth_management.ClientCertificate
:members:

.. autoclass:: neo4j.auth_management.ClientCertificateProvider
:members:

.. autoclass:: neo4j.auth_management.ClientCertificateProviders
:members:

.. autoclass:: neo4j.auth_management.RotatingClientCertificateProvider
:show-inheritance:
:members:


.. _user-agent-ref:
Expand Down
8 changes: 8 additions & 0 deletions docs/source/async_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,14 @@ https://github.com/neo4j/neo4j-python-driver/wiki/preview-features
.. versionadded:: 5.19

.. autoclass:: neo4j.auth_management.AsyncClientCertificateProvider
:members:

.. autoclass:: neo4j.auth_management.AsyncClientCertificateProviders
:members:

.. autoclass:: neo4j.auth_management.AsyncRotatingClientCertificateProvider
:show-inheritance:
:members:



Expand Down
44 changes: 31 additions & 13 deletions src/neo4j/_async/auth_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import annotations

import abc
import typing as t
from logging import getLogger

Expand Down Expand Up @@ -110,7 +111,8 @@ async def handle_security_exception(


class AsyncAuthManagers:
"""A collection of :class:`.AsyncAuthManager` factories.
"""
A collection of :class:`.AsyncAuthManager` factories.

.. versionadded:: 5.8

Expand All @@ -124,7 +126,8 @@ class AsyncAuthManagers:

@staticmethod
def static(auth: _TAuth) -> AsyncAuthManager:
"""Create a static auth manager.
"""
Create a static auth manager.

The manager will always return the auth info provided at its creation.

Expand Down Expand Up @@ -163,7 +166,8 @@ def static(auth: _TAuth) -> AsyncAuthManager:
def basic(
provider: t.Callable[[], t.Awaitable[_TAuth]]
) -> AsyncAuthManager:
"""Create an auth manager handling basic auth password rotation.
"""
Create an auth manager handling basic auth password rotation.

This factory wraps the provider function in an auth manager
implementation that caches the provided auth info until the server
Expand Down Expand Up @@ -230,7 +234,8 @@ async def wrapped_provider() -> ExpiringAuth:
def bearer(
provider: t.Callable[[], t.Awaitable[ExpiringAuth]]
) -> AsyncAuthManager:
"""Create an auth manager for potentially expiring bearer auth tokens.
"""
Create an auth manager for potentially expiring bearer auth tokens.

This factory wraps the provider function in an auth manager
implementation that caches the provided auth info until either the
Expand Down Expand Up @@ -310,10 +315,9 @@ async def get_certificate(self) -> t.Optional[ClientCertificate]:
return cert


@preview("Mutual TLS is a preview feature.")
class AsyncRotatingClientCertificateProvider(AsyncClientCertificateProvider):
"""
Implementation of a certificate provider that can rotate certificates.
Abstract base class for certificate providers that can rotate certificates.

The provider will make the driver use the initial certificate for all
connections until the certificate is updated using the
Expand Down Expand Up @@ -367,10 +371,26 @@ class AsyncRotatingClientCertificateProvider(AsyncClientCertificateProvider):
# rotated again
...

:param initial_cert: The certificate to use initially.

.. versionadded:: 5.19

.. versionchanged:: 5.24

Turned this class into an abstract class to make the actual
implementation internal. This entails removing the possibility to
directly instantiate this class. Please use the factory method
:meth:`.AsyncClientCertificateProviders.rotating` instead.
"""

@abc.abstractmethod
async def update_certificate(self, cert: ClientCertificate) -> None:
"""
Update the certificate to use for new connections.
"""


class _AsyncNeo4jRotatingClientCertificateProvider(
AsyncRotatingClientCertificateProvider
):
def __init__(self, initial_cert: ClientCertificate) -> None:
self._cert: t.Optional[ClientCertificate] = initial_cert
self._lock = AsyncCooperativeLock()
Expand All @@ -381,15 +401,13 @@ async def get_certificate(self) -> t.Optional[ClientCertificate]:
return cert

async def update_certificate(self, cert: ClientCertificate) -> None:
"""
Update the certificate to use for new connections.
"""
async with self._lock:
self._cert = cert


class AsyncClientCertificateProviders:
"""A collection of :class:`.AsyncClientCertificateProvider` factories.
"""
A collection of :class:`.AsyncClientCertificateProvider` factories.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
Expand Down Expand Up @@ -419,4 +437,4 @@ def rotating(

.. seealso:: :class:`.AsyncRotatingClientCertificateProvider`
"""
return AsyncRotatingClientCertificateProvider(initial_cert)
return _AsyncNeo4jRotatingClientCertificateProvider(initial_cert)
33 changes: 24 additions & 9 deletions src/neo4j/_auth_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@

@dataclass
class ExpiringAuth:
"""Represents potentially expiring authentication information.
"""
Represents potentially expiring authentication information.

This class is used with :meth:`.AuthManagers.bearer` and
:meth:`.AsyncAuthManagers.bearer`.
Expand Down Expand Up @@ -68,7 +69,8 @@ class ExpiringAuth:
expires_at: t.Optional[float] = None

def expires_in(self, seconds: float) -> ExpiringAuth:
"""Return a (flat) copy of this object with a new expiration time.
"""
Return a (flat) copy of this object with a new expiration time.

This is a convenience method for creating an :class:`.ExpiringAuth`
for a relative expiration time ("expires in" instead of "expires at").
Expand Down Expand Up @@ -96,7 +98,8 @@ def expiring_auth_has_expired(auth: ExpiringAuth) -> bool:


class AuthManager(metaclass=abc.ABCMeta):
"""Baseclass for authentication information managers.
"""
Abstract base class for authentication information managers.

The driver provides some default implementations of this class in
:class:`.AuthManagers` for convenience.
Expand Down Expand Up @@ -132,7 +135,8 @@ class AuthManager(metaclass=abc.ABCMeta):

@abc.abstractmethod
def get_auth(self) -> _TAuth:
"""Return the current authentication information.
"""
Return the current authentication information.

The driver will call this method very frequently. It is recommended
to implement some form of caching to avoid unnecessary overhead.
Expand All @@ -151,7 +155,8 @@ def get_auth(self) -> _TAuth:
def handle_security_exception(
self, auth: _TAuth, error: Neo4jError
) -> bool:
"""Handle the server indicating authentication failure.
"""
Handle the server indicating authentication failure.

The driver will call this method when the server returns any
`Neo.ClientError.Security.*` error. The error will then be processed
Expand All @@ -174,7 +179,8 @@ def handle_security_exception(


class AsyncAuthManager(_Protocol, metaclass=abc.ABCMeta):
"""Async version of :class:`.AuthManager`.
"""
Async version of :class:`.AuthManager`.

.. seealso:: :class:`.AuthManager`

Expand Down Expand Up @@ -234,10 +240,14 @@ class ClientCertificate:

class ClientCertificateProvider(_Protocol, metaclass=abc.ABCMeta):
"""
Provides a client certificate to the driver for mutual TLS.
Interface for providing a client certificate to the driver for mutual TLS.

This is an abstract base class (:class:`abc.ABC`) as well as a protocol
(:class:`typing.Protocol`). Meaning you can either inherit from it or just
implement all required method on a class to satisfy the type constraints.

The package provides some default implementations of this class in
:class:`.AsyncClientCertificateProviders` for convenience.
:class:`.ClientCertificateProviders` for convenience.

The driver will call :meth:`.get_certificate` to check if the client wants
the driver to use as new certificate for mutual TLS.
Expand Down Expand Up @@ -286,12 +296,17 @@ class AsyncClientCertificateProvider(_Protocol, metaclass=abc.ABCMeta):
"""
Async version of :class:`.ClientCertificateProvider`.

The package provides some default implementations of this class in
:class:`.AsyncClientCertificateProviders` for convenience.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
See also
https://github.com/neo4j/neo4j-python-driver/wiki/preview-features

.. seealso:: :class:`.ClientCertificateProvider`
.. seealso::
:class:`.ClientCertificateProvider`,
:class:`.AsyncClientCertificateProviders`

.. versionadded:: 5.19
"""
Expand Down
4 changes: 2 additions & 2 deletions src/neo4j/_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class TrustCustomCAs(TrustStore):
authority at the specified paths. This option is primarily intended for
self-signed and custom certificates.

:param certificates (str): paths to the certificates to trust.
:param certificates: paths to the certificates to trust.
Those are not the certificates you expect to see from the server but
the CA certificates you expect to be used to sign the server's
certificate.
Expand All @@ -118,7 +118,7 @@ class TrustCustomCAs(TrustStore):
)
)
"""
def __init__(self, *certificates):
def __init__(self, *certificates: str):
self.certs = certificates


Expand Down
44 changes: 31 additions & 13 deletions src/neo4j/_sync/auth_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import annotations

import abc
import typing as t
from logging import getLogger

Expand Down Expand Up @@ -110,7 +111,8 @@ def handle_security_exception(


class AuthManagers:
"""A collection of :class:`.AuthManager` factories.
"""
A collection of :class:`.AuthManager` factories.

.. versionadded:: 5.8

Expand All @@ -124,7 +126,8 @@ class AuthManagers:

@staticmethod
def static(auth: _TAuth) -> AuthManager:
"""Create a static auth manager.
"""
Create a static auth manager.

The manager will always return the auth info provided at its creation.

Expand Down Expand Up @@ -163,7 +166,8 @@ def static(auth: _TAuth) -> AuthManager:
def basic(
provider: t.Callable[[], t.Union[_TAuth]]
) -> AuthManager:
"""Create an auth manager handling basic auth password rotation.
"""
Create an auth manager handling basic auth password rotation.

This factory wraps the provider function in an auth manager
implementation that caches the provided auth info until the server
Expand Down Expand Up @@ -230,7 +234,8 @@ def wrapped_provider() -> ExpiringAuth:
def bearer(
provider: t.Callable[[], t.Union[ExpiringAuth]]
) -> AuthManager:
"""Create an auth manager for potentially expiring bearer auth tokens.
"""
Create an auth manager for potentially expiring bearer auth tokens.

This factory wraps the provider function in an auth manager
implementation that caches the provided auth info until either the
Expand Down Expand Up @@ -310,10 +315,9 @@ def get_certificate(self) -> t.Optional[ClientCertificate]:
return cert


@preview("Mutual TLS is a preview feature.")
class RotatingClientCertificateProvider(ClientCertificateProvider):
"""
Implementation of a certificate provider that can rotate certificates.
Abstract base class for certificate providers that can rotate certificates.

The provider will make the driver use the initial certificate for all
connections until the certificate is updated using the
Expand Down Expand Up @@ -367,10 +371,26 @@ class RotatingClientCertificateProvider(ClientCertificateProvider):
# rotated again
...

:param initial_cert: The certificate to use initially.

.. versionadded:: 5.19

.. versionchanged:: 5.24

Turned this class into an abstract class to make the actual
implementation internal. This entails removing the possibility to
directly instantiate this class. Please use the factory method
:meth:`.ClientCertificateProviders.rotating` instead.
"""

@abc.abstractmethod
def update_certificate(self, cert: ClientCertificate) -> None:
"""
Update the certificate to use for new connections.
"""


class _Neo4jRotatingClientCertificateProvider(
RotatingClientCertificateProvider
):
def __init__(self, initial_cert: ClientCertificate) -> None:
self._cert: t.Optional[ClientCertificate] = initial_cert
self._lock = CooperativeLock()
Expand All @@ -381,15 +401,13 @@ def get_certificate(self) -> t.Optional[ClientCertificate]:
return cert

def update_certificate(self, cert: ClientCertificate) -> None:
"""
Update the certificate to use for new connections.
"""
with self._lock:
self._cert = cert


class ClientCertificateProviders:
"""A collection of :class:`.ClientCertificateProvider` factories.
"""
A collection of :class:`.ClientCertificateProvider` factories.

**This is a preview** (see :ref:`filter-warnings-ref`).
It might be changed without following the deprecation policy.
Expand Down Expand Up @@ -419,4 +437,4 @@ def rotating(

.. seealso:: :class:`.RotatingClientCertificateProvider`
"""
return RotatingClientCertificateProvider(initial_cert)
return _Neo4jRotatingClientCertificateProvider(initial_cert)
Loading