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 917d74b  add credentials file support for CQLSH
917d74b is described below

commit 917d74bd35712f8374ecefd1d890d02916162daa
Author: Bowen Song <bowens...@users.noreply.github.com>
AuthorDate: Sat Sep 25 17:16:53 2021 +0100

    add credentials file support for CQLSH
    
    patch by Bowen Song; reviewed by Brian Houser, Stefan Miklosovic and 
Brandon Williams for CASSANDRA-16983
---
 CHANGES.txt                |  1 +
 bin/cqlsh.py               | 89 +++++++++++++++++++++++++++++++++++++++++-----
 conf/cqlshrc.sample        |  4 +--
 conf/credentials.sample    | 25 +++++++++++++
 doc/source/tools/cqlsh.rst | 14 ++++++++
 5 files changed, 122 insertions(+), 11 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 8e06776..f04525b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * add credentials file support to CQLSH (CASSANDRA-16983)
  * Skip remaining bytes in the Envelope buffer when a ProtocolException is 
thrown to avoid double decoding (CASSANDRA-17026)
  * Allow reverse iteration of resources during permissions checking 
(CASSANDRA-17016)
  * Add feature to verify correct ownership of attached locations on disk at 
startup (CASSANDRA-16879)
diff --git a/bin/cqlsh.py b/bin/cqlsh.py
index 37f839d..aa005ce 100755
--- a/bin/cqlsh.py
+++ b/bin/cqlsh.py
@@ -21,11 +21,13 @@ from __future__ import division, unicode_literals, 
print_function
 import cmd
 import codecs
 import csv
+import errno
 import getpass
 import optparse
 import os
 import platform
 import re
+import stat
 import sys
 import traceback
 import warnings
