API refactor
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-07 16:25:52 +09:00
parent 76d0d86211
commit 91c7e04474
1171 changed files with 81940 additions and 44117 deletions

View File

@@ -0,0 +1,34 @@
from __future__ import absolute_import
import platform
from kafka.sasl.gssapi import SaslMechanismGSSAPI
from kafka.sasl.msk import SaslMechanismAwsMskIam
from kafka.sasl.oauth import SaslMechanismOAuth
from kafka.sasl.plain import SaslMechanismPlain
from kafka.sasl.scram import SaslMechanismScram
from kafka.sasl.sspi import SaslMechanismSSPI
SASL_MECHANISMS = {}
def register_sasl_mechanism(name, klass, overwrite=False):
if not overwrite and name in SASL_MECHANISMS:
raise ValueError('Sasl mechanism %s already defined!' % name)
SASL_MECHANISMS[name] = klass
def get_sasl_mechanism(name):
return SASL_MECHANISMS[name]
register_sasl_mechanism('AWS_MSK_IAM', SaslMechanismAwsMskIam)
if platform.system() == 'Windows':
register_sasl_mechanism('GSSAPI', SaslMechanismSSPI)
else:
register_sasl_mechanism('GSSAPI', SaslMechanismGSSAPI)
register_sasl_mechanism('OAUTHBEARER', SaslMechanismOAuth)
register_sasl_mechanism('PLAIN', SaslMechanismPlain)
register_sasl_mechanism('SCRAM-SHA-256', SaslMechanismScram)
register_sasl_mechanism('SCRAM-SHA-512', SaslMechanismScram)

View File

@@ -0,0 +1,33 @@
from __future__ import absolute_import
import abc
from kafka.vendor.six import add_metaclass
@add_metaclass(abc.ABCMeta)
class SaslMechanism(object):
@abc.abstractmethod
def __init__(self, **config):
pass
@abc.abstractmethod
def auth_bytes(self):
pass
@abc.abstractmethod
def receive(self, auth_bytes):
pass
@abc.abstractmethod
def is_done(self):
pass
@abc.abstractmethod
def is_authenticated(self):
pass
def auth_details(self):
if not self.is_authenticated:
raise RuntimeError('Not authenticated yet!')
return 'Authenticated via SASL'

View File

@@ -0,0 +1,96 @@
from __future__ import absolute_import
import struct
# needed for SASL_GSSAPI authentication:
try:
import gssapi
from gssapi.raw.misc import GSSError
except (ImportError, OSError):
#no gssapi available, will disable gssapi mechanism
gssapi = None
GSSError = None
from kafka.sasl.abc import SaslMechanism
class SaslMechanismGSSAPI(SaslMechanism):
# Establish security context and negotiate protection level
# For reference RFC 2222, section 7.2.1
SASL_QOP_AUTH = 1
SASL_QOP_AUTH_INT = 2
SASL_QOP_AUTH_CONF = 4
def __init__(self, **config):
assert gssapi is not None, 'GSSAPI lib not available'
if 'sasl_kerberos_name' not in config and 'sasl_kerberos_service_name' not in config:
raise ValueError('sasl_kerberos_service_name or sasl_kerberos_name required for GSSAPI sasl configuration')
self._is_done = False
self._is_authenticated = False
self.gssapi_name = None
if config.get('sasl_kerberos_name', None) is not None:
self.auth_id = str(config['sasl_kerberos_name'])
if isinstance(config['sasl_kerberos_name'], gssapi.Name):
self.gssapi_name = config['sasl_kerberos_name']
else:
kerberos_domain_name = config.get('sasl_kerberos_domain_name', '') or config.get('host', '')
self.auth_id = config['sasl_kerberos_service_name'] + '@' + kerberos_domain_name
if self.gssapi_name is None:
self.gssapi_name = gssapi.Name(self.auth_id, name_type=gssapi.NameType.hostbased_service).canonicalize(gssapi.MechType.kerberos)
self._client_ctx = gssapi.SecurityContext(name=self.gssapi_name, usage='initiate')
self._next_token = self._client_ctx.step(None)
def auth_bytes(self):
# GSSAPI Auth does not have a final broker->client message
# so mark is_done after the final auth_bytes are provided
# in practice we'll still receive a response when using SaslAuthenticate
# but not when using the prior unframed approach.
if self._is_authenticated:
self._is_done = True
return self._next_token or b''
def receive(self, auth_bytes):
if not self._client_ctx.complete:
# The server will send a token back. Processing of this token either
# establishes a security context, or it needs further token exchange.
# The gssapi will be able to identify the needed next step.
self._next_token = self._client_ctx.step(auth_bytes)
elif self._is_done:
# The final step of gssapi is send, so we do not expect any additional bytes
# however, allow an empty message to support SaslAuthenticate response
if auth_bytes != b'':
raise ValueError("Unexpected receive auth_bytes after sasl/gssapi completion")
else:
# unwraps message containing supported protection levels and msg size
msg = self._client_ctx.unwrap(auth_bytes).message
# Kafka currently doesn't support integrity or confidentiality security layers, so we
# simply set QoP to 'auth' only (first octet). We reuse the max message size proposed
# by the server
client_flags = self.SASL_QOP_AUTH
server_flags = struct.Struct('>b').unpack(msg[0:1])[0]
message_parts = [
struct.Struct('>b').pack(client_flags & server_flags),
msg[1:], # always agree to max message size from server
self.auth_id.encode('utf-8'),
]
# add authorization identity to the response, and GSS-wrap
self._next_token = self._client_ctx.wrap(b''.join(message_parts), False).message
# We need to identify the last token in auth_bytes();
# we can't rely on client_ctx.complete because it becomes True after generating
# the second-to-last token (after calling .step(auth_bytes) for the final time)
# We could introduce an additional state variable (i.e., self._final_token),
# but instead we just set _is_authenticated. Since the plugin interface does
# not read is_authenticated() until after is_done() is True, this should be fine.
self._is_authenticated = True
def is_done(self):
return self._is_done
def is_authenticated(self):
return self._is_authenticated
def auth_details(self):
if not self.is_authenticated:
raise RuntimeError('Not authenticated yet!')
return 'Authenticated as %s to %s via SASL / GSSAPI' % (self._client_ctx.initiator_name, self._client_ctx.target_name)

