From f643246a84a29d1cdc72e637542c1cefd95fc01c Mon Sep 17 00:00:00 2001 From: mwiegand Date: Mon, 11 May 2026 22:00:16 +0200 Subject: [PATCH] cli: apply min-length password policy in create-user Same validate_new_password used by the web change-password flow, so the policy is enforced uniformly across CLI and HTTP entry points. Existing CLI tests bumped to passwords that satisfy the new floor. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/cli.py | 7 ++++--- l4d2web/tests/test_auth.py | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/l4d2web/cli.py b/l4d2web/cli.py index 8ac84e3..d71fc01 100644 --- a/l4d2web/cli.py +++ b/l4d2web/cli.py @@ -5,7 +5,7 @@ import click from sqlalchemy.exc import IntegrityError from sqlalchemy import select -from l4d2web.auth import hash_password +from l4d2web.auth import hash_password, validate_new_password from l4d2web.db import session_scope from l4d2web.models import Overlay, User from l4d2web.services.overlay_creation import ( @@ -31,8 +31,9 @@ def create_user(username: str, admin: bool) -> None: password = os.getenv("LEFT4ME_ADMIN_PASSWORD") if password is None: password = click.prompt("Password", hide_input=True, confirmation_prompt=True) - if password == "": - raise click.ClickException("password must not be empty") + policy_error = validate_new_password(password) + if policy_error is not None: + raise click.ClickException(policy_error) try: with session_scope() as db: diff --git a/l4d2web/tests/test_auth.py b/l4d2web/tests/test_auth.py index 18d83ef..38f2f07 100644 --- a/l4d2web/tests/test_auth.py +++ b/l4d2web/tests/test_auth.py @@ -202,7 +202,7 @@ def test_login_stamps_password_changed_at_in_session(client) -> None: def test_create_user_cli_uses_environment_password(tmp_path, monkeypatch) -> None: db_url = f"sqlite:///{tmp_path/'create_user.db'}" monkeypatch.setenv("DATABASE_URL", db_url) - monkeypatch.setenv("LEFT4ME_ADMIN_PASSWORD", "secret") + monkeypatch.setenv("LEFT4ME_ADMIN_PASSWORD", "secretpw1") app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"}) init_db() @@ -215,6 +215,19 @@ def test_create_user_cli_uses_environment_password(tmp_path, monkeypatch) -> Non assert user.admin is True +def test_create_user_cli_rejects_short_password(tmp_path, monkeypatch) -> None: + db_url = f"sqlite:///{tmp_path/'short_pw.db'}" + monkeypatch.setenv("DATABASE_URL", db_url) + monkeypatch.setenv("LEFT4ME_ADMIN_PASSWORD", "short7x") + app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"}) + init_db() + + result = app.test_cli_runner().invoke(args=["create-user", "admin", "--admin"]) + + assert result.exit_code != 0 + assert "at least 8" in result.output + + def test_create_user_cli_rejects_empty_environment_password(tmp_path, monkeypatch) -> None: db_url = f"sqlite:///{tmp_path/'empty_password.db'}" monkeypatch.setenv("DATABASE_URL", db_url) @@ -247,7 +260,7 @@ def test_validate_new_password_accepts_min_length(): def test_create_user_cli_rejects_duplicate_username(tmp_path, monkeypatch) -> None: db_url = f"sqlite:///{tmp_path/'duplicate_user.db'}" monkeypatch.setenv("DATABASE_URL", db_url) - monkeypatch.setenv("LEFT4ME_ADMIN_PASSWORD", "secret") + monkeypatch.setenv("LEFT4ME_ADMIN_PASSWORD", "secretpw1") app = create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"}) init_db() with session_scope() as session: