mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 05:30:39 +00:00
fix(dingtalk): validate access token expiry (#7450)
* fix(dingtalk): validate access token expiry * fix(dingtalk): reject token lifetimes within refresh margin --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
63211d5da6
commit
b3b1424799
@ -1346,6 +1346,8 @@ Scheduled-task result and lifecycle notices can also be pushed to a bound Feishu
|
||||
DingTalk's Markdown fallback displays tables as labeled blockquotes, preserving escaped pipes (`\|`) within cells and empty leading or trailing cells so values stay under the correct column labels.
|
||||
|
||||
|
||||
DingTalk token caching uses a two-hour fallback when the provider's token lifetime is invalid or no longer than the five-minute refresh margin, avoiding a refresh on every request.
|
||||
|
||||
When DeerFlow runs in Docker Compose, IM channels execute inside the `gateway` container. In that case, do not point `channels.langgraph_url` or `channels.gateway_url` at `localhost`; use container service names such as `http://gateway:8001/api` and `http://gateway:8001`, or set `DEER_FLOW_CHANNELS_LANGGRAPH_URL` and `DEER_FLOW_CHANNELS_GATEWAY_URL`.
|
||||
|
||||
**Commands**
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1101,14 +1101,20 @@ class DingTalkChannel(Channel):
|
||||
raise ValueError("DingTalk access token response did not contain a usable accessToken")
|
||||
|
||||
raw_expires_in = data.get("expireIn", 7200)
|
||||
now = time.monotonic()
|
||||
try:
|
||||
expires_in = int(raw_expires_in)
|
||||
except (TypeError, ValueError):
|
||||
# A lifetime within the refresh margin is already stale.
|
||||
if expires_in <= _TOKEN_REFRESH_MARGIN_SECONDS:
|
||||
raise ValueError
|
||||
expires_at = now + expires_in - _TOKEN_REFRESH_MARGIN_SECONDS
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
logger.warning("[DingTalk] invalid expireIn value %r, using default 7200s", raw_expires_in)
|
||||
expires_in = 7200
|
||||
expires_at = now + expires_in - _TOKEN_REFRESH_MARGIN_SECONDS
|
||||
|
||||
self._cached_token = access_token.strip()
|
||||
self._token_expires_at = time.monotonic() + expires_in - _TOKEN_REFRESH_MARGIN_SECONDS
|
||||
self._token_expires_at = expires_at
|
||||
return self._cached_token
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -1023,22 +1023,23 @@ class TestAccessTokenValidation:
|
||||
|
||||
_run(go())
|
||||
|
||||
def test_invalid_expire_in_uses_default(self):
|
||||
@pytest.mark.parametrize("expire_in", ["invalid", float("inf"), 10**400, 0, -1, 1, 100, 299, 300, "300"])
|
||||
def test_invalid_expire_in_uses_default(self, expire_in):
|
||||
async def go():
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
bus = MessageBus()
|
||||
channel = DingTalkChannel(bus, config={})
|
||||
channel._client_id = "k"
|
||||
channel._client_secret = "s"
|
||||
call_count = 0
|
||||
|
||||
class FakeResponse:
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return {"accessToken": "tok_ok", "expireIn": "invalid"}
|
||||
return {"accessToken": "tok_ok", "expireIn": expire_in}
|
||||
|
||||
class FakeClient:
|
||||
async def __aenter__(self):
|
||||
@ -1048,20 +1049,29 @@ class TestAccessTokenValidation:
|
||||
pass
|
||||
|
||||
async def post(self, url, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return FakeResponse()
|
||||
|
||||
before = time.monotonic()
|
||||
with patch("app.channels.dingtalk.httpx.AsyncClient", return_value=FakeClient()):
|
||||
with (
|
||||
patch("app.channels.dingtalk.httpx.AsyncClient", return_value=FakeClient()),
|
||||
patch("app.channels.dingtalk.time.monotonic", return_value=1000.0) as clock,
|
||||
):
|
||||
token = await channel._get_access_token()
|
||||
clock.return_value = 1005.0
|
||||
cached_token = await channel._get_access_token()
|
||||
|
||||
assert token == "tok_ok"
|
||||
assert channel._token_expires_at > before
|
||||
assert cached_token == "tok_ok"
|
||||
assert channel._token_expires_at == 7900.0
|
||||
assert call_count == 1
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
class TestTokenCaching:
|
||||
def test_token_is_cached_across_calls(self):
|
||||
@pytest.mark.parametrize(("expire_in", "expected_expiry"), [(301, 1001.0), (3600, 4300.0), (7200, 7900.0)])
|
||||
def test_token_is_cached_across_calls(self, expire_in, expected_expiry):
|
||||
async def go():
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -1077,7 +1087,7 @@ class TestTokenCaching:
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return {"accessToken": "tok_abc", "expireIn": 7200}
|
||||
return {"accessToken": "tok_abc", "expireIn": expire_in}
|
||||
|
||||
class FakeClient:
|
||||
async def __aenter__(self):
|
||||
@ -1091,13 +1101,22 @@ class TestTokenCaching:
|
||||
call_count += 1
|
||||
return FakeResponse()
|
||||
|
||||
with patch("app.channels.dingtalk.httpx.AsyncClient", return_value=FakeClient()):
|
||||
with (
|
||||
patch("app.channels.dingtalk.httpx.AsyncClient", return_value=FakeClient()),
|
||||
patch("app.channels.dingtalk.time.monotonic", return_value=1000.0) as clock,
|
||||
):
|
||||
t1 = await channel._get_access_token()
|
||||
clock.return_value = 1000.5
|
||||
t2 = await channel._get_access_token()
|
||||
assert channel._token_expires_at == expected_expiry
|
||||
assert call_count == 1
|
||||
clock.return_value = expected_expiry
|
||||
t3 = await channel._get_access_token()
|
||||
|
||||
assert t1 == "tok_abc"
|
||||
assert t2 == "tok_abc"
|
||||
assert call_count == 1
|
||||
assert t3 == "tok_abc"
|
||||
assert call_count == 2
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user