fix: resolve download 404 caused by file UUID / Celery task ID mismatch\n\nThe download route checked access using the file UUID from the URL,\nbut the session and usage_events only stored the Celery task ID.\nThese are different UUIDs, causing all downloads to return 404.\n\nFixes:\n- Add has_download_access() to check file_history table as fallback\n- Update assert_web/api_task_access to use file_history lookup\n- Remember file UUID in session when task status returns SUCCESS"

This commit is contained in:
Your Name
2026-03-20 10:07:48 +02:00
parent 94b23e511e
commit 0174f935c3
3 changed files with 39 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ from app.services.policy_service import (
resolve_api_actor,
resolve_web_actor,
)
from app.utils.auth import remember_task_access
tasks_bp = Blueprint("tasks", __name__)
@@ -52,6 +53,17 @@ def get_task_status(task_id: str):
task_result = result.result or {}
response["result"] = task_result
# Remember the file UUID in the session so the download route can verify access.
# The download URL contains a different UUID than the Celery task ID.
download_url = task_result.get("download_url", "")
if download_url:
parts = download_url.split("/")
# URL format: /api/download/<file_uuid>/<filename>
if len(parts) >= 4:
file_uuid = parts[3]
if file_uuid != task_id:
remember_task_access(file_uuid)
elif result.state == "FAILURE":
response["error"] = str(result.info) if result.info else "Task failed."

View File

@@ -678,6 +678,23 @@ def has_task_access(user_id: int, source: str, task_id: str) -> bool:
return row is not None
def has_download_access(user_id: int, file_task_id: str) -> bool:
"""Return whether one user owns a file_history entry whose download_url contains the given file task id."""
pattern = f"/api/download/{file_task_id}/"
with _connect() as conn:
row = conn.execute(
"""
SELECT 1
FROM file_history
WHERE user_id = ? AND download_url LIKE ?
LIMIT 1
""",
(user_id, f"%{pattern}%"),
).fetchone()
return row is not None
# ---------------------------------------------------------------------------
# Password reset tokens
# ---------------------------------------------------------------------------

View File

@@ -8,6 +8,7 @@ from app.services.account_service import (
get_api_key_actor,
get_user_by_id,
get_current_period_month,
has_download_access,
has_task_access,
normalize_plan,
record_usage_event,
@@ -227,7 +228,12 @@ def build_task_tracking_kwargs(actor: ActorContext) -> dict:
def assert_api_task_access(actor: ActorContext, task_id: str):
"""Ensure one API actor can poll one task id."""
if actor.user_id is None or not has_task_access(actor.user_id, "api", task_id):
if actor.user_id is None:
raise PolicyError("Task not found.", 404)
if has_task_access(actor.user_id, "api", task_id):
return
if has_download_access(actor.user_id, task_id):
return
raise PolicyError("Task not found.", 404)
@@ -236,6 +242,9 @@ def assert_web_task_access(actor: ActorContext, task_id: str):
if actor.user_id is not None and has_task_access(actor.user_id, "web", task_id):
return
if actor.user_id is not None and has_download_access(actor.user_id, task_id):
return
if has_session_task_access(task_id):
return