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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user