feat: implement RBAC permissions system

Add role-based access control with hierarchy: super_admin > team_admin >
engineer > viewer. Adds is_super_admin boolean to User model (migration 010),
centralized backend permissions module, frontend usePermissions hook, and
UI enforcement (conditional Create/Edit buttons, editor redirect for viewers,
role badge in header). All endpoint admin checks updated from role=="admin"
to is_super_admin.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-05 02:42:44 -05:00
parent d7c5c8c9ce
commit 34daa26a67
20 changed files with 428 additions and 65 deletions

View File

@@ -7,7 +7,7 @@ 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
from app.api.deps import get_current_user, require_engineer_or_admin
from app.models.user import User
from app.models.step_library import StepLibrary, StepRating
from app.models.step_category import StepCategory
@@ -33,14 +33,16 @@ def can_view_step(user: User, step: StepLibrary) -> bool:
if step.visibility == 'private':
return step.created_by == user.id
if step.visibility == 'team':
return step.team_id == user.team_id or user.role == 'admin'
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.role == 'admin':
if user.is_super_admin:
return True
if user.role == 'viewer':
return False
return step.created_by == user.id
@@ -300,9 +302,9 @@ async def get_step(
async def create_step(
step_data: StepLibraryCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
current_user: User = Depends(require_engineer_or_admin)
):
"""Create a new step."""
"""Create a new step (engineers and admins only, not viewers)."""
# Validate category if provided
if step_data.category_id:
cat_result = await db.execute(
@@ -316,7 +318,7 @@ async def create_step(
# Team validation: can only set team_id to own team
team_id = step_data.team_id
if team_id and team_id != current_user.team_id and current_user.role != 'admin':
if team_id and team_id != current_user.team_id and not current_user.is_super_admin:
raise HTTPException(status_code=403, detail="Cannot create step for another team")
step = StepLibrary(