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]

Reply via email to