Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Features
* Make `--progress` and `--checkpoint` strictly by statement.
* Allow more characters in passwords read from a file.
* Show sponsors and contributors separately in startup messages.
* Add support for expired password (sandbox) mode (#440)
* Add support for expired password (sandbox) mode (#440).
* Limit `--password` without an argument to the final position.


Bug Fixes
Expand All @@ -22,6 +23,7 @@ Bug Fixes
* Run empty `--execute` arguments instead of ignoring the flag.
* Exit with error when the `--batch` argument is an empty string.
* Avoid logging SSH passwords.
* Allow passwords defined at the CLI to start with dash.


Internal
Expand Down
2 changes: 2 additions & 0 deletions mycli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@
# MySQL error codes not available in pymysql.constants.ER
ER_MUST_CHANGE_PASSWORD_LOGIN = 1862
ER_MUST_CHANGE_PASSWORD = 1820

EMPTY_PASSWORD_FLAG_SENTINEL = -1
13 changes: 7 additions & 6 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
DEFAULT_HOST,
DEFAULT_PORT,
DEFAULT_WIDTH,
EMPTY_PASSWORD_FLAG_SENTINEL,
ER_MUST_CHANGE_PASSWORD_LOGIN,
ISSUES_URL,
REPO_URL,
Expand Down Expand Up @@ -87,8 +88,6 @@
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
sqlparse.engine.grouping.MAX_GROUPING_TOKENS = None # type: ignore[assignment]

EMPTY_PASSWORD_FLAG_SENTINEL = -1


class IntOrStringClickParamType(click.ParamType):
name = 'text' # display as TEXT in helpdoc
Expand Down Expand Up @@ -1221,9 +1220,11 @@ class CliArgs:
'--password',
'password',
type=INT_OR_STRING_CLICK_TYPE,
is_flag=False,
flag_value=EMPTY_PASSWORD_FLAG_SENTINEL,
help='Prompt for (or pass in cleartext) the password to connect to the database.',
help=dedent(
"""Password to connect to the database.
Use with a value to set the password at the CLI, or alone in the last position to request a prompt.
"""
),
)
password_file: str | None = clickdc.option(
type=click.Path(),
Expand Down Expand Up @@ -1918,7 +1919,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
def main() -> int | None:
try:
result = click_entrypoint.main(
filtered_sys_argv(),
filtered_sys_argv(), # type: ignore[arg-type]
standalone_mode=False, # disable builtin exception handling
prog_name='mycli',
)
Expand Down
15 changes: 12 additions & 3 deletions mycli/packages/cli_utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from __future__ import annotations

import sys
from typing import Sequence

from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL


def filtered_sys_argv() -> Sequence[str | int]:
args: Sequence[str | int] = sys.argv[1:]
password_flag_forms = ['-p', '--pass', '--password']

def filtered_sys_argv() -> list[str]:
args = sys.argv[1:]
if args == ['-h']:
args = ['--help']
return args

if args and args[-1] in password_flag_forms:
args = list(args) + [EMPTY_PASSWORD_FLAG_SENTINEL]

return list(args)


def is_valid_connection_scheme(text: str) -> tuple[bool, str | None]:
Expand Down
8 changes: 8 additions & 0 deletions test/pytests/test_cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL
from mycli.packages import cli_utils
from mycli.packages.cli_utils import (
filtered_sys_argv,
Expand All @@ -22,6 +23,13 @@ def test_filtered_sys_argv(monkeypatch, argv, expected):
assert filtered_sys_argv() == expected


@pytest.mark.parametrize('password_flag', ['-p', '--pass', '--password'])
def test_filtered_sys_argv_appends_empty_password_sentinel(monkeypatch, password_flag):
monkeypatch.setattr(cli_utils.sys, 'argv', ['mycli', 'database', password_flag])

assert filtered_sys_argv() == ['database', password_flag, EMPTY_PASSWORD_FLAG_SENTINEL]


@pytest.mark.parametrize(
('text', 'is_valid', 'invalid_scheme'),
[
Expand Down
60 changes: 1 addition & 59 deletions test/pytests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DEFAULT_USER,
TEST_DATABASE,
)
from mycli.main import EMPTY_PASSWORD_FLAG_SENTINEL, MyCli, click_entrypoint
from mycli.main import MyCli, click_entrypoint
import mycli.main_modes.repl as repl_mode
import mycli.packages.special
from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
Expand Down Expand Up @@ -1218,64 +1218,6 @@ def run_query(self, query, new_line=True):
)


def test_password_flag_uses_sentinel(monkeypatch):
class Formatter:
format_name = None

class Logger:
def debug(self, *args, **args_dict):
pass

def warning(self, *args, **args_dict):
pass

class MockMyCli:
config = {
'main': {},
'alias_dsn': {},
'connection': {
'default_keepalive_ticks': 0,
},
}

def __init__(self, **_args):
self.logger = Logger()
self.destructive_warning = False
self.main_formatter = Formatter()
self.redirect_formatter = Formatter()
self.ssl_mode = 'auto'
self.my_cnf = {'client': {}, 'mysqld': {}}
self.default_keepalive_ticks = 0

def connect(self, **args):
MockMyCli.connect_args = args

def run_query(self, query, new_line=True):
pass

import mycli.main

monkeypatch.setattr(mycli.main, 'MyCli', MockMyCli)
runner = CliRunner()

result = runner.invoke(
mycli.main.click_entrypoint,
args=[
'--user',
'user',
'--host',
DEFAULT_HOST,
'--port',
f'{DEFAULT_PORT}',
'--database',
'database',
'--password',
],
)
assert result.exit_code == 0, result.output + ' ' + str(result.exception)
assert MockMyCli.connect_args['passwd'] == EMPTY_PASSWORD_FLAG_SENTINEL


def test_password_option_uses_cleartext_value(monkeypatch):
class Formatter:
format_name = None
Expand Down
Loading