54 lines
2.0 KiB
Python
54 lines
2.0 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from codexpi.config import load_or_create_secret
|
|
from codexpi.security import LoginLimiter
|
|
|
|
|
|
def test_progressive_login_delay_is_capped():
|
|
limiter = LoginLimiter(attempts=20)
|
|
delays = []
|
|
for _ in range(8):
|
|
assert limiter.allow("100.64.0.10")
|
|
delays.append(limiter.penalty("100.64.0.10"))
|
|
assert delays[:5] == [0.25, 0.5, 1.0, 2.0, 4.0]
|
|
assert delays[-1] == 4.0
|
|
|
|
|
|
def test_login_security_log_contains_no_credentials(logged_in):
|
|
_, app, *_ = logged_in
|
|
records = [json.loads(line) for line in (app.state.settings.data_dir / "security.log").read_text(encoding="utf-8").splitlines()]
|
|
assert [row["event"] for row in records[:2]] == ["auth.login_succeeded", "session.created"]
|
|
rendered = json.dumps(records)
|
|
assert "a-very-long-test-password" not in rendered
|
|
assert "csrf" not in rendered.lower()
|
|
assert "cookie" not in rendered.lower()
|
|
|
|
|
|
def test_full_access_change_is_audited(logged_in):
|
|
client, app, projects, _, headers = logged_in
|
|
root = projects / "full-access"
|
|
root.mkdir()
|
|
project = client.post("/api/projects", headers=headers, json={"name": "Full", "path": str(root)}).json()
|
|
response = client.put(
|
|
f"/api/projects/{project['id']}/full-access",
|
|
headers=headers,
|
|
json={"enabled": True, "acknowledged": True},
|
|
)
|
|
assert response.status_code == 200
|
|
log = (app.state.settings.data_dir / "security.log").read_text(encoding="utf-8")
|
|
assert '"event":"security.full_access_changed"' in log
|
|
assert '"outcome":"enabled"' in log
|
|
|
|
|
|
def test_application_secret_can_come_from_external_file(tmp_path: Path, monkeypatch):
|
|
external = tmp_path / "mounted-secret"
|
|
external.write_text("x" * 48, encoding="utf-8")
|
|
generated_data = tmp_path / "data"
|
|
generated_data.mkdir()
|
|
monkeypatch.setenv("CODEXPI_SECRET_KEY_FILE", str(external))
|
|
assert load_or_create_secret(generated_data) == "x" * 48
|
|
assert not (generated_data / "secret_key").exists()
|