Skip to content

Commit cd49b3f

Browse files
ceacheStephenSorriaux
authored andcommitted
feat(core): improve SASL interface (#546)
Move SASL configuration out of auth_data into its own dictionary which exposes more SASL features (e.g. server service name, client principal...). Legacy syntax is still supported for backward compatibilty. Remove SASL from auth_data and place it between 'connection' and 'zookeeper protocol level authentication' to simplify connection logic and bring code in line with the protocol stack (SASL wraps Zookeeper, not the other way around). Consistent exception, `AuthFailedError`, raised during authentication failure between SASL and ZK authentication. New 'SASLException' exception raised in case of SASL intrisinc failures. Add support for GSSAPI (Kerberos). Example connection using Digest-MD5: client = KazooClient( sasl_options={'mechanism': 'DIGEST-MD5', 'username': 'myusername', 'password': 'mypassword'} ) Example connection using GSSAPI (with some optional settings): client = KazooClient( sasl_options={'mechanism': 'GSSAPI', 'service': 'myzk', # optional 'principal': '[email protected]'} # optional )
1 parent 0ba3634 commit cd49b3f

File tree

11 files changed

+280
-132
lines changed

11 files changed

+280
-132
lines changed

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ matrix:
4848
env: ZOOKEEPER_VERSION=3.3.6 TOX_VENV=py36
4949
- python: '3.6'
5050
env: ZOOKEEPER_VERSION=3.4.13 TOX_VENV=py36 DEPLOY=true
51+
- python: '3.6'
52+
env: ZOOKEEPER_VERSION=3.4.13 TOX_VENV=py36-sasl
5153
- python: '3.6'
5254
env: ZOOKEEPER_VERSION=3.5.4-beta TOX_VENV=py36
55+
- python: '3.6'
56+
env: ZOOKEEPER_VERSION=3.5.4-beta TOX_VENV=py36-sasl
5357
- python: pypy
5458
env: ZOOKEEPER_VERSION=3.3.6 TOX_VENV=pypy
5559
- python: pypy

ensure-zookeeper-env.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,4 @@ cd $HERE
2929

3030
# Yield execution to venv command
3131

32-
$*
33-
32+
exec $*

kazoo/client.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070

7171
LOST_STATES = (KeeperState.EXPIRED_SESSION, KeeperState.AUTH_FAILED,
7272
KeeperState.CLOSED)
73-
ENVI_VERSION = re.compile('([\d\.]*).*', re.DOTALL)
73+
ENVI_VERSION = re.compile(r'([\d\.]*).*', re.DOTALL)
7474
ENVI_VERSION_KEY = 'zookeeper.version'
7575
log = logging.getLogger(__name__)
7676

@@ -102,8 +102,8 @@ class KazooClient(object):
102102
"""
103103
def __init__(self, hosts='127.0.0.1:2181',
104104
timeout=10.0, client_id=None, handler=None,
105-
default_acl=None, auth_data=None, read_only=None,
106-
randomize_hosts=True, connection_retry=None,
105+
default_acl=None, auth_data=None, sasl_options=None,
106+
read_only=None, randomize_hosts=True, connection_retry=None,
107107
command_retry=None, logger=None, keyfile=None,
108108
keyfile_password=None, certfile=None, ca=None,
109109
use_ssl=False, verify_certs=True, **kwargs):
@@ -123,6 +123,31 @@ def __init__(self, hosts='127.0.0.1:2181',
123123
A list of authentication credentials to use for the
124124
connection. Should be a list of (scheme, credential)
125125
tuples as :meth:`add_auth` takes.
126+
:param sasl_options:
127+
SASL options for the connection, if SASL support is to be used.
128+
Should be a dict of SASL options passed to the underlying
129+
`pure-sasl <https://pypi.org/project/pure-sasl>`_ library.
130+
131+
For example using the DIGEST-MD5 mechnism:
132+
133+
.. code-block:: python
134+
135+
sasl_options = {
136+
'mechanism': 'DIGEST-MD5',
137+
'username': 'myusername',
138+
'password': 'mypassword'
139+
}
140+
141+
For GSSAPI, using the running process' ticket cache:
142+
143+
.. code-block:: python
144+
145+
sasl_options = {
146+
'mechanism': 'GSSAPI',
147+
'service': 'myzk', # optional
148+
'principal': '[email protected]' # optional
149+
}
150+
126151
:param read_only: Allow connections to read only servers.
127152
:param randomize_hosts: By default randomize host selection.
128153
:param connection_retry:
@@ -174,6 +199,9 @@ def __init__(self, hosts='127.0.0.1:2181',
174199
.. versionadded:: 1.2
175200
The connection_retry, command_retry and logger options.
176201
202+
.. versionadded:: 2.7
203+
The sasl_options option.
204+
177205
"""
178206
self.logger = logger or log
179207

@@ -273,9 +301,39 @@ def __init__(self, hosts='127.0.0.1:2181',
273301
sleep_func=self.handler.sleep_func,
274302
**retry_keys)
275303

304+
# Managing legacy SASL options
305+
for scheme, auth in self.auth_data:
306+
if scheme != 'sasl':
307+
continue
308+
if sasl_options:
309+
raise ConfigurationError(
310+
'Multiple SASL configurations provided'
311+
)
312+
warnings.warn(
313+
'Passing SASL configuration as part of the auth_data is '
314+
'deprecated, please use the sasl_options configuration '
315+
'instead', DeprecationWarning, stacklevel=2
316+
)
317+
username, password = auth.split(':')
318+
# Generate an equivalent SASL configuration
319+
sasl_options = {
320+
'username': username,
321+
'password': password,
322+
'mechanism': 'DIGEST-MD5',
323+
'service': 'zookeeper',
324+
'principal': 'zk-sasl-md5',
325+
}
326+
# Cleanup
327+
self.auth_data = set([
328+
(scheme, auth)
329+
for scheme, auth in self.auth_data
330+
if scheme != 'sasl'
331+
])
332+
276333
self._conn_retry.interrupt = lambda: self._stopped.is_set()
277334
self._connection = ConnectionHandler(
278-
self, self._conn_retry.copy(), logger=self.logger)
335+
self, self._conn_retry.copy(), logger=self.logger,
336+
sasl_options=sasl_options)
279337

280338
# Every retry call should have its own copy of the retry helper
281339
# to avoid shared retry counts
@@ -303,15 +361,6 @@ def _retry(*args, **kwargs):
303361
self.Semaphore = partial(Semaphore, self)
304362
self.ShallowParty = partial(ShallowParty, self)
305363

306-
# Managing SASL client
307-
self.use_sasl = False
308-
for scheme, auth in self.auth_data:
309-
if scheme == "sasl":
310-
self.use_sasl = True
311-
# Could be used later for GSSAPI implementation
312-
self.sasl_server_principal = "zk-sasl-md5"
313-
break
314-
315364
# If we got any unhandled keywords, complain like Python would
316365
if kwargs:
317366
raise TypeError('__init__() got unexpected keyword arguments: %s'
@@ -560,7 +609,7 @@ def _call(self, request, async_object):
560609
"Connection has been closed"))
561610
try:
562611
write_sock.send(b'\0')
563-
except:
612+
except: # NOQA
564613
async_object.set_exception(ConnectionClosedError(
565614
"Connection has been closed"))
566615

@@ -737,12 +786,8 @@ def add_auth(self, scheme, credential):
737786
"""Send credentials to server.
738787
739788
:param scheme: authentication scheme (default supported:
740-
"digest", "sasl"). Note that "sasl" scheme is
741-
requiring "pure-sasl" library to be
742-
installed.
789+
"digest").
743790
:param credential: the credential -- value depends on scheme.
744-
"digest": user:password
745-
"sasl": user:password
746791
747792
:returns: True if it was successful.
748793
:rtype: bool

kazoo/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ class WriterNotClosedException(KazooException):
4343
"""
4444

4545

46+
class SASLException(KazooException):
47+
"""Raised if SASL encountered a (local) error.
48+
49+
.. versionadded:: 2.7.0
50+
"""
51+
52+
4653
def _invalid_error_code():
4754
raise RuntimeError('Invalid error code')
4855

0 commit comments

Comments
 (0)