Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/gateway/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
rag,
run,
template_wizard,
teams,
tracing,
users,
webhook,
Expand All @@ -36,6 +37,10 @@
# 예: api_router.include_router(user.router, prefix="/users", tags=["users"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(teams.router, prefix="/teams", tags=["teams"])
api_router.include_router(
teams.permissions_router, prefix="/permissions", tags=["permissions"]
)
api_router.include_router(llm.router, prefix="/llm", tags=["llm"])
api_router.include_router(
prompt_wizard.router, prefix="/prompt-wizard", tags=["prompt-wizard"]
Expand Down
54 changes: 53 additions & 1 deletion apps/gateway/api/v1/endpoints/deployment.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import uuid
from typing import List

from fastapi import APIRouter, Depends, Response
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy.orm import Session

from apps.gateway.auth.dependencies import get_current_user
from apps.gateway.auth.permissions import ensure_workflow_permission
from apps.gateway.utils.audit import audit
from apps.gateway.services.deployment_service import DeploymentService
from apps.shared.audit.actions import AuditAction
from apps.shared.db.models.app import App
from apps.shared.db.models.user import User
from apps.shared.db.models.workflow_deployment import WorkflowDeployment
from apps.shared.db.session import get_db
from apps.shared.schemas.deployment import DeploymentCreate, DeploymentResponse

router = APIRouter()


def _deployment_workflow_id(db: Session, deployment_id: str):
deployment = (
db.query(WorkflowDeployment)
.filter(WorkflowDeployment.id == deployment_id)
.first()
)
if not deployment:
raise HTTPException(status_code=404, detail="Deployment not found")
app = db.query(App).filter(App.id == deployment.app_id).first()
if not app or not app.workflow_id:
raise HTTPException(status_code=404, detail="Workflow not found")
return app.workflow_id


@router.post("", response_model=DeploymentResponse)
@audit(AuditAction.WORKFLOW_DEPLOY)
def create_deployment(
Expand All @@ -25,6 +43,10 @@ def create_deployment(
워크플로우를 배포합니다.
[TEST] bugfix/KAN-000, gateway 배포를 위해 주석 추가
"""
app = db.query(App).filter(App.id == deployment_in.app_id).first()
if not app or not app.workflow_id:
raise HTTPException(status_code=404, detail="App not found")
ensure_workflow_permission(db, current_user, app.workflow_id, "deploy")
return DeploymentService.create_deployment(db, deployment_in, current_user.id)


Expand All @@ -41,6 +63,30 @@ def get_deployments(
특정 앱의 배포 이력을 조회합니다.
app_id 또는 workflow_id 중 하나는 필수입니다.
"""
target_workflow_id = workflow_id
if app_id:
app = db.query(App).filter(App.id == app_id).first()
Comment on lines +66 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Authorize the app_id that is actually listed

When both app_id and workflow_id are supplied, this check authorizes only the supplied workflow_id because the app lookup is skipped, while DeploymentService.list_deployments still returns deployments for app_id when it is present. A caller with read access to any workflow can therefore pass that workflow_id together with another app's id and read that app's deployment graph snapshots; resolve the app's workflow and reject mismatches or ignore app_id in this case.

Useful? React with 👍 / 👎.

if not app:
return []
if not app.workflow_id:
return []
target_workflow_id = app.workflow_id
ensure_workflow_permission(db, current_user, target_workflow_id, "read")
if workflow_id:
try:
supplied_workflow_id = uuid.UUID(str(workflow_id))
except (TypeError, ValueError):
raise HTTPException(
status_code=400, detail="app_id does not match workflow_id"
)
if uuid.UUID(str(target_workflow_id)) != supplied_workflow_id:
raise HTTPException(
status_code=400, detail="app_id does not match workflow_id"
)
elif target_workflow_id:
ensure_workflow_permission(db, current_user, target_workflow_id, "read")
else:
return []
return DeploymentService.list_deployments(
db,
app_id=app_id,
Expand Down Expand Up @@ -73,6 +119,8 @@ def get_deployment(
"""
특정 배포 ID의 상세 정보를 조회합니다.
"""
workflow_id = _deployment_workflow_id(db, deployment_id)
ensure_workflow_permission(db, current_user, workflow_id, "read")
return DeploymentService.get_deployment(db, deployment_id)


Expand Down Expand Up @@ -145,6 +193,8 @@ def toggle_deployment(
"""
from apps.gateway.services.scheduler_service import get_scheduler_service

workflow_id = _deployment_workflow_id(db, deployment_id)
ensure_workflow_permission(db, current_user, workflow_id, "deploy")
scheduler = get_scheduler_service()
return DeploymentService.toggle_deployment(db, deployment_id, scheduler)

Expand All @@ -161,5 +211,7 @@ def delete_deployment(
"""
from apps.gateway.services.scheduler_service import get_scheduler_service

workflow_id = _deployment_workflow_id(db, deployment_id)
ensure_workflow_permission(db, current_user, workflow_id, "manage")
scheduler = get_scheduler_service()
return DeploymentService.delete_deployment(db, deployment_id, scheduler)
28 changes: 25 additions & 3 deletions apps/gateway/api/v1/endpoints/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlalchemy.orm import Session

from apps.gateway.auth.dependencies import get_current_user
from apps.gateway.auth.permissions import ensure_llm_credential_permission
from apps.gateway.utils.audit import audit
from apps.gateway.services.llm_service import LLMService
from apps.shared.audit.actions import AuditAction
Expand All @@ -20,14 +21,23 @@
LLMModelResponse,
LLMProviderResponse,
)
from apps.shared.services.tracing.access import TraceAccessService

router = APIRouter()


def _require_system_admin(db: Session, current_user: User):
if not TraceAccessService.is_system_admin(db, current_user):
raise HTTPException(status_code=403, detail="system_admin_required")

# --- Providers (System) ---


@router.get("/providers", response_model=List[LLMProviderResponse])
def get_system_providers(db: Session = Depends(get_db)):
def get_system_providers(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
List all system-defined LLM providers and their models.
"""
Expand Down Expand Up @@ -91,6 +101,10 @@ def register_credential(
"""
try:
return LLMService.register_credential(db, current_user.id, request)
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc))
except HTTPException:
raise
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
Expand All @@ -108,10 +122,13 @@ def delete_credential(
Delete a user credential.
"""
try:
ensure_llm_credential_permission(db, current_user, credential_id, "write")
deleted = LLMService.delete_credential(db, credential_id, current_user.id)
if not deleted:
raise HTTPException(status_code=404, detail="Credential not found")
return {"message": "Credential deleted", "id": str(credential_id)}
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

Expand All @@ -127,9 +144,12 @@ def sync_credential_models(
해당 크리덴셜 기준으로 모델 매핑을 재동기화합니다.
"""
try:
ensure_llm_credential_permission(db, current_user, credential_id, "write")
return LLMService.sync_credential_models(
db, current_user.id, credential_id, purge_unverified=purge_unverified
)
except HTTPException:
raise
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc))
except Exception as exc:
Expand Down Expand Up @@ -199,12 +219,13 @@ def get_top_expensive_models(
@router.post("/models/sync-pricing")
def sync_system_pricing(
db: Session = Depends(get_db),
# Optional: Admin only
current_user: User = Depends(get_current_user),
):
"""
[Admin] Sync all DB models with hardcoded system prices.
Useful when system price list is updated.
"""
_require_system_admin(db, current_user)
try:
result = LLMService.sync_system_prices(db)
return result
Expand All @@ -218,11 +239,12 @@ def update_model_pricing(
model_id: UUID,
pricing: LLMModelPricingUpdate,
db: Session = Depends(get_db),
# Optional: Admin only
current_user: User = Depends(get_current_user),
):
"""
[Admin] Manually update pricing for a specific model.
"""
_require_system_admin(db, current_user)
try:
model = LLMService.update_model_pricing(
db, model_id, pricing.input_price_1k, pricing.output_price_1k
Expand Down
Loading