Roles audit and revamp

This commit is contained in:
Pavel committed 2026-09-29 10:42:39 +04:00
1 parent 208d34abc7
commit 98afa5d93c
134 files changed
+13725 -1702

No files matched your search

+33
View File
@@ -187,6 +187,23 @@ Index(
team_resource_grants_table.c.resource_id,
)
# Per-asset sharing switches set by the owner (migration 0038). A missing row
# means every switch is at its default; keys are validated in
# ``docsgpt/api/user/resource_access.py``.
resource_share_settings_table = Table(
"resource_share_settings",
metadata,
Column("resource_type", Text, primary_key=True),
Column("resource_id", UUID(as_uuid=True), primary_key=True),
Column("settings", JSONB, nullable=False, server_default="{}"),
Column("updated_by", Text),
Column("updated_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
CheckConstraint(
"resource_type IN ('agent', 'source', 'prompt', 'tool')",
name="resource_share_settings_type_check",
),
)
prompts_table = Table(
"prompts",
@@ -218,6 +235,22 @@ user_tools_table = Table(
Column("legacy_mongo_id", Text),
)
# A grantee's personal "In my chats" switch for a tool shared with them
# (migration 0038). The owner's own switch stays ``user_tools.status``.
user_tool_preferences_table = Table(
"user_tool_preferences",
metadata,
Column("user_id", Text, primary_key=True),
Column(
"tool_id",
UUID(as_uuid=True),
ForeignKey("user_tools.id", ondelete="CASCADE"),
primary_key=True,
),
Column("in_chat", Boolean, nullable=False, server_default="false"),
Column("updated_at", DateTime(timezone=True), nullable=False, server_default=func.now()),
)
token_usage_table = Table(
"token_usage",
metadata,
@@ -107,8 +107,9 @@ class TeamResourceGrantsRepository:
that one member (the caller must have validated they're a team member).
``ON CONFLICT`` on the functional dedup index makes re-sharing
last-write-wins on ``access_level``. The caller MUST have verified
``granted_by`` owns the resource (dispatched by ``resource_type``) — the
polymorphic table has no FK to catch a type/id mismatch.
``granted_by`` holds ``share`` on the resource (``resource_access.require``,
dispatched by ``resource_type``) and pass the real owner as ``owner_id`` —
the polymorphic table has no FK to catch a type/id mismatch.
"""
result = self._conn.execute(
text(
@@ -0,0 +1,91 @@
"""Repository for the ``user_tool_preferences`` table.
A grantee's personal "In my chats" switch for a tool shared with them. The
owner's own switch stays in ``user_tools.status``; a missing row here means
off, so sharing a tool never adds it to anyone's chats.
"""
from __future__ import annotations
from typing import Iterable
from sqlalchemy import Connection, text
from docsgpt.storage.db.base_repository import looks_like_uuid
class UserToolPreferencesRepository:
"""Per-user, per-tool chat preferences for shared tools."""
def __init__(self, conn: Connection) -> None:
self._conn = conn
def set_in_chat(self, user_id: str, tool_id: str, in_chat: bool) -> None:
"""Upsert the caller's "In my chats" switch for one tool.
Args:
user_id: The grantee's user id.
tool_id: The shared tool's UUID.
in_chat: Whether the tool joins the grantee's agentless chats.
"""
self._conn.execute(
text(
"""
INSERT INTO user_tool_preferences (user_id, tool_id, in_chat)
VALUES (:user_id, CAST(:tool_id AS uuid), :in_chat)
ON CONFLICT (user_id, tool_id)
DO UPDATE SET in_chat = EXCLUDED.in_chat, updated_at = now()
"""
),
{"user_id": user_id, "tool_id": str(tool_id), "in_chat": bool(in_chat)},
)
def in_chat_many(self, user_id: str, tool_ids: Iterable[str]) -> dict[str, bool]:
"""``tool_id -> in_chat`` for the given tools; missing rows are False.
Args:
user_id: The grantee's user id.
tool_ids: Tool ids to look up (non-UUIDs are ignored).
Returns:
A dict with one entry per UUID-shaped input id.
"""
ids = [str(t) for t in tool_ids if looks_like_uuid(str(t))]
out = {tid: False for tid in ids}
if not ids or not user_id:
return out
rows = self._conn.execute(
text(
"""
SELECT tool_id, in_chat FROM user_tool_preferences
WHERE user_id = :user_id AND tool_id = ANY(CAST(:ids AS uuid[]))
"""
),
{"user_id": user_id, "ids": ids},
).fetchall()
for tool_id, in_chat in rows:
out[str(tool_id)] = bool(in_chat)
return out
def list_in_chat_tool_ids(self, user_id: str) -> list[str]:
"""Ids of tools the user switched into their chats (any owner).
Args:
user_id: The grantee's user id.
Returns:
Tool ids as strings; access must still be re-checked by the caller.
"""
if not user_id:
return []
rows = self._conn.execute(
text(
"""
SELECT tool_id FROM user_tool_preferences
WHERE user_id = :user_id AND in_chat = true
ORDER BY updated_at
"""
),
{"user_id": user_id},
).fetchall()
return [str(r[0]) for r in rows]