mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 06:17:23 +00:00
Gate an API tool action only when it sends the owner's saved values
Every API tool counted as holding the owner's credentials, so outside callers were refused any write on one even when it sends nothing the owner stored. Now an action counts only when its headers or query parameters carry a saved value (sealed or legacy plaintext) or the tool stores credentials; the allowlist lists just those writes.
This commit is contained in:
1 parent
029189fe0c
commit
080b75c81e
6 files changed
+77
-22
No files matched your search
@@ -491,9 +491,10 @@ class TestToolPrefetch:
|
||||
def test_api_key_callers_prefetch_like_someone_else(self, pg_conn):
|
||||
"""A widget or API run carries the owner's id, but the caller is not the owner."""
|
||||
agent_id, _ = _agent(pg_conn)
|
||||
key = {"type": "object", "properties": {"X-Key": {"type": "string", "value": "", "has_value": True}}}
|
||||
api = str(UserToolsRepository(pg_conn).create(OWNER, "api_tool", config={"actions": {
|
||||
"status": {"url": "https://x.test/s", "method": "GET", "active": True},
|
||||
"notify": {"url": "https://x.test/n", "method": "POST", "active": True},
|
||||
"status": {"url": "https://x.test/s", "method": "GET", "active": True, "headers": key},
|
||||
"notify": {"url": "https://x.test/n", "method": "POST", "active": True, "headers": key},
|
||||
}})["id"])
|
||||
AgentsRepository(pg_conn).update_by_id(agent_id, {"tools": [api]})
|
||||
required = {"api_tool": {None}}
|
||||
|
||||
@@ -244,9 +244,18 @@ def _stored_tool(name: str, action: dict, config: dict) -> dict:
|
||||
return {"id": "tool-9", "user_id": "alice", "name": name, "config": config, "actions": [action]}
|
||||
|
||||
|
||||
def _api_tool(method: str) -> dict:
|
||||
tool = _stored_tool("api_tool", {}, {"actions": {"call": {"url": "https://x.test", "method": method,
|
||||
"active": True, "require_approval": False}}})
|
||||
def _api_tool(method: str, header: dict | None = None, **config) -> dict:
|
||||
"""An API tool with one ``call`` action; ``header`` is its Authorization header spec.
|
||||
|
||||
By default the header holds a sealed secret (``has_value``), the way a
|
||||
saved key is stored.
|
||||
"""
|
||||
action = {"url": "https://x.test", "method": method, "active": True, "require_approval": False}
|
||||
if header is None:
|
||||
header = {"type": "string", "value": "", "has_value": True}
|
||||
if header:
|
||||
action["headers"] = {"type": "object", "properties": {"Authorization": header}}
|
||||
tool = _stored_tool("api_tool", {}, {"actions": {"call": action}, **config})
|
||||
tool["actions"] = []
|
||||
return tool
|
||||
|
||||
@@ -266,6 +275,23 @@ class TestOwnerHeldCredentialsWithoutAConnection:
|
||||
assert pause["pause_type"] == "headless_denied"
|
||||
assert "Access details" in pause["deny_reason"]
|
||||
|
||||
@pytest.mark.parametrize("header", [
|
||||
{"type": "string", "value": "", "has_value": True},
|
||||
{"type": "string", "value": "Bearer legacy-plaintext"},
|
||||
])
|
||||
def test_api_tool_with_a_stored_header_value_is_gated(self, header):
|
||||
pause = _pause(self._caller(public_link_caller=True), _api_tool("POST", header), action="call")
|
||||
assert pause["pause_type"] == "headless_denied"
|
||||
|
||||
def test_api_tool_with_stored_credentials_in_config_is_gated(self):
|
||||
tool = _api_tool("POST", {}, encrypted_credentials="blob")
|
||||
assert _pause(self._caller(public_link_caller=True), tool, action="call")["pause_type"] == "headless_denied"
|
||||
|
||||
@pytest.mark.parametrize("header", [{}, {"type": "string", "value": "", "has_value": False},
|
||||
{"type": "string", "filled_by_llm": True}])
|
||||
def test_api_tool_without_credentials_is_not_gated(self, header):
|
||||
assert _pause(self._caller(public_link_caller=True), _api_tool("POST", header), action="call") is None
|
||||
|
||||
def test_api_tool_read_runs(self):
|
||||
assert _pause(self._caller(public_link_caller=True), _api_tool("GET"), action="call") is None
|
||||
|
||||
|
||||
@@ -313,9 +313,12 @@ class TestOwnerCredentialWrites:
|
||||
from docsgpt.storage.db.repositories.user_tools import UserToolsRepository
|
||||
|
||||
repo = UserToolsRepository(pg_conn)
|
||||
key = {"type": "object", "properties": {"X-Key": {"type": "string", "value": "", "has_value": True}}}
|
||||
repo.create("alice", "api_tool", config={"actions": {
|
||||
"status": {"url": "https://x.test/s", "method": "GET", "active": True},
|
||||
"notify": {"url": "https://x.test/n", "method": "POST", "active": True},
|
||||
"status": {"url": "https://x.test/s", "method": "GET", "active": True, "headers": key},
|
||||
"notify": {"url": "https://x.test/n", "method": "POST", "active": True, "headers": key},
|
||||
# A write that sends nothing of the owner's is not listed.
|
||||
"ping": {"url": "https://x.test/p", "method": "POST", "active": True},
|
||||
}})
|
||||
repo.create("alice", "mcp_tool", config={"server_url": "https://m.test/mcp", "auth_type": "bearer"},
|
||||
actions=[{"name": "create_issue", "active": True}, {"name": "list_issues", "active": True}])
|
||||
|
||||
Reference in new issue
Block a user