@@ -209,8 +211,9 @@ parser.add_option('--debug', action='store_true',
 parser.add_option('--coverage', action='store_true',
                   help='Collect coverage data')
 parser.add_option("--encoding", help="Specify a non-default encoding for 
output."
-                  + " (Default: %s)" % (UTF8,))
+                  " (Default: %s)" % (UTF8,))
 parser.add_option("--cqlshrc", help="Specify an alternative cqlshrc file 
location.")
+parser.add_option("--credentials", help="Specify an alternative credentials 
file location.")
 parser.add_option('--cqlversion', default=None,
                   help='Specify a particular CQL version, '
                        'by default the highest version supported by the server 
will be used.'
@@ -226,6 +229,13 @@ parser.add_option("--request-timeout", 
default=DEFAULT_REQUEST_TIMEOUT_SECONDS,
 parser.add_option("-t", "--tty", action='store_true', dest='tty',
                   help='Force tty mode (command prompt).')
 
+# This is a hidden option to suppress the warning when the -p/--password 
command line option is used.
+# Power users may use this option if they know no other people has access to 
the system where cqlsh is run or don't care about security.
+# Use of this option in scripting is discouraged. Please use a (temporary) 
credentials file where possible.
+# The Cassandra distributed tests (dtests) also use this option in some tests 
when a well-known password is supplied via the command line.
+parser.add_option("--insecure-password-without-warning", action='store_true', 
dest='insecure_password_without_warning',
+                  help=optparse.SUPPRESS_HELP)
+
 optvalues = optparse.Values()
 (options, arguments) = parser.parse_args(sys.argv[1:], values=optvalues)
 
@@ -251,9 +261,9 @@ OLD_CONFIG_FILE = os.path.expanduser(os.path.join('~', 
'.cqlshrc'))
 if os.path.exists(OLD_CONFIG_FILE):
     if os.path.exists(CONFIG_FILE):
         print('\nWarning: cqlshrc config files were found at both the old 
location ({0})'
-              + ' and the new location ({1}), the old config file will not be 
migrated to the new'
-              + ' location, and the new location will be used for now.  You 
should manually'
-              + ' consolidate the config files at the new location and remove 
the old file.'
+              ' and the new location ({1}), the old config file will not be 
migrated to the new'
+              ' location, and the new location will be used for now.  You 
should manually'
+              ' consolidate the config files at the new location and remove 
the old file.'
               .format(OLD_CONFIG_FILE, CONFIG_FILE))
     else:
         os.rename(OLD_CONFIG_FILE, CONFIG_FILE)
@@ -2107,6 +2117,26 @@ def should_use_color():
     return True
 
 
+def is_file_secure(filename):
+    if is_win:
+        # We simply cannot tell whether the file is seucre on Windows,
+        # because os.stat().st_uid is always 0 and os.stat().st_mode is 
meaningless
+        return True
+
+    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.SafeConfigParser() if sys.version_info < (3, 2) 
else configparser.ConfigParser()
     configs.read(CONFIG_FILE)
@@ -2114,9 +2144,20 @@ def read_options(cmdlineargs, environment):
     rawconfigs = configparser.RawConfigParser()
     rawconfigs.read(CONFIG_FILE)
 
+    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):
+            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 = option_with_default(configs.get, 'authentication', 
'username')
-    optvalues.password = option_with_default(rawconfigs.get, 'authentication', 
'password')
+    optvalues.username = None
+    optvalues.password = None
+    optvalues.credentials = 
os.path.expanduser(option_with_default(configs.get, 'authentication', 
'credentials',
+                                                                   
os.path.join(HISTORY_DIR, 'credentials')))
     optvalues.keyspace = option_with_default(configs.get, 'authentication', 
'keyspace')
     optvalues.browser = option_with_default(configs.get, 'ui', 'browser', None)
     optvalues.completekey = option_with_default(configs.get, 'ui', 
'completekey',
@@ -2153,8 +2194,38 @@ def read_options(cmdlineargs, environment):
     optvalues.connect_timeout = option_with_default(configs.getint, 
'connection', 'timeout', DEFAULT_CONNECT_TIMEOUT_SECONDS)
     optvalues.request_timeout = option_with_default(configs.getint, 
'connection', 'request_timeout', DEFAULT_REQUEST_TIMEOUT_SECONDS)
     optvalues.execute = None
+    optvalues.insecure_password_without_warning = False
 
     (options, arguments) = parser.parse_args(cmdlineargs, values=optvalues)
+
+    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"
+              "\n  b. the file is readable by group or other."
+              "\nPlease ensure the file is owned by the current user and is 
not readable by group or other."
+              "\nOn a Linux or UNIX-like system, you often can do this by 
using the `chown` and `chmod` commands:"
+              "\n  chown YOUR_USERNAME credentials"
+              "\n  chmod 600 credentials\n".format(options.credentials),
+              file=sys.stderr)
+        options.credentials = ''  # ConfigParser.read() will ignore unreadable 
files
+
+    if not options.username:
+        credentials = configparser.SafeConfigParser() if sys.version_info < 
(3, 2) else configparser.ConfigParser()
+        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)
+
+    if not options.password:
+        rawcredentials = configparser.RawConfigParser()
+        rawcredentials.read(options.credentials)
+
+        # 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)
+    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)
+
     # Make sure some user values read from the command line are in unicode
     options.execute = maybe_ensure_text(options.execute)
     options.username = maybe_ensure_text(options.username)
@@ -2295,9 +2366,9 @@ def main(options, hostname, port):
             # does contain a TZ part) was specified
             if options.time_format != DEFAULT_TIMESTAMP_FORMAT:
                 sys.stderr.write("Warning: custom timestamp format specified 
in cqlshrc, "
-                                 + "but local timezone could not be 
detected.\n"
-                                 + "Either install Python 'tzlocal' module for 
auto-detection "
-                                 + "or specify client timezone in your 
cqlshrc.\n\n")
+                                 "but local timezone could not be detected.\n"
+                                 "Either install Python 'tzlocal' module for 
auto-detection "
+                                 "or specify client timezone in your 
cqlshrc.\n\n")
 
     try:
         shell = Shell(hostname,
diff --git a/conf/cqlshrc.sample b/conf/cqlshrc.sample
index 2c00d4a..e79d970 100644
--- a/conf/cqlshrc.sample
+++ b/conf/cqlshrc.sample
@@ -19,8 +19,8 @@
 
 [authentication]
 ;; If Cassandra has auth enabled, fill out these options
-; username = fred
-; password = !!bang!!$
+;; Path to the credentials file, an initial ~ or ~user is expanded to that 
user's home directory
+; credentials = ~/.cassandra/credentials
 ; keyspace = ks1
 
 
diff --git a/conf/credentials.sample b/conf/credentials.sample
new file mode 100644
index 0000000..9b5c644
--- /dev/null
+++ b/conf/credentials.sample
@@ -0,0 +1,25 @@
+; 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.
+;
+; Sample ~/.cassandra/credentials file.
+;
+; Please ensure this file is owned by the user and is not readable by group 
and other users
+
+[plain_text_auth]
+; username = fred
+; password = !!bang!!$
+
diff --git a/doc/source/tools/cqlsh.rst b/doc/source/tools/cqlsh.rst
index 2f47554..d9dd057 100644
--- a/doc/source/tools/cqlsh.rst
+++ b/doc/source/tools/cqlsh.rst
@@ -47,6 +47,16 @@ The ``cqlshrc`` file holds configuration options for cqlsh.  
By default this is
 Example config values and documentation can be found in the 
``conf/cqlshrc.sample`` file of a tarball installation.  You
 can also view the latest version of `cqlshrc online 
<https://github.com/apache/cassandra/blob/trunk/conf/cqlshrc.sample>`__.
 
+credentials
+^^^^^^^^^^^
+
+The ``credentials`` file holds authentication credentials for cqlsh.  By 
default this is in the user's home directory at
+``~/.cassandra/credentials``, but a custom location can be specified with the 
``--credentials`` option.
+This file must be owned by the same user running the ``cqlsh``, and it must 
not be readable by group and other.
+
+Example config values and documentation can be found in the 
``conf/credentials.sample`` file of a tarball installation.  You
+can also view the latest version of `credentials online 
<https://github.com/apache/cassandra/blob/trunk/conf/credentials.sample>`__.
+
 
 Command Line Options
 ^^^^^^^^^^^^^^^^^^^^
@@ -77,6 +87,7 @@ Options:
 ``-p`` ``--password``
   Password to authenticate against Cassandra with, should
   be used in conjunction with ``--user``
+  Insecure. Use ``credentials`` file if you can.
 
 ``-k`` ``--keyspace``
   Keyspace to authenticate to, should be used in conjunction
@@ -94,6 +105,9 @@ Options:
 ``--cqlshrc``
   Specify a non-default location for the ``cqlshrc`` file
 
+``--credentials``
+  Specify a non-default location for the ``credentials`` file
+
 ``-e`` ``--execute``
   Execute the given statement, then exit
 

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org

Reply via email to