From 30a82efea479b8d79c14687476d9569ca4c91b96 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Thu, 13 Feb 2025 13:40:02 +0200 Subject: [PATCH] htpasswd: split add and set commands --- kvmd/apps/htpasswd/__init__.py | 32 +++++++++++++++++++++--- testenv/tests/apps/htpasswd/test_main.py | 26 +++++++++++++------ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/kvmd/apps/htpasswd/__init__.py b/kvmd/apps/htpasswd/__init__.py index e67efbf6..f6f9177b 100644 --- a/kvmd/apps/htpasswd/__init__.py +++ b/kvmd/apps/htpasswd/__init__.py @@ -44,7 +44,7 @@ from .. import init # ===== def _get_htpasswd_path(config: Section) -> str: if config.kvmd.auth.internal.type != "htpasswd": - raise SystemExit(f"Error: KVMD internal auth not using 'htpasswd'" + raise SystemExit(f"Error: KVMD internal auth does not use 'htpasswd'" f" (now configured {config.kvmd.auth.internal.type!r})") return config.kvmd.auth.internal.file @@ -100,20 +100,40 @@ def _cmd_list(config: Section, _: argparse.Namespace) -> None: print(user) -def _cmd_set(config: Section, options: argparse.Namespace) -> None: +def _change_user(config: Section, options: argparse.Namespace, create: bool) -> None: with _get_htpasswd_for_write(config) as htpasswd: + assert options.user == options.user.strip() + assert options.user + has_user = (options.user in htpasswd.users()) + if create: + if has_user: + raise SystemExit(f"The user {options.user!r} is already exists") + else: + if not has_user: + raise SystemExit(f"The user {options.user!r} is not exist") + if options.read_stdin: passwd = valid_passwd(input()) else: passwd = valid_passwd(getpass.getpass("Password: ", stream=sys.stderr)) if valid_passwd(getpass.getpass("Repeat: ", stream=sys.stderr)) != passwd: raise SystemExit("Sorry, passwords do not match") + htpasswd.set_password(options.user, passwd) + if has_user and not options.quiet: _print_invalidate_tip(True) +def _cmd_add(config: Section, options: argparse.Namespace) -> None: + _change_user(config, options, create=True) + + +def _cmd_set(config: Section, options: argparse.Namespace) -> None: + _change_user(config, options, create=False) + + def _cmd_delete(config: Section, options: argparse.Namespace) -> None: with _get_htpasswd_for_write(config) as htpasswd: has_user = (options.user in htpasswd.users()) @@ -141,7 +161,13 @@ def main(argv: (list[str] | None)=None) -> None: sub = subparsers.add_parser("list", help="List users") sub.set_defaults(cmd=_cmd_list) - sub = subparsers.add_parser("set", help="Create user or change password") + sub = subparsers.add_parser("add", help="Add user") + sub.add_argument("user", type=valid_user) + sub.add_argument("-i", "--read-stdin", action="store_true", help="Read password from stdin") + sub.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note") + sub.set_defaults(cmd=_cmd_add) + + sub = subparsers.add_parser("set", help="Change user's password") sub.add_argument("user", type=valid_user) sub.add_argument("-i", "--read-stdin", action="store_true", help="Read password from stdin") sub.add_argument("-q", "--quiet", action="store_true", help="Don't show invalidation note") diff --git a/testenv/tests/apps/htpasswd/test_main.py b/testenv/tests/apps/htpasswd/test_main.py index d66a887c..8ce70cf6 100644 --- a/testenv/tests/apps/htpasswd/test_main.py +++ b/testenv/tests/apps/htpasswd/test_main.py @@ -71,24 +71,32 @@ def test_ok__list(htpasswd: KvmdHtpasswdFile, capsys) -> None: # type: ignore # ===== -def test_ok__set_change_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore +def test_ok__set_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore old_users = set(htpasswd.users()) if old_users: assert htpasswd.check_password("admin", _make_passwd("admin")) mocker.patch.object(builtins, "input", (lambda: " test ")) + _run_htpasswd(["set", "admin", "--read-stdin"], htpasswd.path) + with pytest.raises(SystemExit, match="The user 'new' is not exist"): + _run_htpasswd(["set", "new", "--read-stdin"], htpasswd.path) + htpasswd.load(force=True) assert htpasswd.check_password("admin", " test ") assert old_users == set(htpasswd.users()) -def test_ok__set_add_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore +def test_ok__add_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore old_users = set(htpasswd.users()) if old_users: mocker.patch.object(builtins, "input", (lambda: " test ")) - _run_htpasswd(["set", "new", "--read-stdin"], htpasswd.path) + + _run_htpasswd(["add", "new", "--read-stdin"], htpasswd.path) + + with pytest.raises(SystemExit, match="The user 'new' is already exists"): + _run_htpasswd(["add", "new", "--read-stdin"], htpasswd.path) htpasswd.load(force=True) assert htpasswd.check_password("new", " test ") @@ -96,20 +104,24 @@ def test_ok__set_add_stdin(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: # ===== -def test_ok__set_change_getpass(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore +def test_ok__set_getpass(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore old_users = set(htpasswd.users()) if old_users: assert htpasswd.check_password("admin", _make_passwd("admin")) mocker.patch.object(getpass, "getpass", (lambda *_, **__: " test ")) + _run_htpasswd(["set", "admin"], htpasswd.path) + with pytest.raises(SystemExit, match="The user 'new' is not exist"): + _run_htpasswd(["set", "new"], htpasswd.path) + htpasswd.load(force=True) assert htpasswd.check_password("admin", " test ") assert old_users == set(htpasswd.users()) -def test_fail__set_change_getpass(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore +def test_fail__set_getpass(htpasswd: KvmdHtpasswdFile, mocker) -> None: # type: ignore old_users = set(htpasswd.users()) if old_users: assert htpasswd.check_password("admin", _make_passwd("admin")) @@ -152,7 +164,7 @@ def test_ok__del(htpasswd: KvmdHtpasswdFile) -> None: # ===== def test_fail__not_htpasswd() -> None: - with pytest.raises(SystemExit, match="Error: KVMD internal auth not using 'htpasswd'"): + with pytest.raises(SystemExit, match="Error: KVMD internal auth does not use 'htpasswd'"): _run_htpasswd(["list"], "", int_type="http") @@ -166,4 +178,4 @@ def test_fail__invalid_passwd(mocker, tmpdir) -> None: # type: ignore open(path, "w").close() # pylint: disable=consider-using-with mocker.patch.object(builtins, "input", (lambda: "\n")) with pytest.raises(SystemExit, match="The argument is not a valid passwd characters"): - _run_htpasswd(["set", "admin", "--read-stdin"], path) + _run_htpasswd(["add", "admin", "--read-stdin"], path)