View File

@@ -0,0 +1,244 @@
from __future__ import absolute_import
import datetime
import hashlib
import hmac
import json
import logging
import string
# needed for AWS_MSK_IAM authentication:
try:
from botocore.session import Session as BotoSession
except ImportError:
# no botocore available, will disable AWS_MSK_IAM mechanism
BotoSession = None
from kafka.errors import KafkaConfigurationError
from kafka.sasl.abc import SaslMechanism
from kafka.vendor.six.moves import urllib
log = logging.getLogger(__name__)
class SaslMechanismAwsMskIam(SaslMechanism):
def __init__(self, **config):
assert BotoSession is not None, 'AWS_MSK_IAM requires the "botocore" package'
assert config.get('security_protocol', '') == 'SASL_SSL', 'AWS_MSK_IAM requires SASL_SSL'
assert 'host' in config, 'AWS_MSK_IAM requires host configuration'
self.host = config['host']
self._auth = None
self._is_done = False
self._is_authenticated = False
def _build_client(self):
session = BotoSession()
credentials = session.get_credentials().get_frozen_credentials()
if not session.get_config_variable('region'):
raise KafkaConfigurationError('Unable to determine region for AWS MSK cluster. Is AWS_DEFAULT_REGION set?')
return AwsMskIamClient(
host=self.host,
access_key=credentials.access_key,
secret_key=credentials.secret_key,
region=session.get_config_variable('region'),
token=credentials.token,
)
def auth_bytes(self):
client = self._build_client()
log.debug("Generating auth token for MSK scope: %s", client._scope)
return client.first_message()
def receive(self, auth_bytes):
self._is_done = True
self._is_authenticated = auth_bytes != b''
self._auth = auth_bytes.decode('utf-8')
def is_done(self):
return self._is_done
def is_authenticated(self):
return self._is_authenticated
def auth_details(self):
if not self.is_authenticated:
raise RuntimeError('Not authenticated yet!')
return 'Authenticated via SASL / AWS_MSK_IAM %s' % (self._auth,)
class AwsMskIamClient:
UNRESERVED_CHARS = string.ascii_letters + string.digits + '-._~'
def __init__(self, host, access_key, secret_key, region, token=None):
"""
Arguments:
host (str): The hostname of the broker.
access_key (str): An AWS_ACCESS_KEY_ID.
secret_key (str): An AWS_SECRET_ACCESS_KEY.
region (str): An AWS_REGION.
token (Optional[str]): An AWS_SESSION_TOKEN if using temporary
credentials.
"""
self.algorithm = 'AWS4-HMAC-SHA256'
self.expires = '900'
self.hashfunc = hashlib.sha256
self.headers = [
('host', host)
]
self.version = '2020_10_22'
self.service = 'kafka-cluster'
self.action = '{}:Connect'.format(self.service)
now = datetime.datetime.utcnow()
self.datestamp = now.strftime('%Y%m%d')
self.timestamp = now.strftime('%Y%m%dT%H%M%SZ')
self.host = host
self.access_key = access_key
self.secret_key = secret_key
self.region = region
self.token = token
@property
def _credential(self):
return '{0.access_key}/{0._scope}'.format(self)
@property
def _scope(self):
return '{0.datestamp}/{0.region}/{0.service}/aws4_request'.format(self)
@property
def _signed_headers(self):
"""
Returns (str):
An alphabetically sorted, semicolon-delimited list of lowercase
request header names.
"""
return ';'.join(sorted(k.lower() for k, _ in self.headers))
@property
def _canonical_headers(self):
"""
Returns (str):
A newline-delited list of header names and values.
Header names are lowercased.
"""
return '\n'.join(map(':'.join, self.headers)) + '\n'
@property
def _canonical_request(self):
"""
Returns (str):
An AWS Signature Version 4 canonical request in the format:
<Method>\n
<Path>\n
<CanonicalQueryString>\n
<CanonicalHeaders>\n
<SignedHeaders>\n
<HashedPayload>
"""
# The hashed_payload is always an empty string for MSK.
hashed_payload = self.hashfunc(b'').hexdigest()
return '\n'.join((
'GET',
'/',
self._canonical_querystring,
self._canonical_headers,
self._signed_headers,
hashed_payload,
))
@property
def _canonical_querystring(self):
"""
Returns (str):
A '&'-separated list of URI-encoded key/value pairs.
"""
params = []
params.append(('Action', self.action))
params.append(('X-Amz-Algorithm', self.algorithm))
params.append(('X-Amz-Credential', self._credential))
params.append(('X-Amz-Date', self.timestamp))
params.append(('X-Amz-Expires', self.expires))
if self.token:
params.append(('X-Amz-Security-Token', self.token))
params.append(('X-Amz-SignedHeaders', self._signed_headers))
return '&'.join(self._uriencode(k) + '=' + self._uriencode(v) for k, v in params)
@property
def _signing_key(self):
"""
Returns (bytes):
An AWS Signature V4 signing key generated from the secret_key, date,
region, service, and request type.
"""
key = self._hmac(('AWS4' + self.secret_key).encode('utf-8'), self.datestamp)
key = self._hmac(key, self.region)
key = self._hmac(key, self.service)
key = self._hmac(key, 'aws4_request')
return key
@property
def _signing_str(self):
"""
Returns (str):
A string used to sign the AWS Signature V4 payload in the format:
<Algorithm>\n
<Timestamp>\n
<Scope>\n
<CanonicalRequestHash>
"""
canonical_request_hash = self.hashfunc(self._canonical_request.encode('utf-8')).hexdigest()
return '\n'.join((self.algorithm, self.timestamp, self._scope, canonical_request_hash))
def _uriencode(self, msg):
"""
Arguments:
msg (str): A string to URI-encode.
Returns (str):
The URI-encoded version of the provided msg, following the encoding
rules specified: https://github.com/aws/aws-msk-iam-auth#uriencode
"""
return urllib.parse.quote(msg, safe=self.UNRESERVED_CHARS)
def _hmac(self, key, msg):
"""
Arguments:
key (bytes): A key to use for the HMAC digest.
msg (str): A value to include in the HMAC digest.
Returns (bytes):
An HMAC digest of the given key and msg.
"""
return hmac.new(key, msg.encode('utf-8'), digestmod=self.hashfunc).digest()
def first_message(self):
"""
Returns (bytes):
An encoded JSON authentication payload that can be sent to the
broker.
"""
signature = hmac.new(
self._signing_key,
self._signing_str.encode('utf-8'),
digestmod=self.hashfunc,
).hexdigest()
msg = {
'version': self.version,
'host': self.host,
'user-agent': 'kafka-python',
'action': self.action,
'x-amz-algorithm': self.algorithm,
'x-amz-credential': self._credential,
'x-amz-date': self.timestamp,
'x-amz-signedheaders': self._signed_headers,
'x-amz-expires': self.expires,
'x-amz-signature': signature,
}
if self.token:
msg['x-amz-security-token'] = self.token
return json.dumps(msg, separators=(',', ':')).encode('utf-8')

