Files
AgentLab/backend/app/auth/access.py
T

113 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""运行、工件的读权限(§3.12「运行、证据、工件(只读)」那一行)。
关登录、builder 及以上:都能读。开登录后 user 角色只能读:
- 自己发起的运行;
- 自己参与的实例:派给过自己(或自己所在的组)的待办,或者自己回复过的待办所在的运行。派给「任何人」的不算:那是没有
应用绑定的运行(画布、定时任务),user 角色不该因此读到;
- 有授权(use / observe / manage,含 everyone 和自己所在的组)的应用里:
- 视图运行(来源是应用视图、按新口径补算);
- observe、manage 授权另能读全部实例;
- 应用发布版本里作为视图固定的工作流版本的正式运行(视图读的是定时任务那次运行,它不带应用 id)。
user 角色读到的查询快照一律按派生遮罩处理、tool_snapshot 的原文不给(api/artifacts.py),这一层只回答「能不能读」。
"""
from __future__ import annotations
from typing import Any
from sqlalchemy import and_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
#: 视图运行的来源(engine/origin.py)
_VIEW_ORIGINS = ("app_view", "app_backfill")
def _unrestricted(principal: Any) -> bool:
return principal.mode == "off" or principal.at_least("builder")
async def app_roles(session: AsyncSession, principal: Any) -> dict[str, set[str]]:
"""这个人在各应用上的授权:{app_id: {use, observe, manage}}。"""
from app.db.models import AppGrant
who = [AppGrant.principal_kind == "everyone",
and_(AppGrant.principal_kind == "user", AppGrant.principal == principal.username)]
if principal.groups:
who.append(and_(AppGrant.principal_kind == "group", AppGrant.principal.in_(list(principal.groups))))
out: dict[str, set[str]] = {}
for app_id, role in (await session.execute(select(AppGrant.app_id, AppGrant.role).where(or_(*who)))).all():
out.setdefault(app_id, set()).add(role)
return out
async def _participated(session: AsyncSession, principal: Any, run_id: str) -> bool:
from app.db.models import Task, TaskAssignment
mine = [and_(TaskAssignment.principal_kind == "user", TaskAssignment.principal == principal.username),
TaskAssignment.responded_by == principal.username]
if principal.groups:
mine.append(and_(TaskAssignment.principal_kind == "group",
TaskAssignment.principal.in_(list(principal.groups))))
hit = (await session.execute(
select(TaskAssignment.id).join(Task, Task.id == TaskAssignment.task_id)
.where(Task.run_id == run_id, or_(*mine)).limit(1))).first()
return hit is not None
async def _viewed_by_granted_app(session: AsyncSession, run: Any, apps: set[str]) -> bool:
"""这次运行的工作流版本是不是被有授权的应用(当前发布版本)作为视图固定着。"""
from app.db.models import App, AppVersion
if run.run_class != "formal" or not run.workflow_id or run.version is None or not apps:
return False
rows = (await session.execute(
select(AppVersion.pins).join(App, and_(App.id == AppVersion.app_id, App.published_version == AppVersion.version))
.where(App.id.in_(list(apps))))).scalars()
for pins in rows:
for pin in pins or []:
if (isinstance(pin, dict) and pin.get("workflow_id") == run.workflow_id
and pin.get("version") == run.version and pin.get("use", "view") == "view"):
return True
return False
async def run_readable(principal: Any, run: Any) -> bool:
"""能不能读这次运行(证据、事件、工件)。run 可以是 Run,也可以是运行 id;不存在的运行读不了。"""
if _unrestricted(principal):
return True
if not principal.username:
return False
from app.db.base import SessionLocal
from app.db.models import Run
async with SessionLocal() as session:
if isinstance(run, str):
run = await session.get(Run, run)
if run is None:
return False
if run.started_by == principal.username:
return True
if await _participated(session, principal, run.id):
return True
apps = await app_roles(session, principal)
if run.app_id and run.app_id in apps:
if run.origin_kind in _VIEW_ORIGINS or apps[run.app_id] & {"observe", "manage"}:
return True
return await _viewed_by_granted_app(session, run, set(apps))
async def artifact_readable(principal: Any, artifact_id: str) -> bool:
"""能不能读这件工件:引用它的某次运行能读就能读。工件按内容寻址,同一件可能被好几次运行引用。"""
if _unrestricted(principal):
return True
from app.db.base import SessionLocal
from app.db.models import Artifact
async with SessionLocal() as session:
run_ids = [r for r in (await session.execute(
select(Artifact.run_id).where(Artifact.id == artifact_id))).scalars() if r]
for run_id in run_ids:
if await run_readable(principal, run_id):
return True
return False