Files
DocsGPT/application/api/user/artifacts/authz.py
T
2026-06-29 19:21:35 +02:00

95 lines
3.7 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
def authorize_artifact_write(conn, artifact: dict, user_id: Optional[str]) -> bool:
"""Authorize a *mutating* artifact operation (e.g. restore).
Stricter than :func:`authorize_artifact`: a write requires an authenticated
owner of the parent. Share links and ``shared_with`` collaborators inherit
read/download access only, so an anonymous (``user_id`` is ``None``) or
share-token-only caller is denied -- they can read an artifact but never
mutate it.
"""
if not user_id:
return False
conversation_id = artifact.get("conversation_id")
workflow_run_id = artifact.get("workflow_run_id")
if conversation_id is not None:
return (
ConversationsRepository(conn).get_owned(str(conversation_id), user_id)
is not None
)
if workflow_run_id is not None:
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