Skip to content

Commit

Permalink
feat(core): improve SASL interface (python-zk#546)
Browse files Browse the repository at this point in the history
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': 'clt@EXAMPLE.COM'}     # optional
  )
  • Loading branch information
ceache authored and StephenSorriaux committed Feb 12, 2019
1 parent 0ba3634 commit cd49b3f
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 132 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ matrix:
env: ZOOKEEPER_VERSION=3.3.6 TOX_VENV=py36
- python: '3.6'
env: ZOOKEEPER_VERSION=3.4.13 TOX_VENV=py36 DEPLOY=true
- python: '3.6'
env: ZOOKEEPER_VERSION=3.4.13 TOX_VENV=py36-sasl
- python: '3.6'
env: ZOOKEEPER_VERSION=3.5.4-beta TOX_VENV=py36
- python: '3.6'
env: ZOOKEEPER_VERSION=3.5.4-beta TOX_VENV=py36-sasl
- python: pypy
env: ZOOKEEPER_VERSION=3.3.6 TOX_VENV=pypy
- python: pypy
Expand Down
3 changes: 1 addition & 2 deletions ensure-zookeeper-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,4 @@ cd $HERE

# Yield execution to venv command

$*

exec $*
83 changes: 64 additions & 19 deletions kazoo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@

LOST_STATES = (KeeperState.EXPIRED_SESSION, KeeperState.AUTH_FAILED,
KeeperState.CLOSED)
ENVI_VERSION = re.compile('([\d\.]*).*', re.DOTALL)
ENVI_VERSION = re.compile(r'([\d\.]*).*', re.DOTALL)
ENVI_VERSION_KEY = 'zookeeper.version'
log = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,8 +102,8 @@ class KazooClient(object):
"""
def __init__(self, hosts='127.0.0.1:2181',
timeout=10.0, client_id=None, handler=None,
default_acl=None, auth_data=None, read_only=None,
randomize_hosts=True, connection_retry=None,
default_acl=None, auth_data=None, sasl_options=None,
read_only=None, randomize_hosts=True, connection_retry=None,
command_retry=None, logger=None, keyfile=None,
keyfile_password=None, certfile=None, ca=None,
use_ssl=False, verify_certs=True, **kwargs):
Expand All @@ -123,6 +123,31 @@ def __init__(self, hosts='127.0.0.1:2181',
A list of authentication credentials to use for the
connection. Should be a list of (scheme, credential)
tuples as :meth:`add_auth` takes.
:param sasl_options:
SASL options for the connection, if SASL support is to be used.
Should be a dict of SASL options passed to the underlying
`pure-sasl <https://pypi.org/project/pure-sasl>`_ library.
For example using the DIGEST-MD5 mechnism:
.. code-block:: python
sasl_options = {
'mechanism': 'DIGEST-MD5',
'username': 'myusername',
'password': 'mypassword'
}
For GSSAPI, using the running process' ticket cache:
.. code-block:: python
sasl_options = {
'mechanism': 'GSSAPI',
'service': 'myzk', # optional
'principal': 'client@EXAMPLE.COM' # optional
}
:param read_only: Allow connections to read only servers.
:param randomize_hosts: By default randomize host selection.
:param connection_retry:
Expand Down Expand Up @@ -174,6 +199,9 @@ def __init__(self, hosts='127.0.0.1:2181',
.. versionadded:: 1.2
The connection_retry, command_retry and logger options.
.. versionadded:: 2.7
The sasl_options option.
"""
self.logger = logger or log

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

# Managing legacy SASL options
for scheme, auth in self.auth_data:
if scheme != 'sasl':
continue
if sasl_options:
raise ConfigurationError(
'Multiple SASL configurations provided'
)
warnings.warn(
'Passing SASL configuration as part of the auth_data is '
'deprecated, please use the sasl_options configuration '
'instead', DeprecationWarning, stacklevel=2
)
username, password = auth.split(':')
# Generate an equivalent SASL configuration
sasl_options = {
'username': username,
'password': password,
'mechanism': 'DIGEST-MD5',
'service': 'zookeeper',
'principal': 'zk-sasl-md5',
}
# Cleanup
self.auth_data = set([
(scheme, auth)
for scheme, auth in self.auth_data
if scheme != 'sasl'
])

self._conn_retry.interrupt = lambda: self._stopped.is_set()
self._connection = ConnectionHandler(
self, self._conn_retry.copy(), logger=self.logger)
self, self._conn_retry.copy(), logger=self.logger,
sasl_options=sasl_options)

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

# Managing SASL client
self.use_sasl = False
for scheme, auth in self.auth_data:
if scheme == "sasl":
self.use_sasl = True
# Could be used later for GSSAPI implementation
self.sasl_server_principal = "zk-sasl-md5"
break

# If we got any unhandled keywords, complain like Python would
if kwargs:
raise TypeError('__init__() got unexpected keyword arguments: %s'
Expand Down Expand Up @@ -560,7 +609,7 @@ def _call(self, request, async_object):
"Connection has been closed"))
try:
write_sock.send(b'\0')
except:
except: # NOQA
async_object.set_exception(ConnectionClosedError(
"Connection has been closed"))

Expand Down Expand Up @@ -737,12 +786,8 @@ def add_auth(self, scheme, credential):
"""Send credentials to server.
:param scheme: authentication scheme (default supported:
"digest", "sasl"). Note that "sasl" scheme is
requiring "pure-sasl" library to be
installed.
"digest").
:param credential: the credential -- value depends on scheme.
"digest": user:password
"sasl": user:password
:returns: True if it was successful.
:rtype: bool
Expand Down
7 changes: 7 additions & 0 deletions kazoo/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ class WriterNotClosedException(KazooException):
"""


class SASLException(KazooException):
"""Raised if SASL encountered a (local) error.
.. versionadded:: 2.7.0
"""


def _invalid_error_code():
raise RuntimeError('Invalid error code')

Expand Down
Loading

0 comments on commit cd49b3f

Please sign in to comment.