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

@@ -17,7 +17,8 @@ from app.schemas.folder import (
FolderReorderRequest,
FolderTreeRequest
)
from app.api.deps import get_current_user
from app.api.deps import get_current_active_user
from app.core.permissions import can_access_tree
router = APIRouter(prefix="/folders", tags=["folders"])
@@ -63,30 +64,10 @@ async def is_descendant(db: AsyncSession, potential_descendant_id: UUID, ancesto
return False
def can_access_tree(user: User, tree: Tree) -> bool:
"""Check if user can access a tree (to add to folder).
User can access tree if:
- Tree is public
- User is the author
- Tree belongs to user's team
- User is a global admin
"""
if tree.is_public:
return True
if user.id == tree.author_id:
return True
if tree.team_id == user.team_id and user.team_id is not None:
return True
if user.is_super_admin:
return True
return False
@router.get("", response_model=list[FolderListResponse])
async def list_folders(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""List all folders for the current user.
@@ -120,7 +101,7 @@ async def list_folders(
async def get_folder(
folder_id: UUID,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Get a specific folder by ID."""
result = await db.execute(
@@ -160,7 +141,7 @@ async def get_folder(
async def create_folder(
folder_data: FolderCreate,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Create a new folder for the current user.
@@ -241,7 +222,7 @@ async def update_folder(
folder_id: UUID,
folder_data: FolderUpdate,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Update a folder.
@@ -352,7 +333,7 @@ async def update_folder(
async def delete_folder(
folder_id: UUID,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Delete a folder.
@@ -384,7 +365,7 @@ async def delete_folder(
async def reorder_folders(
reorder_data: FolderReorderRequest,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Reorder folders by providing folder IDs in desired order."""
# Get all user's folders
@@ -414,7 +395,7 @@ async def add_tree_to_folder(
folder_id: UUID,
request: FolderTreeRequest,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Add a tree to a folder."""
# Get folder with trees
@@ -474,7 +455,7 @@ async def remove_tree_from_folder(
folder_id: UUID,
tree_id: UUID,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Remove a tree from a folder."""
# Get folder with trees
@@ -519,7 +500,7 @@ async def remove_tree_from_folder(
async def get_folder_tree_ids(
folder_id: UUID,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)]
current_user: Annotated[User, Depends(get_current_active_user)]
):
"""Get all tree IDs in a folder.