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

416 lines
13 KiB
Python

"""Regression tests for tenant-scoped administration."""
from __future__ import annotations
def auth_headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
def _create_org_admin(user_store, *, org_id: str, username: str, password: str):
return user_store.create_user(
org_id=org_id,
username=username,
password=password,
name=username,
role="admin",
email=f"{username}@example.com",
must_setup=False,
)
def test_tenant_admin_cannot_provision_into_another_org(
client, user_store, login
):
org_b = user_store.create_org("Organization B", org_id="org-b")
admin_a = _create_org_admin(
user_store, org_id="org-default", username="admin-a", password="admin-a-password"
)
token = login(admin_a["username"], "admin-a-password")["token"]
response = client.post(
"/api/admin/users",
json={
"username": "cross-org-user",
"name": "Cross Org User",
"password": "cross-org-password",
"role": "user",
"org_id": org_b["id"],
},
headers=auth_headers(token),
)
assert response.status_code == 403, response.get_json()
assert user_store.get_user_or_none("cross-org-user") is None
def test_tenant_admin_can_provision_without_org_id_into_own_org(
client, user_store, login
):
admin_a = _create_org_admin(
user_store, org_id="org-default", username="admin-own", password="admin-own-password"
)
token = login(admin_a["username"], "admin-own-password")["token"]
response = client.post(
"/api/admin/users",
json={
"username": "own-org-user",
"name": "Own Org User",
"password": "own-org-password",
"role": "user",
},
headers=auth_headers(token),
)
assert response.status_code == 201, response.get_json()
assert response.get_json()["org_id"] == "org-default"
assert user_store.get_user("own-org-user")["org_id"] == "org-default"
def test_tenant_admin_cannot_update_user_in_another_org(
client, user_store, login
):
org_b = user_store.create_org("Organization B", org_id="org-b")
admin_a = _create_org_admin(
user_store, org_id="org-default", username="admin-update", password="admin-update-password"
)
victim = user_store.create_user(
org_id=org_b["id"],
username="org-b-victim",
password="victim-password",
name="Org B Victim",
role="user",
email="org-b-victim@example.com",
must_setup=False,
)
before = dict(victim)
token = login(admin_a["username"], "admin-update-password")["token"]
response = client.put(
"/api/admin/users/org-b-victim",
json={"password": "attacker-password", "email": "attacker@example.com"},
headers=auth_headers(token),
)
assert response.status_code in (403, 404), response.get_json()
assert user_store.get_user("org-b-victim") == before
def test_tenant_admin_cannot_update_super_admin(
client, user_store, login
):
admin_a = _create_org_admin(
user_store, org_id="org-default", username="admin-no-root", password="admin-no-root-password"
)
super_admin_before = dict(user_store.get_user("admin"))
token = login(admin_a["username"], "admin-no-root-password")["token"]
response = client.put(
"/api/admin/users/admin",
json={"password": "attacker-password", "email": "attacker@example.com"},
headers=auth_headers(token),
)
assert response.status_code in (403, 404), response.get_json()
assert user_store.get_user("admin") == super_admin_before
def test_tenant_admin_cannot_change_role_or_active_state(
client, user_store, login
):
admin_a = _create_org_admin(
user_store, org_id="org-default", username="admin-role", password="admin-role-password"
)
target = user_store.create_user(
org_id="org-default",
username="same-org-user",
password="same-org-password",
name="Same Org User",
role="user",
must_setup=False,
)
before = dict(target)
token = login(admin_a["username"], "admin-role-password")["token"]
role_response = client.put(
"/api/admin/users/same-org-user",
json={"role": "admin"},
headers=auth_headers(token),
)
active_response = client.put(
"/api/admin/users/same-org-user",
json={"active": False},
headers=auth_headers(token),
)
assert role_response.status_code == 403, role_response.get_json()
assert active_response.status_code == 403, active_response.get_json()
assert user_store.get_user("same-org-user") == before
def test_admin_password_update_rejects_weak_password_without_mutation(
client, user_store, login
):
admin = _create_org_admin(
user_store, org_id="org-default", username="admin-password-policy", password="admin-password-policy"
)
target = user_store.create_user(
org_id="org-default",
username="password-policy-target",
password="target-original-password",
name="Password Policy Target",
role="user",
must_setup=False,
)
before = user_store.get_user(target["username"])
token = login(admin["username"], "admin-password-policy")["token"]
response = client.put(
"/api/admin/users/password-policy-target",
json={"password": "12345678901"},
headers=auth_headers(token),
)
assert response.status_code == 400, response.get_json()
assert user_store.get_user(target["username"]) == before
def test_admin_profile_update_invalid_email_does_not_partially_change_password(
client, user_store, login
):
admin = _create_org_admin(
user_store, org_id="org-default", username="admin-profile-policy", password="admin-profile-policy"
)
target = user_store.create_user(
org_id="org-default",
username="profile-policy-target",
password="target-original-password",
name="Profile Policy Target",
role="user",
must_setup=False,
)
before = user_store.get_user(target["username"])
token = login(admin["username"], "admin-profile-policy")["token"]
response = client.put(
"/api/admin/users/profile-policy-target",
json={"password": "target-updated-password", "email": "invalid-email"},
headers=auth_headers(token),
)
assert response.status_code == 400, response.get_json()
assert user_store.get_user(target["username"]) == before
def test_new_org_weak_password_does_not_create_orphan_org(
client, user_store, login
):
user_store.complete_setup(
"admin",
"bootstrap@example.com",
"bootstrap-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "bootstrap-admin-password")["token"]
org_ids_before = {org["id"] for org in user_store.orgs.all()}
response = client.post(
"/api/admin/users",
json={
"username": "weak-new-org-admin",
"password": "12345678901",
"role": "admin",
"new_org": True,
},
headers=auth_headers(token),
)
assert response.status_code == 400, response.get_json()
assert user_store.get_user_or_none("weak-new-org-admin") is None
assert {org["id"] for org in user_store.orgs.all()} == org_ids_before
def test_new_org_invalid_email_does_not_create_orphan_org(
client, user_store, login
):
user_store.complete_setup(
"admin",
"bootstrap-email@example.com",
"bootstrap-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "bootstrap-admin-password")["token"]
org_ids_before = {org["id"] for org in user_store.orgs.all()}
response = client.post(
"/api/admin/users",
json={
"username": "invalid-email-new-org-admin",
"email": "not-an-email",
"password": "valid-new-org-password",
"role": "admin",
"new_org": True,
},
headers=auth_headers(token),
)
assert response.status_code == 400, response.get_json()
assert user_store.get_user_or_none("invalid-email-new-org-admin") is None
assert {org["id"] for org in user_store.orgs.all()} == org_ids_before
def test_new_org_non_string_email_does_not_create_orphan_org(
client, user_store, login
):
user_store.complete_setup(
"admin",
"bootstrap-type@example.com",
"bootstrap-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "bootstrap-admin-password")["token"]
org_ids_before = {org["id"] for org in user_store.orgs.all()}
response = client.post(
"/api/admin/users",
json={
"username": "type-email-new-org-admin",
"email": 123,
"password": "valid-new-org-password",
"role": "admin",
"new_org": True,
},
headers=auth_headers(token),
)
assert response.status_code == 400, response.get_json()
assert user_store.get_user_or_none("type-email-new-org-admin") is None
assert {org["id"] for org in user_store.orgs.all()} == org_ids_before
def test_admin_create_rejects_non_string_name_without_mutation(
client, user_store, login
):
user_store.complete_setup(
"admin",
"bootstrap-name@example.com",
"bootstrap-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "bootstrap-admin-password")["token"]
response = client.post(
"/api/admin/users",
json={
"username": "non-string-name-user",
"name": 123,
"password": "valid-created-password",
"role": "user",
},
headers=auth_headers(token),
)
assert response.status_code == 400, response.get_json()
assert user_store.get_user_or_none("non-string-name-user") is None
def test_new_org_unexpected_user_write_failure_rolls_back_org(
client, user_store, login, monkeypatch
):
user_store.complete_setup(
"admin",
"bootstrap-write@example.com",
"bootstrap-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "bootstrap-admin-password")["token"]
org_ids_before = {org["id"] for org in user_store.orgs.all()}
def fail_user_write(**_kwargs):
raise RuntimeError("simulated persistence failure")
monkeypatch.setattr(user_store, "create_user", fail_user_write)
response = client.post(
"/api/admin/users",
json={
"username": "failed-new-org-admin",
"password": "valid-new-org-password",
"role": "admin",
"new_org": True,
},
headers=auth_headers(token),
)
assert response.status_code == 500, response.get_json()
assert {org["id"] for org in user_store.orgs.all()} == org_ids_before
def test_super_admin_can_provision_into_existing_org(
client, user_store, login
):
org_b = user_store.create_org("Organization B", org_id="org-b")
user_store.complete_setup(
"admin",
"admin@example.com",
"bootstrap-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "bootstrap-password")["token"]
response = client.post(
"/api/admin/users",
json={
"username": "platform-created",
"name": "Platform Created",
"password": "platform-password",
"role": "user",
"org_id": org_b["id"],
},
headers=auth_headers(token),
)
assert response.status_code == 201, response.get_json()
assert user_store.get_user("platform-created")["org_id"] == org_b["id"]
def test_super_admin_group_listing_spans_all_orgs(client, user_store, login):
user_store.complete_setup(
"admin",
"platform@example.com",
"platform-admin-password",
accepted_terms=True,
accepted_terms_at="2026-08-13T00:00:00Z",
)
super_token = login("admin", "platform-admin-password")["token"]
org_b = user_store.create_org("Organization B", org_id="org-b")
org_b_admin = user_store.create_user(
org_id=org_b["id"],
username="org-b-group-admin",
password="org-b-group-password",
name="Org B Group Admin",
role="admin",
must_setup=False,
)
org_b_token = login(
org_b_admin["username"], "org-b-group-password"
)["token"]
created = client.post(
"/api/groups",
json={"product": "Org B product", "segment": "Org B segment"},
headers=auth_headers(org_b_token),
)
assert created.status_code == 201, created.get_json()
gid = created.get_json()["group"]["id"]
response = client.get("/api/groups", headers=auth_headers(super_token))
assert response.status_code == 200, response.get_json()
assert gid in {group["id"] for group in response.get_json()["groups"]}