mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 16:13:23 +00:00
Serve artifact metadata and bytes over HTTP with parent-derived authorization: conversation-parented artifacts inherit conversation access (owner, shared_with, or a public share token whose conversation matches the parent), workflow-run artifacts check run ownership, and access fails closed when the parent is missing or deleted. Adds list/get/versions/download/restore routes, an authenticated storage-agnostic download (sanitized Content-Disposition, 302 to a short-lived private S3 presigned URL when that strategy is configured), a generate_presigned_url primitive on the storage base and S3 backend, and generalizes the tools artifact endpoint for documents/files. Shared authorization helpers live in a dedicated module used by both surfaces.
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Parent-derived authorization helpers for artifact access."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from flask import request
|
|
|
|
from application.storage.db.repositories.agents import AgentsRepository
|
|
from application.storage.db.repositories.conversations import ConversationsRepository
|
|
from application.storage.db.repositories.shared_conversations import (
|
|
SharedConversationsRepository,
|
|
)
|
|
from application.storage.db.repositories.workflow_runs import WorkflowRunsRepository
|
|
from application.storage.db.session import db_readonly
|
|
|
|
|
|
def resolve_authenticated_user() -> Optional[str]:
|
|
"""Resolve the caller to a user id via decoded JWT ``sub`` or api_key→owner."""
|
|
decoded_token = getattr(request, "decoded_token", None)
|
|
if decoded_token:
|
|
return decoded_token.get("sub")
|
|
|
|
api_key = request.args.get("api_key") or request.form.get("api_key")
|
|
if api_key:
|
|
with db_readonly() as conn:
|
|
agent = AgentsRepository(conn).find_by_key(api_key)
|
|
if agent:
|
|
return agent.get("user_id")
|
|
return None
|
|
|
|
|
|
def user_can_access_conversation(
|
|
conn, conversation_id: str, user_id: Optional[str], share_token: Optional[str]
|
|
) -> bool:
|
|
"""Allow if the caller owns/shares the conversation, or holds a valid share token.
|
|
|
|
Reuses ``ConversationsRepository.get`` (owner OR ``shared_with``) so artifact
|
|
access tracks message access; a publicly shared link inherits download access
|
|
via its share token (see ``SharedConversationsRepository.find_by_uuid``).
|
|
"""
|
|
if user_id:
|
|
if ConversationsRepository(conn).get(conversation_id, user_id) is not None:
|
|
return True
|
|
if share_token:
|
|
shared = SharedConversationsRepository(conn).find_by_uuid(share_token)
|
|
if shared and str(shared.get("conversation_id")) == str(conversation_id):
|
|
return True
|
|
return False
|
|
|
|
|
|
def authorize_artifact(conn, artifact: dict, user_id: Optional[str]) -> bool:
|
|
"""Authorize an artifact by resolving its parent; missing parent fails closed."""
|
|
conversation_id = artifact.get("conversation_id")
|
|
workflow_run_id = artifact.get("workflow_run_id")
|
|
share_token = request.args.get("share_token")
|
|
|
|
if conversation_id is not None:
|
|
return user_can_access_conversation(
|
|
conn, str(conversation_id), user_id, share_token
|
|
)
|
|
if workflow_run_id is not None:
|
|
if not user_id:
|
|
return False
|
|
run = WorkflowRunsRepository(conn).get(str(workflow_run_id))
|
|
return run is not None and run.get("user_id") == user_id
|
|
# No parent row reachable -> deny (e.g. deleted conversation/run).
|
|
return False
|