diff --git a/CHANGES.txt b/CHANGES.txt index c1c386495f..e3e19ffe37 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -322,6 +322,7 @@ Merged from 4.1: * Disk usage guardrail cannot be disabled when failure threshold is reached (CASSANDRA-21057) * ReadCommandController should close fast to avoid deadlock when building secondary index (CASSANDRA-19564) Merged from 4.0: + * Add option to disable cqlsh history (CASSANDRA-21180) * Rate limit password changes (CASSANDRA-21202) * Node does not send multiple inflight echos (CASSANDRA-18866) * Obsolete expired SSTables before compaction starts (CASSANDRA-19776) diff --git a/conf/cqlshrc.sample b/conf/cqlshrc.sample index c396edfed9..3c957a79a2 100644 --- a/conf/cqlshrc.sample +++ b/conf/cqlshrc.sample @@ -104,6 +104,15 @@ port = 9042 ; max_trace_wait = 10.0 +[history] +;; Controls whether command history is saved to ~/.cassandra/cqlsh_history +;; When disabled, existing history will still be loaded but new commands +;; will not be saved. This can be useful for security purposes to prevent +;; sensitive commands (e.g., those containing passwords) from being persisted. +;; This can also be controlled via the --disable-history command line option. +; disabled = false + + ;[ssl] ; certfile = ~/keys/cassandra.cert diff --git a/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc b/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc index 2b5c20b1d9..0302cee514 100644 --- a/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc +++ b/doc/modules/cassandra/pages/managing/tools/cqlsh.adoc @@ -57,8 +57,8 @@ Example config values and documentation can be found in the [[cql_history]] == cql history -All CQL commands you execute are written to a history file. By default, CQL history will be written to `~/.cassandra/cql_history`. You can change this default by setting the environment variable `CQL_HISTORY` like `~/some/other/path/to/cqlsh_history` where `cqlsh_history` is a file. All parent directories to history file will be created if they do not exist. If you do not want to persist history, you can do so by setting CQL_HISTORY to /dev/null. -This feature is supported from Cassandra 4.1. +All CQL commands you execute are written to a history file. By default, CQL history will be written to `~/.cassandra/cql_history`. +You can change this default by setting the environment variable `CQL_HISTORY` like `~/some/other/path/to/cqlsh_history` where `cqlsh_history` is a file. All parent directories to history file will be created if they do not exist. If you do not want to persist history, you can do so by setting CQL_HISTORY to `/dev/null`. This can also be controlled via the --disable-history command line option or from the cqlshrc file. Disabling history is supported since Cassandra 4.0. == Command Line Options diff --git a/pylib/cqlshlib/cqlshmain.py b/pylib/cqlshlib/cqlshmain.py index 8b26e33078..c13256dd40 100755 --- a/pylib/cqlshlib/cqlshmain.py +++ b/pylib/cqlshlib/cqlshmain.py @@ -25,6 +25,7 @@ import subprocess import sys import time import traceback +from typing import Any import warnings import webbrowser from contextlib import contextmanager @@ -120,6 +121,16 @@ try: except OSError: print('\nWarning: Cannot create directory at `%s`. Command history will not be saved. Please check what was the environment property CQL_HISTORY set to.\n' % HISTORY_DIR) +OLD_HISTORY = os.path.expanduser(os.path.join('~', '.cqlsh_history')) +if os.path.exists(OLD_HISTORY): + if os.path.exists(HISTORY): + print('\nWarning: .cqlsh_history files were found at both the old location ({0})' + + ' and the new location ({1}), the old file will not be migrated to the new' + + ' location, and the new location will be used for now. You should manually' + + ' consolidate these files at the new location, and remove the old file.' + .format(OLD_HISTORY, HISTORY)) + else: + os.rename(OLD_HISTORY, HISTORY) # END history config @@ -272,7 +283,8 @@ class Shell(cmd.Cmd): protocol_version=None, connect_timeout=DEFAULT_CONNECT_TIMEOUT_SECONDS, is_subshell=False, - auth_provider=None): + auth_provider=None, + disable_history=False): cmd.Cmd.__init__(self, completekey=completekey) self.hostname = hostname self.port = port @@ -354,6 +366,14 @@ class Shell(cmd.Cmd): self.check_build_versions() if tty: + # Inform users about history logging if not disabled + if not disable_history and readline is not None: + print() + print("ATTENTION: All commands will be saved to history file: %s" % HISTORY) + print("This may include sensitive information such as passwords.") + print("To disable history, use --disable-history or set 'disabled = true' in the [history] section of cqlshrc.") + print("See https://cassandra.apache.org/doc/latest/tools/cqlsh.html for more information.") + print() self.reset_prompt() self.report_connection() print('Use HELP for help.') @@ -1896,8 +1916,8 @@ class Shell(cmd.Cmd): delims += '.' readline.set_completer_delims(delims) - def save_history(self): - if readline is not None: + def save_history(self, history_disabled=False): + if readline is not None and not history_disabled: try: readline.write_history_file(HISTORY) except IOError: @@ -2037,7 +2057,7 @@ def read_options(cmdlineargs, parser, config_file, cql_dir, environment=os.envir argvalues.request_timeout = option_with_default(configs.getint, 'connection', 'request_timeout', Shell.DEFAULT_REQUEST_TIMEOUT_SECONDS) argvalues.execute = None argvalues.insecure_password_without_warning = False - + argvalues.disable_history = option_with_default(configs.getboolean, 'history', 'disabled', False) options, arguments = parser.parse_known_args(cmdlineargs, argvalues) # Credentials from cqlshrc will be expanded, @@ -2208,6 +2228,8 @@ def main(cmdline, pkgpath): help='Specify the default request timeout in seconds (default: %(default)s seconds).') parser.add_argument("-t", "--tty", action='store_true', dest='tty', help='Force tty mode (command prompt).') + parser.add_argument('--disable-history', default=False, action='store_true', + help='Disable saving of history (existing history will still be loaded)') # 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. @@ -2233,6 +2255,18 @@ def main(cmdline, pkgpath): config_file = default_cqlshrc cql_dir = os.path.dirname(config_file) + + 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.' + .format(old_config_file, config_file)) + else: + os.rename(old_config_file, config_file) + (options, hostname, port) = read_options(cmdline, parser, config_file, cql_dir) docspath = get_docspath(pkgpath) @@ -2332,7 +2366,8 @@ def main(cmdline, pkgpath): config_file=config_file, cred_file=options.credentials, username=options.username, - password=options.password)) + password=options.password), + disable_history=options.disable_history) except KeyboardInterrupt: sys.exit('Connection aborted.') except CQL_ERRORS as e: @@ -2353,7 +2388,7 @@ def main(cmdline, pkgpath): shell.init_history() shell.cmdloop() - shell.save_history() + shell.save_history(options.disable_history) if shell.batch_mode and shell.statement_error: sys.exit(2)