feat(auth): password file management
Add muxplex/auth.py with password file management functions: - get_password_path(): returns ~/.config/muxplex/password - load_password(): reads password file, returns None if not found - generate_and_save_password(): generates random password, writes with 0600 permissions Add muxplex/tests/test_auth.py with 5 tests covering all functions. Co-authored-by: Amplifier <amplifier@invalid>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
muxplex authentication — password management, secret management,
|
||||
session cookies, PAM integration, and request middleware.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config directory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _config_dir() -> Path:
|
||||
"""Return ~/.config/muxplex, creating it (mode 0700) if needed."""
|
||||
d = Path.home() / ".config" / "muxplex"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Password file management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_password_path() -> Path:
|
||||
"""Return the path to the password file: ~/.config/muxplex/password."""
|
||||
return Path.home() / ".config" / "muxplex" / "password"
|
||||
|
||||
|
||||
def load_password() -> str | None:
|
||||
"""Read the password file if it exists, return None otherwise."""
|
||||
path = get_password_path()
|
||||
if not path.exists():
|
||||
return None
|
||||
return path.read_text().strip()
|
||||
|
||||
|
||||
def generate_and_save_password() -> str:
|
||||
"""Generate a random password, write it to the password file (0600), return it."""
|
||||
pw = secrets.token_urlsafe(20)
|
||||
path = get_password_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(pw + "\n")
|
||||
path.chmod(0o600)
|
||||
return pw
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Tests for muxplex/auth.py — authentication module."""
|
||||
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Password file management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_password_path_returns_expected_path(monkeypatch, tmp_path):
|
||||
"""get_password_path() returns ~/.config/muxplex/password."""
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
||||
from muxplex.auth import get_password_path
|
||||
|
||||
assert get_password_path() == tmp_path / ".config" / "muxplex" / "password"
|
||||
|
||||
|
||||
def test_load_password_returns_none_when_no_file(monkeypatch, tmp_path):
|
||||
"""load_password() returns None when password file does not exist."""
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
||||
from muxplex.auth import load_password
|
||||
|
||||
assert load_password() is None
|
||||
|
||||
|
||||
def test_load_password_reads_existing_file(monkeypatch, tmp_path):
|
||||
"""load_password() reads and strips the password file contents."""
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
||||
pw_path = tmp_path / ".config" / "muxplex" / "password"
|
||||
pw_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
pw_path.write_text("my-secret-password\n")
|
||||
pw_path.chmod(0o600)
|
||||
|
||||
from muxplex.auth import load_password
|
||||
|
||||
assert load_password() == "my-secret-password"
|
||||
|
||||
|
||||
def test_generate_and_save_password_creates_file(monkeypatch, tmp_path):
|
||||
"""generate_and_save_password() creates the file and returns a non-empty string."""
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
||||
from muxplex.auth import generate_and_save_password
|
||||
|
||||
pw = generate_and_save_password()
|
||||
assert isinstance(pw, str)
|
||||
assert len(pw) > 10
|
||||
|
||||
pw_path = tmp_path / ".config" / "muxplex" / "password"
|
||||
assert pw_path.exists()
|
||||
assert pw_path.read_text().strip() == pw
|
||||
|
||||
|
||||
def test_generate_and_save_password_sets_0600_permissions(monkeypatch, tmp_path):
|
||||
"""generate_and_save_password() sets the file to mode 0600."""
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
||||
from muxplex.auth import generate_and_save_password
|
||||
|
||||
generate_and_save_password()
|
||||
pw_path = tmp_path / ".config" / "muxplex" / "password"
|
||||
mode = stat.S_IMODE(pw_path.stat().st_mode)
|
||||
assert mode == 0o600
|
||||
Reference in New Issue
Block a user