diff --git a/backend/controllers/plugin_v1.go b/backend/controllers/plugin_v1.go index 8504040..7d17bc7 100644 --- a/backend/controllers/plugin_v1.go +++ b/backend/controllers/plugin_v1.go @@ -5,7 +5,6 @@ import ( "errors" "io" "net/http" - "strings" "git.pcux.cn/xuans/Smail/dto/response" "git.pcux.cn/xuans/Smail/hooks" @@ -18,56 +17,12 @@ import ( const maxTelegramUserSettingsBodyBytes int64 = 16 << 10 -type telegramSettingsRequest struct { - Settings pluginservice.TelegramUserSettings `json:"settings"` - Version uint64 `json:"version"` -} - func GetTelegramUserSettings(ctx *context.Context, w http.ResponseWriter, req *http.Request) { - if !authorizedPluginUser(ctx) { - response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w) - return - } - service := hooks.PluginConfigService() - if service == nil { - response.NewErrorResponse(response.ServerError, "插件配置服务不可用", "").FPrint(w) - return - } - settings, err := service.GetOwnTelegramSettings(ctx.UserID) - if err != nil { - writeTelegramSettingsError(w, err) - return - } - response.NewSuccessResponse(settings).FPrint(w) + TelegramUserSettings(ctx, w, req) } func UpdateTelegramUserSettings(ctx *context.Context, w http.ResponseWriter, req *http.Request) { - if !authorizedPluginUser(ctx) { - response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w) - return - } - var input telegramSettingsRequest - if !decodePluginJSONBody(w, req, &input) { - return - } - if err := pluginservice.ValidateTelegramUserSettings(input.Settings); err != nil { - writeTelegramSettingsError(w, err) - return - } - service := hooks.PluginConfigService() - if service == nil { - response.NewErrorResponse(response.ServerError, "插件配置服务不可用", "").FPrint(w) - return - } - settings, err := service.UpdateOwnTelegramSettings(ctx.UserID, input.Settings, input.Version) - if err != nil { - writeTelegramSettingsError(w, err) - return - } - if err := hooks.SyncTelegramUserSettings(ctx.UserID, settings.Settings); err != nil { - log.WithError(err).Warnf("Telegram 用户设置同步失败,用户 ID=%d", ctx.UserID) - } - response.NewSuccessResponse(settings).FPrint(w) + TelegramUserSettings(ctx, w, req) } func GetTelegramBotInfo(ctx *context.Context, w http.ResponseWriter, req *http.Request) { @@ -95,43 +50,7 @@ func GetTelegramBotInfo(ctx *context.Context, w http.ResponseWriter, req *http.R } func TestTelegramMessage(ctx *context.Context, w http.ResponseWriter, req *http.Request) { - if !authenticatedPluginUser(ctx) { - response.NewErrorResponse(response.NeedLogin, "请先登录", "").FPrint(w) - return - } - var input telegramSettingsRequest - if !decodePluginJSONBody(w, req, &input) { - return - } - if strings.TrimSpace(input.Settings.ChatID) == "" { - response.NewErrorResponse(response.ParamsError, "请输入 Telegram 聊天 ID", "").FPrint(w) - return - } - if err := pluginservice.ValidateTelegramUserSettings(input.Settings); err != nil { - writeTelegramSettingsError(w, err) - return - } - hook, ok := hooks.GetHook("telegram_push") - if !ok { - response.NewErrorResponse(response.ServerError, "Telegram 插件不可用", "").FPrint(w) - return - } - provider, ok := hook.(framework.TestMessageProvider) - if !ok { - response.NewErrorResponse(response.ServerError, "Telegram 插件不支持测试消息", "").FPrint(w) - return - } - payload, err := json.Marshal(input.Settings) - if err != nil { - response.NewErrorResponse(response.ServerError, "测试消息参数处理失败", "").FPrint(w) - return - } - if err := provider.TestMessage(payload); err != nil { - log.WithError(err).Warn("发送 Telegram 测试消息失败") - response.NewErrorResponse(response.ServerError, "发送 Telegram 测试消息失败", "").FPrint(w) - return - } - response.NewSuccessResponse(map[string]string{"message": "测试消息已发送"}).FPrint(w) + TestBoundTelegramMessage(ctx, w, req) } func authenticatedPluginUser(ctx *context.Context) bool { diff --git a/backend/controllers/plugin_v1_test.go b/backend/controllers/plugin_v1_test.go index c29da84..1c70919 100644 --- a/backend/controllers/plugin_v1_test.go +++ b/backend/controllers/plugin_v1_test.go @@ -50,30 +50,36 @@ func TestTelegramUserSettingsRequiresLogin(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/api/v1/plugin/settings/telegram_push/user", nil) recorder := httptest.NewRecorder() GetTelegramUserSettings(nil, recorder, request) - if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "请先登录") { + if recorder.Code != http.StatusForbidden || !strings.Contains(recorder.Body.String(), "无权访问或请求不满足安全条件") { t.Fatalf("unauthenticated GET status=%d body=%s", recorder.Code, recorder.Body.String()) } request = httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{}`)) recorder = httptest.NewRecorder() UpdateTelegramUserSettings(&appcontext.Context{}, recorder, request) - if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "请先登录") { + if recorder.Code != http.StatusForbidden || !strings.Contains(recorder.Body.String(), "无权访问或请求不满足安全条件") { t.Fatalf("unauthenticated PUT status=%d body=%s", recorder.Code, recorder.Body.String()) } } func TestTelegramUserSettingsRejectsUnknownAndTrailingFields(t *testing.T) { - hooks.SetPluginConfigService(nil) - t.Cleanup(func() { hooks.SetPluginConfigService(nil) }) + service, engine := newTelegramControllerConfigService(t) + previousService := hooks.PluginConfigService() + hooks.SetPluginConfigService(service) + t.Cleanup(func() { hooks.SetPluginConfigService(previousService) }) + user := models.User{Account: "telegram-parse-user", Name: "telegram-parse-user", Password: "hash"} + if _, err := engine.Insert(&user); err != nil { + t.Fatal(err) + } cases := []string{ - `{"settings":{"chatId":"123","userId":99},"version":0}`, - `{"settings":{"chatId":"123"},"version":0} {}`, + `{"enabled":false,"chatId":"123","version":0}`, + `{"enabled":false,"version":0} {}`, } for _, body := range cases { recorder := httptest.NewRecorder() request := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", bytes.NewBufferString(body)) - UpdateTelegramUserSettings(&appcontext.Context{UserID: 7}, recorder, request) + UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID, RecentAuth: true}, recorder, request) if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "请求参数格式不正确") { t.Fatalf("body=%s status=%d response=%s", body, recorder.Code, recorder.Body.String()) } @@ -92,8 +98,8 @@ func TestTelegramUserSettingsUsesSessionUserAndVersion(t *testing.T) { } firstRecorder := httptest.NewRecorder() - firstRequest := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{"settings":{"chatId":"-1001234567890","showContent":true},"version":0}`)) - UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID}, firstRecorder, firstRequest) + firstRequest := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{"enabled":false,"showContent":true,"version":0}`)) + UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID, RecentAuth: true}, firstRecorder, firstRequest) if firstRecorder.Code != http.StatusOK || !strings.Contains(firstRecorder.Body.String(), `"version":1`) { t.Fatalf("first update status=%d body=%s", firstRecorder.Code, firstRecorder.Body.String()) } @@ -102,15 +108,15 @@ func TestTelegramUserSettingsUsesSessionUserAndVersion(t *testing.T) { } staleRecorder := httptest.NewRecorder() - staleRequest := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{"settings":{"chatId":"123"},"version":0}`)) - UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID}, staleRecorder, staleRequest) + staleRequest := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{"enabled":false,"showContent":false,"version":0}`)) + UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID, RecentAuth: true}, staleRecorder, staleRequest) if staleRecorder.Code != http.StatusOK || !strings.Contains(staleRecorder.Body.String(), "配置已被其他操作更新") { t.Fatalf("stale update status=%d body=%s", staleRecorder.Code, staleRecorder.Body.String()) } getRecorder := httptest.NewRecorder() GetTelegramUserSettings(&appcontext.Context{UserID: user.ID}, getRecorder, httptest.NewRequest(http.MethodGet, "/api/v1/plugin/settings/telegram_push/user", nil)) - if getRecorder.Code != http.StatusOK || !strings.Contains(getRecorder.Body.String(), "-1001234567890") { + if getRecorder.Code != http.StatusOK || !strings.Contains(getRecorder.Body.String(), `"bound":false`) || strings.Contains(getRecorder.Body.String(), "-1001234567890") { t.Fatalf("GET status=%d body=%s", getRecorder.Code, getRecorder.Body.String()) } } @@ -131,30 +137,30 @@ func TestTelegramBotInfoDoesNotExposeSecret(t *testing.T) { } } -func TestTelegramTestMessageValidatesChatIDAndDoesNotPersist(t *testing.T) { +func TestTelegramTestMessageRequiresCurrentAccountBinding(t *testing.T) { + service, engine := newTelegramControllerConfigService(t) + previousService := hooks.PluginConfigService() + hooks.SetPluginConfigService(service) + t.Cleanup(func() { hooks.SetPluginConfigService(previousService) }) + + user := models.User{Account: "telegram-test-user", Name: "telegram-test-user", Password: "hash"} + if _, err := engine.Insert(&user); err != nil { + t.Fatal(err) + } + previousHooks := hooks.HookList hook := &telegramControllerTestHook{} hooks.HookList = map[string]framework.EmailHook{"telegram_push": hook} t.Cleanup(func() { hooks.HookList = previousHooks }) - invalidRecorder := httptest.NewRecorder() - invalidRequest := httptest.NewRequest(http.MethodPost, "/api/v1/plugin/settings/telegram_push/test", strings.NewReader(`{"settings":{"chatId":"not-a-chat"}}`)) - TestTelegramMessage(&appcontext.Context{UserID: 7}, invalidRecorder, invalidRequest) - if invalidRecorder.Code != http.StatusOK || !strings.Contains(invalidRecorder.Body.String(), "聊天 ID 格式不正确") { - t.Fatalf("invalid message status=%d body=%s", invalidRecorder.Code, invalidRecorder.Body.String()) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/api/v1/plugin/settings/telegram_push/test", strings.NewReader(`{"settings":{"chatId":"123"}}`)) + TestTelegramMessage(&appcontext.Context{UserID: user.ID, RecentAuth: true}, recorder, request) + if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "请先绑定本人 Telegram 并启用通知") { + t.Fatalf("unbound test message status=%d body=%s", recorder.Code, recorder.Body.String()) } if hook.testPayload != nil { - t.Fatal("invalid test message reached plugin") - } - - validRecorder := httptest.NewRecorder() - validRequest := httptest.NewRequest(http.MethodPost, "/api/v1/plugin/settings/telegram_push/test", strings.NewReader(`{"settings":{"chatId":"123"},"version":99}`)) - TestTelegramMessage(&appcontext.Context{UserID: 7}, validRecorder, validRequest) - if validRecorder.Code != http.StatusOK || !strings.Contains(validRecorder.Body.String(), "测试消息已发送") { - t.Fatalf("valid message status=%d body=%s", validRecorder.Code, validRecorder.Body.String()) - } - if !strings.Contains(string(hook.testPayload), `"chatId":"123"`) { - t.Fatalf("plugin did not receive validated settings: %s", hook.testPayload) + t.Fatal("caller-supplied Chat ID reached plugin without a verified binding") } } diff --git a/backend/services/access/matrix_test.go b/backend/services/access/matrix_test.go index 6d8fe7c..b1b7c2c 100644 --- a/backend/services/access/matrix_test.go +++ b/backend/services/access/matrix_test.go @@ -22,6 +22,7 @@ func TestCapabilityMatrix(t *testing.T) { {name: "admin writes user plugin settings", req: Request{ActorRole: RoleAdmin, Capability: CapabilityPluginWriteAnyUser, TargetRole: RoleUser}, allowed: true}, {name: "admin cannot write admin plugin settings", req: Request{ActorRole: RoleAdmin, Capability: CapabilityPluginWriteAnyUser, TargetRole: RoleAdmin}}, {name: "super admin writes all user plugin settings", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityPluginWriteAnyUser, TargetRole: RoleSuperAdmin}, allowed: true}, + {name: "super admin writes own plugin settings", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityPluginWriteOwn, OwnResource: true}, allowed: true}, {name: "user writes own plugin settings", req: Request{ActorRole: RoleUser, Capability: CapabilityPluginWriteOwn, OwnResource: true}, allowed: true}, {name: "user cannot write another plugin settings", req: Request{ActorRole: RoleUser, Capability: CapabilityPluginWriteOwn}}, {name: "authenticated super admin changes own password", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityPasswordChangeOwn, OwnResource: true}, allowed: true},