From 50290c71f3f2fa36c2752343d9bdc1a953bd85e9 Mon Sep 17 00:00:00 2001 From: Daniel Robbins Date: Fri, 31 Oct 2025 20:11:29 -0400 Subject: [PATCH] auth: Add support for multiple ZAP authenticators in same process Enable running multiple ZAP authenticators concurrently within a single process by allowing custom socket addresses in the start() method. This exposes functionality available in libzmq's C API that was previously inaccessible from Python. Previously, all authenticators were hardcoded to bind to the single address "inproc://zeromq.zap.01", preventing multiple authenticators from coexisting. This limitation made it impossible to apply different authentication policies to different socket groups in the same process. I've been using this patch privately for years, because my code needs it. Time to send it upstream. Changes: - Add optional socket_addr parameter to Authenticator.start() - Add optional socket_addr parameter to AsyncioAuthenticator.start() - Add optional socket_addr parameter to ThreadAuthenticator.start() - All parameters default to "inproc://zeromq.zap.01" for backward compatibility - Add documentation with usage examples This change is fully backward compatible - existing code continues to work without modification. --- zmq/auth/asyncio.py | 25 +++++++++++++++++++++---- zmq/auth/base.py | 33 ++++++++++++++++++++++++++++++--- zmq/auth/thread.py | 17 ++++++++++++++--- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/zmq/auth/asyncio.py b/zmq/auth/asyncio.py index 8b4915c12..6473b0bf4 100644 --- a/zmq/auth/asyncio.py +++ b/zmq/auth/asyncio.py @@ -17,7 +17,13 @@ class AsyncioAuthenticator(Authenticator): - """ZAP authentication for use in the asyncio IO loop""" + """ZAP authentication for use in the asyncio IO loop + + .. versionadded:: 27.2 + Multiple authenticators can now run in the same process + by specifying different socket addresses in ``start()``. + See :class:`zmq.auth.Authenticator` for details and examples. + """ __poller: Optional[Poller] __task: Any @@ -46,9 +52,20 @@ async def __handle_zap(self) -> None: msg = self.zap_socket.recv_multipart() await self.handle_zap_message(msg) - def start(self) -> None: - """Start ZAP authentication""" - super().start() + def start(self, socket_addr="inproc://zeromq.zap.01") -> None: + """Start ZAP authentication + + Parameters + ---------- + socket_addr : str, optional + The address to bind the ZAP socket to. + Default is "inproc://zeromq.zap.01" + + .. versionadded:: 27.2 + Support for custom socket addresses, enabling multiple + authenticators in the same process. + """ + super().start(socket_addr) self.__poller = Poller() self.__poller.register(self.zap_socket, zmq.POLLIN) self.__task = asyncio.ensure_future(self.__handle_zap()) diff --git a/zmq/auth/base.py b/zmq/auth/base.py index c862b60c1..40cc95387 100644 --- a/zmq/auth/base.py +++ b/zmq/auth/base.py @@ -35,6 +35,22 @@ class Authenticator: main thread, other authentication classes (such as :mod:`zmq.auth.thread`) are provided. + Multiple Authenticators + ----------------------- + + .. versionadded:: 27.2 + + Multiple authenticators can run in the same process by binding to different + ZAP socket addresses. This allows different authentication policies for + different sets of sockets within the same application:: + + # Create two authenticators with different policies + frontend_auth = zmq.auth.asyncio.AsyncioAuthenticator() + frontend_auth.start(socket_addr="inproc://zap-frontend") + + backend_auth = zmq.auth.asyncio.AsyncioAuthenticator() + backend_auth.start(socket_addr="inproc://zap-backend") + Note: - libzmq provides four levels of security: default NULL (which the Authenticator does @@ -77,11 +93,22 @@ def __init__( self.certs = {} self.log = log or logging.getLogger('zmq.auth') - def start(self) -> None: - """Create and bind the ZAP socket""" + def start(self, socket_addr="inproc://zeromq.zap.01") -> None: + """Create and bind the ZAP socket + + Parameters + ---------- + socket_addr : str, optional + The address to bind the ZAP socket to. + Default is "inproc://zeromq.zap.01" + + .. versionadded:: 27.2 + Support for custom socket addresses, enabling multiple + authenticators in the same process. + """ self.zap_socket = self.context.socket(zmq.REP, socket_class=zmq.Socket) self.zap_socket.linger = 1 - self.zap_socket.bind("inproc://zeromq.zap.01") + self.zap_socket.bind(socket_addr) self.log.debug("Starting") def stop(self) -> None: diff --git a/zmq/auth/thread.py b/zmq/auth/thread.py index a227c4bd5..dc6836df9 100644 --- a/zmq/auth/thread.py +++ b/zmq/auth/thread.py @@ -100,10 +100,21 @@ def __init__( self.pipe_endpoint = f"inproc://{id(self)}.inproc" self.thread = None # type: ignore - def start(self) -> None: - """Start the authentication thread""" + def start(self, socket_addr="inproc://zeromq.zap.01") -> None: + """Start the authentication thread + + Parameters + ---------- + socket_addr : str, optional + The address to bind the ZAP socket to. + Default is "inproc://zeromq.zap.01" + + .. versionadded:: 27.2 + Support for custom socket addresses, enabling multiple + authenticators in the same process. + """ # start the Authenticator - super().start() + super().start(socket_addr) # create a socket pair to communicate with auth thread. self.pipe = self.context.socket(zmq.PAIR, socket_class=zmq.Socket)