diff --git a/common_util/common_util_auth.py b/common_util/common_util_auth.py index 11e26c51..e04d7d7c 100644 --- a/common_util/common_util_auth.py +++ b/common_util/common_util_auth.py @@ -175,3 +175,36 @@ async def require_company( if session["company_id"] is None and session["role"] != "SYSTEM_ADMIN": raise HTTPException(status_code=403, detail="회사 연결이 필요합니다.") return session + + +async def require_project_access( + request: Request, session: dict[str, Any] = Depends(verify_session) +) -> dict[str, Any]: + """URL의 `project_id`가 **내 회사 것인지** 대조한다 (2026-09-01 신설). + + 지금까지 B03~B07 라우터는 로그인·회사 소속만 확인하고 URL의 프로젝트가 누구 것인지는 + 보지 않았다. 프로젝트 id만 알면 남의 회사 자료를 읽고 쓸 수 있었다 — 특히 B07 도각 + 저장은 그 회사의 공용 양식을 덮어쓴다. + + `protected_with_company` 목록에 함께 걸어 두면 경로에 `project_id`가 있는 요청만 + 검사하고 나머지는 그대로 지나간다. 검사는 한 곳에서만 돈다. + """ + project_id = request.path_params.get("project_id") + if not project_id: + return session + if session["role"] == "SYSTEM_ADMIN": + return session + + pool = get_db_pool() + async with pool.acquire() as connection, connection.cursor() as cursor: + await cursor.execute( + "SELECT company_id FROM projects WHERE id = %s AND deleted_at IS NULL", + (str(project_id),), + ) + row = await cursor.fetchone() + + if not row: + raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.") + if row[0] != session["company_id"]: + raise HTTPException(status_code=403, detail="다른 회사의 프로젝트입니다.") + return session diff --git a/main.py b/main.py index 243996d5..b3d4cc96 100644 --- a/main.py +++ b/main.py @@ -49,7 +49,11 @@ from B06_Section.B06_Section_Router_Confirm import ( ) from B07_DesignDetail.B07_DesignDetail_Router import router as b07_design_router from B08_Quantity.B08_Quantity_Router import router as b08_quantity_router -from common_util.common_util_auth import require_company, verify_session +from common_util.common_util_auth import ( + require_company, + require_project_access, + verify_session, +) from common_util.common_util_resource_monitor import sample_resources_loop from common_util.common_util_temp_cleanup import cleanup_expired_temp_uploads_loop from config.config_db import close_db_pool, get_db_pool, init_db_pool @@ -366,7 +370,13 @@ app.include_router(a09_security_router) app.include_router(b01_dashboard_router) app.include_router(b02_proj_register_router) protected = [Depends(verify_session)] -protected_with_company = [Depends(verify_session), Depends(require_company)] +# 프로젝트 경로가 붙는 라우터는 그 프로젝트가 내 회사 것인지도 함께 본다 +# (2026-09-01 — 지금까지 id만 알면 남의 회사 자료에 닿았다). +protected_with_company = [ + Depends(verify_session), + Depends(require_company), + Depends(require_project_access), +] app.include_router(b03_file_input_router, dependencies=protected_with_company) app.include_router(b03_temp_upload_router, dependencies=protected) app.include_router(b03_temp_attach_router, dependencies=protected_with_company)