mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 22:13:08 +00:00
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
125 lines
4.9 KiB
Python
125 lines
4.9 KiB
Python
"""Cross-cutting helpers for team resource sharing.
|
|
|
|
Centralises the polymorphic dispatch over the four shareable resource types so
|
|
the grant endpoint and the per-entity list/get/update paths share one source of
|
|
truth for "does this user own resource R?" and "what can this user see/edit via
|
|
their teams?". Every shareable repo exposes the same ``get_any(id, user_id)``
|
|
ownership accessor, which is what makes the dispatch uniform.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import Connection
|
|
|
|
from docsgpt.storage.db.base_repository import looks_like_uuid
|
|
from docsgpt.storage.db.repositories.agents import AgentsRepository
|
|
from docsgpt.storage.db.repositories.prompts import PromptsRepository
|
|
from docsgpt.storage.db.repositories.sources import SourcesRepository
|
|
from docsgpt.storage.db.repositories.team_resource_grants import (
|
|
TeamResourceGrantsRepository,
|
|
)
|
|
from docsgpt.storage.db.repositories.team_scope import TeamScopeRepository
|
|
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
|
|
|
RESOURCE_TYPES = ("agent", "source", "prompt", "tool")
|
|
|
|
_REPO_FOR_TYPE = {
|
|
"agent": AgentsRepository,
|
|
"source": SourcesRepository,
|
|
"prompt": PromptsRepository,
|
|
"tool": UserToolsRepository,
|
|
}
|
|
|
|
|
|
def is_valid_resource_type(resource_type: str) -> bool:
|
|
return resource_type in _REPO_FOR_TYPE
|
|
|
|
|
|
def owns_resource(
|
|
conn: Connection, resource_type: str, resource_id: str, user_id: str
|
|
) -> bool:
|
|
"""True if ``user_id`` is the OWNER of the resource.
|
|
|
|
Dispatches to the correct repo by ``resource_type`` — this is the guard that
|
|
stops a caller registering a grant with a mismatched ``resource_type`` /
|
|
``resource_id`` (the polymorphic grant table has no FK to catch it). Owners
|
|
only — team-granted ``editor`` access does NOT make you an owner.
|
|
"""
|
|
repo_cls = _REPO_FOR_TYPE.get(resource_type)
|
|
if repo_cls is None:
|
|
return False
|
|
return repo_cls(conn).get_any(resource_id, user_id) is not None
|
|
|
|
|
|
def team_access_for(
|
|
conn: Connection, user_id: str, resource_type: str, resource_id: str
|
|
) -> Optional[str]:
|
|
"""Strongest team access (``editor``/``viewer``) ``user_id`` has on a resource.
|
|
|
|
None when no team grant reaches the user (they may still be the owner — that
|
|
is a separate dual-key check at the repo).
|
|
"""
|
|
return TeamScopeRepository(conn).effective_access(user_id, resource_type, resource_id)
|
|
|
|
|
|
def visible_ids_via_teams(
|
|
conn: Connection, user_id: str, resource_type: str
|
|
) -> set[str]:
|
|
"""The id set of ``resource_type`` shared to any team ``user_id`` belongs to."""
|
|
return TeamScopeRepository(conn).visible_resource_ids(user_id, resource_type)
|
|
|
|
|
|
def visible_with_access(
|
|
conn: Connection, user_id: str, resource_type: str
|
|
) -> dict[str, str]:
|
|
"""Map ``resource_id -> strongest access`` shared to the user's teams."""
|
|
return TeamScopeRepository(conn).visible_with_access(user_id, resource_type)
|
|
|
|
|
|
def effective_write_owner(
|
|
conn: Connection, resource_type: str, resource_id: str, user_id: str
|
|
) -> Optional[str]:
|
|
"""The owner id to write a resource AS, or None if the caller can't write.
|
|
|
|
Returns ``user_id`` when the caller owns the resource, or the resource's
|
|
real ``owner_id`` when the caller holds a team ``editor`` grant — so callers
|
|
can pass the result straight to the existing owner-scoped ``update(id, owner,
|
|
...)`` repo methods (which match on ``WHERE id AND user_id = :owner``) without
|
|
a separate ownerless write path. None means viewer-only or no access → the
|
|
route should answer 403/404. Delete is never authorized here — owner-only.
|
|
"""
|
|
if owns_resource(conn, resource_type, resource_id, user_id):
|
|
return user_id
|
|
# Past the ownership check, only canonical-UUID resources can carry a team
|
|
# grant; a legacy/non-UUID id can't, and casting it would poison the txn.
|
|
if not looks_like_uuid(resource_id):
|
|
return None
|
|
grants = TeamResourceGrantsRepository(conn).list_for_resource(
|
|
resource_type, resource_id
|
|
)
|
|
if not grants:
|
|
return None
|
|
if TeamScopeRepository(conn).can_write(user_id, resource_type, resource_id):
|
|
# All grant rows carry the same denormalised owner_id.
|
|
return grants[0].get("owner_id")
|
|
return None
|
|
|
|
|
|
def can_access(
|
|
conn: Connection, resource_type: str, resource_id: str, user_id: str
|
|
) -> bool:
|
|
"""True if ``user_id`` owns the resource OR has any team grant on it (read).
|
|
|
|
This is the write-path gate for *referencing* a resource (e.g. attaching a
|
|
``source_id`` to an agent): you may reference what you own or what a team has
|
|
shared with you directly. Transitive access *through* a shared agent is a
|
|
separate, run-time concept and is intentionally NOT gated here.
|
|
"""
|
|
if not resource_id:
|
|
return True
|
|
if owns_resource(conn, resource_type, resource_id, user_id):
|
|
return True
|
|
return TeamScopeRepository(conn).can_read(user_id, resource_type, resource_id)
|