View File

@@ -0,0 +1,100 @@
from __future__ import absolute_import
import abc
import logging
from kafka.sasl.abc import SaslMechanism
log = logging.getLogger(__name__)
class SaslMechanismOAuth(SaslMechanism):
def __init__(self, **config):
assert 'sasl_oauth_token_provider' in config, 'sasl_oauth_token_provider required for OAUTHBEARER sasl'
assert isinstance(config['sasl_oauth_token_provider'], AbstractTokenProvider), \
'sasl_oauth_token_provider must implement kafka.sasl.oauth.AbstractTokenProvider'
self.token_provider = config['sasl_oauth_token_provider']
self._error = None
self._is_done = False
self._is_authenticated = False
def auth_bytes(self):
if self._error:
# Server should respond to this with SaslAuthenticate failure, which ends the auth process
return self._error
token = self.token_provider.token()
extensions = self._token_extensions()
return "n,,\x01auth=Bearer {}{}\x01\x01".format(token, extensions).encode('utf-8')
def receive(self, auth_bytes):
if auth_bytes != b'':
error = auth_bytes.decode('utf-8')
log.debug("Sending x01 response to server after receiving SASL OAuth error: %s", error)
self._error = b'\x01'
else:
self._is_done = True
self._is_authenticated = True
def is_done(self):
return self._is_done
def is_authenticated(self):
return self._is_authenticated
def _token_extensions(self):
"""
Return a string representation of the OPTIONAL key-value pairs that can be sent with an OAUTHBEARER
initial request.
"""
# Builds up a string separated by \x01 via a dict of key value pairs
extensions = self.token_provider.extensions()
msg = '\x01'.join(['{}={}'.format(k, v) for k, v in extensions.items()])
return '\x01' + msg if msg else ''
def auth_details(self):
if not self.is_authenticated:
raise RuntimeError('Not authenticated yet!')
return 'Authenticated via SASL / OAuth'
# This statement is compatible with both Python 2.7 & 3+
ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})
class AbstractTokenProvider(ABC):
"""
A Token Provider must be used for the SASL OAuthBearer protocol.
The implementation should ensure token reuse so that multiple
calls at connect time do not create multiple tokens. The implementation
should also periodically refresh the token in order to guarantee
that each call returns an unexpired token. A timeout error should
be returned after a short period of inactivity so that the
broker can log debugging info and retry.
Token Providers MUST implement the token() method
"""
def __init__(self, **config):
pass
@abc.abstractmethod
def token(self):
"""
Returns a (str) ID/Access Token to be sent to the Kafka
client.
"""
pass
def extensions(self):
"""
This is an OPTIONAL method that may be implemented.
Returns a map of key-value pairs that can
be sent with the SASL/OAUTHBEARER initial client request. If
not implemented, the values are ignored. This feature is only available
in Kafka >= 2.1.0.
All returned keys and values should be type str
"""
return {}

