Phase B addresses 7 high-severity gaps from the permissions audit: - B1: Enforce tree access check on session start via can_access_tree - B2: Replace all inline permission helpers with centralized permissions.py - B3: Fix require_engineer_or_admin to check is_team_admin before role - B4: Add is_active field on User with enforcement in get_current_active_user - B5: Add admin user management endpoints (list, get, role, team-admin, deactivate, activate) - B6: Add rate limiting on auth/invite endpoints via slowapi (disabled in DEBUG) - B7: Implement refresh token rotation with JTI-based revocation and meaningful logout Also reduces access token TTL from 15 to 5 minutes and updates CLAUDE.md with SaaS/MSP context for future planning sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
"""Integration tests for admin user management endpoints."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestAdminEndpoints:
|
|
"""Test suite for admin user management endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_users_as_admin(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test listing users as a super admin."""
|
|
response = await client.get(
|
|
"/api/v1/admin/users", headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
users = response.json()
|
|
assert len(users) >= 2 # admin + test_user
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_users_as_non_admin(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test that non-admin users cannot list users."""
|
|
response = await client.get(
|
|
"/api/v1/admin/users", headers=auth_headers
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_user_as_admin(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test getting user details as admin."""
|
|
user_id = test_user["user_data"]["id"]
|
|
response = await client.get(
|
|
f"/api/v1/admin/users/{user_id}", headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email"] == test_user["email"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_change_user_role(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test changing a user's role to viewer."""
|
|
user_id = test_user["user_data"]["id"]
|
|
response = await client.put(
|
|
f"/api/v1/admin/users/{user_id}/role",
|
|
json={"role": "viewer"},
|
|
headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["role"] == "viewer"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_change_role_invalid(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test that invalid role values are rejected."""
|
|
user_id = test_user["user_data"]["id"]
|
|
response = await client.put(
|
|
f"/api/v1/admin/users/{user_id}/role",
|
|
json={"role": "admin"},
|
|
headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cannot_change_own_role(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_admin: dict
|
|
):
|
|
"""Test that admin cannot change their own role."""
|
|
admin_id = test_admin["user_data"]["id"]
|
|
response = await client.put(
|
|
f"/api/v1/admin/users/{admin_id}/role",
|
|
json={"role": "viewer"},
|
|
headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "own role" in response.json()["detail"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deactivate_user(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test deactivating a user."""
|
|
user_id = test_user["user_data"]["id"]
|
|
response = await client.put(
|
|
f"/api/v1/admin/users/{user_id}/deactivate",
|
|
headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["is_active"] is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_activate_user(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_user: dict
|
|
):
|
|
"""Test reactivating a user."""
|
|
user_id = test_user["user_data"]["id"]
|
|
# Deactivate first
|
|
await client.put(
|
|
f"/api/v1/admin/users/{user_id}/deactivate",
|
|
headers=admin_auth_headers
|
|
)
|
|
# Then reactivate
|
|
response = await client.put(
|
|
f"/api/v1/admin/users/{user_id}/activate",
|
|
headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["is_active"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cannot_deactivate_self(
|
|
self, client: AsyncClient, admin_auth_headers: dict, test_admin: dict
|
|
):
|
|
"""Test that admin cannot deactivate themselves."""
|
|
admin_id = test_admin["user_data"]["id"]
|
|
response = await client.put(
|
|
f"/api/v1/admin/users/{admin_id}/deactivate",
|
|
headers=admin_auth_headers
|
|
)
|
|
assert response.status_code == 400
|
|
assert "own account" in response.json()["detail"].lower()
|