From 41b701d96f038b1f9fe4dd09d83c06faee811bf6 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Tue, 23 Dec 2025 21:55:24 -0800 Subject: [PATCH 1/7] [feat] Update SSL option to connect securely by default --- changelog.md | 6 +++++- mycli/main.py | 2 +- test/features/db_utils.py | 31 ++++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/changelog.md b/changelog.md index f2132346..7fd9fa36 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,10 @@ -1.42.0 (2025/12/20) +Upcoming (TBD) ============== +Features +-------- +* Update the default SSL value to connect securely by default. + Bug Fixes -------- * Update the prompt display logic to handle an edge case where a socket is used without diff --git a/mycli/main.py b/mycli/main.py index 6f9965b5..3e948b78 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1343,7 +1343,7 @@ def get_last_query(self) -> str | None: @click.option("--ssh-key-filename", help="Private key filename (identify file) for the ssh connection.") @click.option("--ssh-config-path", help="Path to ssh configuration.", default=os.path.expanduser("~") + "/.ssh/config") @click.option("--ssh-config-host", help="Host to connect to ssh server reading from ssh configuration.") -@click.option("--ssl", "ssl_enable", is_flag=True, help="Enable SSL for connection (automatically enabled with other flags).") +@click.option("--ssl", "ssl_enable", is_flag=True, default=True, help="Enable SSL for connection (automatically enabled with other flags).") @click.option("--ssl-ca", help="CA file in PEM format.", type=click.Path(exists=True)) @click.option("--ssl-capath", help="CA directory.") @click.option("--ssl-cert", help="X509 cert in PEM format.", type=click.Path(exists=True)) diff --git a/test/features/db_utils.py b/test/features/db_utils.py index 5c81b661..4a2813a4 100644 --- a/test/features/db_utils.py +++ b/test/features/db_utils.py @@ -1,5 +1,7 @@ # type: ignore +import ssl + import pymysql @@ -14,8 +16,11 @@ def create_db(hostname="localhost", port=3306, username=None, password=None, dbn :return: """ + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.VerifyMode.CERT_NONE cn = pymysql.connect( - host=hostname, port=port, user=username, password=password, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor + host=hostname, port=port, user=username, password=password, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, ssl=ctx ) with cn.cursor() as cr: @@ -39,8 +44,18 @@ def create_cn(hostname, port, password, username, dbname): :return: psycopg2.connection """ + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.VerifyMode.CERT_NONE cn = pymysql.connect( - host=hostname, port=port, user=username, password=password, db=dbname, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor + host=hostname, + port=port, + user=username, + password=password, + db=dbname, + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + ssl=ctx, ) return cn @@ -56,8 +71,18 @@ def drop_db(hostname="localhost", port=3306, username=None, password=None, dbnam :param dbname: string """ + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.VerifyMode.CERT_NONE cn = pymysql.connect( - host=hostname, port=port, user=username, password=password, db=dbname, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor + host=hostname, + port=port, + user=username, + password=password, + db=dbname, + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + ssl=ctx, ) with cn.cursor() as cr: From ba33233bcf887aa344c594fcc1082e71cda22031 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Tue, 23 Dec 2025 22:38:31 -0800 Subject: [PATCH 2/7] Added the --no-ssl option. Updated the changelog. Added to the .gitignore to be less annoying. --- .gitignore | 3 +++ changelog.md | 2 +- mycli/main.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 970fcd4f..1fb195db 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ .venv/ venv/ + +.myclirc +uv.lock diff --git a/changelog.md b/changelog.md index 7fd9fa36..cc29055d 100644 --- a/changelog.md +++ b/changelog.md @@ -3,7 +3,7 @@ Upcoming (TBD) Features -------- -* Update the default SSL value to connect securely by default. +* Update the default SSL value to connect securely by default. Add a --no-ssl option to disable it. Bug Fixes -------- diff --git a/mycli/main.py b/mycli/main.py index 3e948b78..9062c1b3 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1343,7 +1343,9 @@ def get_last_query(self) -> str | None: @click.option("--ssh-key-filename", help="Private key filename (identify file) for the ssh connection.") @click.option("--ssh-config-path", help="Path to ssh configuration.", default=os.path.expanduser("~") + "/.ssh/config") @click.option("--ssh-config-host", help="Host to connect to ssh server reading from ssh configuration.") -@click.option("--ssl", "ssl_enable", is_flag=True, default=True, help="Enable SSL for connection (automatically enabled with other flags).") +@click.option( + "--ssl/--no-ssl", "ssl_enable", is_flag=True, default=True, help="Enable SSL for connection (automatically enabled with other flags)." +) @click.option("--ssl-ca", help="CA file in PEM format.", type=click.Path(exists=True)) @click.option("--ssl-capath", help="CA directory.") @click.option("--ssl-cert", help="X509 cert in PEM format.", type=click.Path(exists=True)) From 3462f0fc2de34e93de0b6d331b972f0759248b2a Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Tue, 30 Dec 2025 15:18:10 -0800 Subject: [PATCH 3/7] Moved connection params to dict to avoid repeating it --- mycli/main.py | 61 +++++++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index b0422639..3812e0c9 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -38,6 +38,7 @@ from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.shortcuts import CompleteStyle, PromptSession from pymysql import OperationalError, err +from pymysql.constants.ER import HANDSHAKE_ERROR from pymysql.cursors import Cursor import sqlglot import sqlparse @@ -506,49 +507,37 @@ def connect( # Connect to the database. def _connect() -> None: + conn_config = { + "database": database, + "user": user, + "password": passwd, + "host": host, + "port": int_port, + "socket": socket, + "charset": charset, + "local_infile": use_local_infile, + "ssl": ssl_config_or_none, + "ssh_user": ssh_user, + "ssh_host": ssh_host, + "ssh_port": int(ssh_port) if ssh_port else None, + "ssh_password": ssh_password, + "ssh_key_filename": ssh_key_filename, + "init_command": init_command, + } try: - self.sqlexecute = SQLExecute( - database, - user, - passwd, - host, - int_port, - socket, - charset, - use_local_infile, - ssl_config_or_none, - ssh_user, - ssh_host, - int(ssh_port) if ssh_port else None, - ssh_password, - ssh_key_filename, - init_command, - ) + self.sqlexecute = SQLExecute(**conn_config) except OperationalError as e: if e.args[0] == ERROR_CODE_ACCESS_DENIED: if password_from_file is not None: - new_passwd = password_from_file + conn_config["password"] = password_from_file else: - new_passwd = click.prompt( + conn_config["password"] = click.prompt( f"Password for {user}", hide_input=True, show_default=False, default='', type=str, err=True ) - self.sqlexecute = SQLExecute( - database, - user, - new_passwd, - host, - int_port, - socket, - charset, - use_local_infile, - ssl_config_or_none, - ssh_user, - ssh_host, - int(ssh_port) if ssh_port else None, - ssh_password, - ssh_key_filename, - init_command, - ) + self.sqlexecute = SQLExecute(**conn_config) + elif e.args[0] == HANDSHAKE_ERROR: + conn_config["ssl"] = None + self.sqlexecute = SQLExecute(**conn_config) else: raise e From 57e1bb86bbaf87cdb4a5c85e54ac8cc8e76a17a7 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Tue, 30 Dec 2025 19:08:32 -0800 Subject: [PATCH 4/7] Added initial logic for a new ssl_mode config/cli option --- mycli/main.py | 126 ++++++++++++++++++++++++++------------ mycli/myclirc | 8 +++ test/features/db_utils.py | 13 +--- test/myclirc | 8 +++ 4 files changed, 105 insertions(+), 50 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 2133f84a..9afb4b53 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -38,7 +38,7 @@ from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.shortcuts import CompleteStyle, PromptSession import pymysql -from pymysql.constants.ER import ERROR_CODE_ACCESS_DENIED, HANDSHAKE_ERROR +from pymysql.constants.ER import HANDSHAKE_ERROR from pymysql.cursors import Cursor import sqlglot import sqlparse @@ -155,6 +155,14 @@ def __init__( self.login_path_as_host = c["main"].as_bool("login_path_as_host") self.post_redirect_command = c['main'].get('post_redirect_command') + # set ssl_mode if a valid option is provided in a config file, otherwise None + ssl_mode = c["ssl"].get("ssl_mode", None) + if ssl_mode not in ("auto", "on", "off", None): + self.echo(f"Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.", err=True, fg="red") + self.ssl_mode = None + else: + self.ssl_mode = ssl_mode + # read from cli argument or user config file self.auto_vertical_output = auto_vertical_output or c["main"].as_bool("auto_vertical_output") self.show_warnings = show_warnings or c["main"].as_bool("show_warnings") @@ -524,37 +532,67 @@ def connect( # Connect to the database. def _connect() -> None: - conn_config = { - "database": database, - "user": user, - "password": passwd, - "host": host, - "port": int_port, - "socket": socket, - "charset": charset, - "local_infile": use_local_infile, - "ssl": ssl_config_or_none, - "ssh_user": ssh_user, - "ssh_host": ssh_host, - "ssh_port": int(ssh_port) if ssh_port else None, - "ssh_password": ssh_password, - "ssh_key_filename": ssh_key_filename, - "init_command": init_command, - } try: - self.sqlexecute = SQLExecute(**conn_config) + self.sqlexecute = SQLExecute( + database, + user, + passwd, + host, + int_port, + socket, + charset, + use_local_infile, + ssl_config_or_none, + ssh_user, + ssh_host, + int(ssh_port) if ssh_port else None, + ssh_password, + ssh_key_filename, + init_command, + ) except pymysql.OperationalError as e: if e.args[0] == ERROR_CODE_ACCESS_DENIED: if password_from_file is not None: - conn_config["password"] = password_from_file + new_passwd = password_from_file else: - conn_config["password"] = click.prompt( + new_passwd = click.prompt( f"Password for {user}", hide_input=True, show_default=False, default='', type=str, err=True ) - self.sqlexecute = SQLExecute(**conn_config) - elif e.args[0] == HANDSHAKE_ERROR: - conn_config["ssl"] = None - self.sqlexecute = SQLExecute(**conn_config) + self.sqlexecute = SQLExecute( + database, + user, + new_passwd, + host, + int_port, + socket, + charset, + use_local_infile, + ssl_config_or_none, + ssh_user, + ssh_host, + int(ssh_port) if ssh_port else None, + ssh_password, + ssh_key_filename, + init_command, + ) + elif e.args[0] == HANDSHAKE_ERROR and ssl is not None and ssl.get("mode", None) == "auto": + self.sqlexecute = SQLExecute( + database, + user, + passwd, + host, + int_port, + socket, + charset, + use_local_infile, + None, + ssh_user, + ssh_host, + int(ssh_port) if ssh_port else None, + ssh_password, + ssh_key_filename, + init_command, + ) else: raise e @@ -1387,6 +1425,7 @@ def get_last_query(self) -> str | None: @click.option("--ssh-key-filename", help="Private key filename (identify file) for the ssh connection.") @click.option("--ssh-config-path", help="Path to ssh configuration.", default=os.path.expanduser("~") + "/.ssh/config") @click.option("--ssh-config-host", help="Host to connect to ssh server reading from ssh configuration.") +@click.option("--ssl-mode", "ssl_mode", default="auto", help="Set desired SSL behavior. auto=preferred, on=required, off=off.", type=str) @click.option( "--ssl/--no-ssl", "ssl_enable", is_flag=True, default=True, help="Enable SSL for connection (automatically enabled with other flags)." ) @@ -1455,6 +1494,7 @@ def cli( auto_vertical_output: bool, show_warnings: bool, local_infile: bool, + ssl_mode: str | None, ssl_enable: bool, ssl_ca: str | None, ssl_capath: str | None, @@ -1597,19 +1637,29 @@ def cli( ssl_verify_server_cert = ssl_verify_server_cert or (params[0].lower() == 'true') ssl_enable = True - ssl = { - "enable": ssl_enable, - "ca": ssl_ca and os.path.expanduser(ssl_ca), - "cert": ssl_cert and os.path.expanduser(ssl_cert), - "key": ssl_key and os.path.expanduser(ssl_key), - "capath": ssl_capath, - "cipher": ssl_cipher, - "tls_version": tls_version, - "check_hostname": ssl_verify_server_cert, - } - - # remove empty ssl options - ssl = {k: v for k, v in ssl.items() if v is not None} + if ssl_mode not in ("auto", "on", "off"): + click.secho(f"Invalid option provided for --ssl-mode: {ssl_mode}. See --help for valid options.", err=True, fg="red") + sys.exit(1) + + ssl_mode = ssl_mode or mycli.ssl_mode # cli option or config option + + if ssl_mode in ("auto", "on"): + ssl = { + "mode": ssl_mode, + "enable": ssl_enable, + "ca": ssl_ca and os.path.expanduser(ssl_ca), + "cert": ssl_cert and os.path.expanduser(ssl_cert), + "key": ssl_key and os.path.expanduser(ssl_key), + "capath": ssl_capath, + "cipher": ssl_cipher, + "tls_version": tls_version, + "check_hostname": ssl_verify_server_cert, + } + + # remove empty ssl options + ssl = {k: v for k, v in ssl.items() if v is not None} + else: + ssl = None if ssh_config_host: ssh_config = read_ssh_config(ssh_config_path).lookup(ssh_config_host) diff --git a/mycli/myclirc b/mycli/myclirc index a9e15808..59183e00 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -190,3 +190,11 @@ output.null = "#808080" [alias_dsn.init-commands] # Define one or more SQL statements per alias (semicolon-separated). # example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" + +[ssl] +# Sets the desired behavior for handling secure connections to the database server. +# Possible values: +# auto = SSL is preferred. Will attempt to connect via SSL, but will fallback to cleartext as needed. +# on = SSL is required. Will attempt to connect via SSL and will fail if a secure connection is not established. +# off = do not use SSL. Will fail if the server requires a secure connection. +ssl_mode = auto diff --git a/test/features/db_utils.py b/test/features/db_utils.py index 4a2813a4..4c64aeed 100644 --- a/test/features/db_utils.py +++ b/test/features/db_utils.py @@ -16,11 +16,8 @@ def create_db(hostname="localhost", port=3306, username=None, password=None, dbn :return: """ - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.VerifyMode.CERT_NONE cn = pymysql.connect( - host=hostname, port=port, user=username, password=password, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, ssl=ctx + host=hostname, port=port, user=username, password=password, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor ) with cn.cursor() as cr: @@ -44,9 +41,6 @@ def create_cn(hostname, port, password, username, dbname): :return: psycopg2.connection """ - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.VerifyMode.CERT_NONE cn = pymysql.connect( host=hostname, port=port, @@ -55,7 +49,6 @@ def create_cn(hostname, port, password, username, dbname): db=dbname, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, - ssl=ctx, ) return cn @@ -71,9 +64,6 @@ def drop_db(hostname="localhost", port=3306, username=None, password=None, dbnam :param dbname: string """ - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.VerifyMode.CERT_NONE cn = pymysql.connect( host=hostname, port=port, @@ -82,7 +72,6 @@ def drop_db(hostname="localhost", port=3306, username=None, password=None, dbnam db=dbname, charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor, - ssl=ctx, ) with cn.cursor() as cr: diff --git a/test/myclirc b/test/myclirc index a19a34ba..7ef2445a 100644 --- a/test/myclirc +++ b/test/myclirc @@ -191,3 +191,11 @@ global_limit = set sql_select_limit=9999 [alias_dsn.init-commands] # Define one or more SQL statements per alias (semicolon-separated). # example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" + +[ssl] +# Sets the desired behavior for handling secure connections to the database server. +# Possible values: +# auto = SSL is preferred. Will attempt to connect via SSL, but will fallback to cleartext as needed. +# on = SSL is required. Will attempt to connect via SSL and will fail if a secure connection is not established. +# off = do not use SSL. Will fail if the server requires a secure connection. +ssl_mode = auto From 5418d4ec4aee92b9ad43afb7312f151ab4506e1f Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Tue, 30 Dec 2025 19:10:01 -0800 Subject: [PATCH 5/7] Removed unused import --- test/features/db_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/features/db_utils.py b/test/features/db_utils.py index 4c64aeed..0d50ab63 100644 --- a/test/features/db_utils.py +++ b/test/features/db_utils.py @@ -1,7 +1,5 @@ # type: ignore -import ssl - import pymysql From f55a05971cac3982bdfc3218cd44eb5dd8453106 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Thu, 1 Jan 2026 18:21:37 -0800 Subject: [PATCH 6/7] Updated logic to handle interaction between new ssl_mode and existing ssl options. Added tests to cover ssl_mode functionality. --- changelog.md | 3 ++- mycli/main.py | 37 ++++++++++++++++++++++-------- test/test_main.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/changelog.md b/changelog.md index cff6d16f..5b473afc 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,8 @@ Upcoming (TBD) Features -------- * Update query processing functions to allow automatic show_warnings to work for more code paths like DDL. -* Update the default SSL value to connect securely by default. Add a --no-ssl option to disable it. +* Add new ssl_mode config / --ssl-mode CLI option to control SSL connection behavior. This setting will supercede the + existing --ssl/--no-ssl CLI options, which will be deprecated in a later release. * Rework reconnect logic to actually reconnect or create a new connection instead of simply changing the database (#746). diff --git a/mycli/main.py b/mycli/main.py index 9afb4b53..84324649 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -1425,10 +1425,13 @@ def get_last_query(self) -> str | None: @click.option("--ssh-key-filename", help="Private key filename (identify file) for the ssh connection.") @click.option("--ssh-config-path", help="Path to ssh configuration.", default=os.path.expanduser("~") + "/.ssh/config") @click.option("--ssh-config-host", help="Host to connect to ssh server reading from ssh configuration.") -@click.option("--ssl-mode", "ssl_mode", default="auto", help="Set desired SSL behavior. auto=preferred, on=required, off=off.", type=str) @click.option( - "--ssl/--no-ssl", "ssl_enable", is_flag=True, default=True, help="Enable SSL for connection (automatically enabled with other flags)." + "--ssl-mode", + "ssl_mode", + help="Set desired SSL behavior. auto=preferred, on=required, off=off.", + type=click.Choice(["auto", "on", "off"]), ) +@click.option("--ssl/--no-ssl", "ssl_enable", default=None, help="Enable SSL for connection (automatically enabled with other flags).") @click.option("--ssl-ca", help="CA file in PEM format.", type=click.Path(exists=True)) @click.option("--ssl-capath", help="CA directory.") @click.option("--ssl-cert", help="X509 cert in PEM format.", type=click.Path(exists=True)) @@ -1444,8 +1447,6 @@ def get_last_query(self) -> str | None: is_flag=True, help=("""Verify server's "Common Name" in its cert against hostname used when connecting. This option is disabled by default."""), ) -# as of 2016-02-15 revocation list is not supported by underling PyMySQL -# library (--ssl-crl and --ssl-crlpath options in vanilla mysql client) @click.version_option(__version__, "-V", "--version", help="Output mycli's version.") @click.option("-v", "--verbose", is_flag=True, help="Verbose output.") @click.option("-D", "--database", "dbname", help="Database to use.") @@ -1541,6 +1542,15 @@ def cli( warn=warn, myclirc=myclirc, ) + + if ssl_enable is not None: + click.secho( + "Warning: The --ssl/--no-ssl CLI options will be deprecated in a future release. " + "Please use the ssl_mode config or --ssl-mode CLI options instead.", + err=True, + fg="yellow", + ) + if list_dsn: try: alias_dsn = mycli.config["alias_dsn"] @@ -1637,13 +1647,21 @@ def cli( ssl_verify_server_cert = ssl_verify_server_cert or (params[0].lower() == 'true') ssl_enable = True - if ssl_mode not in ("auto", "on", "off"): - click.secho(f"Invalid option provided for --ssl-mode: {ssl_mode}. See --help for valid options.", err=True, fg="red") - sys.exit(1) - ssl_mode = ssl_mode or mycli.ssl_mode # cli option or config option - if ssl_mode in ("auto", "on"): + # if there is a mismatch between the ssl_mode value and other sources of ssl config, show a warning + # specifically using "is False" to not pickup the case where ssl_enable is None (not set by the user) + if ssl_enable and ssl_mode == "off" or ssl_enable is False and ssl_mode in ("auto", "on"): + click.secho( + f"Warning: The current ssl_mode value of '{ssl_mode}' is overriding the value provided by " + f"either the --ssl/--no-ssl CLI options or a DSN URI parameter (ssl={ssl_enable}).", + err=True, + fg="yellow", + ) + + # configure SSL if ssl_mode is auto/on or if + # ssl_enable = True (from --ssl or a DSN URI) and ssl_mode is None + if ssl_mode in ("auto", "on") or (ssl_enable and ssl_mode is None): ssl = { "mode": ssl_mode, "enable": ssl_enable, @@ -1655,7 +1673,6 @@ def cli( "tls_version": tls_version, "check_hostname": ssl_verify_server_cert, } - # remove empty ssl options ssl = {k: v for k, v in ssl.items() if v is not None} else: diff --git a/test/test_main.py b/test/test_main.py index 04ac5c18..909508bb 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -1,6 +1,7 @@ # type: ignore from collections import namedtuple +import csv import os import shutil from tempfile import NamedTemporaryFile @@ -38,6 +39,61 @@ ] +@dbtest +def test_ssl_mode_on(executor, capsys): + runner = CliRunner() + ssl_mode = "on" + sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'" + result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode], input=sql) + result_dict = next(csv.DictReader(result.stdout.split("\n"))) + ssl_cipher = result_dict["VARIABLE_VALUE"] + assert ssl_cipher + + +@dbtest +def test_ssl_mode_auto(executor, capsys): + runner = CliRunner() + ssl_mode = "auto" + sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'" + result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode], input=sql) + result_dict = next(csv.DictReader(result.stdout.split("\n"))) + ssl_cipher = result_dict["VARIABLE_VALUE"] + assert ssl_cipher + + +@dbtest +def test_ssl_mode_off(executor, capsys): + runner = CliRunner() + ssl_mode = "off" + sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'" + result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode], input=sql) + result_dict = next(csv.DictReader(result.stdout.split("\n"))) + ssl_cipher = result_dict["VARIABLE_VALUE"] + assert not ssl_cipher + + +@dbtest +def test_ssl_mode_overrides_ssl(executor, capsys): + runner = CliRunner() + ssl_mode = "off" + sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'" + result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode, "--ssl"], input=sql) + result_dict = next(csv.DictReader(result.stdout.split("\n"))) + ssl_cipher = result_dict["VARIABLE_VALUE"] + assert not ssl_cipher + + +@dbtest +def test_ssl_mode_overrides_no_ssl(executor, capsys): + runner = CliRunner() + ssl_mode = "on" + sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'" + result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode, "--no-ssl"], input=sql) + result_dict = next(csv.DictReader(result.stdout.split("\n"))) + ssl_cipher = result_dict["VARIABLE_VALUE"] + assert ssl_cipher + + @dbtest def test_reconnect_no_database(executor, capsys): m = MyCli() @@ -509,6 +565,7 @@ def __init__(self, **args): self.destructive_warning = False self.main_formatter = Formatter() self.redirect_formatter = Formatter() + self.ssl_mode = "auto" def connect(self, **args): MockMyCli.connect_args = args @@ -673,6 +730,7 @@ def __init__(self, **args): self.destructive_warning = False self.main_formatter = Formatter() self.redirect_formatter = Formatter() + self.ssl_mode = "auto" def connect(self, **args): MockMyCli.connect_args = args From 7a698ec4634bc1bca50079d25678e5ee8d38ad98 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Fri, 2 Jan 2026 12:00:08 -0800 Subject: [PATCH 7/7] Moved the new ssl_mode config option to the main section. Updated ssl/no-ssl deprecation warning. Updated changelog to match. --- changelog.md | 2 +- mycli/main.py | 4 ++-- mycli/myclirc | 15 +++++++-------- test/myclirc | 15 +++++++-------- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/changelog.md b/changelog.md index 5b473afc..f8b5a279 100644 --- a/changelog.md +++ b/changelog.md @@ -5,7 +5,7 @@ Features -------- * Update query processing functions to allow automatic show_warnings to work for more code paths like DDL. * Add new ssl_mode config / --ssl-mode CLI option to control SSL connection behavior. This setting will supercede the - existing --ssl/--no-ssl CLI options, which will be deprecated in a later release. + existing --ssl/--no-ssl CLI options, which are deprecated and will be removed in a future release. * Rework reconnect logic to actually reconnect or create a new connection instead of simply changing the database (#746). diff --git a/mycli/main.py b/mycli/main.py index 84324649..8a35de3c 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -156,7 +156,7 @@ def __init__( self.post_redirect_command = c['main'].get('post_redirect_command') # set ssl_mode if a valid option is provided in a config file, otherwise None - ssl_mode = c["ssl"].get("ssl_mode", None) + ssl_mode = c["main"].get("ssl_mode", None) if ssl_mode not in ("auto", "on", "off", None): self.echo(f"Invalid config option provided for ssl_mode ({ssl_mode}); ignoring.", err=True, fg="red") self.ssl_mode = None @@ -1545,7 +1545,7 @@ def cli( if ssl_enable is not None: click.secho( - "Warning: The --ssl/--no-ssl CLI options will be deprecated in a future release. " + "Warning: The --ssl/--no-ssl CLI options are deprecated and will be removed in a future release. " "Please use the ssl_mode config or --ssl-mode CLI options instead.", err=True, fg="yellow", diff --git a/mycli/myclirc b/mycli/myclirc index 59183e00..872a904f 100644 --- a/mycli/myclirc +++ b/mycli/myclirc @@ -5,6 +5,13 @@ # after executing a SQL statement when applicable. show_warnings = False +# Sets the desired behavior for handling secure connections to the database server. +# Possible values: +# auto = SSL is preferred. Will attempt to connect via SSL, but will fallback to cleartext as needed. +# on = SSL is required. Will attempt to connect via SSL and will fail if a secure connection is not established. +# off = do not use SSL. Will fail if the server requires a secure connection. +ssl_mode = auto + # Enables context sensitive auto-completion. If this is disabled the all # possible completions will be listed. smart_completion = True @@ -190,11 +197,3 @@ output.null = "#808080" [alias_dsn.init-commands] # Define one or more SQL statements per alias (semicolon-separated). # example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" - -[ssl] -# Sets the desired behavior for handling secure connections to the database server. -# Possible values: -# auto = SSL is preferred. Will attempt to connect via SSL, but will fallback to cleartext as needed. -# on = SSL is required. Will attempt to connect via SSL and will fail if a secure connection is not established. -# off = do not use SSL. Will fail if the server requires a secure connection. -ssl_mode = auto diff --git a/test/myclirc b/test/myclirc index 7ef2445a..8c9e807e 100644 --- a/test/myclirc +++ b/test/myclirc @@ -5,6 +5,13 @@ # after executing a SQL statement when applicable. show_warnings = False +# Sets the desired behavior for handling secure connections to the database server. +# Possible values: +# auto = SSL is preferred. Will attempt to connect via SSL, but will fallback to cleartext as needed. +# on = SSL is required. Will attempt to connect via SSL and will fail if a secure connection is not established. +# off = do not use SSL. Will fail if the server requires a secure connection. +ssl_mode = auto + # Enables context sensitive auto-completion. If this is disabled the all # possible completions will be listed. smart_completion = True @@ -191,11 +198,3 @@ global_limit = set sql_select_limit=9999 [alias_dsn.init-commands] # Define one or more SQL statements per alias (semicolon-separated). # example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'" - -[ssl] -# Sets the desired behavior for handling secure connections to the database server. -# Possible values: -# auto = SSL is preferred. Will attempt to connect via SSL, but will fallback to cleartext as needed. -# on = SSL is required. Will attempt to connect via SSL and will fail if a secure connection is not established. -# off = do not use SSL. Will fail if the server requires a secure connection. -ssl_mode = auto