@@ -54,6 +54,15 @@ class SSLWantReadError(Exception):
54
54
class SSLWantWriteError (Exception ):
55
55
pass
56
56
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
+
57
66
class ConnectionStates (object ):
58
67
DISCONNECTING = '<disconnecting>'
59
68
DISCONNECTED = '<disconnected>'
@@ -167,9 +176,13 @@ class BrokerConnection(object):
167
176
'metric_group_prefix' : '' ,
168
177
'sasl_mechanism' : 'PLAIN' ,
169
178
'sasl_plain_username' : None ,
170
- 'sasl_plain_password' : None
179
+ 'sasl_plain_password' : None ,
180
+ 'sasl_kerberos_service_name' :'kafka'
171
181
}
172
- SASL_MECHANISMS = ('PLAIN' ,)
182
+ if gssapi is None :
183
+ SASL_MECHANISMS = ('PLAIN' ,)
184
+ else :
185
+ SASL_MECHANISMS = ('PLAIN' , 'GSSAPI' )
173
186
174
187
def __init__ (self , host , port , afi , ** configs ):
175
188
self .hostname = host
@@ -206,6 +219,9 @@ def __init__(self, host, port, afi, **configs):
206
219
if self .config ['sasl_mechanism' ] == 'PLAIN' :
207
220
assert self .config ['sasl_plain_username' ] is not None , 'sasl_plain_username required for PLAIN sasl'
208
221
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'
209
225
210
226
self .state = ConnectionStates .DISCONNECTED
211
227
self ._reset_reconnect_backoff ()
@@ -437,6 +453,8 @@ def _handle_sasl_handshake_response(self, future, response):
437
453
438
454
if self .config ['sasl_mechanism' ] == 'PLAIN' :
439
455
return self ._try_authenticate_plain (future )
456
+ elif self .config ['sasl_mechanism' ] == 'GSSAPI' :
457
+ return self ._try_authenticate_gssapi (future )
440
458
else :
441
459
return future .failure (
442
460
Errors .UnsupportedSaslMechanismError (
@@ -481,6 +499,61 @@ def _try_authenticate_plain(self, future):
481
499
482
500
return future .success (True )
483
501
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
+
484
557
def blacked_out (self ):
485
558
"""
486
559
Return true if we are disconnected from the given node and can't
0 commit comments