Skip to content

Commit 0c78f70

Browse files
Harald-Berghoffdpkp
authored andcommitted
added gssapi support (Kerberos) for SASL (#1152)
1 parent c8237fc commit 0c78f70

File tree

1 file changed

+75
-2
lines changed

1 file changed

+75
-2
lines changed

kafka/conn.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ class SSLWantReadError(Exception):
5454
class SSLWantWriteError(Exception):
5555
pass
5656

57+
# needed for SASL_GSSAPI authentication:
58+
try:
59+
import gssapi
60+
from gssapi.raw.misc import GSSError
61+
except ImportError:
62+
#no gssapi available, will disable gssapi mechanism
63+
gssapi = None
64+
GSSError = None
65+
5766
class ConnectionStates(object):
5867
DISCONNECTING = '<disconnecting>'
5968
DISCONNECTED = '<disconnected>'
@@ -167,9 +176,13 @@ class BrokerConnection(object):
167176
'metric_group_prefix': '',
168177
'sasl_mechanism': 'PLAIN',
169178
'sasl_plain_username': None,
170-
'sasl_plain_password': None
179+
'sasl_plain_password': None,
180+
'sasl_kerberos_service_name':'kafka'
171181
}
172-
SASL_MECHANISMS = ('PLAIN',)
182+
if gssapi is None:
183+
SASL_MECHANISMS = ('PLAIN',)
184+
else:
185+
SASL_MECHANISMS = ('PLAIN', 'GSSAPI')
173186

174187
def __init__(self, host, port, afi, **configs):
175188
self.hostname = host
@@ -206,6 +219,9 @@ def __init__(self, host, port, afi, **configs):
206219
if self.config['sasl_mechanism'] == 'PLAIN':
207220
assert self.config['sasl_plain_username'] is not None, 'sasl_plain_username required for PLAIN sasl'
208221
assert self.config['sasl_plain_password'] is not None, 'sasl_plain_password required for PLAIN sasl'
222+
if self.config['sasl_mechanism'] == 'GSSAPI':
223+
assert gssapi is not None, 'GSSAPI lib not available'
224+
assert self.config['sasl_kerberos_service_name'] is not None, 'sasl_servicename_kafka required for GSSAPI sasl'
209225

210226
self.state = ConnectionStates.DISCONNECTED
211227
self._reset_reconnect_backoff()
@@ -437,6 +453,8 @@ def _handle_sasl_handshake_response(self, future, response):
437453

438454
if self.config['sasl_mechanism'] == 'PLAIN':
439455
return self._try_authenticate_plain(future)
456+
elif self.config['sasl_mechanism'] == 'GSSAPI':
457+
return self._try_authenticate_gssapi(future)
440458
else:
441459
return future.failure(
442460
Errors.UnsupportedSaslMechanismError(
@@ -481,6 +499,61 @@ def _try_authenticate_plain(self, future):
481499

482500
return future.success(True)
483501

502+
def _try_authenticate_gssapi(self, future):
503+
504+
data = b''
505+
gssname = self.config['sasl_kerberos_service_name'] + '@' + self.hostname
506+
ctx_Name = gssapi.Name(gssname, name_type=gssapi.NameType.hostbased_service)
507+
ctx_CanonName = ctx_Name.canonicalize(gssapi.MechType.kerberos)
508+
log.debug('%s: canonical Servicename: %s', self, ctx_CanonName)
509+
ctx_Context = gssapi.SecurityContext(name=ctx_CanonName, usage='initiate')
510+
#Exchange tokens until authentication either suceeded or failed:
511+
received_token = None
512+
try:
513+
while not ctx_Context.complete:
514+
#calculate the output token
515+
try:
516+
output_token = ctx_Context.step(received_token)
517+
except GSSError as e:
518+
log.exception("%s: Error invalid token received from server", self)
519+
error = Errors.ConnectionError("%s: %s" % (self, e))
520+
521+
if not output_token:
522+
if ctx_Context.complete:
523+
log.debug("%s: Security Context complete ", self)
524+
log.debug("%s: Successful GSSAPI handshake for %s", self, ctx_Context.initiator_name)
525+
break
526+
try:
527+
self._sock.setblocking(True)
528+
# Send output token
529+
msg = output_token
530+
size = Int32.encode(len(msg))
531+
self._sock.sendall(size + msg)
532+
533+
# The server will send a token back. processing of this token either
534+
# establishes a security context, or needs further token exchange
535+
# the gssapi will be able to identify the needed next step
536+
# The connection is closed on failure
537+
response = self._sock.recv(2000)
538+
self._sock.setblocking(False)
539+
540+
except (AssertionError, ConnectionError) as e:
541+
log.exception("%s: Error receiving reply from server", self)
542+
error = Errors.ConnectionError("%s: %s" % (self, e))
543+
future.failure(error)
544+
self.close(error=error)
545+
546+
#pass the received token back to gssapi, strip the first 4 bytes
547+
received_token = response[4:]
548+
549+
except Exception as e:
550+
log.exception("%s: GSSAPI handshake error", self)
551+
error = Errors.ConnectionError("%s: %s" % (self, e))
552+
future.failure(error)
553+
self.close(error=error)
554+
555+
return future.success(True)
556+
484557
def blacked_out(self):
485558
"""
486559
Return true if we are disconnected from the given node and can't

0 commit comments

Comments
 (0)