mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-06-29 15:31:05 +08:00
fix: require explicit anonymous webhook access (#14890)
### What problem does this PR solve? Fixes #14882 Agent webhook execution currently fails open when the saved webhook `security` block is missing/empty, or when `auth_type` is set to `none`. This allows unauthenticated webhook invocation without an explicit operator opt-in. This PR makes anonymous webhook access explicit: - Rejects missing or empty webhook security config. - Requires `allow_anonymous: true` when `auth_type` is `none`. - Preserves explicit anonymous webhooks by having the frontend serialize `allow_anonymous: true` when the user selects `None` auth. - Updates webhook unit tests to cover both denied implicit-anonymous configs and allowed explicit-anonymous configs. ### Type of change - [x] Bug Fix - [x] Security hardening - [x] Test ### Tests - [x] `ZHIPU_AI_API_KEY=dummy uv run python -m pytest --confcutdir=test/testcases/test_web_api/test_agent_app test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py` - [x] `uv run ruff check api/apps/restful_apis/agent_api.py test/testcases/test_web_api/test_agent_app/test_agents_webhook_unit.py` - [x] `npm exec eslint src/pages/agent/utils.ts src/pages/agent/form/begin-form/schema.ts` --------- Co-authored-by: Zhichang Yu <yuzhichang@gmail.com>
This commit is contained in:
@@ -167,6 +167,12 @@ def _default_webhook_params(
|
||||
}
|
||||
|
||||
|
||||
def _anonymous_security(**overrides):
|
||||
security = {"auth_type": "none", "allow_anonymous": True}
|
||||
security.update(overrides)
|
||||
return security
|
||||
|
||||
|
||||
def _make_webhook_cvs(module, *, params=None, dsl=None, canvas_category=None):
|
||||
if dsl is None:
|
||||
if params is None:
|
||||
@@ -654,7 +660,15 @@ def test_webhook_security_dispatch(monkeypatch):
|
||||
_DummyRequest(headers={"Content-Type": "application/json"}, json_body={}, args={"a": "b"}),
|
||||
)
|
||||
|
||||
for security in ({}, {"auth_type": "none"}):
|
||||
for security, message in (
|
||||
({}, "Webhook security is required"),
|
||||
({"auth_type": "none"}, "Anonymous webhook access requires allow_anonymous"),
|
||||
):
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), message)
|
||||
|
||||
for security in (_anonymous_security(), _anonymous_security(allow_anonymous="true")):
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs))
|
||||
res = _run(module.webhook("agent-1"))
|
||||
@@ -666,6 +680,30 @@ def test_webhook_security_dispatch(monkeypatch):
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Unsupported auth_type")
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_webhook_test_requires_owner(monkeypatch):
|
||||
module = _load_agents_app(monkeypatch)
|
||||
_patch_background_task(monkeypatch, module)
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"request",
|
||||
_DummyRequest(path="/api/v1/agents/agent-1/webhook/test", headers={"Content-Type": "application/json"}, json_body={}),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [])
|
||||
denied = _run(module.webhook_test(agent_id="agent-1"))
|
||||
assert denied["code"] == module.RetCode.OPERATING_ERROR
|
||||
assert "Only the owner" in denied["message"]
|
||||
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=_anonymous_security()))
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [cvs])
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
allowed = _run(module.webhook_test(agent_id="agent-1"))
|
||||
assert hasattr(allowed, "status_code")
|
||||
assert allowed.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_webhook_max_body_size(monkeypatch):
|
||||
module = _load_agents_app(monkeypatch)
|
||||
@@ -674,18 +712,18 @@ def test_webhook_max_body_size(monkeypatch):
|
||||
base_request = _DummyRequest(headers={"Content-Type": "application/json"}, json_body={})
|
||||
monkeypatch.setattr(module, "request", base_request)
|
||||
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security={"auth_type": "none"}))
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=_anonymous_security()))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
res = _run(module.webhook("agent-1"))
|
||||
assert hasattr(res, "status_code")
|
||||
assert res.status_code == 200
|
||||
|
||||
security = {"auth_type": "none", "max_body_size": "123"}
|
||||
security = _anonymous_security(max_body_size="123")
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Invalid max_body_size format")
|
||||
|
||||
security = {"auth_type": "none", "max_body_size": "11mb"}
|
||||
security = _anonymous_security(max_body_size="11mb")
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "exceeds maximum allowed size")
|
||||
@@ -695,11 +733,35 @@ def test_webhook_max_body_size(monkeypatch):
|
||||
"request",
|
||||
_DummyRequest(headers={"Content-Type": "application/json"}, json_body={}, content_length=2048),
|
||||
)
|
||||
security = {"auth_type": "none", "max_body_size": "1kb"}
|
||||
security = _anonymous_security(max_body_size="1kb")
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Request body too large")
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"request",
|
||||
_DummyRequest(headers={"Content-Type": "application/json"}, json_body={}, content_length=10 * 1024 * 1024 + 1),
|
||||
)
|
||||
security = _anonymous_security()
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Request body too large")
|
||||
|
||||
token_security = {"auth_type": "token", "token": {"token_header": "X-TOKEN", "token_value": "ok"}}
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=token_security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"request",
|
||||
_DummyRequest(
|
||||
headers={"Content-Type": "application/json", "X-TOKEN": "ok"},
|
||||
json_body={},
|
||||
content_length=10 * 1024 * 1024 + 1,
|
||||
),
|
||||
)
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Request body too large")
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_webhook_ip_whitelist(monkeypatch):
|
||||
@@ -713,14 +775,14 @@ def test_webhook_ip_whitelist(monkeypatch):
|
||||
)
|
||||
|
||||
for whitelist in ([], ["127.0.0.0/24"], ["127.0.0.1"]):
|
||||
security = {"auth_type": "none", "ip_whitelist": whitelist}
|
||||
security = _anonymous_security(ip_whitelist=whitelist)
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs))
|
||||
res = _run(module.webhook("agent-1"))
|
||||
assert hasattr(res, "status_code"), res
|
||||
assert res.status_code == 200
|
||||
|
||||
security = {"auth_type": "none", "ip_whitelist": ["10.0.0.1"]}
|
||||
security = _anonymous_security(ip_whitelist=["10.0.0.1"])
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "is not allowed")
|
||||
@@ -733,25 +795,47 @@ def test_webhook_rate_limit(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(module, "request", _DummyRequest(headers={"Content-Type": "application/json"}, json_body={}))
|
||||
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security={"auth_type": "none"}))
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=_anonymous_security()))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
res = _run(module.webhook("agent-1"))
|
||||
assert hasattr(res, "status_code")
|
||||
assert res.status_code == 200
|
||||
|
||||
bad_limit = {"auth_type": "none", "rate_limit": {"limit": 0, "per": "minute"}}
|
||||
module.REDIS_CONN.bucket_result = [0]
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=_anonymous_security()))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Too many requests")
|
||||
|
||||
module.REDIS_CONN.bucket_result = [1]
|
||||
token_security = {"auth_type": "token", "token": {"token_header": "X-TOKEN", "token_value": "ok"}}
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=token_security))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"request",
|
||||
_DummyRequest(headers={"Content-Type": "application/json", "X-TOKEN": "ok"}, json_body={}),
|
||||
)
|
||||
res = _run(module.webhook("agent-1"))
|
||||
assert hasattr(res, "status_code")
|
||||
assert res.status_code == 200
|
||||
|
||||
module.REDIS_CONN.bucket_result = [0]
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Too many requests")
|
||||
|
||||
module.REDIS_CONN.bucket_result = [1]
|
||||
bad_limit = _anonymous_security(rate_limit={"limit": 0, "per": "minute"})
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=bad_limit))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "rate_limit.limit must be > 0")
|
||||
|
||||
bad_per = {"auth_type": "none", "rate_limit": {"limit": 1, "per": "week"}}
|
||||
bad_per = _anonymous_security(rate_limit={"limit": 1, "per": "week"})
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=bad_per))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Invalid rate_limit.per")
|
||||
|
||||
module.REDIS_CONN.bucket_result = [0]
|
||||
module.REDIS_CONN.bucket_exc = None
|
||||
denied = {"auth_type": "none", "rate_limit": {"limit": 1, "per": "minute"}}
|
||||
denied = _anonymous_security(rate_limit={"limit": 1, "per": "minute"})
|
||||
cvs = _make_webhook_cvs(module, params=_default_webhook_params(security=denied))
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
_assert_bad_request(_run(module.webhook("agent-1")), "Too many requests")
|
||||
@@ -869,7 +953,7 @@ def test_webhook_parse_request_branches(monkeypatch):
|
||||
module = _load_agents_app(monkeypatch)
|
||||
_patch_background_task(monkeypatch, module)
|
||||
|
||||
security = {"auth_type": "none"}
|
||||
security = _anonymous_security()
|
||||
params = _default_webhook_params(security=security, content_types="application/json")
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
@@ -935,7 +1019,7 @@ def test_webhook_parse_request_branches(monkeypatch):
|
||||
def test_webhook_canvas_constructor_exception(monkeypatch):
|
||||
module = _load_agents_app(monkeypatch)
|
||||
|
||||
params = _default_webhook_params(security={"auth_type": "none"})
|
||||
params = _default_webhook_params(security=_anonymous_security())
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
monkeypatch.setattr(
|
||||
@@ -1048,7 +1132,7 @@ def test_webhook_parse_request_form_and_raw_body_paths(monkeypatch):
|
||||
module = _load_agents_app(monkeypatch)
|
||||
_patch_background_task(monkeypatch, module)
|
||||
|
||||
security = {"auth_type": "none"}
|
||||
security = _anonymous_security()
|
||||
|
||||
def _run_with(params, req):
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
@@ -1137,7 +1221,7 @@ def test_webhook_schema_extract_cast_defaults_and_validation_errors(monkeypatch)
|
||||
}
|
||||
|
||||
params = _default_webhook_params(
|
||||
security={"auth_type": "none"},
|
||||
security=_anonymous_security(),
|
||||
content_types="application/json",
|
||||
schema=base_schema,
|
||||
)
|
||||
@@ -1216,7 +1300,7 @@ def test_webhook_schema_extract_cast_defaults_and_validation_errors(monkeypatch)
|
||||
|
||||
for schema, body_payload, expected_substring in failure_cases:
|
||||
params = _default_webhook_params(
|
||||
security={"auth_type": "none"},
|
||||
security=_anonymous_security(),
|
||||
content_types="application/json",
|
||||
schema=schema,
|
||||
)
|
||||
@@ -1238,7 +1322,7 @@ def test_webhook_immediate_response_status_and_template_validation(monkeypatch):
|
||||
|
||||
def _run_case(response_cfg):
|
||||
params = _default_webhook_params(
|
||||
security={"auth_type": "none"},
|
||||
security=_anonymous_security(),
|
||||
content_types="application/json",
|
||||
response=response_cfg,
|
||||
)
|
||||
@@ -1301,8 +1385,9 @@ def test_webhook_background_run_success_and_error_trace_paths(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(module, "Canvas", _CanvasSuccess)
|
||||
|
||||
params = _default_webhook_params(security={"auth_type": "none"}, content_types="application/json")
|
||||
params = _default_webhook_params(security=_anonymous_security(), content_types="application/json")
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [cvs])
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
@@ -1310,7 +1395,7 @@ def test_webhook_background_run_success_and_error_trace_paths(monkeypatch):
|
||||
_DummyRequest(path="/api/v1/agents/agent-1/webhook/test", headers={"Content-Type": "application/json"}, json_body={}),
|
||||
)
|
||||
|
||||
res = _run(module.webhook("agent-1"))
|
||||
res = _run(module.webhook_test(agent_id="agent-1"))
|
||||
assert res.status_code == 200
|
||||
assert len(tasks) == 1
|
||||
_run(tasks.pop(0))
|
||||
@@ -1332,8 +1417,9 @@ def test_webhook_background_run_success_and_error_trace_paths(monkeypatch):
|
||||
tasks.clear()
|
||||
redis_store.clear()
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [cvs])
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs))
|
||||
res = _run(module.webhook("agent-1"))
|
||||
res = _run(module.webhook_test(agent_id="agent-1"))
|
||||
assert res.status_code == 200
|
||||
_run(tasks.pop(0))
|
||||
trace_obj = json.loads(redis_store[key])
|
||||
@@ -1348,8 +1434,9 @@ def test_webhook_background_run_success_and_error_trace_paths(monkeypatch):
|
||||
monkeypatch.setattr(module.REDIS_CONN, "set_obj", lambda *_args, **_kwargs: None)
|
||||
tasks.clear()
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [cvs])
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id, _cvs=cvs: (True, _cvs))
|
||||
_run(module.webhook("agent-1"))
|
||||
_run(module.webhook_test(agent_id="agent-1"))
|
||||
_run(tasks.pop(0))
|
||||
assert any("Failed to append webhook trace" in msg for msg in log_messages)
|
||||
|
||||
@@ -1363,11 +1450,12 @@ def test_webhook_sse_success_and_exception_paths(monkeypatch):
|
||||
monkeypatch.setattr(module.REDIS_CONN, "set_obj", lambda key, obj, _ttl: redis_store.__setitem__(key, json.dumps(obj)))
|
||||
|
||||
params = _default_webhook_params(
|
||||
security={"auth_type": "none"},
|
||||
security=_anonymous_security(),
|
||||
content_types="application/json",
|
||||
execution_mode="Deferred",
|
||||
)
|
||||
cvs = _make_webhook_cvs(module, params=params)
|
||||
monkeypatch.setattr(module.UserCanvasService, "query", lambda **_kwargs: [cvs])
|
||||
monkeypatch.setattr(module.UserCanvasService, "get_by_id", lambda _id: (True, cvs))
|
||||
|
||||
class _CanvasSSESuccess(_StubCanvas):
|
||||
@@ -1383,7 +1471,7 @@ def test_webhook_sse_success_and_exception_paths(monkeypatch):
|
||||
"request",
|
||||
_DummyRequest(path="/api/v1/agents/agent-1/webhook/test", headers={"Content-Type": "application/json"}, json_body={}),
|
||||
)
|
||||
res = _run(module.webhook("agent-1"))
|
||||
res = _run(module.webhook_test(agent_id="agent-1"))
|
||||
assert res.status_code == 201
|
||||
payload = json.loads(_run(res.get_data(as_text=True)))
|
||||
assert payload == {"message": "<think></think>Hello", "success": True, "code": 201}
|
||||
@@ -1399,7 +1487,7 @@ def test_webhook_sse_success_and_exception_paths(monkeypatch):
|
||||
"request",
|
||||
_DummyRequest(path="/api/v1/agents/agent-1/webhook/test", headers={"Content-Type": "application/json"}, json_body={}),
|
||||
)
|
||||
res = _run(module.webhook("agent-1"))
|
||||
res = _run(module.webhook_test(agent_id="agent-1"))
|
||||
assert res.status_code == 400
|
||||
payload = json.loads(_run(res.get_data(as_text=True)))
|
||||
assert payload["code"] == 400
|
||||
|
||||
Reference in New Issue
Block a user