"""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)