Spaces:
Running
Running
# Copyright DataStax, Inc. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
import socket | |
import logging | |
try: | |
import kerberos | |
_have_kerberos = True | |
except ImportError: | |
_have_kerberos = False | |
try: | |
from puresasl.client import SASLClient | |
_have_puresasl = True | |
except ImportError: | |
_have_puresasl = False | |
try: | |
from puresasl.client import SASLClient | |
except ImportError: | |
SASLClient = None | |
log = logging.getLogger(__name__) | |
# Custom payload keys related to DSE Unified Auth | |
_proxy_execute_key = 'ProxyExecute' | |
class AuthProvider(object): | |
""" | |
An abstract class that defines the interface that will be used for | |
creating :class:`~.Authenticator` instances when opening new | |
connections to Cassandra. | |
.. versionadded:: 2.0.0 | |
""" | |
def new_authenticator(self, host): | |
""" | |
Implementations of this class should return a new instance | |
of :class:`~.Authenticator` or one of its subclasses. | |
""" | |
raise NotImplementedError() | |
class Authenticator(object): | |
""" | |
An abstract class that handles SASL authentication with Cassandra servers. | |
Each time a new connection is created and the server requires authentication, | |
a new instance of this class will be created by the corresponding | |
:class:`~.AuthProvider` to handler that authentication. The lifecycle of the | |
new :class:`~.Authenticator` will the be: | |
1) The :meth:`~.initial_response()` method will be called. The return | |
value will be sent to the server to initiate the handshake. | |
2) The server will respond to each client response by either issuing a | |
challenge or indicating that the authentication is complete (successful or not). | |
If a new challenge is issued, :meth:`~.evaluate_challenge()` | |
will be called to produce a response that will be sent to the | |
server. This challenge/response negotiation will continue until the server | |
responds that authentication is successful (or an :exc:`~.AuthenticationFailed` | |
is raised). | |
3) When the server indicates that authentication is successful, | |
:meth:`~.on_authentication_success` will be called a token string that | |
that the server may optionally have sent. | |
The exact nature of the negotiation between the client and server is specific | |
to the authentication mechanism configured server-side. | |
.. versionadded:: 2.0.0 | |
""" | |
server_authenticator_class = None | |
""" Set during the connection AUTHENTICATE phase """ | |
def initial_response(self): | |
""" | |
Returns an message to send to the server to initiate the SASL handshake. | |
:const:`None` may be returned to send an empty message. | |
""" | |
return None | |
def evaluate_challenge(self, challenge): | |
""" | |
Called when the server sends a challenge message. Generally, this method | |
should return :const:`None` when authentication is complete from a | |
client perspective. Otherwise, a string should be returned. | |
""" | |
raise NotImplementedError() | |
def on_authentication_success(self, token): | |
""" | |
Called when the server indicates that authentication was successful. | |
Depending on the authentication mechanism, `token` may be :const:`None` | |
or a string. | |
""" | |
pass | |
class PlainTextAuthProvider(AuthProvider): | |
""" | |
An :class:`~.AuthProvider` that works with Cassandra's PasswordAuthenticator. | |
Example usage:: | |
from cassandra.cluster import Cluster | |
from cassandra.auth import PlainTextAuthProvider | |
auth_provider = PlainTextAuthProvider( | |
username='cassandra', password='cassandra') | |
cluster = Cluster(auth_provider=auth_provider) | |
.. versionadded:: 2.0.0 | |
""" | |
def __init__(self, username, password): | |
self.username = username | |
self.password = password | |
def new_authenticator(self, host): | |
return PlainTextAuthenticator(self.username, self.password) | |
class TransitionalModePlainTextAuthProvider(object): | |
""" | |
An :class:`~.AuthProvider` that works with DSE TransitionalModePlainTextAuthenticator. | |
Example usage:: | |
from cassandra.cluster import Cluster | |
from cassandra.auth import TransitionalModePlainTextAuthProvider | |
auth_provider = TransitionalModePlainTextAuthProvider() | |
cluster = Cluster(auth_provider=auth_provider) | |
.. warning:: TransitionalModePlainTextAuthProvider will be removed in cassandra-driver | |
4.0. The transitional mode will be handled internally without the need | |
of any auth provider. | |
""" | |
def __init__(self): | |
# TODO remove next major | |
log.warning("TransitionalModePlainTextAuthProvider will be removed in cassandra-driver " | |
"4.0. The transitional mode will be handled internally without the need " | |
"of any auth provider.") | |
def new_authenticator(self, host): | |
return TransitionalModePlainTextAuthenticator() | |
class SaslAuthProvider(AuthProvider): | |
""" | |
An :class:`~.AuthProvider` supporting general SASL auth mechanisms | |
Suitable for GSSAPI or other SASL mechanisms | |
Example usage:: | |
from cassandra.cluster import Cluster | |
from cassandra.auth import SaslAuthProvider | |
sasl_kwargs = {'service': 'something', | |
'mechanism': 'GSSAPI', | |
'qops': 'auth'.split(',')} | |
auth_provider = SaslAuthProvider(**sasl_kwargs) | |
cluster = Cluster(auth_provider=auth_provider) | |
.. versionadded:: 2.1.4 | |
""" | |
def __init__(self, **sasl_kwargs): | |
if SASLClient is None: | |
raise ImportError('The puresasl library has not been installed') | |
if 'host' in sasl_kwargs: | |
raise ValueError("kwargs should not contain 'host' since it is passed dynamically to new_authenticator") | |
self.sasl_kwargs = sasl_kwargs | |
def new_authenticator(self, host): | |
return SaslAuthenticator(host, **self.sasl_kwargs) | |
class SaslAuthenticator(Authenticator): | |
""" | |
A pass-through :class:`~.Authenticator` using the third party package | |
'pure-sasl' for authentication | |
.. versionadded:: 2.1.4 | |
""" | |
def __init__(self, host, service, mechanism='GSSAPI', **sasl_kwargs): | |
if SASLClient is None: | |
raise ImportError('The puresasl library has not been installed') | |
self.sasl = SASLClient(host, service, mechanism, **sasl_kwargs) | |
def initial_response(self): | |
return self.sasl.process() | |
def evaluate_challenge(self, challenge): | |
return self.sasl.process(challenge) | |
# TODO remove me next major | |
DSEPlainTextAuthProvider = PlainTextAuthProvider | |
class DSEGSSAPIAuthProvider(AuthProvider): | |
""" | |
Auth provider for GSS API authentication. Works with legacy `KerberosAuthenticator` | |
or `DseAuthenticator` if `kerberos` scheme is enabled. | |
""" | |
def __init__(self, service='dse', qops=('auth',), resolve_host_name=True, **properties): | |
""" | |
:param service: name of the service | |
:param qops: iterable of "Quality of Protection" allowed; see ``puresasl.QOP`` | |
:param resolve_host_name: boolean flag indicating whether the authenticator should reverse-lookup an FQDN when | |
creating a new authenticator. Default is ``True``, which will resolve, or return the numeric address if there is no PTR | |
record. Setting ``False`` creates the authenticator with the numeric address known by Cassandra | |
:param properties: additional keyword properties to pass for the ``puresasl.mechanisms.GSSAPIMechanism`` class. | |
Presently, 'principal' (user) is the only one referenced in the ``pure-sasl`` implementation | |
""" | |
if not _have_puresasl: | |
raise ImportError('The puresasl library has not been installed') | |
if not _have_kerberos: | |
raise ImportError('The kerberos library has not been installed') | |
self.service = service | |
self.qops = qops | |
self.resolve_host_name = resolve_host_name | |
self.properties = properties | |
def new_authenticator(self, host): | |
if self.resolve_host_name: | |
host = socket.getnameinfo((host, 0), 0)[0] | |
return GSSAPIAuthenticator(host, self.service, self.qops, self.properties) | |
class BaseDSEAuthenticator(Authenticator): | |
def get_mechanism(self): | |
raise NotImplementedError("get_mechanism not implemented") | |
def get_initial_challenge(self): | |
raise NotImplementedError("get_initial_challenge not implemented") | |
def initial_response(self): | |
if self.server_authenticator_class == "com.datastax.bdp.cassandra.auth.DseAuthenticator": | |
return self.get_mechanism() | |
else: | |
return self.evaluate_challenge(self.get_initial_challenge()) | |
class PlainTextAuthenticator(BaseDSEAuthenticator): | |
def __init__(self, username, password): | |
self.username = username | |
self.password = password | |
def get_mechanism(self): | |
return b"PLAIN" | |
def get_initial_challenge(self): | |
return b"PLAIN-START" | |
def evaluate_challenge(self, challenge): | |
if challenge == b'PLAIN-START': | |
data = "\x00%s\x00%s" % (self.username, self.password) | |
return data.encode() | |
raise Exception('Did not receive a valid challenge response from server') | |
class TransitionalModePlainTextAuthenticator(PlainTextAuthenticator): | |
""" | |
Authenticator that accounts for DSE authentication is configured with transitional mode. | |
""" | |
def __init__(self): | |
super(TransitionalModePlainTextAuthenticator, self).__init__('', '') | |
class GSSAPIAuthenticator(BaseDSEAuthenticator): | |
def __init__(self, host, service, qops, properties): | |
properties = properties or {} | |
self.sasl = SASLClient(host, service, 'GSSAPI', qops=qops, **properties) | |
def get_mechanism(self): | |
return b"GSSAPI" | |
def get_initial_challenge(self): | |
return b"GSSAPI-START" | |
def evaluate_challenge(self, challenge): | |
if challenge == b'GSSAPI-START': | |
return self.sasl.process() | |
else: | |
return self.sasl.process(challenge) | |