Merge security/critical-auth-fixes into main
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ from sqlalchemy import func as sqlfunc
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.config import get_db
|
||||
from app.api.deps import get_current_user_or_apikey
|
||||
from app.api.deps import get_current_user_or_apikey, require_admin
|
||||
from app.api.rbac import check_project_role, ensure_can_edit_milestone
|
||||
from app.models import models
|
||||
from app.models.apikey import APIKey
|
||||
@@ -60,7 +60,8 @@ class APIKeyResponse(BaseModel):
|
||||
|
||||
|
||||
@router.post("/api-keys", response_model=APIKeyResponse, status_code=status.HTTP_201_CREATED, tags=["API Keys"])
|
||||
def create_api_key(data: APIKeyCreate, db: Session = Depends(get_db)):
|
||||
def create_api_key(data: APIKeyCreate, db: Session = Depends(get_db),
|
||||
_: models.User = Depends(require_admin)):
|
||||
user = db.query(models.User).filter(models.User.id == data.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -73,15 +74,22 @@ def create_api_key(data: APIKeyCreate, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/api-keys", response_model=List[APIKeyResponse], tags=["API Keys"])
|
||||
def list_api_keys(user_id: int = None, db: Session = Depends(get_db)):
|
||||
def list_api_keys(user_id: int = None, db: Session = Depends(get_db),
|
||||
_: models.User = Depends(require_admin)):
|
||||
query = db.query(APIKey)
|
||||
if user_id:
|
||||
query = query.filter(APIKey.user_id == user_id)
|
||||
return query.all()
|
||||
keys = query.all()
|
||||
# Never expose the full secret on listing; show only a masked prefix.
|
||||
for k in keys:
|
||||
if k.key and len(k.key) > 8:
|
||||
k.key = k.key[:6] + "…" + k.key[-2:]
|
||||
return keys
|
||||
|
||||
|
||||
@router.delete("/api-keys/{key_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["API Keys"])
|
||||
def revoke_api_key(key_id: int, db: Session = Depends(get_db)):
|
||||
def revoke_api_key(key_id: int, db: Session = Depends(get_db),
|
||||
_: models.User = Depends(require_admin)):
|
||||
key_obj = db.query(APIKey).filter(APIKey.id == key_id).first()
|
||||
if not key_obj:
|
||||
raise HTTPException(status_code=404, detail="API key not found")
|
||||
@@ -106,7 +114,8 @@ class ActivityLogResponse(BaseModel):
|
||||
|
||||
@router.get("/activity", response_model=List[ActivityLogResponse], tags=["Activity"])
|
||||
def list_activity(entity_type: str = None, entity_id: int = None, user_id: int = None,
|
||||
limit: int = 50, db: Session = Depends(get_db)):
|
||||
limit: int = 50, db: Session = Depends(get_db),
|
||||
_: models.User = Depends(get_current_user_or_apikey)):
|
||||
query = db.query(ActivityLog)
|
||||
if entity_type:
|
||||
query = query.filter(ActivityLog.entity_type == entity_type)
|
||||
@@ -199,8 +208,10 @@ def update_milestone(milestone_id: str, ms_update: schemas.MilestoneUpdate, db:
|
||||
|
||||
|
||||
@router.delete("/milestones/{milestone_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Milestones"])
|
||||
def delete_milestone(milestone_id: str, db: Session = Depends(get_db)):
|
||||
def delete_milestone(milestone_id: str, db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
ms = _resolve_milestone(db, milestone_id)
|
||||
ensure_can_edit_milestone(db, current_user.id, ms)
|
||||
db.delete(ms)
|
||||
db.commit()
|
||||
return None
|
||||
@@ -322,16 +333,18 @@ class WorkLogResponse(BaseModel):
|
||||
|
||||
|
||||
@router.post("/worklogs", response_model=WorkLogResponse, status_code=status.HTTP_201_CREATED, tags=["Time Tracking"])
|
||||
def create_worklog(wl: WorkLogCreate, db: Session = Depends(get_db)):
|
||||
def create_worklog(wl: WorkLogCreate, db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
task = db.query(Task).filter(Task.id == wl.task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
user = db.query(models.User).filter(models.User.id == wl.user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if wl.hours <= 0:
|
||||
raise HTTPException(status_code=400, detail="Hours must be positive")
|
||||
db_wl = WorkLog(**wl.model_dump())
|
||||
data = wl.model_dump()
|
||||
# Worklogs are always attributed to the caller (non-admins cannot log time for others).
|
||||
if not current_user.is_admin or not data.get("user_id"):
|
||||
data["user_id"] = current_user.id
|
||||
db_wl = WorkLog(**data)
|
||||
db.add(db_wl)
|
||||
db.commit()
|
||||
db.refresh(db_wl)
|
||||
@@ -370,10 +383,13 @@ def task_worklog_summary(task_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.delete("/worklogs/{worklog_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Time Tracking"])
|
||||
def delete_worklog(worklog_id: int, db: Session = Depends(get_db)):
|
||||
def delete_worklog(worklog_id: int, db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
wl = db.query(WorkLog).filter(WorkLog.id == worklog_id).first()
|
||||
if not wl:
|
||||
raise HTTPException(status_code=404, detail="Work log not found")
|
||||
if not current_user.is_admin and wl.user_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Cannot delete another user's worklog")
|
||||
db.delete(wl)
|
||||
db.commit()
|
||||
return None
|
||||
@@ -382,7 +398,8 @@ def delete_worklog(worklog_id: int, db: Session = Depends(get_db)):
|
||||
# ============ Export ============
|
||||
|
||||
@router.get("/export/tasks", tags=["Export"])
|
||||
def export_tasks_csv(project_id: int = None, db: Session = Depends(get_db)):
|
||||
def export_tasks_csv(project_id: int = None, db: Session = Depends(get_db),
|
||||
_: models.User = Depends(get_current_user_or_apikey)):
|
||||
query = db.query(Task)
|
||||
if project_id:
|
||||
query = query.filter(Task.project_id == project_id)
|
||||
|
||||
@@ -576,8 +576,10 @@ def take_task(
|
||||
# ---- Assignment ----
|
||||
|
||||
@router.post("/tasks/{task_code}/assign")
|
||||
def assign_task(task_code: str, assignee_id: int, db: Session = Depends(get_db)):
|
||||
def assign_task(task_code: str, assignee_id: int, db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
task = _resolve_task(db, task_code)
|
||||
ensure_can_edit_task(db, current_user.id, task)
|
||||
user = db.query(models.User).filter(models.User.id == assignee_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -765,7 +767,8 @@ def batch_transition(
|
||||
|
||||
|
||||
@router.post("/tasks/batch/assign")
|
||||
def batch_assign(data: BatchAssign, db: Session = Depends(get_db)):
|
||||
def batch_assign(data: BatchAssign, db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user_or_apikey)):
|
||||
user = db.query(models.User).filter(models.User.id == data.assignee_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Assignee not found")
|
||||
@@ -773,6 +776,7 @@ def batch_assign(data: BatchAssign, db: Session = Depends(get_db)):
|
||||
for task_code in data.task_codes:
|
||||
task = db.query(Task).filter(Task.task_code == task_code).first()
|
||||
if task:
|
||||
ensure_can_edit_task(db, current_user.id, task)
|
||||
task.assignee_id = data.assignee_id
|
||||
updated.append(task.task_code)
|
||||
db.commit()
|
||||
|
||||
@@ -5,11 +5,13 @@ from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import get_db
|
||||
from app.api.deps import require_admin
|
||||
from app.models.webhook import Webhook, WebhookLog
|
||||
from app.schemas.webhook import WebhookCreate, WebhookUpdate, WebhookResponse, WebhookLogResponse
|
||||
from app.services.webhook import fire_webhooks_sync
|
||||
|
||||
router = APIRouter(prefix="/webhooks", tags=["Webhooks"])
|
||||
# Webhook management is admin-only (registration, inspection, retry, logs).
|
||||
router = APIRouter(prefix="/webhooks", tags=["Webhooks"], dependencies=[Depends(require_admin)])
|
||||
|
||||
|
||||
@router.post("", response_model=WebhookResponse, status_code=status.HTTP_201_CREATED)
|
||||
|
||||
Reference in New Issue
Block a user