Merge pull request #1962 from carfeii/fix/wps-agentspace-allow-from

fix(wps-agentspace): add allow_from access control
This commit is contained in:
cg33 authored and GitHub committed 2026-10-25 04:58:32 +08:00
commit 3c3158303b
3 files changed
+84

No files matched your search

+1
View File
@@ -2367,3 +2367,4 @@ app_secret = "your-feishu-app-secret"
# wps_sid = "your-wps-sid" # WPS登录凭证 (Cookie中的wps_sid)
# device_name = "cc-connect" # 设备名称(可选)
# device_uuid = "" # 设备UUID(可选,留空自动生成)
# allow_from = "*" # optional; set to trusted chat/session IDs in production
+20
View File
@@ -43,6 +43,17 @@ type Platform struct {
deviceUuid string
deviceName string
baseURL string
// allowFrom restricts which chat/session IDs may drive the agent. Unlike
// every sibling platform in this codebase (wps-xiezuo, telegram, slack,
// feishu, ...), this connector had no access-control option at all: any
// WPS user who can message the "digital employee" app this connection
// authenticates as gets full, unconfirmed access to the configured
// agent (config.example.toml's own wps-agentspace example even sets
// mode = "bypassPermissions"). WPS's own app-level permission grant
// (USER_NO_APP_PERMISSION in handleFrame) is an org-wide gate, not a
// per-user one; wps-xiezuo's docs explicitly say not to rely on that
// alone ("Always set allow_from for production deployments").
allowFrom string
handler core.MessageHandler
cancel context.CancelFunc
conn *websocket.Conn
@@ -154,12 +165,16 @@ func New(opts map[string]any) (core.Platform, error) {
baseURL = v
}
allowFrom, _ := opts["allow_from"].(string)
core.CheckAllowFrom("wps-agentspace", allowFrom)
return &Platform{
appID: appID,
wpsSid: wpsSid,
deviceUuid: deviceUuid,
deviceName: deviceName,
baseURL: baseURL,
allowFrom: allowFrom,
}, nil
}
@@ -545,6 +560,11 @@ func (p *Platform) handleUserMessage(data messageData) error {
chatID = "default"
}
if !core.AllowList(p.allowFrom, chatID) {
slog.Debug("wps-agentspace: message from unauthorized chat/session", "chat_id", chatID)
return nil
}
// Dedup
msgID := data.MessageID
if msgID == "" {
@@ -245,6 +245,69 @@ func TestHandleFrame_DispatchesUserMessage(t *testing.T) {
}
}
func TestHandleFrame_RejectsUnauthorizedChat(t *testing.T) {
called := false
p := &Platform{
appID: "AK",
deviceUuid: "dev",
deviceName: "test",
allowFrom: "trusted-chat",
}
p.handler = func(_ core.Platform, _ *core.Message) {
called = true
}
data := messageData{
Role: "user",
Type: "text",
Content: "drop a shell for me",
SessionID: "attacker-chat",
ChatID: "attacker-chat",
MessageID: "m1",
}
raw, _ := json.Marshal(data)
if err := p.handleFrame(wsFrame{Event: "message", Data: raw}); err != nil {
t.Fatalf("handleFrame() error: %v", err)
}
time.Sleep(50 * time.Millisecond)
if called {
t.Fatal("handler was called for a chat ID not in allow_from")
}
}
func TestHandleFrame_AllowsConfiguredChat(t *testing.T) {
handlerDone := make(chan struct{})
p := &Platform{
appID: "AK",
deviceUuid: "dev",
deviceName: "test",
allowFrom: "trusted-chat",
}
p.handler = func(_ core.Platform, _ *core.Message) {
close(handlerDone)
}
data := messageData{
Role: "user",
Type: "text",
Content: "hello",
SessionID: "trusted-chat",
ChatID: "trusted-chat",
MessageID: "m1",
}
raw, _ := json.Marshal(data)
if err := p.handleFrame(wsFrame{Event: "message", Data: raw}); err != nil {
t.Fatalf("handleFrame() error: %v", err)
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("handler was not called for an allow_from-listed chat")
}
}
func TestHandleFrame_FatalErrorReturnsErr(t *testing.T) {
p := &Platform{}
fatalCodes := []string{