View File

@@ -0,0 +1,41 @@
from __future__ import absolute_import
import logging
from kafka.sasl.abc import SaslMechanism
log = logging.getLogger(__name__)
class SaslMechanismPlain(SaslMechanism):
def __init__(self, **config):
if config.get('security_protocol', '') == 'SASL_PLAINTEXT':
log.warning('Sending username and password in the clear')
assert 'sasl_plain_username' in config, 'sasl_plain_username required for PLAIN sasl'
assert 'sasl_plain_password' in config, 'sasl_plain_password required for PLAIN sasl'
self.username = config['sasl_plain_username']
self.password = config['sasl_plain_password']
self._is_done = False
self._is_authenticated = False
def auth_bytes(self):
# Send PLAIN credentials per RFC-4616
return bytes('\0'.join([self.username, self.username, self.password]).encode('utf-8'))
def receive(self, auth_bytes):
self._is_done = True
self._is_authenticated = auth_bytes == b''
def is_done(self):
return self._is_done
def is_authenticated(self):
return self._is_authenticated
def auth_details(self):
if not self.is_authenticated:
raise RuntimeError('Not authenticated yet!')
return 'Authenticated as %s via SASL / Plain' % self.username

View File

@@ -0,0 +1,133 @@
from __future__ import absolute_import
import base64
import hashlib
import hmac
import logging
import uuid
from kafka.sasl.abc import SaslMechanism
from kafka.vendor import six
log = logging.getLogger(__name__)
if six.PY2:
def xor_bytes(left, right):
return bytearray(ord(lb) ^ ord(rb) for lb, rb in zip(left, right))
else:
def xor_bytes(left, right):
return bytes(lb ^ rb for lb, rb in zip(left, right))
class SaslMechanismScram(SaslMechanism):
def __init__(self, **config):
assert 'sasl_plain_username' in config, 'sasl_plain_username required for SCRAM sasl'
assert 'sasl_plain_password' in config, 'sasl_plain_password required for SCRAM sasl'
assert config.get('sasl_mechanism', '') in ScramClient.MECHANISMS, 'Unrecognized SCRAM mechanism'
if config.get('security_protocol', '') == 'SASL_PLAINTEXT':
log.warning('Exchanging credentials in the clear during Sasl Authentication')
self.username = config['sasl_plain_username']
self.mechanism = config['sasl_mechanism']
self._scram_client = ScramClient(
config['sasl_plain_username'],
config['sasl_plain_password'],
config['sasl_mechanism']
)
self._state = 0
def auth_bytes(self):
if self._state == 0:
return self._scram_client.first_message()
elif self._state == 1:
return self._scram_client.final_message()
else:
raise ValueError('No auth_bytes for state: %s' % self._state)
def receive(self, auth_bytes):
if self._state == 0:
self._scram_client.process_server_first_message(auth_bytes)
elif self._state == 1:
self._scram_client.process_server_final_message(auth_bytes)
else:
raise ValueError('Cannot receive bytes in state: %s' % self._state)
self._state += 1
return self.is_done()
def is_done(self):
return self._state == 2
def is_authenticated(self):
# receive raises if authentication fails...?
return self._state == 2
def auth_details(self):
if not self.is_authenticated:
raise RuntimeError('Not authenticated yet!')
return 'Authenticated as %s via SASL / %s' % (self.username, self.mechanism)
class ScramClient:
MECHANISMS = {
'SCRAM-SHA-256': hashlib.sha256,
'SCRAM-SHA-512': hashlib.sha512
}
def __init__(self, user, password, mechanism):
self.nonce = str(uuid.uuid4()).replace('-', '').encode('utf-8')
self.auth_message = b''
self.salted_password = None
self.user = user.encode('utf-8')
self.password = password.encode('utf-8')
self.hashfunc = self.MECHANISMS[mechanism]
self.hashname = ''.join(mechanism.lower().split('-')[1:3])
self.stored_key = None
self.client_key = None
self.client_signature = None
self.client_proof = None
self.server_key = None
self.server_signature = None
def first_message(self):
client_first_bare = b'n=' + self.user + b',r=' + self.nonce
self.auth_message += client_first_bare
return b'n,,' + client_first_bare
def process_server_first_message(self, server_first_message):
self.auth_message += b',' + server_first_message
params = dict(pair.split('=', 1) for pair in server_first_message.decode('utf-8').split(','))
server_nonce = params['r'].encode('utf-8')
if not server_nonce.startswith(self.nonce):
raise ValueError("Server nonce, did not start with client nonce!")
self.nonce = server_nonce
self.auth_message += b',c=biws,r=' + self.nonce
salt = base64.b64decode(params['s'].encode('utf-8'))
iterations = int(params['i'])
self.create_salted_password(salt, iterations)
self.client_key = self.hmac(self.salted_password, b'Client Key')
self.stored_key = self.hashfunc(self.client_key).digest()
self.client_signature = self.hmac(self.stored_key, self.auth_message)
self.client_proof = xor_bytes(self.client_key, self.client_signature)
self.server_key = self.hmac(self.salted_password, b'Server Key')
self.server_signature = self.hmac(self.server_key, self.auth_message)
def hmac(self, key, msg):
return hmac.new(key, msg, digestmod=self.hashfunc).digest()
def create_salted_password(self, salt, iterations):
self.salted_password = hashlib.pbkdf2_hmac(
self.hashname, self.password, salt, iterations
)
def final_message(self):
return b'c=biws,r=' + self.nonce + b',p=' + base64.b64encode(self.client_proof)
def process_server_final_message(self, server_final_message):
params = dict(pair.split('=', 1) for pair in server_final_message.decode('utf-8').split(','))
if self.server_signature != base64.b64decode(params['v'].encode('utf-8')):
raise ValueError("Server sent wrong signature!")

