fed8b2eed7
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
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 application.storage.db.base_repository import looks_like_uuid
|
|
from application.storage.db.repositories.agents import AgentsRepository
|
|
from application.storage.db.repositories.prompts import PromptsRepository
|
|
from application.storage.db.repositories.sources import SourcesRepository
|
|
from application.storage.db.repositories.team_resource_grants import (
|
|
TeamResourceGrantsRepository,
|
|
)
|
|
from application.storage.db.repositories.team_scope import TeamScopeRepository
|
|
from application.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)
|