This is an automated email from the ASF dual-hosted git repository.
smiklosovic pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new 381c2a4fa8 add plugin support for CQLSH
381c2a4fa8 is described below
commit 381c2a4fa82db80543fd67da6669de022d7576c4
Author: Bhouse99 <[email protected]>
AuthorDate: Wed Mar 30 13:37:22 2022 -0700
add plugin support for CQLSH
patch by Brian Houser; reviewed by Stefan Miklosovic and Brandon Williams
for CASSANDRA-16456
---
.gitignore | 1 +
CHANGES.txt | 1 +
bin/cqlsh.py | 68 ++++----
build.xml | 5 +-
conf/cqlshrc.sample | 8 +
conf/credentials.sample | 2 +-
lib/puresasl-internal-only-0.6.2.zip | Bin 0 -> 9085 bytes
pylib/cqlshlib/authproviderhandling.py | 176 +++++++++++++++++++
pylib/cqlshlib/test/test_authproviderhandling.py | 190 +++++++++++++++++++++
.../complex_auth_provider | 10 ++
.../complex_auth_provider_creds | 3 +
.../complex_auth_provider_with_pass | 11 ++
.../test_authproviderhandling_config/empty_example | 2 +
.../full_plain_text_example | 10 ++
.../illegal_example | 5 +
.../no_classname_example | 5 +
.../partial_example | 8 +
.../plain_text_full_creds | 3 +
.../plain_text_partial_creds | 2 +
.../plain_text_partial_example | 8 +
pylib/cqlshlib/util.py | 19 ++-
21 files changed, 501 insertions(+), 36 deletions(-)
diff --git a/.gitignore b/.gitignore
index a99fd14310..c8e79b312a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ lib/
pylib/src/
**/cqlshlib.xml
!lib/cassandra-driver-internal-only-*.zip
+!lib/puresasl-*.zip
# C* debs
build-stamp
diff --git a/CHANGES.txt b/CHANGES.txt
index 4ae12a7c97..7850ca8e5b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
4.1
+ * Add plugin support for CQLSH (CASSANDRA-16456)
* Add guardrail to disallow querying with ALLOW FILTERING (CASSANDRA-17370)
* Enhance SnakeYAML properties to be reusable outside of YAML parsing,
support camel case conversion to snake case, and add support to ignore
properties (CASSANDRA-17166)
* nodetool compact should support using a key string to find the range to
avoid operators having to manually do this (CASSANDRA-17537)
diff --git a/bin/cqlsh.py b/bin/cqlsh.py
index c412d20ddf..637c95e70a 100755
--- a/bin/cqlsh.py
+++ b/bin/cqlsh.py
@@ -113,7 +113,7 @@ if cql_zip:
sys.path.insert(0, os.path.join(cql_zip, 'cassandra-driver-' + ver))
# the driver needs dependencies
-third_parties = ('six-')
+third_parties = ('six-', 'puresasl-')
for lib in third_parties:
lib_zip = find_zip(lib)
@@ -145,7 +145,7 @@ cqlshlibdir = os.path.join(CASSANDRA_PATH, 'pylib')
if os.path.isdir(cqlshlibdir):
sys.path.insert(0, cqlshlibdir)
-from cqlshlib import cql3handling, pylexotron, sslhandling, cqlshhandling
+from cqlshlib import cql3handling, pylexotron, sslhandling, cqlshhandling,
authproviderhandling
from cqlshlib.copyutil import ExportTask, ImportTask
from cqlshlib.displaying import (ANSI_RESET, BLUE, COLUMN_NAME_COLORS, CYAN,
RED, WHITE, FormattedValue, colorme)
@@ -154,6 +154,8 @@ from cqlshlib.formatting import (DEFAULT_DATE_FORMAT,
DEFAULT_NANOTIME_FORMAT,
format_by_type)
from cqlshlib.tracing import print_trace, print_trace_session
from cqlshlib.util import get_file_encoding_bomsize
+from cqlshlib.util import is_file_secure
+
DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 9042
@@ -426,7 +428,7 @@ class Shell(cmd.Cmd):
default_page_size = 100
def __init__(self, hostname, port, color=False,
- username=None, password=None, encoding=None, stdin=None,
tty=True,
+ username=None, encoding=None, stdin=None, tty=True,
completekey=DEFAULT_COMPLETEKEY, browser=None, use_conn=None,
cqlver=None, keyspace=None,
tracing_enabled=False, expand_enabled=False,
@@ -442,16 +444,21 @@ class Shell(cmd.Cmd):
request_timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
protocol_version=None,
connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS,
- is_subshell=False):
+ is_subshell=False,
+ auth_provider=None):
cmd.Cmd.__init__(self, completekey=completekey)
self.hostname = hostname
self.port = port
- self.auth_provider = None
- if username:
- if not password:
- password = getpass.getpass()
- self.auth_provider = PlainTextAuthProvider(username=username,
password=password)
+ self.auth_provider = auth_provider
self.username = username
+
+ if isinstance(auth_provider, PlainTextAuthProvider):
+ self.username = auth_provider.username
+ if not auth_provider.password:
+ # if no password is provided, we need to query the user to get
one.
+ password = getpass.getpass()
+ self.auth_provider =
PlainTextAuthProvider(username=auth_provider.username, password=password)
+
self.keyspace = keyspace
self.ssl = ssl
self.tracing_enabled = tracing_enabled
@@ -1613,10 +1620,8 @@ class Shell(cmd.Cmd):
except IOError as e:
self.printerr('Could not open %r: %s' % (fname, e))
return
- username = self.auth_provider.username if self.auth_provider else None
- password = self.auth_provider.password if self.auth_provider else None
subshell = Shell(self.hostname, self.port, color=self.color,
- username=username, password=password,
+ username=self.username,
encoding=self.encoding, stdin=f, tty=False,
use_conn=self.conn,
cqlver=self.cql_version,
keyspace=self.current_keyspace,
tracing_enabled=self.tracing_enabled,
@@ -1629,7 +1634,8 @@ class Shell(cmd.Cmd):
max_trace_wait=self.max_trace_wait, ssl=self.ssl,
request_timeout=self.session.default_timeout,
connect_timeout=self.conn.connect_timeout,
- is_subshell=True)
+ is_subshell=True,
+ auth_provider=self.auth_provider)
# duplicate coverage related settings in subshell
if self.coverage:
subshell.coverage = True
@@ -2077,21 +2083,6 @@ def should_use_color():
return True
-def is_file_secure(filename):
- try:
- st = os.stat(filename)
- except OSError as e:
- if e.errno != errno.ENOENT:
- raise
- return True # the file doesn't exists, the security of it is
irrelevant
-
- uid = os.getuid()
-
- # Skip enforcing the file owner and UID matching for the root user (uid ==
0).
- # This is to allow "sudo cqlsh" to work with user owned credentials file.
- return (uid == 0 or st.st_uid == uid) and stat.S_IMODE(st.st_mode) &
(stat.S_IRGRP | stat.S_IROTH) == 0
-
-
def read_options(cmdlineargs, environment):
configs = configparser.ConfigParser()
configs.read(CONFIG_FILE)
@@ -2102,13 +2093,14 @@ def read_options(cmdlineargs, environment):
username_from_cqlshrc = option_with_default(configs.get, 'authentication',
'username')
password_from_cqlshrc = option_with_default(rawconfigs.get,
'authentication', 'password')
if username_from_cqlshrc or password_from_cqlshrc:
- if password_from_cqlshrc and not is_file_secure(CONFIG_FILE):
+ if password_from_cqlshrc and not
is_file_secure(os.path.expanduser(CONFIG_FILE)):
print("\nWarning: Password is found in an insecure cqlshrc file.
The file is owned or readable by other users on the system.",
end='', file=sys.stderr)
print("\nNotice: Credentials in the cqlshrc file is deprecated and
will be ignored in the future."
"\nPlease use a credentials file to specify the username and
password.\n", file=sys.stderr)
optvalues = optparse.Values()
+
optvalues.username = None
optvalues.password = None
optvalues.credentials =
os.path.expanduser(option_with_default(configs.get, 'authentication',
'credentials',
@@ -2153,6 +2145,13 @@ def read_options(cmdlineargs, environment):
(options, arguments) = parser.parse_args(cmdlineargs, values=optvalues)
+ # Credentials from cqlshrc will be expanded,
+ # credentials from the command line are also expanded if there is a
space...
+ # we need the following so that these two scenarios will work
+ # cqlsh --credentials=~/.cassandra/creds
+ # cqlsh --credentials ~/.cassandra/creds
+ options.credentials = os.path.expanduser(options.credentials)
+
if not is_file_secure(options.credentials):
print("\nWarning: Credentials file '{0}' exists but is not used,
because:"
"\n a. the file owner is not the current user; or"
@@ -2169,7 +2168,7 @@ def read_options(cmdlineargs, environment):
credentials.read(options.credentials)
# use the username from credentials file but fallback to cqlshrc if
username is absent from the command line parameters
- options.username = option_with_default(credentials.get,
'plain_text_auth', 'username', username_from_cqlshrc)
+ options.username = username_from_cqlshrc
if not options.password:
rawcredentials = configparser.RawConfigParser()
@@ -2177,6 +2176,7 @@ def read_options(cmdlineargs, environment):
# handling password in the same way as username, priority cli >
credentials > cqlshrc
options.password = option_with_default(rawcredentials.get,
'plain_text_auth', 'password', password_from_cqlshrc)
+ options.password = password_from_cqlshrc
elif not options.insecure_password_without_warning:
print("\nWarning: Using a password on the command line interface can
be insecure."
"\nRecommendation: use the credentials file to securely provide
the password.\n", file=sys.stderr)
@@ -2330,7 +2330,6 @@ def main(options, hostname, port):
port,
color=options.color,
username=options.username,
- password=options.password,
stdin=stdin,
tty=options.tty,
completekey=options.completekey,
@@ -2349,7 +2348,12 @@ def main(options, hostname, port):
single_statement=options.execute,
request_timeout=options.request_timeout,
connect_timeout=options.connect_timeout,
- encoding=options.encoding)
+ encoding=options.encoding,
+ auth_provider=authproviderhandling.load_auth_provider(
+ config_file=CONFIG_FILE,
+ cred_file=options.credentials,
+ username=options.username,
+ password=options.password))
except KeyboardInterrupt:
sys.exit('Connection aborted.')
except CQL_ERRORS as e:
diff --git a/build.xml b/build.xml
index 5bb53855f3..8b1bf4c9f9 100644
--- a/build.xml
+++ b/build.xml
@@ -392,7 +392,7 @@
<target name="realclean" depends="clean" description="Remove the entire
build directory and all downloaded artifacts">
<delete>
- <fileset dir="${build.lib}"
excludes="cassandra-driver-internal-only-*"/>
+ <fileset dir="${build.lib}"
excludes="cassandra-driver-internal-only-*,puresasl-internal-only-*"/>
</delete>
<delete dir="${build.dir}" />
<delete dir="${doc.dir}/build" />
@@ -1333,9 +1333,10 @@
<exclude name="ide/nbproject/private/**" />
</tarfileset>
- <!-- python driver -->
+ <!-- python driver, puresasl for SASL / GSSAPI -->
<tarfileset dir="${basedir}" prefix="${final.name}-src">
<include name="lib/cassandra-driver-internal-only-**" />
+ <include name="lib/puresasl-internal-only-**" />
</tarfileset>
<!-- Shell includes in bin/ and tools/bin/ -->
diff --git a/conf/cqlshrc.sample b/conf/cqlshrc.sample
index 0da2b6dba5..4878b589bc 100644
--- a/conf/cqlshrc.sample
+++ b/conf/cqlshrc.sample
@@ -24,6 +24,14 @@
; keyspace = ks1
+[auth_provider]
+;; you can specify any auth provider found in your python environment
+;; module and class will be used to dynamically load the class
+;; all other properties found here and in the credentials file under the class
name
+;; will be passed to the constructor
+; module = cassandra.auth
+; classname = PlainTextAuthProvider
+; username = user1
[ui]
;; Whether or not to display query results with colors
diff --git a/conf/credentials.sample b/conf/credentials.sample
index 9b5c644e9d..23d0beb71b 100644
--- a/conf/credentials.sample
+++ b/conf/credentials.sample
@@ -19,7 +19,7 @@
;
; Please ensure this file is owned by the user and is not readable by group
and other users
-[plain_text_auth]
+[PlainTextAuthProvider]
; username = fred
; password = !!bang!!$
diff --git a/lib/puresasl-internal-only-0.6.2.zip
b/lib/puresasl-internal-only-0.6.2.zip
new file mode 100644
index 0000000000..8314a045f6
Binary files /dev/null and b/lib/puresasl-internal-only-0.6.2.zip differ
diff --git a/pylib/cqlshlib/authproviderhandling.py
b/pylib/cqlshlib/authproviderhandling.py
new file mode 100644
index 0000000000..68031e5101
--- /dev/null
+++ b/pylib/cqlshlib/authproviderhandling.py
@@ -0,0 +1,176 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+"""
+Handles loading of AuthProvider for CQLSH authentication.
+"""
+
+import configparser
+import sys
+from importlib import import_module
+from cqlshlib.util import is_file_secure
+
+
+def _warn_for_plain_text_security(config_file, provider_settings):
+ """
+ Call when using PlainTextAuthProvider
+ check to see if password appears in the basic provider settings
+ as this is a security risk
+
+ Will write errors to stderr
+ """
+ if 'password' in provider_settings:
+ if not is_file_secure(config_file):
+ print("""\nWarning: Password is found in an insecure cqlshrc file.
+ The file is owned or readable by other users on the
system.""",
+ end='',
+ file=sys.stderr)
+ print("""\nNotice: Credentials in the cqlshrc file is deprecated and
+ will be ignored in the future.\n
+ Please use a credentials file to
+ specify the username and password.\n""",
+ file=sys.stderr)
+
+
+def load_auth_provider(config_file=None, cred_file=None, username=None,
password=None):
+ """
+ Function which loads an auth provider from available config.
+
+ Params:
+ * config_file ..: path to cqlsh config file (usually ~/.cassandra/cqlshrc).
+ * cred_file ....: path to cqlsh credentials file (default is
~/.cassandra/credentials).
+ * username .....: override used to return PlainTextAuthProvider according
to legacy case
+ * password .....: override used to return PlainTextAuthProvider according
to legacy case
+
+ Will attempt to load an auth provider from available config file, using
what's found in
+ credentials file as an override.
+
+ Config file is expected to list module name /class in the *auth_provider*
+ section for dynamic loading (which is to be of type auth_provider)
+
+ Additional params passed to the constructor of class should be specified
+ in the *auth_provider* section and can be freely named to match
+ auth provider's expectation.
+
+ If passed username and password these will be overridden and passed to
auth provider
+
+ None is returned if no possible auth provider is found, and no
username/password can be
+ returned. If a username is found, system will assume that
PlainTextAuthProvider was
+ specified
+
+ EXAMPLE CQLSHRC:
+ # .. inside cqlshrc file
+
+ [auth_provider]
+ module = cassandra.auth
+ classname = PlainTextAuthProvider
+ username = user1
+ password = password1
+
+ if credentials file is specified put relevant properties under the class
name
+ EXAMPLE
+ # ... inside credentials file for above example
+ [PlainTextAuthProvider]
+ password = password2
+
+ Credential attributes will override found in the cqlshrc.
+ in the above example, PlainTextAuthProvider would be used with a password
of 'password2',
+ and username of 'user1'
+ """
+
+ def get_settings_from_config(section_name,
+ conf_file,
+
interpolation=configparser.BasicInterpolation()):
+ """
+ Returns dict from section_name, and ini based conf_file
+
+ * section_name ..: Section to read map of properties from (ex:
[auth_provider])
+ * conf_file .....: Ini based config file to read. Will return empty
dict if None.
+ * interpolation .: Interpolation to use.
+
+ If section is not found, or conf_file is None, function will return an
empty dictionary.
+ """
+ conf = configparser.ConfigParser(interpolation=interpolation)
+ if conf_file is None:
+ return {}
+
+ conf.read(conf_file)
+ if section_name in conf.sections():
+ return dict(conf.items(section_name))
+ return {}
+
+ def get_cred_file_settings(classname, creds_file):
+ # Since this is the credentials file we may be encountering raw strings
+ # as these are what passwords, or security tokens may inadvertently
fall into
+ # we don't want interpolation to mess with them.
+ return get_settings_from_config(
+ section_name=classname,
+ conf_file=creds_file,
+ interpolation=None)
+
+ def get_auth_provider_settings(conf_file):
+ return get_settings_from_config(
+ section_name='auth_provider',
+ conf_file=conf_file)
+
+ def get_legacy_settings(legacy_username, legacy_password):
+ result = {}
+ if legacy_username is not None:
+ result['username'] = legacy_username
+ if legacy_password is not None:
+ result['password'] = legacy_password
+ return result
+
+ provider_settings = get_auth_provider_settings(config_file)
+
+ module_name = provider_settings.pop('module', None)
+ class_name = provider_settings.pop('classname', None)
+
+ if module_name is None and class_name is None:
+ # not specified, default to plaintext auth provider
+ module_name = 'cassandra.auth'
+ class_name = 'PlainTextAuthProvider'
+ elif module_name is None or class_name is None:
+ # then this was PARTIALLY specified.
+ return None
+
+ credential_settings = get_cred_file_settings(class_name, cred_file)
+
+ if module_name == 'cassandra.auth' and class_name ==
'PlainTextAuthProvider':
+ # merge credential settings as overrides on top of provider settings.
+
+ # we need to ensure that password property gets "set" in all cases.
+ # this is to support the ability to give the user a prompt in other
parts
+ # of the code.
+ _warn_for_plain_text_security(config_file, provider_settings)
+ ctor_args = {'password': None,
+ **provider_settings,
+ **credential_settings,
+ **get_legacy_settings(username, password)}
+ # if no username, we can't create PlainTextAuthProvider
+ if 'username' not in ctor_args:
+ return None
+ else:
+ # merge credential settings as overrides on top of provider settings.
+ ctor_args = {**provider_settings,
+ **credential_settings,
+ **get_legacy_settings(username, password)}
+
+ # Load class definitions
+ module = import_module(module_name)
+ auth_provider_klass = getattr(module, class_name)
+
+ # instantiate the class
+ return auth_provider_klass(**ctor_args)
diff --git a/pylib/cqlshlib/test/test_authproviderhandling.py
b/pylib/cqlshlib/test/test_authproviderhandling.py
new file mode 100644
index 0000000000..19a61334fd
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling.py
@@ -0,0 +1,190 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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 unittest
+import io
+import os
+import sys
+import pytest
+
+from cassandra.auth import PlainTextAuthProvider
+from cqlshlib.authproviderhandling import load_auth_provider
+
+
+def construct_config_path(config_file_name):
+ return os.path.join(os.path.dirname(__file__),
+ 'test_authproviderhandling_config',
+ config_file_name)
+
+
+# Simple class to help verify AuthProviders that don't need arguments.
+class NoUserNamePlainTextAuthProvider(PlainTextAuthProvider):
+ def __init__(self):
+ super(NoUserNamePlainTextAuthProvider, self).__init__('', '')
+
+
+class ComplexTextAuthProvider(PlainTextAuthProvider):
+ def __init__(self, username, password='default_pass', extra_flag=None):
+ super(ComplexTextAuthProvider, self).__init__(username, password)
+ self.extra_flag = extra_flag
+
+
+def _assert_auth_provider_matches(actual, klass, expected_props):
+ """
+ Assert that the provider matches class and properties
+ * actual ..........: Thing to compare with it
+ * klass ...........: Class to ensure this matches to (ie
PlainTextAuthProvider)
+ * expected_props ..: Dict of var properties to match
+ """
+ assert isinstance(actual, klass)
+ assert expected_props == vars(actual)
+
+class CustomAuthProviderTest(unittest.TestCase):
+
+ def setUp(self):
+ self._captured_std_err = io.StringIO()
+ sys.stderr = self._captured_std_err
+
+ def tearDown(self):
+ self._captured_std_err.close()
+ sys.stdout = sys.__stderr__
+
+ def test_no_warning_insecure_if_no_pass(self):
+ load_auth_provider(construct_config_path('plain_text_partial_example'))
+ err_msg = self._captured_std_err.getvalue()
+ assert err_msg == ''
+
+ def test_insecure_creds(self):
+ load_auth_provider(construct_config_path('full_plain_text_example'))
+ err_msg = self._captured_std_err.getvalue()
+ assert "Notice:" in err_msg
+ assert "Warning:" in err_msg
+
+ def test_creds_not_checked_for_non_plaintext(self):
+
load_auth_provider(construct_config_path('complex_auth_provider_with_pass'))
+ err_msg = self._captured_std_err.getvalue()
+ assert err_msg == ''
+
+ def test_partial_property_example(self):
+ actual = load_auth_provider(construct_config_path('partial_example'))
+ _assert_auth_provider_matches(
+ actual,
+ NoUserNamePlainTextAuthProvider,
+ {"username": '',
+ "password": ''})
+
+ def test_full_property_example(self):
+ actual =
load_auth_provider(construct_config_path('full_plain_text_example'))
+ _assert_auth_provider_matches(
+ actual,
+ PlainTextAuthProvider,
+ {"username": 'user1',
+ "password": 'pass1'})
+
+ def test_empty_example(self):
+ actual = load_auth_provider(construct_config_path('empty_example'))
+ assert actual is None
+
+ def test_plaintextauth_when_not_defined(self):
+ creds_file = construct_config_path('plain_text_full_creds')
+ actual = load_auth_provider(cred_file=creds_file)
+ _assert_auth_provider_matches(
+ actual,
+ PlainTextAuthProvider,
+ {"username": 'user2',
+ "password": 'pass2'})
+
+ def test_no_cqlshrc_file(self):
+ actual = load_auth_provider()
+ assert actual is None
+
+ def test_no_classname_example(self):
+ actual =
load_auth_provider(construct_config_path('no_classname_example'))
+ assert actual is None
+
+ def test_improper_config_example(self):
+ with pytest.raises(ModuleNotFoundError) as error:
+ load_auth_provider(construct_config_path('illegal_example'))
+ assert error is not None
+
+ def test_username_password_passed_from_commandline(self):
+ creds_file = construct_config_path('complex_auth_provider_creds')
+ cqlshrc = construct_config_path('complex_auth_provider')
+
+ actual = load_auth_provider(cqlshrc, creds_file, 'user-from-legacy',
'pass-from-legacy')
+ _assert_auth_provider_matches(
+ actual,
+ ComplexTextAuthProvider,
+ {"username": 'user-from-legacy',
+ "password": 'pass-from-legacy',
+ "extra_flag": 'flag2'})
+
+ def test_creds_example(self):
+ creds_file = construct_config_path('complex_auth_provider_creds')
+ cqlshrc = construct_config_path('complex_auth_provider')
+
+ actual = load_auth_provider(cqlshrc, creds_file)
+ _assert_auth_provider_matches(
+ actual,
+ ComplexTextAuthProvider,
+ {"username": 'user1',
+ "password": 'pass2',
+ "extra_flag": 'flag2'})
+
+ def test_legacy_example_use_passed_username(self):
+ creds_file = construct_config_path('plain_text_partial_creds')
+ cqlshrc = construct_config_path('plain_text_partial_example')
+
+ actual = load_auth_provider(cqlshrc, creds_file, 'user3')
+ _assert_auth_provider_matches(
+ actual,
+ PlainTextAuthProvider,
+ {"username": 'user3',
+ "password": 'pass2'})
+
+ def test_legacy_example_no_auth_provider_given(self):
+ cqlshrc = construct_config_path('empty_example')
+ creds_file = construct_config_path('complex_auth_provider_creds')
+
+ actual = load_auth_provider(cqlshrc, creds_file, 'user3', 'pass3')
+ _assert_auth_provider_matches(
+ actual,
+ PlainTextAuthProvider,
+ {"username": 'user3',
+ "password": 'pass3'})
+
+ def test_shouldnt_pass_no_password_when_alt_auth_provider(self):
+ cqlshrc = construct_config_path('complex_auth_provider')
+ creds_file = None
+
+ actual = load_auth_provider(cqlshrc, creds_file, 'user3')
+ _assert_auth_provider_matches(
+ actual,
+ ComplexTextAuthProvider,
+ {"username": 'user3',
+ "password": 'default_pass',
+ "extra_flag": 'flag1'})
+
+ def test_legacy_example_no_password(self):
+ cqlshrc = construct_config_path('plain_text_partial_example')
+ creds_file = None
+
+ actual = load_auth_provider(cqlshrc, creds_file, 'user3')
+ _assert_auth_provider_matches(
+ actual,
+ PlainTextAuthProvider,
+ {"username": 'user3',
+ "password": None})
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider
b/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider
new file mode 100644
index 0000000000..879b7a66d6
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider
@@ -0,0 +1,10 @@
+; Config for a custom auth provider that uses the auth_provider field
+; ComplexTextAuthProvider is a PlainTextAuthProvider in the driver which
+; takes an extra field (extra_flag).
+; used by unit testing
+
+[auth_provider]
+module = cqlshlib.test.test_authproviderhandling
+classname = ComplexTextAuthProvider
+username = user1
+extra_flag = flag1
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider_creds
b/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider_creds
new file mode 100644
index 0000000000..bb102bc1ac
--- /dev/null
+++
b/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider_creds
@@ -0,0 +1,3 @@
+[ComplexTextAuthProvider]
+extra_flag = flag2
+password = pass2
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider_with_pass
b/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider_with_pass
new file mode 100644
index 0000000000..c322008750
--- /dev/null
+++
b/pylib/cqlshlib/test/test_authproviderhandling_config/complex_auth_provider_with_pass
@@ -0,0 +1,11 @@
+; Config for a custom auth provider that uses the auth_provider field
+; ComplexTextAuthProvider is a PlainTextAuthProvider in the driver which
+; takes an extra field (extra_flag).
+; used by unit testing
+
+[auth_provider]
+module = cqlshlib.test.test_authproviderhandling
+classname = ComplexTextAuthProvider
+username = user1
+password = pass1
+extra_flag = flag1
diff --git a/pylib/cqlshlib/test/test_authproviderhandling_config/empty_example
b/pylib/cqlshlib/test/test_authproviderhandling_config/empty_example
new file mode 100644
index 0000000000..3dfda04654
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling_config/empty_example
@@ -0,0 +1,2 @@
+; Config for a custom auth provider that uses only the auth_provider field
+
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/full_plain_text_example
b/pylib/cqlshlib/test/test_authproviderhandling_config/full_plain_text_example
new file mode 100644
index 0000000000..b962e63d53
--- /dev/null
+++
b/pylib/cqlshlib/test/test_authproviderhandling_config/full_plain_text_example
@@ -0,0 +1,10 @@
+; Config for a custom auth provider that uses all possible fields
+; This example loads the PlainTextAuthProvider and passes username and
password to constructor
+; dynamically.
+; used by unit testing
+
+[auth_provider]
+module = cassandra.auth
+classname = PlainTextAuthProvider
+username = user1
+password = pass1
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/illegal_example
b/pylib/cqlshlib/test/test_authproviderhandling_config/illegal_example
new file mode 100644
index 0000000000..615fe9f184
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling_config/illegal_example
@@ -0,0 +1,5 @@
+; Example that shouldn't work
+
+[auth_provider]
+module = nowhere.illegal.wrong
+classname = badclass
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/no_classname_example
b/pylib/cqlshlib/test/test_authproviderhandling_config/no_classname_example
new file mode 100644
index 0000000000..cf27bfd435
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling_config/no_classname_example
@@ -0,0 +1,5 @@
+; Config for a custom auth provider that uses only the auth_provider field
+; this version doesn't have a classname, but has a module name.
+
+[auth_provider]
+module = cqlshlib.test
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/partial_example
b/pylib/cqlshlib/test/test_authproviderhandling_config/partial_example
new file mode 100644
index 0000000000..23be26e3ee
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling_config/partial_example
@@ -0,0 +1,8 @@
+; Config for a custom auth provider that uses only the auth_provider field
+; NoUserNamePlainTextAuthProvider is a PlainTextAuthProvider in the driver
which
+; doesn't take a username or password.
+; used by unit testing
+
+[auth_provider]
+module = cqlshlib.test.test_authproviderhandling
+classname = NoUserNamePlainTextAuthProvider
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_full_creds
b/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_full_creds
new file mode 100644
index 0000000000..3cd44708c3
--- /dev/null
+++ b/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_full_creds
@@ -0,0 +1,3 @@
+[PlainTextAuthProvider]
+password = pass2
+username = user2
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_partial_creds
b/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_partial_creds
new file mode 100644
index 0000000000..1faf24dbb1
--- /dev/null
+++
b/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_partial_creds
@@ -0,0 +1,2 @@
+[PlainTextAuthProvider]
+password = pass2
diff --git
a/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_partial_example
b/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_partial_example
new file mode 100644
index 0000000000..37baebdd2e
--- /dev/null
+++
b/pylib/cqlshlib/test/test_authproviderhandling_config/plain_text_partial_example
@@ -0,0 +1,8 @@
+; Config for a custom auth provider that uses some possible fields
+; validate that the partial breakdown works successfully
+; used by unit testing
+
+[auth_provider]
+module = cassandra.auth
+classname = PlainTextAuthProvider
+username = user1
diff --git a/pylib/cqlshlib/util.py b/pylib/cqlshlib/util.py
index f29141d80f..144586aae0 100644
--- a/pylib/cqlshlib/util.py
+++ b/pylib/cqlshlib/util.py
@@ -18,7 +18,9 @@
import cProfile
import codecs
import pstats
-
+import os
+import errno
+import stat
from datetime import timedelta, tzinfo
from io import StringIO
@@ -112,6 +114,21 @@ def trim_if_present(s, prefix):
return s
+def is_file_secure(filename):
+ try:
+ st = os.stat(filename)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ # the file doesn't exist, the security of it is irrelevant
+ return True
+ uid = os.getuid()
+
+ # Skip enforcing the file owner and UID matching for the root user (uid ==
0).
+ # This is to allow "sudo cqlsh" to work with user owned credentials file.
+ return (uid == 0 or st.st_uid == uid) and stat.S_IMODE(st.st_mode) &
(stat.S_IRGRP | stat.S_IROTH) == 0
+
+
def get_file_encoding_bomsize(filename):
"""
Checks the beginning of a file for a Unicode BOM. Based on this check,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]