feat: add reset-secret CLI subcommand
- Add reset_secret() function that regenerates the signing secret - Uses secrets.token_urlsafe(32) for secure key generation - Creates parent directories if they don't exist - Sets file permissions to 0o600 (owner read/write only) - Prints warning that all active sessions are now invalid - Register 'reset-secret' subparser in main() arg parser - Add dispatch branch for 'reset-secret' command Tests added: - test_reset_secret_writes_new_secret: secret file exists with content > 20 chars - test_reset_secret_sets_0600_permissions: file mode is 0o600 - test_reset_secret_prints_warning: output contains 'invalid' or 'warning' Co-authored-by: Amplifier <amplifier@sourcegraph.com>
This commit is contained in:
+20
-1
@@ -5,12 +5,25 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from muxplex.auth import load_password, pam_available
|
||||
import secrets as _secrets
|
||||
|
||||
from muxplex.auth import get_secret_path, load_password, pam_available
|
||||
|
||||
# Module-level path constants (overridable in tests via monkeypatch)
|
||||
_system_service_path = Path("/etc/systemd/system/muxplex.service")
|
||||
|
||||
|
||||
def reset_secret() -> None:
|
||||
"""Regenerate the signing secret and warn that all sessions are now invalid."""
|
||||
path = get_secret_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
secret = _secrets.token_urlsafe(32)
|
||||
path.write_text(secret + "\n")
|
||||
path.chmod(0o600)
|
||||
print(f"Secret written to {path}")
|
||||
print("Warning: all active sessions are now invalid.")
|
||||
|
||||
|
||||
def show_password() -> None:
|
||||
"""Print the current muxplex password or indicate PAM mode."""
|
||||
auth_mode = os.environ.get("MUXPLEX_AUTH", "").lower()
|
||||
@@ -118,12 +131,18 @@ def main() -> None:
|
||||
|
||||
sub.add_parser("show-password", help="Show the current muxplex password")
|
||||
|
||||
sub.add_parser(
|
||||
"reset-secret", help="Regenerate signing secret (invalidates sessions)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "install-service":
|
||||
install_service(system=args.system)
|
||||
elif args.command == "show-password":
|
||||
show_password()
|
||||
elif args.command == "reset-secret":
|
||||
reset_secret()
|
||||
else:
|
||||
serve(
|
||||
host=args.host, port=args.port, auth=args.auth, session_ttl=args.session_ttl
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for muxplex/cli.py — CLI entry point."""
|
||||
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -211,6 +212,54 @@ def test_show_password_pam_mode(monkeypatch, capsys):
|
||||
assert "pam" in captured.out.lower()
|
||||
|
||||
|
||||
def test_reset_secret_writes_new_secret(tmp_path, monkeypatch):
|
||||
"""reset_secret() writes a new secret file with content longer than 20 chars."""
|
||||
from muxplex.cli import reset_secret
|
||||
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
|
||||
reset_secret()
|
||||
|
||||
secret_path = fake_home / ".config" / "muxplex" / "secret"
|
||||
assert secret_path.exists(), "Secret file must be created"
|
||||
content = secret_path.read_text().strip()
|
||||
assert len(content) > 20, f"Secret must be longer than 20 chars, got {len(content)}"
|
||||
|
||||
|
||||
def test_reset_secret_sets_0600_permissions(tmp_path, monkeypatch):
|
||||
"""reset_secret() sets file permissions to 0o600."""
|
||||
from muxplex.cli import reset_secret
|
||||
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
|
||||
reset_secret()
|
||||
|
||||
secret_path = fake_home / ".config" / "muxplex" / "secret"
|
||||
file_mode = stat.S_IMODE(secret_path.stat().st_mode)
|
||||
assert file_mode == 0o600, f"Expected 0o600, got {oct(file_mode)}"
|
||||
|
||||
|
||||
def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys):
|
||||
"""reset_secret() prints a warning that sessions are now invalid."""
|
||||
from muxplex.cli import reset_secret
|
||||
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
|
||||
reset_secret()
|
||||
|
||||
captured = capsys.readouterr()
|
||||
output_lower = captured.out.lower()
|
||||
assert "invalid" in output_lower or "warning" in output_lower, (
|
||||
f"Expected 'invalid' or 'warning' in output, got: {captured.out!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_dunder_main_calls_main():
|
||||
"""python -m muxplex must call cli.main()."""
|
||||
import importlib.util
|
||||
|
||||
Reference in New Issue
Block a user