650 lines
23 KiB
Python
650 lines
23 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from yuqei_contract_api.config import ContractApiSettings, check_runtime_config
|
|
from yuqei_contract_api.identity import (
|
|
InMemoryIdentityRepository,
|
|
LastPlatformAdministratorError,
|
|
TenantUserUpdateRequest,
|
|
)
|
|
from yuqei_contract_api.main import create_app
|
|
|
|
|
|
AUTH_VALUE = "x" * 48
|
|
REGISTRATION_AGREEMENT_ACCEPTANCES = [
|
|
{"agreement_type": "user_terms", "version": "2026-08-01-draft", "accepted": True},
|
|
{"agreement_type": "privacy_policy", "version": "2026-08-01-draft", "accepted": True},
|
|
{"agreement_type": "service_agreement", "version": "2026-08-01-draft", "accepted": True},
|
|
{"agreement_type": "ai_use_notice", "version": "2026-08-01-draft", "accepted": True},
|
|
]
|
|
|
|
|
|
def make_settings(tmp_path: Path) -> ContractApiSettings:
|
|
return ContractApiSettings(
|
|
environment="staging",
|
|
database_url=f"sqlite+pysqlite:///{tmp_path / 'proxy-auth.db'}",
|
|
database_auto_create=True,
|
|
seed_demo_data=False,
|
|
file_storage_dir=str(tmp_path / "files"),
|
|
web_proxy_shared_secret=AUTH_VALUE,
|
|
require_web_proxy_auth=True,
|
|
public_policy_registration_fail_closed=False,
|
|
)
|
|
|
|
|
|
def test_health_remains_public_for_container_liveness(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
|
|
response = client.get("/health")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_non_public_requests_require_the_server_side_proxy_key(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
|
|
missing = client.get("/api/v1/contracts")
|
|
wrong = client.get("/api/v1/contracts", headers={"X-Contract-Proxy-Key": "wrong"})
|
|
missing_user = client.get(
|
|
"/api/v1/contracts",
|
|
headers={"X-Contract-Proxy-Key": AUTH_VALUE, "X-Tenant-Id": "default"},
|
|
)
|
|
valid = client.get(
|
|
"/api/v1/contracts",
|
|
headers={
|
|
"X-Contract-Proxy-Key": AUTH_VALUE,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": "user-current-admin",
|
|
},
|
|
)
|
|
|
|
assert missing.status_code == 401
|
|
assert wrong.status_code == 401
|
|
assert missing_user.status_code == 401
|
|
assert missing_user.json()["detail"] == "Authenticated user context is required."
|
|
assert valid.status_code == 200
|
|
|
|
|
|
def test_platform_access_uses_fresh_repository_permission_without_roster_pii(tmp_path: Path) -> None:
|
|
app = create_app(make_settings(tmp_path))
|
|
client = TestClient(app)
|
|
platform_admin_headers = {
|
|
"X-Contract-Proxy-Key": AUTH_VALUE,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": "user-current-admin",
|
|
}
|
|
organization_admin = client.post(
|
|
"/api/v1/identity/users",
|
|
headers=platform_admin_headers,
|
|
json={
|
|
"email": "organization-admin@example.test",
|
|
"display_name": "Organization Admin",
|
|
"role": "admin",
|
|
"status": "active",
|
|
},
|
|
)
|
|
assert organization_admin.status_code == 200
|
|
|
|
allowed = client.get("/api/v1/identity/platform-access", headers=platform_admin_headers)
|
|
denied = client.get(
|
|
"/api/v1/identity/platform-access",
|
|
headers={
|
|
"X-Contract-Proxy-Key": AUTH_VALUE,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": organization_admin.json()["id"],
|
|
},
|
|
)
|
|
spoofed_without_proxy_auth = client.get(
|
|
"/api/v1/identity/platform-access",
|
|
headers={"X-Tenant-Id": "default", "X-User-Id": "user-current-admin"},
|
|
)
|
|
|
|
assert allowed.status_code == 200
|
|
assert allowed.json() == {
|
|
"platform_admin": True,
|
|
"capabilities": ["prompt_templates.manage"],
|
|
}
|
|
assert denied.status_code == 200
|
|
assert denied.json() == {"platform_admin": False, "capabilities": []}
|
|
assert spoofed_without_proxy_auth.status_code == 401
|
|
|
|
|
|
def test_playbook_fallback_requires_contract_edit_permission(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
admin_headers = {
|
|
"X-Contract-Proxy-Key": AUTH_VALUE,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": "user-current-admin",
|
|
}
|
|
approver = client.post(
|
|
"/api/v1/identity/users",
|
|
headers=admin_headers,
|
|
json={
|
|
"email": "fallback-approver@example.test",
|
|
"display_name": "Fallback Approver",
|
|
"role": "approver",
|
|
"status": "active",
|
|
},
|
|
)
|
|
assert approver.status_code == 200
|
|
approver_headers = {
|
|
"X-Contract-Proxy-Key": AUTH_VALUE,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": approver.json()["id"],
|
|
}
|
|
denied = client.post(
|
|
"/api/v1/contracts/contract-does-not-matter/playbook-fallbacks",
|
|
headers=approver_headers,
|
|
json={
|
|
"review_task_id": "review-does-not-matter",
|
|
"snapshot_id": "playbook-snapshot-does-not-matter",
|
|
"rule_id": "approval-rule-does-not-matter",
|
|
},
|
|
)
|
|
assert denied.status_code == 403
|
|
denied_adoption = client.post(
|
|
"/api/v1/contracts/contract-does-not-matter/playbook-fallbacks/execution-does-not-matter/adopt",
|
|
headers=approver_headers,
|
|
json={
|
|
"expected_candidate_body_hash": "0" * 64,
|
|
"confirmed": True,
|
|
},
|
|
)
|
|
assert denied_adoption.status_code == 403
|
|
denied_combined = client.post(
|
|
"/api/v1/contracts/contract-does-not-matter/playbook-candidates",
|
|
headers=approver_headers,
|
|
json={
|
|
"review_task_id": "review-does-not-matter",
|
|
"snapshot_id": "snapshot-does-not-matter",
|
|
"decisions": [{"rule_id": "rule-does-not-matter", "decision": "apply"}],
|
|
},
|
|
)
|
|
assert denied_combined.status_code == 403
|
|
denied_combined_adoption = client.post(
|
|
"/api/v1/contracts/contract-does-not-matter/playbook-candidates/candidate-does-not-matter/adopt",
|
|
headers=approver_headers,
|
|
json={"expected_candidate_body_hash": "0" * 64, "confirmed": True},
|
|
)
|
|
assert denied_combined_adoption.status_code == 403
|
|
denied_combined_withdrawal = client.post(
|
|
"/api/v1/contracts/contract-does-not-matter/playbook-candidates/candidate-does-not-matter/withdraw",
|
|
headers=approver_headers,
|
|
json={"reason": "无权限撤销"},
|
|
)
|
|
assert denied_combined_withdrawal.status_code == 403
|
|
|
|
|
|
def test_public_policy_proxy_bypass_is_limited_to_exact_read_routes(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
|
|
assert client.get("/api/v1/public-policies/current").status_code == 404
|
|
assert client.get("/api/v1/public-policies/current/user_terms").status_code == 404
|
|
assert client.get("/api/v1/public-policies/currentness").status_code == 401
|
|
assert client.get("/api/v1/public-policies/current/not_a_policy").status_code == 401
|
|
assert client.get("/api/v1/public-policies/current/user_terms/extra").status_code == 401
|
|
|
|
|
|
def test_required_agreement_routes_reject_missing_user_context(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
headers = {"X-Contract-Proxy-Key": AUTH_VALUE, "X-Tenant-Id": "default"}
|
|
|
|
required = client.get("/api/v1/identity/agreements/required", headers=headers)
|
|
accepted = client.post(
|
|
"/api/v1/identity/agreements/consents",
|
|
headers=headers,
|
|
json={"items": []},
|
|
)
|
|
|
|
assert required.status_code == 401
|
|
assert accepted.status_code == 401
|
|
|
|
|
|
def test_public_policy_preflight_requires_platform_admin_even_without_proxy_auth(tmp_path: Path) -> None:
|
|
settings = make_settings(tmp_path).model_copy(update={"require_web_proxy_auth": False})
|
|
client = TestClient(create_app(settings))
|
|
|
|
response = client.get("/api/v1/admin/public-policy-promotion/preflight")
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_production_config_requires_proxy_auth() -> None:
|
|
settings = ContractApiSettings(
|
|
environment="production",
|
|
database_url="postgresql+psycopg://contract@postgres:5432/yuqei_contract",
|
|
database_auto_create=False,
|
|
seed_demo_data=False,
|
|
)
|
|
|
|
checks = {item.name: item for item in check_runtime_config(settings).checks}
|
|
|
|
assert checks["web_proxy_auth"].status == "error"
|
|
|
|
|
|
def test_authenticated_tenant_cannot_override_identity_scope(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
proxy_headers = {"X-Contract-Proxy-Key": AUTH_VALUE}
|
|
credential_value = "S" * 8 + "1" * 4
|
|
|
|
registered = client.post(
|
|
"/api/v1/identity/register-company",
|
|
headers={**proxy_headers, "X-Tenant-Id": "default", "X-User-Id": "user-current-admin"},
|
|
json={
|
|
"tenant_id": "tenant-secure",
|
|
"company_name": "Secure Tenant",
|
|
"admin_email": "owner@secure.example",
|
|
"admin_display_name": "Tenant Owner",
|
|
"admin_password": credential_value,
|
|
"agreement_acceptances": REGISTRATION_AGREEMENT_ACCEPTANCES,
|
|
},
|
|
)
|
|
assert registered.status_code == 200
|
|
session = registered.json()
|
|
tenant_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": session["tenant"]["id"],
|
|
"X-User-Id": session["user"]["id"],
|
|
}
|
|
|
|
own_scope = client.get("/api/v1/identity/users?tenant_id=tenant-secure", headers=tenant_headers)
|
|
foreign_scope = client.get("/api/v1/identity/users?tenant_id=default", headers=tenant_headers)
|
|
|
|
assert own_scope.status_code == 200
|
|
assert foreign_scope.status_code == 403
|
|
|
|
admin_audit = client.get(
|
|
"/api/v1/identity/agreements/consents?tenant_id=tenant-secure",
|
|
headers=tenant_headers,
|
|
)
|
|
assert admin_audit.status_code == 200
|
|
assert admin_audit.json()["total"] == 4
|
|
|
|
observer_response = client.post(
|
|
"/api/v1/identity/users",
|
|
headers=tenant_headers,
|
|
json={
|
|
"email": "observer@secure.example",
|
|
"display_name": "Consent Observer",
|
|
"role": "observer",
|
|
"status": "active",
|
|
},
|
|
)
|
|
assert observer_response.status_code == 200
|
|
observer = observer_response.json()
|
|
observer_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": "tenant-secure",
|
|
"X-User-Id": observer["id"],
|
|
}
|
|
own_consent_audit = client.get(
|
|
f"/api/v1/identity/agreements/consents?user_id={observer['id']}",
|
|
headers=observer_headers,
|
|
)
|
|
workspace_consent_audit = client.get(
|
|
"/api/v1/identity/agreements/consents",
|
|
headers=observer_headers,
|
|
)
|
|
|
|
assert own_consent_audit.status_code == 200
|
|
assert own_consent_audit.json()["total"] == 0
|
|
assert workspace_consent_audit.status_code == 403
|
|
|
|
authoritative_account = client.get("/api/v1/identity/account", headers=observer_headers)
|
|
forbidden_identity_fields = client.patch(
|
|
"/api/v1/identity/account",
|
|
headers=observer_headers,
|
|
json={
|
|
"tenant_id": "default",
|
|
"user_id": session["user"]["id"],
|
|
"email": "forged@example.com",
|
|
"role": "admin",
|
|
},
|
|
)
|
|
allowed_profile = client.patch(
|
|
"/api/v1/identity/account",
|
|
headers=observer_headers,
|
|
json={"display_name": "Consent Observer Updated", "timezone": "Asia/Shanghai"},
|
|
)
|
|
|
|
assert authoritative_account.status_code == 200
|
|
assert authoritative_account.json()["email"] == observer["email"]
|
|
assert authoritative_account.json()["role"] == "observer"
|
|
assert forbidden_identity_fields.status_code == 422
|
|
assert allowed_profile.status_code == 200
|
|
assert allowed_profile.json()["email"] == observer["email"]
|
|
assert allowed_profile.json()["role"] == "observer"
|
|
assert allowed_profile.json()["display_name"] == "Consent Observer Updated"
|
|
|
|
|
|
def test_email_verification_is_scoped_and_refreshes_self_session(tmp_path: Path) -> None:
|
|
settings = make_settings(tmp_path).model_copy(
|
|
update={"identity_challenge_resend_cooldown_seconds": 0}
|
|
)
|
|
client = TestClient(create_app(settings))
|
|
proxy_headers = {"X-Contract-Proxy-Key": AUTH_VALUE}
|
|
registered = client.post(
|
|
"/api/v1/identity/register-company",
|
|
headers={**proxy_headers, "X-Tenant-Id": "default", "X-User-Id": "user-current-admin"},
|
|
json={
|
|
"tenant_id": "tenant-email-scope",
|
|
"company_name": "Email Scope Tenant",
|
|
"admin_email": "owner@email-scope.example",
|
|
"admin_display_name": "Workspace Owner",
|
|
"admin_password": "OwnerPassword123", # pragma: allowlist secret
|
|
"agreement_acceptances": REGISTRATION_AGREEMENT_ACCEPTANCES,
|
|
},
|
|
)
|
|
assert registered.status_code == 200
|
|
owner = registered.json()["user"]
|
|
owner_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": "tenant-email-scope",
|
|
"X-User-Id": owner["id"],
|
|
}
|
|
admin_response = client.post(
|
|
"/api/v1/identity/users",
|
|
headers=owner_headers,
|
|
json={
|
|
"email": "admin@email-scope.example",
|
|
"display_name": "Workspace Admin",
|
|
"role": "admin",
|
|
"status": "active",
|
|
},
|
|
)
|
|
assert admin_response.status_code == 200
|
|
workspace_admin = admin_response.json()
|
|
demoted = client.patch(
|
|
f"/api/v1/identity/users/{owner['id']}",
|
|
headers=owner_headers,
|
|
json={"role": "business"},
|
|
)
|
|
assert demoted.status_code == 200
|
|
admin_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": "tenant-email-scope",
|
|
"X-User-Id": workspace_admin["id"],
|
|
}
|
|
|
|
forbidden_send = client.post(
|
|
"/api/v1/identity/email-verification/send",
|
|
headers=owner_headers,
|
|
json={"email": workspace_admin["email"]},
|
|
)
|
|
foreign_workspace_send = client.post(
|
|
"/api/v1/identity/email-verification/send",
|
|
headers=owner_headers,
|
|
json={"tenant_id": "default", "email": owner["email"]},
|
|
)
|
|
assert forbidden_send.status_code == 403
|
|
assert foreign_workspace_send.status_code == 403
|
|
|
|
admin_managed = client.post(
|
|
"/api/v1/identity/email-verification/send",
|
|
headers=admin_headers,
|
|
json={"email": owner["email"]},
|
|
)
|
|
assert admin_managed.status_code == 200
|
|
admin_confirmed = client.post(
|
|
"/api/v1/identity/email-verification/confirm",
|
|
headers=admin_headers,
|
|
json={
|
|
"challenge_id": admin_managed.json()["id"],
|
|
"code": admin_managed.json()["dev_code"],
|
|
},
|
|
)
|
|
assert admin_confirmed.status_code == 200
|
|
assert admin_confirmed.json()["session"] is None
|
|
|
|
self_challenge = client.post(
|
|
"/api/v1/identity/email-verification/send",
|
|
headers=owner_headers,
|
|
json={"email": owner["email"]},
|
|
)
|
|
assert self_challenge.status_code == 200
|
|
self_confirmed = client.post(
|
|
"/api/v1/identity/email-verification/confirm",
|
|
headers=owner_headers,
|
|
json={
|
|
"challenge_id": self_challenge.json()["id"],
|
|
"code": self_challenge.json()["dev_code"],
|
|
},
|
|
)
|
|
assert self_confirmed.status_code == 200
|
|
refreshed_session = self_confirmed.json()["session"]
|
|
assert refreshed_session["user"]["id"] == owner["id"]
|
|
assert refreshed_session["tenant"]["id"] == "tenant-email-scope"
|
|
assert refreshed_session["global_account"]["email_verified_at"] is not None
|
|
|
|
admin_challenge = client.post(
|
|
"/api/v1/identity/email-verification/send",
|
|
headers=admin_headers,
|
|
json={"email": workspace_admin["email"]},
|
|
)
|
|
assert admin_challenge.status_code == 200
|
|
cross_account_confirm = client.post(
|
|
"/api/v1/identity/email-verification/confirm",
|
|
headers=owner_headers,
|
|
json={
|
|
"challenge_id": admin_challenge.json()["id"],
|
|
"code": admin_challenge.json()["dev_code"],
|
|
},
|
|
)
|
|
assert cross_account_confirm.status_code == 403
|
|
admin_self_confirmed = client.post(
|
|
"/api/v1/identity/email-verification/confirm",
|
|
headers=admin_headers,
|
|
json={
|
|
"challenge_id": admin_challenge.json()["id"],
|
|
"code": admin_challenge.json()["dev_code"],
|
|
},
|
|
)
|
|
assert admin_self_confirmed.status_code == 200
|
|
|
|
|
|
def test_platform_admin_api_uses_explicit_permission_claim(tmp_path: Path) -> None:
|
|
client = TestClient(create_app(make_settings(tmp_path)))
|
|
proxy_headers = {"X-Contract-Proxy-Key": AUTH_VALUE, "X-Tenant-Id": "default"}
|
|
|
|
explicit_admin = client.post(
|
|
"/api/v1/identity/tenants",
|
|
headers={**proxy_headers, "X-User-Id": "user-current-admin"},
|
|
json={"id": "tenant-explicit-platform-admin", "name": "Explicit Admin Co"},
|
|
)
|
|
legacy_id_only = client.post(
|
|
"/api/v1/identity/tenants",
|
|
headers={**proxy_headers, "X-User-Id": "current-admin"},
|
|
json={"id": "tenant-legacy-id-only", "name": "Legacy ID Co"},
|
|
)
|
|
|
|
assert explicit_admin.status_code == 200
|
|
assert legacy_id_only.status_code == 403
|
|
|
|
|
|
def test_platform_admin_roster_and_final_admin_guard(tmp_path: Path) -> None:
|
|
app = create_app(make_settings(tmp_path))
|
|
identity_repository = app.state.contract_repository.identity
|
|
second_admin = identity_repository.bootstrap_admin_credential(
|
|
tenant_id="default",
|
|
company_name="Platform Operations",
|
|
email="second-platform-admin@example.com",
|
|
display_name="Second Platform Admin",
|
|
password="SecondAdminPassword123", # pragma: allowlist secret
|
|
)
|
|
assert second_admin.status == "created"
|
|
|
|
client = TestClient(app)
|
|
proxy_headers = {"X-Contract-Proxy-Key": AUTH_VALUE}
|
|
first_admin_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": "user-current-admin",
|
|
}
|
|
second_admin_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": "default",
|
|
"X-User-Id": second_admin.user_id,
|
|
}
|
|
|
|
roster = client.get(
|
|
"/api/v1/identity/platform-administrators",
|
|
headers=first_admin_headers,
|
|
)
|
|
assert roster.status_code == 200
|
|
assert roster.json()["total"] == 2
|
|
assert roster.json()["active_total"] == 2
|
|
assert {item["user_id"] for item in roster.json()["items"]} == {
|
|
"user-current-admin",
|
|
second_admin.user_id,
|
|
}
|
|
assert all("password" not in item and "credential" not in item for item in roster.json()["items"])
|
|
|
|
tenant_admin_session = client.post(
|
|
"/api/v1/identity/register-company",
|
|
headers={**proxy_headers, "X-Tenant-Id": "default", "X-User-Id": "user-current-admin"},
|
|
json={
|
|
"tenant_id": "tenant-platform-boundary",
|
|
"company_name": "Tenant Boundary Co",
|
|
"admin_email": "owner@tenant-boundary.example",
|
|
"admin_display_name": "Tenant Boundary Owner",
|
|
"admin_password": "TenantOwnerPassword123", # pragma: allowlist secret
|
|
"agreement_acceptances": REGISTRATION_AGREEMENT_ACCEPTANCES,
|
|
},
|
|
)
|
|
assert tenant_admin_session.status_code == 200
|
|
tenant_admin = tenant_admin_session.json()["user"]
|
|
tenant_admin_headers = {
|
|
**proxy_headers,
|
|
"X-Tenant-Id": "tenant-platform-boundary",
|
|
"X-User-Id": tenant_admin["id"],
|
|
}
|
|
assert client.get(
|
|
"/api/v1/identity/platform-administrators",
|
|
headers=tenant_admin_headers,
|
|
).status_code == 403
|
|
assert client.patch(
|
|
f"/api/v1/identity/users/{second_admin.user_id}",
|
|
headers=tenant_admin_headers,
|
|
json={"status": "disabled"},
|
|
).status_code == 403
|
|
|
|
first_login_before_rotation = client.post(
|
|
"/api/v1/identity/login",
|
|
headers=proxy_headers,
|
|
json={
|
|
"tenant_id": "default",
|
|
"email": "admin@example.com",
|
|
"password": "admin654321", # pragma: allowlist secret
|
|
},
|
|
)
|
|
second_login_before_rotation = client.post(
|
|
"/api/v1/identity/login",
|
|
headers=proxy_headers,
|
|
json={
|
|
"tenant_id": "default",
|
|
"email": "second-platform-admin@example.com",
|
|
"password": "SecondAdminPassword123", # pragma: allowlist secret
|
|
},
|
|
)
|
|
assert first_login_before_rotation.status_code == 200
|
|
assert second_login_before_rotation.status_code == 200
|
|
|
|
rotated = identity_repository.bootstrap_admin_credential(
|
|
tenant_id="default",
|
|
company_name="Platform Operations",
|
|
email="second-platform-admin@example.com",
|
|
display_name="Second Platform Admin",
|
|
password="SecondAdminRotated123", # pragma: allowlist secret
|
|
force_rotate=True,
|
|
)
|
|
assert rotated.status == "rotated"
|
|
assert client.post(
|
|
"/api/v1/identity/login",
|
|
headers=proxy_headers,
|
|
json={
|
|
"tenant_id": "default",
|
|
"email": "second-platform-admin@example.com",
|
|
"password": "SecondAdminPassword123", # pragma: allowlist secret
|
|
},
|
|
).status_code == 401
|
|
assert client.post(
|
|
"/api/v1/identity/login",
|
|
headers=proxy_headers,
|
|
json={
|
|
"tenant_id": "default",
|
|
"email": "second-platform-admin@example.com",
|
|
"password": "SecondAdminRotated123", # pragma: allowlist secret
|
|
},
|
|
).status_code == 200
|
|
assert client.post(
|
|
"/api/v1/identity/login",
|
|
headers=proxy_headers,
|
|
json={
|
|
"tenant_id": "default",
|
|
"email": "admin@example.com",
|
|
"password": "admin654321", # pragma: allowlist secret
|
|
},
|
|
).status_code == 200
|
|
|
|
disabled = client.patch(
|
|
f"/api/v1/identity/users/{second_admin.user_id}",
|
|
headers=first_admin_headers,
|
|
json={"status": "disabled", "updated_by": "user-current-admin"},
|
|
)
|
|
assert disabled.status_code == 200
|
|
assert disabled.json()["status"] == "disabled"
|
|
|
|
lost_platform_access = client.post(
|
|
"/api/v1/identity/tenants",
|
|
headers=second_admin_headers,
|
|
json={"id": "tenant-disabled-admin", "name": "Disabled Admin Tenant"},
|
|
)
|
|
assert lost_platform_access.status_code == 403
|
|
|
|
final_admin_deactivation = client.patch(
|
|
"/api/v1/identity/users/user-current-admin",
|
|
headers=first_admin_headers,
|
|
json={"status": "disabled", "updated_by": "user-current-admin"},
|
|
)
|
|
assert final_admin_deactivation.status_code == 409
|
|
assert final_admin_deactivation.json() == {
|
|
"detail": "The final active platform administrator cannot be disabled."
|
|
}
|
|
|
|
final_roster = client.get(
|
|
"/api/v1/identity/platform-administrators",
|
|
headers=first_admin_headers,
|
|
)
|
|
assert final_roster.status_code == 200
|
|
assert final_roster.json()["total"] == 2
|
|
assert final_roster.json()["active_total"] == 1
|
|
statuses = {item["user_id"]: item["status"] for item in final_roster.json()["items"]}
|
|
assert statuses == {
|
|
"user-current-admin": "active",
|
|
second_admin.user_id: "disabled",
|
|
}
|
|
|
|
|
|
def test_in_memory_identity_repository_protects_final_platform_admin() -> None:
|
|
identity_repository = InMemoryIdentityRepository()
|
|
|
|
roster = identity_repository.list_platform_administrators()
|
|
assert roster.total == roster.active_total == 1
|
|
assert roster.items[0].user_id == "user-current-admin"
|
|
|
|
with pytest.raises(
|
|
LastPlatformAdministratorError,
|
|
match="final active platform administrator",
|
|
):
|
|
identity_repository.update_user(
|
|
"user-current-admin",
|
|
TenantUserUpdateRequest(status="disabled"),
|
|
)
|
|
|
|
assert identity_repository.has_platform_permission(
|
|
tenant_id="default",
|
|
user_id="user-current-admin",
|
|
)
|