Scope workspace roots per account
This commit is contained in:
@@ -236,10 +236,15 @@ class WorkspaceAgentRequest(AgentRequest):
|
||||
|
||||
|
||||
@app.post("/v1/agent/workspace/files", dependencies=[Depends(get_authenticated_user_id)])
|
||||
async def list_workspace_files(request: WorkspaceFilesRequest) -> dict[str, Any]:
|
||||
async def list_workspace_files(
|
||||
request: WorkspaceFilesRequest,
|
||||
user_id: str = Depends(get_authenticated_user_id),
|
||||
) -> dict[str, Any]:
|
||||
"""Return bounded relative file names from the folder chosen by the desktop user."""
|
||||
try:
|
||||
root = workspace.selected_root(request.workspace_path)
|
||||
root = workspace.selected_root(request.workspace_path, user_id=user_id)
|
||||
except workspace.WorkspaceAccessDenied as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
if root is None:
|
||||
@@ -592,7 +597,9 @@ async def index_workspace_knowledge(
|
||||
) -> dict[str, Any]:
|
||||
"""Index selected files and optionally add local semantic vectors."""
|
||||
try:
|
||||
root = workspace.selected_root(request.workspace_path)
|
||||
root = workspace.selected_root(request.workspace_path, user_id=user_id)
|
||||
except workspace.WorkspaceAccessDenied as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
if root is None:
|
||||
@@ -721,7 +728,9 @@ def delete_workspace_knowledge(
|
||||
) -> dict[str, Any]:
|
||||
"""Delete selected files from this user's local knowledge index."""
|
||||
try:
|
||||
root = workspace.selected_root(request.workspace_path)
|
||||
root = workspace.selected_root(request.workspace_path, user_id=user_id)
|
||||
except workspace.WorkspaceAccessDenied as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
if root is None:
|
||||
@@ -747,7 +756,9 @@ async def search_workspace_knowledge(
|
||||
user_id: str = Depends(get_authenticated_user_id),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
root = workspace.selected_root(request.workspace_path)
|
||||
root = workspace.selected_root(request.workspace_path, user_id=user_id)
|
||||
except workspace.WorkspaceAccessDenied as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
if root is None:
|
||||
@@ -1453,7 +1464,9 @@ async def _execute_agent(
|
||||
|
||||
await report("يتحقق من المهمة ومساحة العمل المحددة.")
|
||||
try:
|
||||
selected_workspace = workspace.selected_root(request.workspace_path)
|
||||
selected_workspace = workspace.selected_root(request.workspace_path, user_id=user_id)
|
||||
except workspace.WorkspaceAccessDenied as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
selected_skill = skills.get_skill(request.skill_id)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import difflib
|
||||
import hashlib
|
||||
import tempfile
|
||||
@@ -29,6 +30,10 @@ MAX_PENDING_PROPOSALS = 32
|
||||
_pending_changes: dict[str, dict[str, object]] = {}
|
||||
|
||||
|
||||
class WorkspaceAccessDenied(ValueError):
|
||||
"""Raised when an authenticated account selects another user's workspace."""
|
||||
|
||||
|
||||
def configured_roots() -> tuple[Path, ...]:
|
||||
"""Return the server administrator's allow-listed workspace roots."""
|
||||
configured = os.getenv("SOVEREIGNAI_ALLOWED_WORKSPACES")
|
||||
@@ -55,10 +60,66 @@ def configured_root() -> Path | None:
|
||||
return roots[0] if roots else None
|
||||
|
||||
|
||||
def selected_root(value: str | None) -> Path | None:
|
||||
"""Resolve an explicitly selected directory, falling back to server config."""
|
||||
def _roots_for_user(user_id: str | None) -> tuple[Path, ...]:
|
||||
roots = configured_roots()
|
||||
from app import auth, database
|
||||
|
||||
if user_id is None or user_id == database.LOCAL_USER_ID:
|
||||
return roots
|
||||
|
||||
email = auth.account_email(user_id)
|
||||
if email is None:
|
||||
raise ValueError("الحساب الحالي لا يملك مساحة عمل مخصصة على الخادم.")
|
||||
raw_mapping = os.getenv("SOVEREIGNAI_USER_WORKSPACES", "")
|
||||
try:
|
||||
mapping = json.loads(raw_mapping) if raw_mapping else {}
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError("إعداد مساحات العمل حسب الحساب غير صالح على الخادم.") from exc
|
||||
if not isinstance(mapping, dict):
|
||||
raise ValueError("إعداد مساحات العمل حسب الحساب يجب أن يكون كائن JSON.")
|
||||
|
||||
resolved_by_email: dict[str, tuple[Path, ...]] = {}
|
||||
for assigned_email, assigned_values in mapping.items():
|
||||
if not isinstance(assigned_email, str) or not isinstance(assigned_values, list):
|
||||
raise ValueError("كل مستخدم في إعداد مساحات العمل يجب أن يقابله مصفوفة مسارات.")
|
||||
assigned_email = assigned_email.strip().casefold()
|
||||
if not assigned_email or assigned_email in resolved_by_email:
|
||||
raise ValueError("البريد مكرر أو فارغ في إعداد مساحات العمل.")
|
||||
assigned_roots: list[Path] = []
|
||||
for item in assigned_values:
|
||||
if not isinstance(item, str) or not item.strip():
|
||||
raise ValueError("مسار مساحة العمل في إعداد الخادم غير صالح.")
|
||||
try:
|
||||
root = Path(item).expanduser().resolve(strict=True)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
raise ValueError("أحد جذور مساحة العمل المخصصة غير موجود أو غير متاح.") from exc
|
||||
if not root.is_dir() or not any(root == allowed or allowed in root.parents for allowed in roots):
|
||||
raise ValueError("جذر الحساب يجب أن يقع داخل قائمة جذور الخادم المصرح بها.")
|
||||
if root in assigned_roots:
|
||||
raise ValueError("جذر مساحة العمل مكرر للحساب نفسه.")
|
||||
assigned_roots.append(root)
|
||||
resolved_by_email[assigned_email] = tuple(assigned_roots)
|
||||
|
||||
# Reject overlapping account roots so one account cannot read a parent or
|
||||
# child directory assigned to another account.
|
||||
owners = list(resolved_by_email.items())
|
||||
for index, (_first_email, first_roots) in enumerate(owners):
|
||||
for _second_email, second_roots in owners[index + 1 :]:
|
||||
for first_root in first_roots:
|
||||
for second_root in second_roots:
|
||||
if first_root == second_root or first_root in second_root.parents or second_root in first_root.parents:
|
||||
raise ValueError("جذور مساحة العمل لحسابين مختلفين متداخلة في إعداد الخادم.")
|
||||
assigned = resolved_by_email.get(email.casefold())
|
||||
if not assigned:
|
||||
raise WorkspaceAccessDenied("لم يخصص مسؤول الخادم مساحة عمل لهذا الحساب.")
|
||||
return assigned
|
||||
|
||||
|
||||
def selected_root(value: str | None, *, user_id: str | None = None) -> Path | None:
|
||||
"""Resolve a selected directory only within this user's server-assigned roots."""
|
||||
allowed_roots = _roots_for_user(user_id)
|
||||
if value is None or not value.strip():
|
||||
return configured_root()
|
||||
return allowed_roots[0] if allowed_roots else None
|
||||
try:
|
||||
root = Path(value).expanduser().resolve(strict=True)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
@@ -69,11 +130,12 @@ def selected_root(value: str | None) -> Path | None:
|
||||
raise ValueError("اختر مجلد مشروع محددًا، وليس جذر القرص.")
|
||||
if root.name.startswith(".") or root.name in IGNORED_PARTS:
|
||||
raise ValueError("لا يمكن استخدام مجلد مخفي أو مستثنى كمساحة عمل.")
|
||||
allowed_roots = configured_roots()
|
||||
if allowed_roots and not any(
|
||||
root == allowed or allowed in root.parents for allowed in allowed_roots
|
||||
):
|
||||
raise ValueError("المساحة المحددة خارج مجلدات المشاريع المصرح بها على الخادم.")
|
||||
raise WorkspaceAccessDenied("المجلد المحدد خارج مساحة العمل المخصصة لهذا الحساب.")
|
||||
if not allowed_roots:
|
||||
raise WorkspaceAccessDenied("لا توجد جذور مساحة عمل مخصصة لهذا الحساب على الخادم.")
|
||||
return root
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user