Files
sales-trainer/backend/tests/test_request_auth_guards.py

378 lines
12 KiB
Python

"""Regression tests for request-level authentication and tenant activity guards."""
from __future__ import annotations
import pytest
from app.api.helpers import ApiError, assert_tenant
from app.auth.users import AuthError, UserStore
from app.config import Config
def auth_headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
def test_must_setup_user_can_only_use_setup_safe_endpoints(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="pending-user",
password="pending-password",
name="Pending User",
role="user",
must_setup=True,
)
token = login(user["username"], "pending-password")["token"]
headers = auth_headers(token)
assert client.get("/api/auth/me", headers=headers).status_code == 200
assert client.post(
"/api/auth/setup",
headers=headers,
json={
"email": "pending@example.com",
"password": "pending-new-password",
"accepted_terms": True,
},
).status_code == 200
def test_must_setup_user_cannot_use_business_routes_before_setup(
client, user_store, login
):
user = user_store.create_user(
org_id="org-default",
username="pending-business",
password="pending-password",
name="Pending Business",
role="user",
must_setup=True,
)
token = login(user["username"], "pending-password")["token"]
headers = auth_headers(token)
response = client.get("/api/groups", headers=headers)
assert response.status_code == 403, response.get_json()
def test_must_setup_user_cannot_use_admin_chat_or_analytics(
client, user_store, login
):
user = user_store.create_user(
org_id="org-default",
username="pending-all-routes",
password="pending-password",
name="Pending User",
role="user",
must_setup=True,
)
token = login(user["username"], "pending-password")["token"]
headers = auth_headers(token)
responses = [
client.post(
"/api/admin/users",
json={"username": "should-not-create", "password": "password"},
headers=headers,
),
client.get("/api/analytics", headers=headers),
client.post(
"/api/chat/missing-group/personas/missing-persona/chat/start",
json={"scenario": "social"},
headers=headers,
),
]
assert [response.status_code for response in responses] == [403, 403, 403]
def test_inactive_user_token_is_rejected(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="inactive-user",
password="inactive-password",
name="Inactive User",
role="user",
must_setup=False,
)
token = login(user["username"], "inactive-password")["token"]
user_store.set_active(user["username"], False)
response = client.get("/api/auth/me", headers=auth_headers(token))
assert response.status_code == 401, response.get_json()
def test_inactive_org_rejects_existing_token(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="inactive-org-user",
password="inactive-org-password",
name="Inactive Org User",
role="user",
must_setup=False,
)
token = login(user["username"], "inactive-org-password")["token"]
user_store.orgs.update("org-default", active=False)
response = client.get("/api/auth/me", headers=auth_headers(token))
assert response.status_code == 401, response.get_json()
def test_missing_org_rejects_token(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="missing-org-user",
password="missing-org-password",
name="Missing Org User",
role="user",
must_setup=False,
)
token = login(user["username"], "missing-org-password")["token"]
user_store.orgs.delete("org-default")
response = client.get("/api/auth/me", headers=auth_headers(token))
assert response.status_code == 401, response.get_json()
def test_token_org_claim_mismatch_is_rejected(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="mismatched-org-user",
password="mismatched-org-password",
name="Mismatched Org User",
role="user",
must_setup=False,
)
token = login(user["username"], "mismatched-org-password")["token"]
payload = user_store.decode_token(token)
payload["org_id"] = "org-other"
forged_token = __import__("jwt").encode(
payload, Config.SECRET_KEY, algorithm=Config.JWT_ALGO
)
response = client.get("/api/auth/me", headers=auth_headers(forged_token))
assert response.status_code == 401, response.get_json()
def test_malformed_jwt_claim_types_are_rejected(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="malformed-claim-user",
password="malformed-claim-password",
name="Malformed Claim User",
role="user",
must_setup=False,
)
token = login(user["username"], "malformed-claim-password")["token"]
payload = user_store.decode_token(token)
jwt = __import__("jwt")
for claim, value in (("sub", 123), ("org_id", 123)):
malformed = dict(payload)
malformed[claim] = value
forged_token = jwt.encode(
malformed, Config.SECRET_KEY, algorithm=Config.JWT_ALGO
)
response = client.get(
"/api/auth/me", headers=auth_headers(forged_token)
)
assert response.status_code == 401, response.get_json()
def test_malformed_jwt_subject_and_org_claims_do_not_500(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="malformed-claim-input-user",
password="malformed-claim-input-password",
name="Malformed Claim Input User",
role="user",
must_setup=False,
)
token = login(user["username"], "malformed-claim-input-password")["token"]
payload = user_store.decode_token(token)
jwt = __import__("jwt")
for claim, value in (("sub", {"nested": True}), ("org_id", ["org-default"])):
malformed = dict(payload)
malformed[claim] = value
forged_token = jwt.encode(
malformed, Config.SECRET_KEY, algorithm=Config.JWT_ALGO
)
response = client.get(
"/api/auth/me", headers=auth_headers(forged_token)
)
assert response.status_code == 401, response.get_json()
def test_malformed_stored_org_id_is_rejected_without_500(client, user_store, login):
user = user_store.create_user(
org_id="org-default",
username="malformed-stored-org-user",
password="malformed-stored-org-password",
name="Malformed Stored Org User",
role="user",
must_setup=False,
)
token = login(user["username"], "malformed-stored-org-password")["token"]
user_store.users.update(user["username"], org_id=123)
response = client.get(
"/api/auth/me", headers=auth_headers(token)
)
assert response.status_code == 401, response.get_json()
def test_login_rejects_user_when_organization_becomes_inactive(
client, user_store
):
user = user_store.create_user(
org_id="org-default",
username="inactive-org-login",
password="inactive-org-login-password",
name="Inactive Org Login",
role="user",
must_setup=False,
)
user_store.orgs.update("org-default", active=False)
response = client.post(
"/api/auth/login",
json={
"username": user["username"],
"password": "inactive-org-login-password",
},
)
assert response.status_code == 401, response.get_json()
def test_non_boolean_active_state_is_fail_closed_for_user_and_org(
client, user_store, login
):
user = user_store.create_user(
org_id="org-default",
username="legacy-active-shape",
password="legacy-active-password",
name="Legacy Active Shape",
role="user",
must_setup=False,
)
token = login(user["username"], "legacy-active-password")["token"]
user_store.users.update(user["username"], active="false")
user_response = client.get("/api/auth/me", headers=auth_headers(token))
assert user_response.status_code == 401, user_response.get_json()
user_store.users.update(user["username"], active=True)
user_store.orgs.update("org-default", active="false")
org_response = client.get("/api/auth/me", headers=auth_headers(token))
assert org_response.status_code == 401, org_response.get_json()
def test_malformed_must_setup_state_cannot_enter_setup_flow(
client, user_store, login
):
user = user_store.create_user(
org_id="org-default",
username="malformed-must-setup",
password="malformed-must-setup-password",
name="Malformed Must Setup",
role="user",
must_setup=False,
)
token = login(user["username"], "malformed-must-setup-password")["token"]
user_store.users.update(user["username"], must_setup="false")
response = client.get(
"/api/auth/me",
headers=auth_headers(token),
)
assert response.status_code == 401, response.get_json()
assert user_store.get_user(user["username"])["must_setup"] == "false"
def test_malformed_must_setup_state_cannot_login(client, user_store):
user = user_store.create_user(
org_id="org-default",
username="malformed-must-setup-login",
password="malformed-must-setup-login-password",
name="Malformed Must Setup Login",
role="user",
must_setup=False,
)
user_store.users.update(user["username"], must_setup="true")
response = client.post(
"/api/auth/login",
json={
"username": user["username"],
"password": "malformed-must-setup-login-password",
},
)
assert response.status_code == 401, response.get_json()
@pytest.mark.parametrize("bad_version", ["corrupt", True, -1, 1.5])
def test_malformed_auth_version_cannot_be_repaired_into_token_reuse(
client, user_store, login, bad_version
):
user = user_store.create_user(
org_id="org-default",
username=f"bad-auth-version-{str(bad_version).replace('.', '-')}",
password="bad-auth-version-password",
name="Bad Auth Version",
role="user",
must_setup=False,
)
token = login(user["username"], "bad-auth-version-password")["token"]
user_store.users.update(user["username"], auth_version=bad_version)
response = client.get("/api/auth/me", headers=auth_headers(token))
assert response.status_code == 401, response.get_json()
with pytest.raises(AuthError):
user_store.set_password(user["username"], "replacement-password")
assert user_store.users.get(user["username"])["auth_version"] == bad_version
@pytest.mark.parametrize(
("field", "value"),
[("password_hash", "not-a-password-hash"), ("role", "unknown-role"), ("org_id", 123)],
)
def test_malformed_login_record_fails_as_invalid_credentials(
client, user_store, field, value
):
user = user_store.create_user(
org_id="org-default",
username=f"malformed-login-{field}",
password="malformed-login-password",
name="Malformed Login",
role="user",
must_setup=False,
)
user_store.users.update(user["username"], **{field: value})
response = client.post(
"/api/auth/login",
json={"username": user["username"], "password": "malformed-login-password"},
)
assert response.status_code == 401, response.get_json()
def test_tenant_guard_rejects_missing_object_tenant(client):
from flask import g
with client.application.test_request_context():
g.user = {"role": "user", "org_id": "org-default"}
g.org_id = "org-default"
with pytest.raises(ApiError, match="permission denied"):
assert_tenant(None)