158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
"""Accounts, passwords, JWT sessions, API keys, snapshots."""
|
|
|
|
import json
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from helpers import PASSWORD
|
|
from rigshare_core import jwt
|
|
import rigshare_core.store as store_module
|
|
|
|
|
|
def make_admin(store, name="admin"):
|
|
return store.setup_admin(name, PASSWORD)
|
|
|
|
|
|
# ----- setup and accounts ------------------------------------------------
|
|
|
|
def test_no_default_accounts(store):
|
|
assert store.needs_setup
|
|
assert store.users == {}
|
|
|
|
|
|
def test_setup_only_once(store):
|
|
user = make_admin(store)
|
|
assert user["perms"]["admin"] and not store.needs_setup
|
|
with pytest.raises(PermissionError):
|
|
store.setup_admin("intruder", PASSWORD)
|
|
|
|
|
|
def test_env_provisioning(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("RIGSHARE_ADMIN_USER", "ops")
|
|
monkeypatch.setenv("RIGSHARE_ADMIN_PASSWORD", PASSWORD)
|
|
s = store_module.Store(str(tmp_path))
|
|
assert s.users["ops"]["perms"]["admin"]
|
|
|
|
|
|
def test_passwords_are_hashed(store, tmp_path):
|
|
make_admin(store)
|
|
raw = (tmp_path / "users.json").read_text()
|
|
assert PASSWORD not in raw
|
|
assert store.users["admin"]["password"].startswith("pbkdf2_sha256$")
|
|
|
|
|
|
def test_authenticate(store):
|
|
make_admin(store)
|
|
assert store.authenticate("admin", PASSWORD)
|
|
assert store.authenticate("admin", "wrong-password") is None
|
|
assert store.authenticate("nobody", PASSWORD) is None
|
|
|
|
|
|
@pytest.mark.parametrize("username,password", [("x", PASSWORD), ("bad name", PASSWORD), ("ok_name", "short")])
|
|
def test_create_user_validation(store, username, password):
|
|
make_admin(store)
|
|
with pytest.raises(ValueError):
|
|
store.create_user(username, password)
|
|
|
|
|
|
def test_last_admin_is_protected(store):
|
|
make_admin(store)
|
|
with pytest.raises(ValueError):
|
|
store.delete_user("admin")
|
|
with pytest.raises(ValueError):
|
|
store.update_user("admin", perms={"admin": False})
|
|
|
|
|
|
def test_password_change_revokes_sessions(store):
|
|
make_admin(store)
|
|
token = store.create_session("admin")
|
|
store.update_user("admin", password="another-password")
|
|
assert store.user_for_token(token) is None
|
|
|
|
|
|
# ----- JWT sessions --------------------------------------------------------
|
|
|
|
def claims_of(token):
|
|
return json.loads(jwt._b64d(token.split(".")[1]))
|
|
|
|
|
|
def test_session_is_jwt(store):
|
|
make_admin(store)
|
|
token = store.create_session("admin")
|
|
assert token.count(".") == 2
|
|
assert store.user_for_token(token)["username"] == "admin"
|
|
|
|
|
|
def test_tampered_token_rejected(store):
|
|
make_admin(store)
|
|
header, body, sig = store.create_session("admin").split(".")
|
|
forged = jwt._b64e(json.dumps({**claims_of(f"{header}.{body}.{sig}"), "sub": "someone"}).encode())
|
|
assert store.user_for_token(f"{header}.{forged}.{sig}") is None
|
|
assert store.user_for_token(f"{header}.{body}.{sig[:-2]}AA") is None
|
|
|
|
|
|
def test_alg_none_rejected(store):
|
|
make_admin(store)
|
|
_, body, _ = store.create_session("admin").split(".")
|
|
none_header = jwt._b64e(b'{"alg":"none","typ":"JWT"}')
|
|
assert store.user_for_token(f"{none_header}.{body}.") is None
|
|
|
|
|
|
def test_expired_token_rejected(store):
|
|
make_admin(store)
|
|
sid = claims_of(store.create_session("admin"))["sid"]
|
|
old = jwt.encode({"iss": "rigshare", "sub": "admin", "sid": sid, "iat": 0, "nbf": 0, "exp": 1}, store.secret)
|
|
assert store.user_for_token(old) is None
|
|
|
|
|
|
def test_logout_revokes_immediately(store):
|
|
make_admin(store)
|
|
token = store.create_session("admin")
|
|
store.revoke_session(token)
|
|
assert store.user_for_token(token) is None
|
|
|
|
|
|
def test_refresh_only_when_old(store, monkeypatch):
|
|
make_admin(store)
|
|
token = store.create_session("admin")
|
|
assert store.refresh_token(token) is None
|
|
monkeypatch.setattr(store_module, "TOKEN_REFRESH", -1)
|
|
fresh = store.refresh_token(token)
|
|
assert fresh and store.user_for_token(fresh)["username"] == "admin"
|
|
assert claims_of(fresh)["sid"] == claims_of(token)["sid"]
|
|
|
|
|
|
def test_rename_keeps_sessions_and_keys(store):
|
|
make_admin(store)
|
|
token = store.create_session("admin")
|
|
key, _ = store.create_api_key("admin", "k")
|
|
store.rename_user("admin", "root")
|
|
assert store.user_for_token(token)["username"] == "root"
|
|
assert store.user_for_token(key)["username"] == "root"
|
|
|
|
|
|
# ----- API keys ---------------------------------------------------------------
|
|
|
|
def test_api_keys(store, tmp_path):
|
|
make_admin(store)
|
|
key, meta = store.create_api_key("admin", "script")
|
|
assert key.startswith("rs_") and key not in (tmp_path / "users.json").read_text()
|
|
assert store.user_for_token(key)["username"] == "admin"
|
|
assert "hash" not in store.list_api_keys("admin")[0]
|
|
store.revoke_api_key("admin", meta["id"])
|
|
assert store.user_for_token(key) is None
|
|
|
|
|
|
# ----- snapshots ----------------------------------------------------------------
|
|
|
|
def test_snapshot_rotation_keeps_labelled(store):
|
|
store.update_config({"snapshot_keep": 2})
|
|
for i in range(4):
|
|
store.add_snapshot({"nodes": []}, "me", "room", "Room")
|
|
time.sleep(0.01)
|
|
store.add_snapshot({"nodes": []}, "me", "room", "Room", label="keep me")
|
|
snaps = store.list_snapshots("room")
|
|
assert len([s for s in snaps if not s["label"]]) == 2
|
|
assert any(s["label"] == "keep me" for s in snaps)
|