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

248 lines
8.0 KiB
Python

"""Knowledge-base visibility and write guards for the multi-user layer."""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from typing import Any
from fastapi import HTTPException
from deeptutor.knowledge.manager import KnowledgeBaseManager
from .context import get_current_user
from .grants import load_grant
from .models import KnowledgeResource
from .paths import get_admin_path_service, get_current_path_service
ADMIN_PREFIX = "admin:kb:"
USER_PREFIX = "user:kb:"
DEFAULT_KB_ALIASES = {"", "default", "current", "selected", "默认", "默认知识库", "当前知识库"}
@lru_cache(maxsize=128)
def _manager_for(base_dir: str) -> KnowledgeBaseManager:
return KnowledgeBaseManager(base_dir=base_dir)
def current_kb_base_dir() -> Path:
return get_current_path_service().get_knowledge_bases_root()
def admin_kb_base_dir() -> Path:
return get_admin_path_service().get_knowledge_bases_root()
def current_kb_manager() -> KnowledgeBaseManager:
return _manager_for(str(current_kb_base_dir().resolve()))
def admin_kb_manager() -> KnowledgeBaseManager:
return _manager_for(str(admin_kb_base_dir().resolve()))
def _strip_resource_prefix(value: str) -> tuple[str | None, str]:
raw = str(value or "").strip()
if raw.startswith(ADMIN_PREFIX):
return "admin", raw[len(ADMIN_PREFIX) :]
if raw.startswith(USER_PREFIX):
return "user", raw[len(USER_PREFIX) :]
return None, raw
def _assigned_admin_names() -> set[str]:
user = get_current_user()
if user.is_admin:
return set()
out: set[str] = set()
for item in load_grant(user.id).get("knowledge_bases", []) or []:
name = str(item.get("name") or item.get("kb_name") or "").strip()
resource_id = str(item.get("resource_id") or item.get("id") or "")
if resource_id.startswith(ADMIN_PREFIX):
name = resource_id[len(ADMIN_PREFIX) :]
if name:
out.add(name)
return out
def resolve_kb(kb_ref: str, *, require_write: bool = False) -> KnowledgeResource:
user = get_current_user()
requested_source, name = _strip_resource_prefix(kb_ref)
if user.is_admin:
manager = admin_kb_manager()
resolved = _resolve_default_or_name(manager, name)
return KnowledgeResource(
id=f"admin:kb:{resolved}",
name=resolved,
base_dir=admin_kb_base_dir(),
source="admin",
assigned=False,
read_only=False,
)
user_manager = current_kb_manager()
assigned_names = _assigned_admin_names()
if requested_source == "admin":
if name not in assigned_names:
raise HTTPException(status_code=403, detail="Knowledge base is not assigned to you")
if require_write:
raise HTTPException(
status_code=403, detail="Assigned admin knowledge bases are read-only"
)
return KnowledgeResource(
id=f"admin:kb:{name}",
name=name,
base_dir=admin_kb_base_dir(),
source="admin",
assigned=True,
read_only=True,
)
if requested_source == "user":
resolved = _resolve_default_or_name(user_manager, name)
return KnowledgeResource(
id=f"user:kb:{resolved}",
name=resolved,
base_dir=current_kb_base_dir(),
source="user",
assigned=False,
read_only=False,
)
if name.lower() in DEFAULT_KB_ALIASES:
resolved = _resolve_default_or_name(user_manager, name)
return KnowledgeResource(
id=f"user:kb:{resolved}",
name=resolved,
base_dir=current_kb_base_dir(),
source="user",
assigned=False,
read_only=False,
)
user_names = set(user_manager.list_knowledge_bases())
if name in user_names:
return KnowledgeResource(
id=f"user:kb:{name}",
name=name,
base_dir=current_kb_base_dir(),
source="user",
assigned=False,
read_only=False,
)
if name in assigned_names:
if require_write:
raise HTTPException(
status_code=403, detail="Assigned admin knowledge bases are read-only"
)
return KnowledgeResource(
id=f"admin:kb:{name}",
name=name,
base_dir=admin_kb_base_dir(),
source="admin",
assigned=True,
read_only=True,
)
raise HTTPException(status_code=404, detail=f"Knowledge base '{name}' not found")
def _resolve_default_or_name(manager: KnowledgeBaseManager, name: str) -> str:
requested = str(name or "").strip()
names = manager.list_knowledge_bases()
if requested and requested in names:
return requested
if requested.lower() in DEFAULT_KB_ALIASES:
default_kb = manager.get_default()
if default_kb and default_kb in names:
return default_kb
raise HTTPException(status_code=404, detail="No default knowledge base is configured")
raise HTTPException(status_code=404, detail=f"Knowledge base '{requested}' not found")
def manager_for_resource(resource: KnowledgeResource) -> KnowledgeBaseManager:
return _manager_for(str(resource.base_dir.resolve()))
def list_visible_knowledge_bases() -> list[dict[str, Any]]:
user = get_current_user()
manager = current_kb_manager()
items: list[dict[str, Any]] = []
for name in manager.list_knowledge_bases():
items.append(
{
"id": f"admin:kb:{name}" if user.is_admin else f"user:kb:{name}",
"name": name,
"source": "admin" if user.is_admin else "user",
"assigned": False,
"read_only": False,
"provenance_label": "Created by you" if not user.is_admin else "Admin workspace",
}
)
if user.is_admin:
return items
admin_manager = admin_kb_manager()
admin_names = set(admin_manager.list_knowledge_bases())
existing_ids = {item["id"] for item in items}
for item in load_grant(user.id).get("knowledge_bases", []) or []:
name = str(item.get("name") or item.get("kb_name") or "").strip()
resource_id = str(item.get("resource_id") or item.get("id") or "")
if resource_id.startswith(ADMIN_PREFIX):
name = resource_id[len(ADMIN_PREFIX) :]
if not name:
continue
rid = f"admin:kb:{name}"
if rid in existing_ids:
continue
items.append(
{
"id": rid,
"name": name,
"source": "admin",
"assigned": True,
"read_only": True,
"available": name in admin_names,
"provenance_label": "Assigned by admin",
"needs_admin_reindex": bool(item.get("needs_admin_reindex", False)),
"embedding_signature": item.get("embedding_signature", ""),
}
)
return items
def assert_writable(kb_ref: str) -> KnowledgeResource:
return resolve_kb(kb_ref, require_write=True)
def resolve_for_rag(kb_ref: str | None) -> KnowledgeResource | None:
if not kb_ref:
return None
resource = resolve_kb(kb_ref, require_write=False)
if resource.assigned:
from .audit import log_usage
log_usage("knowledge_base", resource.id, "rag_query")
return resource
def resolve_kb_metadata(kb_ref: str | None) -> dict[str, Any] | None:
"""Access-checked KB metadata (``type`` / ``vault_path`` / …) for ``kb_ref``.
Returns ``None`` when the reference is empty or not accessible to the
current user. A pure read with no usage audit (unlike
:func:`resolve_for_rag`) — safe to call while resolving capability bindings.
"""
if not kb_ref:
return None
try:
resource = resolve_kb(str(kb_ref), require_write=False)
except HTTPException:
return None
manager = _manager_for(str(resource.base_dir.resolve()))
return manager.get_metadata(resource.name)