145 lines
6.2 KiB
Python
145 lines
6.2 KiB
Python
"""Eigene Passwörter: Einmal-Passwort, Pflichtwechsel, Sperre, Zurücksetzen (→ Konzept 5.1)."""
|
|
|
|
import re
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import select
|
|
|
|
from app.identity import passwords
|
|
from app.identity.models import User
|
|
from helpers import Browser
|
|
|
|
|
|
@pytest.fixture
|
|
def admin(browser, people, db):
|
|
with db.session() as session:
|
|
session.get(User, people["anna"]).is_admin = True
|
|
return browser("anna")
|
|
|
|
|
|
def issued(html: str) -> str:
|
|
return re.search(r'<code class="text-lg">([^<]+)</code>', html).group(1)
|
|
|
|
|
|
def client_for(app) -> TestClient:
|
|
client = TestClient(app)
|
|
client.__enter__()
|
|
return client
|
|
|
|
|
|
def login(client: TestClient, username: str, password: str):
|
|
return client.post("/login", data={"username": username, "password": password}, follow_redirects=False)
|
|
|
|
|
|
def csrf(client: TestClient) -> str:
|
|
return re.search(r'name="_csrf" value="([^"]+)"', client.get("/password").text).group(1)
|
|
|
|
|
|
def test_anlegen_erstanmeldung_und_wechsel(admin, app, db):
|
|
response = admin.post("/admin/users", {"username": "erik", "display_name": "Erik"})
|
|
secret = issued(response.text)
|
|
with db.session() as session:
|
|
user = session.scalar(select(User).where(User.username == "erik"))
|
|
assert user.must_change_password and secret not in user.password_hash
|
|
client = client_for(app)
|
|
assert login(client, "erik", secret).status_code == 303
|
|
# Bis zum Wechsel führt jede Seite zur Passwortseite
|
|
assert client.get("/workspaces", follow_redirects=False).headers["location"] == "/password"
|
|
token = csrf(client)
|
|
weak = client.post("/password", data={"_csrf": token, "current": secret, "new": "kurz", "repeat": "kurz"})
|
|
assert weak.status_code == 400 and "12 Zeichen" in weak.text
|
|
response = client.post("/password", data={"_csrf": token, "current": secret, "new": "Sonnige Tage am See 7",
|
|
"repeat": "Sonnige Tage am See 7"}, follow_redirects=False)
|
|
assert response.status_code == 303
|
|
assert client.get("/workspaces").status_code == 200
|
|
# Einmal-Passwort gilt nicht mehr, das eigene schon
|
|
other = client_for(app)
|
|
assert login(other, "erik", secret).status_code == 401
|
|
assert login(other, "erik", "Sonnige Tage am See 7").status_code == 303
|
|
|
|
|
|
def test_demo_passwort_nur_ohne_eigenes(admin, app, db, people):
|
|
from helpers import PASSWORD
|
|
secret = issued(admin.post(f"/admin/users/{people['ben']}/reset-password").text)
|
|
client = client_for(app)
|
|
assert login(client, "ben", PASSWORD).status_code == 401
|
|
assert login(client, "ben", secret).status_code == 303
|
|
|
|
|
|
def test_ohne_demo_passwort_nur_eigene(settings, db, people):
|
|
from app.main import create_app
|
|
app = create_app(settings.model_copy(update={"demo_password": ""}))
|
|
with db.session() as session:
|
|
session.get(User, people["ben"]).password_hash = passwords.hash_password("Ein langes Passwort 1")
|
|
client = client_for(app)
|
|
assert login(client, "anna", "").status_code in (401, 422)
|
|
assert login(client, "ben", "Ein langes Passwort 1").status_code == 303
|
|
|
|
|
|
def test_sperre_nach_fehlversuchen(app, db, people, settings):
|
|
with db.session() as session:
|
|
session.get(User, people["ben"]).password_hash = passwords.hash_password("Ein langes Passwort 1")
|
|
client = client_for(app)
|
|
for _ in range(settings.login_max_failures):
|
|
assert login(client, "ben", "falsch").status_code == 401
|
|
# Gesperrt: auch das richtige Passwort hilft nicht, die Meldung bleibt gleich
|
|
response = login(client, "ben", "Ein langes Passwort 1")
|
|
assert response.status_code == 401 and "Nutzername oder Passwort falsch" in response.text
|
|
with db.session() as session:
|
|
assert session.get(User, people["ben"]).locked_until is not None
|
|
|
|
|
|
def test_zuruecksetzen_entsperrt_und_beendet_sitzungen(admin, browser, db, people):
|
|
ben = browser("ben")
|
|
with db.session() as session:
|
|
from datetime import timedelta
|
|
|
|
from app.core.database import now
|
|
session.get(User, people["ben"]).locked_until = now() + timedelta(minutes=10)
|
|
secret = issued(admin.post(f"/admin/users/{people['ben']}/reset-password").text)
|
|
assert ben.get("/workspaces", follow_redirects=False).status_code == 303
|
|
assert login(ben.client, "ben", secret).status_code == 303
|
|
|
|
|
|
def test_passwort_aendern_beendet_andere_sitzungen(app, db, people):
|
|
with db.session() as session:
|
|
session.get(User, people["ben"]).password_hash = passwords.hash_password("Ein langes Passwort 1")
|
|
first, second = client_for(app), client_for(app)
|
|
login(first, "ben", "Ein langes Passwort 1")
|
|
login(second, "ben", "Ein langes Passwort 1")
|
|
wrong = first.post("/password", data={"_csrf": csrf(first), "current": "falsch", "new": "Neues langes Passwort 2",
|
|
"repeat": "Neues langes Passwort 2"})
|
|
assert wrong.status_code == 400 and "stimmt nicht" in wrong.text
|
|
first.post("/password", data={"_csrf": csrf(first), "current": "Ein langes Passwort 1",
|
|
"new": "Neues langes Passwort 2", "repeat": "Neues langes Passwort 2"})
|
|
assert first.get("/workspaces").status_code == 200
|
|
assert second.get("/workspaces", follow_redirects=False).status_code == 303
|
|
|
|
|
|
@pytest.mark.parametrize("password", ["kurz", "benbenbenbenben1", "aaaaaaaaaaaaaaa"])
|
|
def test_passwortregeln(password):
|
|
from app.core.errors import Invalid
|
|
with pytest.raises(Invalid):
|
|
passwords.check_policy(password, "ben")
|
|
|
|
|
|
def test_einmal_passwoerter_sind_verschieden_und_lang():
|
|
values = {passwords.one_time() for _ in range(50)}
|
|
assert len(values) == 50 and all(len(v) == 19 for v in values)
|
|
|
|
|
|
def test_create_admin_cli(settings, db, people, capsys, app):
|
|
from app.cli import create_admin_user
|
|
create_admin_user(settings, "root")
|
|
secret = capsys.readouterr().out.rsplit(": ", 1)[1].strip()
|
|
client = client_for(app)
|
|
assert login(client, "root", secret).status_code == 303
|
|
assert client.get("/admin/users", follow_redirects=False).headers["location"] == "/password"
|
|
|
|
|
|
def test_nicht_admin_kann_nicht_zuruecksetzen(browser, people):
|
|
assert browser("ben").post(f"/admin/users/{people['clara']}/reset-password").status_code == 403
|
|
|
|
|
|
__all__ = ["Browser"]
|