View File

@@ -0,0 +1,111 @@
from __future__ import absolute_import
import logging
# Windows-only
try:
import sspi
import pywintypes
import sspicon
import win32security
except ImportError:
sspi = None
from kafka.sasl.abc import SaslMechanism
log = logging.getLogger(__name__)
class SaslMechanismSSPI(SaslMechanism):
# Establish security context and negotiate protection level
# For reference see RFC 4752, section 3
SASL_QOP_AUTH = 1
SASL_QOP_AUTH_INT = 2
SASL_QOP_AUTH_CONF = 4
def __init__(self, **config):
assert sspi is not None, 'No GSSAPI lib available (gssapi or sspi)'
if 'sasl_kerberos_name' not in config and 'sasl_kerberos_service_name' not in config:
raise ValueError('sasl_kerberos_service_name or sasl_kerberos_name required for GSSAPI sasl configuration')
self._is_done = False
self._is_authenticated = False
if config.get('sasl_kerberos_name', None) is not None:
self.auth_id = str(config['sasl_kerberos_name'])
else:
kerberos_domain_name = config.get('sasl_kerberos_domain_name', '') or config.get('host', '')
self.auth_id = config['sasl_kerberos_service_name'] + '/' + kerberos_domain_name
scheme = "Kerberos" # Do not try with Negotiate for SASL authentication. Tokens are different.
# https://docs.microsoft.com/en-us/windows/win32/secauthn/context-requirements
flags = (
sspicon.ISC_REQ_MUTUAL_AUTH | # mutual authentication
sspicon.ISC_REQ_INTEGRITY | # check for integrity
sspicon.ISC_REQ_SEQUENCE_DETECT | # enable out-of-order messages
sspicon.ISC_REQ_CONFIDENTIALITY # request confidentiality
)
self._client_ctx = sspi.ClientAuth(scheme, targetspn=self.auth_id, scflags=flags)
self._next_token = self._client_ctx.step(None)
def auth_bytes(self):
# GSSAPI Auth does not have a final broker->client message
# so mark is_done after the final auth_bytes are provided
# in practice we'll still receive a response when using SaslAuthenticate
# but not when using the prior unframed approach.
if self._client_ctx.authenticated:
self._is_done = True
self._is_authenticated = True
return self._next_token or b''
def receive(self, auth_bytes):
log.debug("Received token from server (size %s)", len(auth_bytes))
if not self._client_ctx.authenticated:
# calculate an output token from kafka token (or None on first iteration)
# https://docs.microsoft.com/en-us/windows/win32/api/sspi/nf-sspi-initializesecuritycontexta
# https://docs.microsoft.com/en-us/windows/win32/secauthn/initializesecuritycontext--kerberos
# authorize method will wrap for us our token in sspi structures
error, auth = self._client_ctx.authorize(auth_bytes)
if len(auth) > 0 and len(auth[0].Buffer):
log.debug("Got token from context")
# this buffer must be sent to the server whatever the result is
self._next_token = auth[0].Buffer
else:
log.debug("Got no token, exchange finished")
# seems to be the end of the loop
self._next_token = b''
elif self._is_done:
# The final step of gssapi is send, so we do not expect any additional bytes
# however, allow an empty message to support SaslAuthenticate response
if auth_bytes != b'':
raise ValueError("Unexpected receive auth_bytes after sasl/gssapi completion")
else:
# Process the security layer negotiation token, sent by the server
# once the security context is established.
# The following part is required by SASL, but not by classic Kerberos.
# See RFC 4752
# unwraps message containing supported protection levels and msg size
msg, _was_encrypted = self._client_ctx.unwrap(auth_bytes)
# Kafka currently doesn't support integrity or confidentiality security layers, so we
# simply set QoP to 'auth' only (first octet). We reuse the max message size proposed
# by the server
client_flags = self.SASL_QOP_AUTH
server_flags = msg[0]
message_parts = [
bytes(client_flags & server_flags),
msg[:1],
self.auth_id.encode('utf-8'),
]
# add authorization identity to the response, and GSS-wrap
self._next_token = self._client_ctx.wrap(b''.join(message_parts), False)
def is_done(self):
return self._is_done
def is_authenticated(self):
return self._is_authenticated
def auth_details(self):
return 'Authenticated as %s to %s via SASL / SSPI/GSSAPI \\o/' % (self._client_ctx.initiator_name, self._client_ctx.service_name)