fix: high-severity security hardening (Phase B permissions audit)

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>
This commit is contained in:
chihlasm
2026-02-05 22:44:05 -05:00
parent 3e0fb92012
commit 71ba0b95a5
27 changed files with 743 additions and 229 deletions

View File

@@ -684,3 +684,40 @@ class TestSessions:
content = response.text
assert '<script>' not in content
assert '&lt;script&gt;' in content
@pytest.mark.asyncio
async def test_start_session_on_others_private_tree_forbidden(
self, client: AsyncClient, auth_headers: dict, test_tree: dict
):
"""Test that a user cannot start a session on another user's private tree."""
# Register a second user
await client.post("/api/v1/auth/register", json={
"email": "other@example.com",
"password": "OtherPassword123!",
"name": "Other User"
})
login_resp = await client.post("/api/v1/auth/login/json", json={
"email": "other@example.com",
"password": "OtherPassword123!"
})
other_headers = {"Authorization": f"Bearer {login_resp.json()['access_token']}"}
# test_tree is owned by test_user (not public, not default)
response = await client.post(
"/api/v1/sessions",
json={"tree_id": test_tree["id"]},
headers=other_headers
)
assert response.status_code == 403
@pytest.mark.asyncio
async def test_start_session_super_admin_any_tree(
self, client: AsyncClient, admin_auth_headers: dict, test_tree: dict
):
"""Test that a super admin can start a session on any tree."""
response = await client.post(
"/api/v1/sessions",
json={"tree_id": test_tree["id"]},
headers=admin_auth_headers
)
assert response.status_code == 201