Commit b398e744 authored by Jim Fulton's avatar Jim Fulton Committed by GitHub

Merge pull request #53 from zopefoundation/client-credentials

Added the ability to pass credentials when creating client storages.
parents 02943acd 9fda3635
Changelog Changelog
========= =========
- Added the ability to pass credentials when creating client storages.
This is experimental in that passing credentials will cause
connections to an ordinary ZEO server to fail, but it facilitates
experimentation with custom ZEO servers. Doing this with custom ZEO
clients would have been awkward due to the many levels of
composition involved.
In the future, we expect to support server security plugins that
consume credentials for authentication (typically over SSL).
Note that credentials are opaque to ZEO. They can be any object with
a true value. The client mearly passes them to the server, which
will someday pass them to a plugin.
5.0.0a1 (2016-07-21) 5.0.0a1 (2016-07-21)
-------------------- --------------------
......
...@@ -99,6 +99,7 @@ class ClientStorage(ZODB.ConflictResolution.ConflictResolvingStorage): ...@@ -99,6 +99,7 @@ class ClientStorage(ZODB.ConflictResolution.ConflictResolvingStorage):
wait=True, wait=True,
drop_cache_rather_verify=True, drop_cache_rather_verify=True,
username=None, password=None, realm=None, username=None, password=None, realm=None,
credentials=None,
# For tests: # For tests:
_client_factory=ZEO.asyncio.client.ClientThread, _client_factory=ZEO.asyncio.client.ClientThread,
): ):
...@@ -261,6 +262,7 @@ class ClientStorage(ZODB.ConflictResolution.ConflictResolvingStorage): ...@@ -261,6 +262,7 @@ class ClientStorage(ZODB.ConflictResolution.ConflictResolvingStorage):
ZEO.asyncio.client.Fallback if read_only_fallback else read_only, ZEO.asyncio.client.Fallback if read_only_fallback else read_only,
wait_timeout or 30, wait_timeout or 30,
ssl = ssl, ssl_server_hostname=ssl_server_hostname, ssl = ssl, ssl_server_hostname=ssl_server_hostname,
credentials=credentials,
) )
self._call = self._server.call self._call = self._server.call
self._async = self._server.async self._async = self._server.async
......
...@@ -73,7 +73,8 @@ class Protocol(base.Protocol): ...@@ -73,7 +73,8 @@ class Protocol(base.Protocol):
def __init__(self, loop, def __init__(self, loop,
addr, client, storage_key, read_only, connect_poll=1, addr, client, storage_key, read_only, connect_poll=1,
heartbeat_interval=60, ssl=None, ssl_server_hostname=None): heartbeat_interval=60, ssl=None, ssl_server_hostname=None,
credentials=None):
"""Create a client interface """Create a client interface
addr is either a host,port tuple or a string file name. addr is either a host,port tuple or a string file name.
...@@ -93,6 +94,7 @@ class Protocol(base.Protocol): ...@@ -93,6 +94,7 @@ class Protocol(base.Protocol):
self.futures = {} # { message_id -> future } self.futures = {} # { message_id -> future }
self.ssl = ssl self.ssl = ssl
self.ssl_server_hostname = ssl_server_hostname self.ssl_server_hostname = ssl_server_hostname
self.credentials = credentials
self.connect() self.connect()
...@@ -178,17 +180,19 @@ class Protocol(base.Protocol): ...@@ -178,17 +180,19 @@ class Protocol(base.Protocol):
self._write(self.protocol_version) self._write(self.protocol_version)
credentials = (self.credentials,) if self.credentials else ()
try: try:
try: try:
server_tid = yield self.fut( server_tid = yield self.fut(
'register', self.storage_key, 'register', self.storage_key,
self.read_only if self.read_only is not Fallback else False, self.read_only if self.read_only is not Fallback else False,
) *credentials)
except ZODB.POSException.ReadOnlyError: except ZODB.POSException.ReadOnlyError:
if self.read_only is Fallback: if self.read_only is Fallback:
self.read_only = True self.read_only = True
server_tid = yield self.fut( server_tid = yield self.fut(
'register', self.storage_key, True) 'register', self.storage_key, True, *credentials)
else: else:
raise raise
else: else:
...@@ -284,7 +288,7 @@ class Client(object): ...@@ -284,7 +288,7 @@ class Client(object):
def __init__(self, loop, def __init__(self, loop,
addrs, client, cache, storage_key, read_only, connect_poll, addrs, client, cache, storage_key, read_only, connect_poll,
register_failed_poll=9, register_failed_poll=9,
ssl=None, ssl_server_hostname=None): ssl=None, ssl_server_hostname=None, credentials=None):
"""Create a client interface """Create a client interface
addr is either a host,port tuple or a string file name. addr is either a host,port tuple or a string file name.
...@@ -302,6 +306,7 @@ class Client(object): ...@@ -302,6 +306,7 @@ class Client(object):
self.client = client self.client = client
self.ssl = ssl self.ssl = ssl
self.ssl_server_hostname = ssl_server_hostname self.ssl_server_hostname = ssl_server_hostname
self.credentials = credentials
for name in Protocol.client_delegated: for name in Protocol.client_delegated:
setattr(self, name, getattr(client, name)) setattr(self, name, getattr(client, name))
self.cache = cache self.cache = cache
...@@ -367,6 +372,7 @@ class Client(object): ...@@ -367,6 +372,7 @@ class Client(object):
self.storage_key, self.read_only, self.connect_poll, self.storage_key, self.read_only, self.connect_poll,
ssl=self.ssl, ssl=self.ssl,
ssl_server_hostname=self.ssl_server_hostname, ssl_server_hostname=self.ssl_server_hostname,
credentials=self.credentials,
) )
for addr in self.addrs for addr in self.addrs
] ]
...@@ -730,10 +736,12 @@ class ClientThread(ClientRunner): ...@@ -730,10 +736,12 @@ class ClientThread(ClientRunner):
def __init__(self, addrs, client, cache, def __init__(self, addrs, client, cache,
storage_key='1', read_only=False, timeout=30, storage_key='1', read_only=False, timeout=30,
disconnect_poll=1, ssl=None, ssl_server_hostname=None): disconnect_poll=1, ssl=None, ssl_server_hostname=None,
credentials=None):
self.set_options(addrs, client, cache, storage_key, read_only, self.set_options(addrs, client, cache, storage_key, read_only,
timeout, disconnect_poll, timeout, disconnect_poll,
ssl=ssl, ssl_server_hostname=ssl_server_hostname) ssl=ssl, ssl_server_hostname=ssl_server_hostname,
credentials=credentials)
self.thread = threading.Thread( self.thread = threading.Thread(
target=self.run, target=self.run,
name="%s zeo client networking thread" % client.__name__, name="%s zeo client networking thread" % client.__name__,
......
"""Clients can pass credentials to a server.
This is an experimental feature to enable server authentication and
authorization.
"""
from zope.testing import setupstack
import unittest
import ZEO.StorageServer
class ClientAuthTests(setupstack.TestCase):
def setUp(self):
self.setUpDirectory()
self.__register = ZEO.StorageServer.ZEOStorage.register
def tearDown(self):
ZEO.StorageServer.ZEOStorage.register = self.__register
def test_passing_credentials(self):
# First, we'll temporarily swap the storage server register
# method with one that let's is see credentials that were passed:
creds_log = []
def register(zs, storage_id, read_only, credentials=self):
creds_log.append(credentials)
return self.__register(zs, storage_id, read_only)
ZEO.StorageServer.ZEOStorage.register = register
# Now start an in process server
addr, stop = ZEO.server()
# If we connect, without providing credentials, then no
# credentials will be passed to register:
client = ZEO.client(addr)
self.assertEqual(creds_log, [self])
client.close()
creds_log.pop()
# But if we pass credentials, they'll be passed to register:
creds = dict(user='me', password='123')
client = ZEO.client(addr, credentials=creds)
self.assertEqual(creds_log, [creds])
client.close()
stop()
def test_suite():
return unittest.makeSuite(ClientAuthTests)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment