将旧Telegram兼容入口收口到当前账户绑定流程,禁止请求体伪造Chat ID,并补充超级管理员本人插件设置权限回归用例。
This commit is contained in:
parent
c7bcde6f0b
commit
0aa6ed67e3
@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.pcux.cn/xuans/Smail/dto/response"
|
"git.pcux.cn/xuans/Smail/dto/response"
|
||||||
"git.pcux.cn/xuans/Smail/hooks"
|
"git.pcux.cn/xuans/Smail/hooks"
|
||||||
@ -18,56 +17,12 @@ import (
|
|||||||
|
|
||||||
const maxTelegramUserSettingsBodyBytes int64 = 16 << 10
|
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) {
|
func GetTelegramUserSettings(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||||
if !authorizedPluginUser(ctx) {
|
TelegramUserSettings(ctx, w, req)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateTelegramUserSettings(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
func UpdateTelegramUserSettings(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||||
if !authorizedPluginUser(ctx) {
|
TelegramUserSettings(ctx, w, req)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTelegramBotInfo(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
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) {
|
func TestTelegramMessage(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||||
if !authenticatedPluginUser(ctx) {
|
TestBoundTelegramMessage(ctx, w, req)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticatedPluginUser(ctx *context.Context) bool {
|
func authenticatedPluginUser(ctx *context.Context) bool {
|
||||||
|
|||||||
@ -50,30 +50,36 @@ func TestTelegramUserSettingsRequiresLogin(t *testing.T) {
|
|||||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/plugin/settings/telegram_push/user", nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/plugin/settings/telegram_push/user", nil)
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
GetTelegramUserSettings(nil, recorder, request)
|
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())
|
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(`{}`))
|
request = httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{}`))
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
UpdateTelegramUserSettings(&appcontext.Context{}, recorder, request)
|
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())
|
t.Fatalf("unauthenticated PUT status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTelegramUserSettingsRejectsUnknownAndTrailingFields(t *testing.T) {
|
func TestTelegramUserSettingsRejectsUnknownAndTrailingFields(t *testing.T) {
|
||||||
hooks.SetPluginConfigService(nil)
|
service, engine := newTelegramControllerConfigService(t)
|
||||||
t.Cleanup(func() { hooks.SetPluginConfigService(nil) })
|
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{
|
cases := []string{
|
||||||
`{"settings":{"chatId":"123","userId":99},"version":0}`,
|
`{"enabled":false,"chatId":"123","version":0}`,
|
||||||
`{"settings":{"chatId":"123"},"version":0} {}`,
|
`{"enabled":false,"version":0} {}`,
|
||||||
}
|
}
|
||||||
for _, body := range cases {
|
for _, body := range cases {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", bytes.NewBufferString(body))
|
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(), "请求参数格式不正确") {
|
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "请求参数格式不正确") {
|
||||||
t.Fatalf("body=%s status=%d response=%s", body, recorder.Code, 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()
|
firstRecorder := httptest.NewRecorder()
|
||||||
firstRequest := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{"settings":{"chatId":"-1001234567890","showContent":true},"version":0}`))
|
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}, firstRecorder, firstRequest)
|
UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID, RecentAuth: true}, firstRecorder, firstRequest)
|
||||||
if firstRecorder.Code != http.StatusOK || !strings.Contains(firstRecorder.Body.String(), `"version":1`) {
|
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())
|
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()
|
staleRecorder := httptest.NewRecorder()
|
||||||
staleRequest := httptest.NewRequest(http.MethodPut, "/api/v1/plugin/settings/telegram_push/user", strings.NewReader(`{"settings":{"chatId":"123"},"version":0}`))
|
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}, staleRecorder, staleRequest)
|
UpdateTelegramUserSettings(&appcontext.Context{UserID: user.ID, RecentAuth: true}, staleRecorder, staleRequest)
|
||||||
if staleRecorder.Code != http.StatusOK || !strings.Contains(staleRecorder.Body.String(), "配置已被其他操作更新") {
|
if staleRecorder.Code != http.StatusOK || !strings.Contains(staleRecorder.Body.String(), "配置已被其他操作更新") {
|
||||||
t.Fatalf("stale update status=%d body=%s", staleRecorder.Code, staleRecorder.Body.String())
|
t.Fatalf("stale update status=%d body=%s", staleRecorder.Code, staleRecorder.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecorder := httptest.NewRecorder()
|
getRecorder := httptest.NewRecorder()
|
||||||
GetTelegramUserSettings(&appcontext.Context{UserID: user.ID}, getRecorder, httptest.NewRequest(http.MethodGet, "/api/v1/plugin/settings/telegram_push/user", nil))
|
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())
|
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
|
previousHooks := hooks.HookList
|
||||||
hook := &telegramControllerTestHook{}
|
hook := &telegramControllerTestHook{}
|
||||||
hooks.HookList = map[string]framework.EmailHook{"telegram_push": hook}
|
hooks.HookList = map[string]framework.EmailHook{"telegram_push": hook}
|
||||||
t.Cleanup(func() { hooks.HookList = previousHooks })
|
t.Cleanup(func() { hooks.HookList = previousHooks })
|
||||||
|
|
||||||
invalidRecorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
invalidRequest := httptest.NewRequest(http.MethodPost, "/api/v1/plugin/settings/telegram_push/test", strings.NewReader(`{"settings":{"chatId":"not-a-chat"}}`))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/plugin/settings/telegram_push/test", strings.NewReader(`{"settings":{"chatId":"123"}}`))
|
||||||
TestTelegramMessage(&appcontext.Context{UserID: 7}, invalidRecorder, invalidRequest)
|
TestTelegramMessage(&appcontext.Context{UserID: user.ID, RecentAuth: true}, recorder, request)
|
||||||
if invalidRecorder.Code != http.StatusOK || !strings.Contains(invalidRecorder.Body.String(), "聊天 ID 格式不正确") {
|
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), "请先绑定本人 Telegram 并启用通知") {
|
||||||
t.Fatalf("invalid message status=%d body=%s", invalidRecorder.Code, invalidRecorder.Body.String())
|
t.Fatalf("unbound test message status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
if hook.testPayload != nil {
|
if hook.testPayload != nil {
|
||||||
t.Fatal("invalid test message reached plugin")
|
t.Fatal("caller-supplied Chat ID reached plugin without a verified binding")
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 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: "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 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 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: "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},
|
{name: "authenticated super admin changes own password", req: Request{ActorRole: RoleSuperAdmin, Capability: CapabilityPasswordChangeOwn, OwnResource: true}, allowed: true},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user