fix: enforce calendar role permissions
This commit is contained in:
@@ -62,10 +62,52 @@ from app.services.slot_immutability import (
|
|||||||
guard_plan_cancel_no_past_retroaction,
|
guard_plan_cancel_no_past_retroaction,
|
||||||
guard_plan_edit_no_past_retroaction,
|
guard_plan_edit_no_past_retroaction,
|
||||||
)
|
)
|
||||||
|
from app.models.role_permission import Permission, RolePermission
|
||||||
|
|
||||||
router = APIRouter(prefix="/calendar", tags=["Calendar"])
|
router = APIRouter(prefix="/calendar", tags=["Calendar"])
|
||||||
|
|
||||||
|
|
||||||
|
def _has_global_permission(db: Session, user: User, permission_name: str) -> bool:
|
||||||
|
if user.is_admin:
|
||||||
|
return True
|
||||||
|
if not user.role_id:
|
||||||
|
return False
|
||||||
|
perm = db.query(Permission).filter(Permission.name == permission_name).first()
|
||||||
|
if not perm:
|
||||||
|
return False
|
||||||
|
return db.query(RolePermission).filter(
|
||||||
|
RolePermission.role_id == user.role_id,
|
||||||
|
RolePermission.permission_id == perm.id,
|
||||||
|
).first() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _require_calendar_permission(db: Session, user: User, permission_name: str) -> User:
|
||||||
|
if _has_global_permission(db, user, permission_name):
|
||||||
|
return user
|
||||||
|
raise HTTPException(status_code=403, detail=f"Calendar permission '{permission_name}' required")
|
||||||
|
|
||||||
|
|
||||||
|
def require_calendar_read(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
return _require_calendar_permission(db, current_user, "calendar.read")
|
||||||
|
|
||||||
|
|
||||||
|
def require_calendar_write(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
return _require_calendar_permission(db, current_user, "calendar.write")
|
||||||
|
|
||||||
|
|
||||||
|
def require_calendar_manage(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
return _require_calendar_permission(db, current_user, "calendar.manage")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# TimeSlot creation (BE-CAL-API-001)
|
# TimeSlot creation (BE-CAL-API-001)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -101,7 +143,7 @@ def _slot_to_response(slot: TimeSlot) -> TimeSlotResponse:
|
|||||||
def create_slot(
|
def create_slot(
|
||||||
payload: TimeSlotCreate,
|
payload: TimeSlotCreate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Create a one-off calendar slot.
|
"""Create a one-off calendar slot.
|
||||||
|
|
||||||
@@ -230,7 +272,7 @@ def _virtual_slot_to_item(vs: dict) -> CalendarSlotItem:
|
|||||||
def get_calendar_day(
|
def get_calendar_day(
|
||||||
date: Optional[date_type] = Query(None, description="Target date (defaults to today)"),
|
date: Optional[date_type] = Query(None, description="Target date (defaults to today)"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_read),
|
||||||
):
|
):
|
||||||
"""Return all calendar slots for the authenticated user on the given date.
|
"""Return all calendar slots for the authenticated user on the given date.
|
||||||
|
|
||||||
@@ -301,7 +343,7 @@ def edit_real_slot(
|
|||||||
slot_id: int,
|
slot_id: int,
|
||||||
payload: TimeSlotEdit,
|
payload: TimeSlotEdit,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Edit an existing real (materialized) slot.
|
"""Edit an existing real (materialized) slot.
|
||||||
|
|
||||||
@@ -380,7 +422,7 @@ def edit_virtual_slot(
|
|||||||
virtual_id: str,
|
virtual_id: str,
|
||||||
payload: TimeSlotEdit,
|
payload: TimeSlotEdit,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Edit a virtual (plan-generated) slot.
|
"""Edit a virtual (plan-generated) slot.
|
||||||
|
|
||||||
@@ -469,7 +511,7 @@ def edit_virtual_slot(
|
|||||||
def cancel_real_slot(
|
def cancel_real_slot(
|
||||||
slot_id: int,
|
slot_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Cancel an existing real (materialized) slot.
|
"""Cancel an existing real (materialized) slot.
|
||||||
|
|
||||||
@@ -516,7 +558,7 @@ def cancel_real_slot(
|
|||||||
def cancel_virtual_slot(
|
def cancel_virtual_slot(
|
||||||
virtual_id: str,
|
virtual_id: str,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Cancel a virtual (plan-generated) slot.
|
"""Cancel a virtual (plan-generated) slot.
|
||||||
|
|
||||||
@@ -596,7 +638,7 @@ def _plan_to_response(plan: SchedulePlan) -> SchedulePlanResponse:
|
|||||||
def create_plan(
|
def create_plan(
|
||||||
payload: SchedulePlanCreate,
|
payload: SchedulePlanCreate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Create a new recurring schedule plan.
|
"""Create a new recurring schedule plan.
|
||||||
|
|
||||||
@@ -632,7 +674,7 @@ def create_plan(
|
|||||||
def list_plans(
|
def list_plans(
|
||||||
include_inactive: bool = Query(False, description="Include cancelled/inactive plans"),
|
include_inactive: bool = Query(False, description="Include cancelled/inactive plans"),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_read),
|
||||||
):
|
):
|
||||||
"""Return all schedule plans for the authenticated user.
|
"""Return all schedule plans for the authenticated user.
|
||||||
|
|
||||||
@@ -658,7 +700,7 @@ def list_plans(
|
|||||||
def get_plan(
|
def get_plan(
|
||||||
plan_id: int,
|
plan_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_read),
|
||||||
):
|
):
|
||||||
"""Return a single schedule plan owned by the authenticated user."""
|
"""Return a single schedule plan owned by the authenticated user."""
|
||||||
plan = (
|
plan = (
|
||||||
@@ -705,7 +747,7 @@ def edit_plan(
|
|||||||
plan_id: int,
|
plan_id: int,
|
||||||
payload: SchedulePlanEdit,
|
payload: SchedulePlanEdit,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Edit an existing schedule plan.
|
"""Edit an existing schedule plan.
|
||||||
|
|
||||||
@@ -792,7 +834,7 @@ def edit_plan(
|
|||||||
def cancel_plan(
|
def cancel_plan(
|
||||||
plan_id: int,
|
plan_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_write),
|
||||||
):
|
):
|
||||||
"""Cancel (soft-delete) a schedule plan.
|
"""Cancel (soft-delete) a schedule plan.
|
||||||
|
|
||||||
@@ -859,7 +901,7 @@ _DATE_LIST_EXCLUDED_STATUSES = {SlotStatus.SKIPPED.value, SlotStatus.ABORTED.val
|
|||||||
)
|
)
|
||||||
def list_dates(
|
def list_dates(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_read),
|
||||||
):
|
):
|
||||||
"""Return a sorted list of future dates that have at least one
|
"""Return a sorted list of future dates that have at least one
|
||||||
materialized (real) slot.
|
materialized (real) slot.
|
||||||
@@ -897,7 +939,7 @@ def list_dates(
|
|||||||
)
|
)
|
||||||
def get_my_workload_config(
|
def get_my_workload_config(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_manage),
|
||||||
):
|
):
|
||||||
"""Return the workload thresholds for the authenticated user.
|
"""Return the workload thresholds for the authenticated user.
|
||||||
|
|
||||||
@@ -916,7 +958,7 @@ def get_my_workload_config(
|
|||||||
def put_my_workload_config(
|
def put_my_workload_config(
|
||||||
payload: MinimumWorkloadConfig,
|
payload: MinimumWorkloadConfig,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_manage),
|
||||||
):
|
):
|
||||||
"""Full replacement of the workload configuration."""
|
"""Full replacement of the workload configuration."""
|
||||||
row = replace_workload_config(db, current_user.id, payload)
|
row = replace_workload_config(db, current_user.id, payload)
|
||||||
@@ -933,7 +975,7 @@ def put_my_workload_config(
|
|||||||
def patch_my_workload_config(
|
def patch_my_workload_config(
|
||||||
payload: MinimumWorkloadUpdate,
|
payload: MinimumWorkloadUpdate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(require_calendar_manage),
|
||||||
):
|
):
|
||||||
"""Partial update — only the provided periods are overwritten."""
|
"""Partial update — only the provided periods are overwritten."""
|
||||||
row = upsert_workload_config(db, current_user.id, payload)
|
row = upsert_workload_config(db, current_user.id, payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user