diff --git a/CHANGES.txt b/CHANGES.txt index 14d1a075bd..ab0e548b0d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -16,6 +16,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 56011f4927..79d719460e 100644 --- a/conf/cqlshrc.sample +++ b/conf/cqlshrc.sample @@ -111,6 +111,16 @@ port = 9042 +[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 bed5bcb458..61ab25de33 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 e3a2c14ae4..5c97fa3f74 100755 --- a/pylib/cqlshlib/cqlshmain.py +++ b/pylib/cqlshlib/cqlshmain.py @@ -150,6 +150,8 @@ parser.add_argument("--request-timeout", default=DEFAULT_REQUEST_TIMEOUT_SECONDS 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. @@ -191,7 +193,6 @@ 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) - # END history config DEFAULT_CQLSHRC = os.path.expanduser(os.path.join('~', '.cassandra', 'cqlshrc')) @@ -206,6 +207,22 @@ else: 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) +OLD_HISTORY = os.path.expanduser(os.path.join('~', '.cqlsh_history')) +if os.path.exists(OLD_HISTORY): + os.rename(OLD_HISTORY, HISTORY) + +# END history/config definition + CQL_ERRORS = ( cassandra.AlreadyExists, cassandra.AuthenticationFailed, cassandra.CoordinationFailure, cassandra.InvalidRequest, cassandra.Timeout, cassandra.Unauthorized, cassandra.OperationTimedOut, @@ -362,7 +379,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 @@ -441,6 +459,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.') @@ -1946,8 +1972,8 @@ class Shell(cmd.Cmd): # configure length of history shown self.max_history_length_shown = 50 - 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: @@ -2089,7 +2115,7 @@ def read_options(cmdlineargs, environment=os.environ): argvalues.request_timeout = option_with_default(configs.getint, 'connection', 'request_timeout', 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, @@ -2345,7 +2371,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: @@ -2366,7 +2393,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)