mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-16 10:15:00 +00:00
fix(pat): close restricted-token paths through workflows, chat, schedules and conversations
A resource restriction was checked on ids in the request, not on what the addressed row pulls in or belongs to. Closed: - Workflow writes for tokens restricted on sources, tools or prompts (a graph names those inside its nodes), and attaching a workflow to an agent unless the token is restricted on workflows too. - Chat for tools-restricted tokens (chat executes tools; rejected at token creation as well), and agent-less chat for tokens restricted on prompts or workflows. - conversation_id on chat: it must belong to the agent being run, or to no agent for agent-less chat. Otherwise the server continued, appended to, or resumed pending tool calls of another agent's conversation. - Schedules for tokens restricted on anything but agents; schedule-id routes for every restricted token. - Conversations and analytics for every restricted token, not only agent-restricted ones. Also: create, first publish and adopt return the agent API key masked to a token without agents:keys; token ids must be canonical UUIDs (urn:uuid: gave a 500); an expired token is reported as expired; token creation takes a per-user advisory lock so the cap cannot be raced; admin revoke-sessions writes a pat_revoked event per token. The UI drops a row whose revoke returns 404 and does not offer a tools restriction next to chat:run.
This commit is contained in:
1 parent
b1ff8f516e
commit
94b5924e36
15 files changed
+393
-64
No files matched your search
@@ -9,6 +9,7 @@ token can neither mint a replacement nor widen itself.
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import jsonify, make_response, request
|
||||
from flask_restx import Namespace, Resource
|
||||
@@ -50,21 +51,35 @@ def _session_user_id():
|
||||
|
||||
|
||||
def _valid_uuid(value: str) -> bool:
|
||||
"""Canonical form only: ``uuid.UUID`` also accepts ``urn:uuid:…`` and braces, which Postgres does not."""
|
||||
try:
|
||||
uuid.UUID(str(value))
|
||||
return str(uuid.UUID(str(value))) == str(value).lower()
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_expired(expires_at) -> bool:
|
||||
if not expires_at:
|
||||
return False
|
||||
try:
|
||||
moment = expires_at if isinstance(expires_at, datetime) else datetime.fromisoformat(str(expires_at))
|
||||
except ValueError:
|
||||
return False
|
||||
if moment.tzinfo is None:
|
||||
moment = moment.replace(tzinfo=timezone.utc)
|
||||
return moment <= datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def serialize_token(row: dict) -> dict:
|
||||
# The row keeps status 'active' until someone revokes it; report what is true for a caller.
|
||||
status = "expired" if row["status"] == "active" and _is_expired(row.get("expires_at")) else row["status"]
|
||||
return {
|
||||
"id": str(row["id"]),
|
||||
"name": row["name"],
|
||||
"token_prefix": row["token_prefix"],
|
||||
"scopes": list(row.get("scopes") or []),
|
||||
"resource_filter": row.get("resource_filter") or {},
|
||||
"status": row["status"],
|
||||
"status": status,
|
||||
"expires_at": row.get("expires_at"),
|
||||
"last_used_at": row.get("last_used_at"),
|
||||
"last_used_ip": row.get("last_used_ip"),
|
||||
@@ -135,6 +150,8 @@ class PersonalAccessTokens(Resource):
|
||||
try:
|
||||
with db_session() as conn:
|
||||
repo = PersonalAccessTokensRepository(conn)
|
||||
# Serialise this user's creates so concurrent requests cannot both pass the cap check.
|
||||
repo.lock_user(user_id)
|
||||
if repo.count_active(user_id) >= settings.PAT_MAX_PER_USER:
|
||||
return _error(
|
||||
f"Token limit reached ({settings.PAT_MAX_PER_USER}); revoke one first", 409
|
||||
|
||||
Reference in new issue
Block a user