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:
@@ -7,7 +7,8 @@ from sqlalchemy import select, or_, and_, func, desc, Integer, case
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import get_current_user, require_engineer_or_admin
|
||||
from app.api.deps import get_current_active_user, require_engineer_or_admin
|
||||
from app.core.permissions import can_view_step, can_edit_step
|
||||
from app.models.user import User
|
||||
from app.models.step_library import StepLibrary, StepRating
|
||||
from app.models.step_category import StepCategory
|
||||
@@ -25,27 +26,6 @@ from app.schemas.step_library import (
|
||||
router = APIRouter(prefix="/steps", tags=["steps"])
|
||||
|
||||
|
||||
# Permission helpers
|
||||
def can_view_step(user: User, step: StepLibrary) -> bool:
|
||||
"""Check if user can view a step based on visibility."""
|
||||
if step.visibility == 'public':
|
||||
return True
|
||||
if step.visibility == 'private':
|
||||
return step.created_by == user.id
|
||||
if step.visibility == 'team':
|
||||
return step.team_id == user.team_id or user.is_super_admin
|
||||
return False
|
||||
|
||||
|
||||
def can_edit_step(user: User, step: StepLibrary) -> bool:
|
||||
"""Check if user can edit/delete a step."""
|
||||
if user.is_super_admin:
|
||||
return True
|
||||
if user.role == 'viewer':
|
||||
return False
|
||||
return step.created_by == user.id
|
||||
|
||||
|
||||
async def get_step_or_404(
|
||||
step_id: UUID,
|
||||
db: AsyncSession,
|
||||
@@ -99,7 +79,7 @@ async def list_steps(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""List steps with filters and pagination."""
|
||||
query = select(StepLibrary).where(
|
||||
@@ -177,7 +157,7 @@ async def search_steps(
|
||||
q: str = Query(..., min_length=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Full-text search for steps."""
|
||||
# Use PostgreSQL full-text search
|
||||
@@ -229,7 +209,7 @@ async def search_steps(
|
||||
async def get_popular_tags(
|
||||
limit: int = Query(20, ge=1, le=50),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get popular tags with usage counts."""
|
||||
# Use unnest to expand arrays and count occurrences
|
||||
@@ -255,7 +235,7 @@ async def get_popular_tags(
|
||||
async def get_step(
|
||||
step_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get a step by ID."""
|
||||
step = await get_step_or_404(step_id, db, current_user, check_view=True)
|
||||
@@ -374,7 +354,7 @@ async def update_step(
|
||||
step_id: UUID,
|
||||
step_data: StepLibraryUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update a step (owner or admin only)."""
|
||||
step = await get_step_or_404(step_id, db, current_user, check_edit=True)
|
||||
@@ -444,7 +424,7 @@ async def update_step(
|
||||
async def delete_step(
|
||||
step_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Soft delete a step (owner or admin only)."""
|
||||
step = await get_step_or_404(step_id, db, current_user, check_edit=True)
|
||||
@@ -462,7 +442,7 @@ async def rate_step(
|
||||
step_id: UUID,
|
||||
rating_data: StepRatingCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Rate a step (1-5 stars with optional review)."""
|
||||
step = await get_step_or_404(step_id, db, current_user, check_view=True)
|
||||
@@ -516,7 +496,7 @@ async def update_rating(
|
||||
step_id: UUID,
|
||||
rating_data: StepRatingUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Update your rating for a step."""
|
||||
step = await get_step_or_404(step_id, db, current_user, check_view=True)
|
||||
@@ -563,7 +543,7 @@ async def update_rating(
|
||||
async def delete_rating(
|
||||
step_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Delete your rating for a step."""
|
||||
step = await get_step_or_404(step_id, db, current_user, check_view=True)
|
||||
@@ -593,7 +573,7 @@ async def get_reviews(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Get reviews for a step."""
|
||||
await get_step_or_404(step_id, db, current_user, check_view=True)
|
||||
|
||||
Reference in New Issue
Block a user