Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

84 lines
3.0 KiB
Python

"""Partner visibility guards for non-admin users.
Partners are admin-managed, process-wide resources: the whole
``/api/v1/partners`` CRUD router is admin-gated, and a partner runs in its own
isolated workspace scope (``data/partners/{id}/``), never the caller's. A
non-admin can't create or manage partners, but an admin can *assign* specific
partners to specific users through the grant system — the same mechanism that
shares knowledge bases and skills.
An assigned user may then see the partner, connect it as a subagent, and
consult it in chat; the consult still drives the partner in its own scope, so
the user only ever exchanges messages with it — exactly as when an admin
consults it. This module is the read-side counterpart of ``skill_access``.
"""
from __future__ import annotations
from typing import Any
from fastapi import HTTPException
from .context import get_current_user
from .grants import load_grant
def assigned_partner_ids(user_id: str | None = None) -> set[str]:
"""The partner ids an admin has assigned to the user (empty for admins)."""
user = get_current_user()
uid = user_id or user.id
return {
str(item.get("partner_id") or item.get("id") or "").strip()
for item in load_grant(uid).get("partners", []) or []
if str(item.get("partner_id") or item.get("id") or "").strip()
}
def assert_partner_allowed(partner_id: str, user_id: str | None = None) -> None:
"""Raise 403 when a non-admin tries to use a partner not assigned to them.
Admins may use any partner; this is a no-op for them (and for single-user
deployments, where the current user resolves to the local admin).
"""
user = get_current_user()
if user.is_admin:
return
if str(partner_id or "").strip() not in assigned_partner_ids(user_id or user.id):
raise HTTPException(status_code=403, detail="Partner is not assigned to you")
# Identity-only card fields a consumer needs (partner list page, connect modal).
# Deliberately excludes channels / llm_selection / tool config so a non-admin
# only ever sees a partner's face, never its wiring.
_CARD_FIELDS = (
"partner_id",
"name",
"description",
"emoji",
"color",
"avatar",
"language",
"running",
)
def _project_card(partner: dict[str, Any]) -> dict[str, Any]:
card = {field: partner.get(field) for field in _CARD_FIELDS}
card["partner_id"] = str(partner.get("partner_id") or "")
return card
def visible_partner_cards() -> list[dict[str, Any]]:
"""Partners the current user may consult: all for an admin, or just the
assigned subset for a non-admin. Returns identity-only card dicts."""
from deeptutor.services.partners import get_partner_manager
everything = get_partner_manager().list_partners()
user = get_current_user()
if user.is_admin:
return [_project_card(item) for item in everything]
allowed = assigned_partner_ids(user.id)
return [
_project_card(item) for item in everything if str(item.get("partner_id") or "") in allowed
]