mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 12:14: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
@@ -250,9 +250,18 @@ class AdminUserSessionsResource(Resource):
|
||||
ok = denylist.deny_user(user_id)
|
||||
with db_session() as conn:
|
||||
# A forced logout that left API credentials alive would not be one.
|
||||
tokens_revoked = PersonalAccessTokensRepository(conn).revoke_all_for_user(
|
||||
revoked_token_ids = PersonalAccessTokensRepository(conn).revoke_all_for_user(
|
||||
user_id, reason="admin_sessions_revoked"
|
||||
)
|
||||
# One pat_revoked event per token, like every other revocation path.
|
||||
for token_id in revoked_token_ids:
|
||||
AuthEventsRepository(conn).insert(
|
||||
user_id,
|
||||
"pat_revoked",
|
||||
ip=request.remote_addr,
|
||||
user_agent=request.headers.get("User-Agent"),
|
||||
metadata={"token_id": token_id, "by": _actor(), "via": "admin_sessions_revoked"},
|
||||
)
|
||||
AuthEventsRepository(conn).insert(
|
||||
user_id,
|
||||
"admin_sessions_revoked",
|
||||
@@ -262,7 +271,7 @@ class AdminUserSessionsResource(Resource):
|
||||
"by": _actor(),
|
||||
"via": "admin_api",
|
||||
"persisted": ok,
|
||||
"personal_access_tokens_revoked": tokens_revoked,
|
||||
"personal_access_tokens_revoked": len(revoked_token_ids),
|
||||
},
|
||||
)
|
||||
return make_response(jsonify({"success": True, "revoked": ok}), 200)
|
||||
|
||||
@@ -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
|
||||
|
||||
+114
-40
@@ -44,7 +44,7 @@ class Rule:
|
||||
open: bool = False
|
||||
in_route: bool = False
|
||||
blocked_by: tuple[str, ...] = ()
|
||||
check: Optional[Callable[[Any, dict], Optional[str]]] = None
|
||||
check: Optional[Callable[[Any, dict, Optional[str]], Optional[str]]] = None
|
||||
|
||||
|
||||
def _rule(scope: Optional[str] = None, *ids: Locator, any_of: tuple[str, ...] = (), **kwargs) -> Rule:
|
||||
@@ -61,6 +61,9 @@ def _rule(scope: Optional[str] = None, *ids: Locator, any_of: tuple[str, ...] =
|
||||
MESSAGE_REPLAY_SCOPES = ("conversations:read", "chat:run")
|
||||
|
||||
_ALL_FAMILIES = ("agents", "sources", "prompts", "tools", "workflows")
|
||||
_WORKFLOW_CONTENT_FAMILIES = ("sources", "tools", "prompts")
|
||||
# A route reached through an agent id can prove the agent; nothing else about it.
|
||||
_NON_AGENT_FAMILIES = ("sources", "prompts", "tools", "workflows")
|
||||
|
||||
# Ids of other families that agent create/update accept in their JSON-or-form body.
|
||||
_AGENT_BODY_REFS = (
|
||||
@@ -72,15 +75,41 @@ _AGENT_BODY_REFS = (
|
||||
)
|
||||
|
||||
|
||||
def _chat_check(request, resource_filter: dict) -> Optional[str]:
|
||||
def _conversation_agent_id(conversation_id: str, user_id: Optional[str]) -> tuple[bool, str]:
|
||||
"""``(found, agent_id)`` for a conversation the user can reach; ``agent_id`` is "" when it has none."""
|
||||
from docsgpt.storage.db.repositories.conversations import ConversationsRepository
|
||||
from docsgpt.storage.db.session import db_readonly
|
||||
|
||||
if not user_id:
|
||||
return False, ""
|
||||
try:
|
||||
with db_readonly() as conn:
|
||||
row = ConversationsRepository(conn).get_any(str(conversation_id), user_id)
|
||||
except Exception:
|
||||
return False, ""
|
||||
if not row:
|
||||
return False, ""
|
||||
return True, str(row.get("agent_id") or "")
|
||||
|
||||
|
||||
def _chat_check(request, resource_filter: dict, user_id: Optional[str]) -> Optional[str]:
|
||||
"""Keep a restricted token's chat traffic inside its allowlists.
|
||||
|
||||
An agent brings its own sources, prompt and tools, which this table cannot
|
||||
see, so a token restricted on any family must name an allowed agent (or,
|
||||
when only sources are restricted, chat against allowed sources directly).
|
||||
An agent ``api_key`` in the body would swap in an arbitrary agent.
|
||||
see, so a restricted token must name an allowed agent. The one exception is
|
||||
a token restricted on sources only, which may chat against allowed sources
|
||||
directly. Everything that could swap in another agent or another set of
|
||||
resources is refused: an agent ``api_key``, an inline workflow, and a
|
||||
``conversation_id`` that belongs to a different agent (the server would
|
||||
otherwise continue, append to, or resume tool calls of that conversation).
|
||||
|
||||
Chat executes tools: an agent's own, or the user's defaults when there is
|
||||
no agent. Neither can be held to a tools allowlist from here, so a token
|
||||
restricted on tools cannot chat at all.
|
||||
"""
|
||||
body = _json_body(request)
|
||||
if "tools" in resource_filter:
|
||||
return "A token restricted to specific tools cannot use chat endpoints"
|
||||
if body.get("api_key"):
|
||||
return "A restricted token cannot chat with an agent API key; pass agent_id"
|
||||
if body.get("workflow"):
|
||||
@@ -88,11 +117,33 @@ def _chat_check(request, resource_filter: dict) -> Optional[str]:
|
||||
return "A restricted token cannot run an inline workflow"
|
||||
agent_ids = _as_ids(body.get("agent_id"))
|
||||
if "agents" in resource_filter:
|
||||
if not agent_ids:
|
||||
if len(agent_ids) != 1:
|
||||
return "This token is restricted to specific agents; pass agent_id"
|
||||
return None # the agent id itself is verified through ``refs``
|
||||
if agent_ids:
|
||||
return "This token is restricted to specific resources and cannot run arbitrary agents"
|
||||
# the agent id itself is verified through ``refs``
|
||||
elif set(resource_filter) - {"sources"}:
|
||||
return "Restrict this token to specific agents to use chat endpoints"
|
||||
elif agent_ids:
|
||||
return "This token is restricted to specific sources and cannot run agents"
|
||||
conversation_id = body.get("conversation_id")
|
||||
if conversation_id:
|
||||
found, conversation_agent = _conversation_agent_id(conversation_id, user_id)
|
||||
expected = agent_ids[0] if agent_ids else ""
|
||||
if not found or _canonical(conversation_agent) != _canonical(expected):
|
||||
return "This conversation does not belong to the agent this token may use"
|
||||
return None
|
||||
|
||||
|
||||
def _agent_body_check(request, resource_filter: dict, user_id: Optional[str]) -> Optional[str]:
|
||||
"""A workflow pulls in its own sources, tools and prompts, which a reference check cannot see.
|
||||
|
||||
A token restricted on any of those may attach a workflow to an agent only
|
||||
when it is also restricted on workflows, so the workflow is one its owner
|
||||
chose (``refs`` then verifies the id).
|
||||
"""
|
||||
if "workflows" in resource_filter or not set(resource_filter) & {"sources", "tools", "prompts"}:
|
||||
return None
|
||||
if _read(request, (BODY, "workflow")):
|
||||
return "A token restricted to specific sources, tools or prompts cannot attach a workflow to an agent"
|
||||
return None
|
||||
|
||||
|
||||
@@ -124,9 +175,9 @@ RULES: dict[tuple[str, str], Rule] = {
|
||||
("/api/guardrails/summary", "GET"): _rule("agents:read", (QUERY, "agent_id")),
|
||||
("/api/agents/folders/", "GET"): _rule("agents:read", open=True),
|
||||
("/api/agents/folders/<string:folder_id>", "GET"): _rule("agents:read"),
|
||||
("/api/create_agent", "POST"): _rule("agents:write", refs=_AGENT_BODY_REFS),
|
||||
("/api/create_agent", "POST"): _rule("agents:write", refs=_AGENT_BODY_REFS, check=_agent_body_check),
|
||||
("/api/update_agent/<string:agent_id>", "PUT"): _rule(
|
||||
"agents:write", (VIEW, "agent_id"), refs=_AGENT_BODY_REFS
|
||||
"agents:write", (VIEW, "agent_id"), refs=_AGENT_BODY_REFS, check=_agent_body_check
|
||||
),
|
||||
("/api/delete_agent", "DELETE"): _rule("agents:write", (QUERY, "id")),
|
||||
("/api/adopt_agent", "POST"): _rule("agents:write"),
|
||||
@@ -142,22 +193,25 @@ RULES: dict[tuple[str, str], Rule] = {
|
||||
("/api/agents/folders/bulk_move", "POST"): _rule("agents:write", (JSON, "agent_ids")),
|
||||
("/api/regenerate_agent_key/<string:agent_id>", "POST"): _rule("agents:keys", (VIEW, "agent_id")),
|
||||
("/api/agent_webhook", "GET"): _rule("agents:keys", (QUERY, "id")),
|
||||
# Schedules hang off an agent.
|
||||
# Schedules hang off an agent, and a schedule runs that agent with a free-form
|
||||
# instruction and stores the output. The agent id proves the agent and nothing
|
||||
# else, so tokens restricted on any other family are kept out; routes that
|
||||
# carry only a schedule id prove nothing and are closed to every restricted token.
|
||||
("/api/agents/<string:agent_id>/schedules", "GET"): _rule(
|
||||
"schedules:read", refs=(("agents", (VIEW, "agent_id")),)
|
||||
"schedules:read", refs=(("agents", (VIEW, "agent_id")),), blocked_by=_NON_AGENT_FAMILIES
|
||||
),
|
||||
("/api/agents/<string:agent_id>/schedules", "POST"): _rule(
|
||||
"schedules:write", refs=(("agents", (VIEW, "agent_id")),), blocked_by=("tools",)
|
||||
"schedules:write", refs=(("agents", (VIEW, "agent_id")),), blocked_by=_NON_AGENT_FAMILIES
|
||||
),
|
||||
("/api/schedules/<string:schedule_id>", "GET"): _rule("schedules:read", blocked_by=("agents",)),
|
||||
("/api/schedules/<string:schedule_id>/runs", "GET"): _rule("schedules:read", blocked_by=("agents",)),
|
||||
("/api/schedules/<string:schedule_id>", "GET"): _rule("schedules:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/schedules/<string:schedule_id>/runs", "GET"): _rule("schedules:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/schedules/<string:schedule_id>/runs/<string:run_id>", "GET"): _rule(
|
||||
"schedules:read", blocked_by=("agents",)
|
||||
"schedules:read", blocked_by=_ALL_FAMILIES
|
||||
),
|
||||
("/api/schedules/<string:schedule_id>", "PUT"): _rule("schedules:write", blocked_by=("agents", "tools")),
|
||||
("/api/schedules/<string:schedule_id>", "PATCH"): _rule("schedules:write", blocked_by=("agents", "tools")),
|
||||
("/api/schedules/<string:schedule_id>", "DELETE"): _rule("schedules:write", blocked_by=("agents",)),
|
||||
("/api/schedules/<string:schedule_id>/run", "POST"): _rule("schedules:write", blocked_by=("agents",)),
|
||||
("/api/schedules/<string:schedule_id>", "PUT"): _rule("schedules:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/schedules/<string:schedule_id>", "PATCH"): _rule("schedules:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/schedules/<string:schedule_id>", "DELETE"): _rule("schedules:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/schedules/<string:schedule_id>/run", "POST"): _rule("schedules:write", blocked_by=_ALL_FAMILIES),
|
||||
# Sources
|
||||
("/api/sources", "GET"): _rule("sources:read", listing=True),
|
||||
# Counted and paged in SQL, so it cannot be narrowed here; restricted tokens use /api/sources.
|
||||
@@ -217,28 +271,32 @@ RULES: dict[tuple[str, str], Rule] = {
|
||||
("/api/user/models/test", "POST"): _rule("models:write"),
|
||||
("/api/user/models/<string:model_id>/test", "POST"): _rule("models:write"),
|
||||
# Workflows
|
||||
("/api/workflows", "POST"): _rule("workflows:write"),
|
||||
# A workflow graph names sources, tools and prompts inside its nodes, out of reach of ``refs``.
|
||||
("/api/workflows", "POST"): _rule("workflows:write", blocked_by=_WORKFLOW_CONTENT_FAMILIES),
|
||||
("/api/workflows/<string:workflow_id>", "GET"): _rule("workflows:read", (VIEW, "workflow_id")),
|
||||
("/api/workflows/<string:workflow_id>", "PUT"): _rule("workflows:write", (VIEW, "workflow_id")),
|
||||
("/api/workflows/<string:workflow_id>", "PUT"): _rule(
|
||||
"workflows:write", (VIEW, "workflow_id"), blocked_by=_WORKFLOW_CONTENT_FAMILIES
|
||||
),
|
||||
("/api/workflows/<string:workflow_id>", "DELETE"): _rule("workflows:write", (VIEW, "workflow_id")),
|
||||
# Conversations and analytics span every agent, so an agent-restricted token is kept out.
|
||||
("/api/get_conversations", "GET"): _rule("conversations:read", blocked_by=("agents",)),
|
||||
("/api/search_conversations", "GET"): _rule("conversations:read", blocked_by=("agents",)),
|
||||
("/api/get_single_conversation", "GET"): _rule("conversations:read", blocked_by=("agents",)),
|
||||
# Conversations and analytics span every agent and carry cited source text and tool
|
||||
# output, so they are closed to every restricted token.
|
||||
("/api/get_conversations", "GET"): _rule("conversations:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/search_conversations", "GET"): _rule("conversations:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_single_conversation", "GET"): _rule("conversations:read", blocked_by=_ALL_FAMILIES),
|
||||
# A message cannot be tied to an allowlist from here, so any restricted token is kept out.
|
||||
("/api/messages/<string:message_id>/tail", "GET"): _rule(
|
||||
any_of=MESSAGE_REPLAY_SCOPES, family=None, blocked_by=_ALL_FAMILIES
|
||||
),
|
||||
("/api/delete_conversation", "POST"): _rule("conversations:write", blocked_by=("agents",)),
|
||||
("/api/delete_all_conversations", "GET"): _rule("conversations:write", blocked_by=("agents",)),
|
||||
("/api/update_conversation_name", "POST"): _rule("conversations:write", blocked_by=("agents",)),
|
||||
("/api/feedback", "POST"): _rule("conversations:write", blocked_by=("agents",)),
|
||||
("/api/get_message_analytics", "POST"): _rule("analytics:read", blocked_by=("agents",)),
|
||||
("/api/get_token_analytics", "POST"): _rule("analytics:read", blocked_by=("agents",)),
|
||||
("/api/get_feedback_analytics", "POST"): _rule("analytics:read", blocked_by=("agents",)),
|
||||
("/api/get_tool_analytics", "POST"): _rule("analytics:read", blocked_by=("agents",)),
|
||||
("/api/get_schedule_analytics", "POST"): _rule("analytics:read", blocked_by=("agents",)),
|
||||
("/api/get_user_logs", "POST"): _rule("analytics:read", blocked_by=("agents",)),
|
||||
("/api/delete_conversation", "POST"): _rule("conversations:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/delete_all_conversations", "GET"): _rule("conversations:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/update_conversation_name", "POST"): _rule("conversations:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/feedback", "POST"): _rule("conversations:write", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_message_analytics", "POST"): _rule("analytics:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_token_analytics", "POST"): _rule("analytics:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_feedback_analytics", "POST"): _rule("analytics:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_tool_analytics", "POST"): _rule("analytics:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_schedule_analytics", "POST"): _rule("analytics:read", blocked_by=_ALL_FAMILIES),
|
||||
("/api/get_user_logs", "POST"): _rule("analytics:read", blocked_by=_ALL_FAMILIES),
|
||||
# Teams (read only)
|
||||
("/api/teams", "GET"): _rule("teams:read"),
|
||||
("/api/teams/<string:team_id>", "GET"): _rule("teams:read"),
|
||||
@@ -395,18 +453,18 @@ def authorize(request, decoded_token: dict) -> Optional[tuple[dict, int]]:
|
||||
resource_filter = decoded_token.get("resource_filter") or {}
|
||||
if not resource_filter:
|
||||
return None
|
||||
reason = _check_resources(request, rule, resource_filter)
|
||||
reason = _check_resources(request, rule, resource_filter, decoded_token.get("sub"))
|
||||
if reason is None:
|
||||
return None
|
||||
return ({"success": False, "error": "resource_not_allowed", "message": reason}, 403)
|
||||
|
||||
|
||||
def _check_resources(request, rule: Rule, resource_filter: dict) -> Optional[str]:
|
||||
def _check_resources(request, rule: Rule, resource_filter: dict, user_id: Optional[str] = None) -> Optional[str]:
|
||||
for family in rule.blocked_by:
|
||||
if family in resource_filter:
|
||||
return f"This endpoint is not available to a token restricted to specific {family}"
|
||||
if rule.check is not None:
|
||||
reason = rule.check(request, resource_filter)
|
||||
reason = rule.check(request, resource_filter, user_id)
|
||||
if reason:
|
||||
return reason
|
||||
for family, locator in rule.refs:
|
||||
@@ -459,3 +517,19 @@ def _is_uuid(value: str) -> bool:
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def may_see_agent_keys(request) -> bool:
|
||||
"""False for a token without ``agents:keys``: it must not receive a plaintext agent API key.
|
||||
|
||||
Create, first publish and adopt all mint a key and used to return it, which
|
||||
handed a deploy token a secret that outlives the token's own revocation.
|
||||
"""
|
||||
decoded = getattr(request, "decoded_token", None)
|
||||
if not is_pat(decoded):
|
||||
return True
|
||||
return "agents:keys" in (decoded.get("scopes") or [])
|
||||
|
||||
|
||||
def mask_agent_key(key: Optional[str]) -> str:
|
||||
return f"{key[:4]}...{key[-4:]}" if key else ""
|
||||
@@ -121,6 +121,9 @@ def normalize_resource_filter(raw: Any, scopes: list[str]) -> dict[str, list[str
|
||||
# chat:run acts on agents and sources, so both may be restricted alongside it.
|
||||
if "chat" in families:
|
||||
families.update({"agents", "sources"})
|
||||
if "tools" in raw and "chat:run" in scopes:
|
||||
# Chat executes tools (an agent's own, or the user's defaults), which cannot be held to an allowlist.
|
||||
raise ValueError("resource_filter.tools cannot be combined with the chat:run scope")
|
||||
out: dict[str, list[str]] = {}
|
||||
for family, ids in raw.items():
|
||||
if family not in FILTERABLE_FAMILIES:
|
||||
|
||||
@@ -9,7 +9,7 @@ from flask_restx import fields, Namespace, Resource
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
|
||||
from docsgpt.api import api
|
||||
from docsgpt.api.pat.rules import filter_listing
|
||||
from docsgpt.api.pat.rules import filter_listing, mask_agent_key, may_see_agent_keys
|
||||
from docsgpt.guardrails.config import AgentConfig
|
||||
from docsgpt.api.user.base import (
|
||||
copy_agent_image_for_user,
|
||||
@@ -763,7 +763,9 @@ class CreateAgent(Resource):
|
||||
except Exception as err:
|
||||
current_app.logger.error(f"Error creating agent: {err}", exc_info=True)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify({"id": new_id, "key": key}), 201)
|
||||
# A token without agents:keys never receives the plaintext agent key.
|
||||
visible_key = key if may_see_agent_keys(request) else mask_agent_key(key)
|
||||
return make_response(jsonify({"id": new_id, "key": visible_key}), 201)
|
||||
|
||||
|
||||
@agents_ns.route("/update_agent/<string:agent_id>")
|
||||
@@ -1307,7 +1309,11 @@ class UpdateAgent(Resource):
|
||||
"message": "Agent updated successfully",
|
||||
}
|
||||
if newly_generated_key:
|
||||
response_data["key"] = newly_generated_key
|
||||
response_data["key"] = (
|
||||
newly_generated_key
|
||||
if may_see_agent_keys(request)
|
||||
else mask_agent_key(newly_generated_key)
|
||||
)
|
||||
return make_response(jsonify(response_data), 200)
|
||||
|
||||
|
||||
@@ -1811,7 +1817,9 @@ class AdoptAgent(Resource):
|
||||
)
|
||||
|
||||
response_agent = _format_agent_output(new_agent, include_key_masked=False)
|
||||
response_agent["key"] = new_key
|
||||
response_agent["key"] = (
|
||||
new_key if may_see_agent_keys(request) else mask_agent_key(new_key)
|
||||
)
|
||||
return make_response(
|
||||
jsonify({"success": True, "agent": response_agent}), 200
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user