Files
checkbot/tests/test_users.py
T

156 lines
5.1 KiB
Python

import os
import pytest
from argon2 import PasswordHasher
from app import audit
from app.crypto import SecretBox
from app.db import Database
from app.users import MIN_PASSWORD_LEN, ROLE_ADMIN, ROLE_VIEWER, UserError, UserStore
PASSWORD = "correct horse battery"
ACTOR = "tester"
@pytest.fixture
def db():
return Database(":memory:")
@pytest.fixture
def store(db):
hasher = PasswordHasher(time_cost=1, memory_cost=8, parallelism=1)
return UserStore(db, SecretBox(os.urandom(32)), hasher)
def test_create_and_authenticate(store):
created = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
assert created.username == "alice"
assert created.is_admin
assert not created.has_totp
assert store.authenticate("alice", PASSWORD).id == created.id
assert store.count() == 1
def test_wrong_password(store):
store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
assert store.authenticate("alice", "wrong password!!") is None
def test_unknown_user(store):
assert store.authenticate("ghost", PASSWORD) is None
def test_password_too_short(store):
with pytest.raises(UserError):
store.create("alice", "x" * (MIN_PASSWORD_LEN - 1), ROLE_ADMIN, ACTOR)
@pytest.mark.parametrize("name", ["ab", "a b c", "ümlaut", "x" * 65, "", "a/b"])
def test_invalid_username(store, name):
with pytest.raises(UserError):
store.create(name, PASSWORD, ROLE_VIEWER, ACTOR)
def test_invalid_role(store):
with pytest.raises(UserError):
store.create("alice", PASSWORD, "root", ACTOR)
def test_duplicate_case_insensitive(store):
store.create("Alice", PASSWORD, ROLE_ADMIN, ACTOR)
with pytest.raises(UserError):
store.create("alice", PASSWORD, ROLE_VIEWER, ACTOR)
def test_last_admin_guard_set_role(store):
admin = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
with pytest.raises(UserError):
store.set_role(admin.id, ROLE_VIEWER, ACTOR)
def test_last_admin_guard_set_disabled(store):
admin = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
with pytest.raises(UserError):
store.set_disabled(admin.id, True, ACTOR)
def test_last_admin_guard_delete(store):
admin = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
with pytest.raises(UserError):
store.delete(admin.id, ACTOR)
def test_second_admin_allows_changes(store):
first = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
store.create("bob", PASSWORD, ROLE_ADMIN, ACTOR)
store.set_role(first.id, ROLE_VIEWER, ACTOR)
assert store.get(first.id).role == ROLE_VIEWER
store.set_disabled(first.id, True, ACTOR)
assert store.get(first.id).disabled
store.delete(first.id, ACTOR)
assert store.get(first.id) is None
def test_viewer_can_be_removed_freely(store):
store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
viewer = store.create("bob", PASSWORD, ROLE_VIEWER, ACTOR)
store.set_disabled(viewer.id, True, ACTOR)
store.delete(viewer.id, ACTOR)
assert store.count() == 1
def test_set_password_bumps_session_version(store):
user = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
store.set_password(user.id, "another long password", ACTOR)
updated = store.get(user.id)
assert updated.session_version == user.session_version + 1
assert store.authenticate("alice", PASSWORD) is None
assert store.authenticate("alice", "another long password") is not None
def test_set_password_validates_length(store):
user = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
with pytest.raises(UserError):
store.set_password(user.id, "short", ACTOR)
def test_disabled_user_cannot_login(store):
store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
bob = store.create("bob", PASSWORD, ROLE_VIEWER, ACTOR)
store.set_disabled(bob.id, True, ACTOR)
assert store.authenticate("bob", PASSWORD) is None
store.set_disabled(bob.id, False, ACTOR)
assert store.authenticate("bob", PASSWORD) is not None
def test_unknown_user_id(store):
with pytest.raises(UserError):
store.set_role(999, ROLE_VIEWER, ACTOR)
def test_totp_secret_encrypted_and_roundtrip(store, db):
user = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
store.set_totp(user.id, secret, ACTOR)
raw = db.query("SELECT totp_secret FROM users WHERE id=?", (user.id,))[0]["totp_secret"]
assert raw and secret not in raw
assert store.totp_secret(user.id) == secret
assert store.get(user.id).has_totp
store.set_totp(user.id, None, ACTOR)
assert store.totp_secret(user.id) is None
assert not store.get(user.id).has_totp
def test_audit_entries_without_password(store, db):
user = store.create("alice", PASSWORD, ROLE_ADMIN, ACTOR)
store.set_password(user.id, "another long password", ACTOR)
store.set_totp(user.id, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", ACTOR)
entries = audit.recent(db)
actions = {e["action"] for e in entries}
assert {"user.create", "user.password", "user.totp.enable"} <= actions
dump = repr(entries)
assert PASSWORD not in dump
assert "another long password" not in dump
assert "GEZDGNBVGY3TQOJQ" not in dump