mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:37:52 +08:00
新增系统设置 step_up_enabled(默认关闭),把敏感操作 2FA 门控做成可开关; 同时将会话 IP/UA 绑定默认值从开启改为关闭,避免用户因 IP 变动登录后掉线。 ## 新增功能 - 敏感操作 step-up 2FA 总开关 step_up_enabled(默认关闭):关闭时账号/代理导出、 备份创建/下载、S3 配置修改、提升管理员等操作恢复门控引入前的直接放行行为; 开启后要求当前会话在 15 分钟内完成过 TOTP step-up 验证。 ## 优化改进 - 会话 IP/UA 绑定默认改为关闭(功能保留,可在设置页按需开启)。 - 开启 step-up 开关需操作者本人已启用 TOTP(防自锁);关闭开关本身作为敏感操作, 需通过 step-up 验证(防止攻击者拿到会话后先关闸再导出/备份)。 - 两个安全开关请求字段改为可空指针(省略=保持现值),避免旧客户端全量保存时 静默重置安全开关。 - 备份恢复(整库覆盖可回滚安全设置)纳入 step-up 门控。 - 审计摘要 diffSettings 补记 step_up_enabled / session_binding_enabled 变更。 ## Bug 修复 - 修复 BackupView 恢复操作 409(恢复进行中)判断未适配 apiClient 扁平化错误对象。
147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type batchLimitsAdminServiceStub struct {
|
|
*stubAdminService
|
|
calls []batchLimitsAdminServiceCall
|
|
}
|
|
|
|
type batchLimitsAdminServiceCall struct {
|
|
userIDs []int64
|
|
concurrency *int
|
|
rpmLimit *int
|
|
}
|
|
|
|
func cloneIntPointer(value *int) *int {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
cloned := *value
|
|
return &cloned
|
|
}
|
|
|
|
func (s *batchLimitsAdminServiceStub) BatchUpdateLimits(_ context.Context, userIDs []int64, concurrency, rpmLimit *int) (int, error) {
|
|
s.calls = append(s.calls, batchLimitsAdminServiceCall{
|
|
userIDs: append([]int64(nil), userIDs...),
|
|
concurrency: cloneIntPointer(concurrency),
|
|
rpmLimit: cloneIntPointer(rpmLimit),
|
|
})
|
|
return len(userIDs), nil
|
|
}
|
|
|
|
func setupBatchLimitsRouter(serviceStub service.AdminService) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
router := gin.New()
|
|
handler := NewUserHandler(serviceStub, nil, nil, nil, nil, nil, nil)
|
|
router.POST("/api/v1/admin/users/batch-limits", handler.BatchUpdateLimits)
|
|
return router
|
|
}
|
|
|
|
func postBatchLimits(t *testing.T, router *gin.Engine, body []byte) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(
|
|
http.MethodPost,
|
|
"/api/v1/admin/users/batch-limits",
|
|
bytes.NewReader(body),
|
|
)
|
|
request.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(recorder, request)
|
|
return recorder
|
|
}
|
|
|
|
func TestUserHandlerBatchUpdateLimitsAcceptsPartialAndZeroValues(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
body string
|
|
expectedConcurrency *int
|
|
expectedRPMLimit *int
|
|
}{
|
|
{name: "concurrency only", body: `{"user_ids":[1,2],"concurrency":10}`, expectedConcurrency: pointerTo(10)},
|
|
{name: "both limits", body: `{"user_ids":[1,2],"concurrency":8,"rpm_limit":60}`, expectedConcurrency: pointerTo(8), expectedRPMLimit: pointerTo(60)},
|
|
{name: "explicit zero", body: `{"user_ids":[1,2],"concurrency":0,"rpm_limit":0}`, expectedConcurrency: pointerTo(0), expectedRPMLimit: pointerTo(0)},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
serviceStub := &batchLimitsAdminServiceStub{stubAdminService: newStubAdminService()}
|
|
recorder := postBatchLimits(t, setupBatchLimitsRouter(serviceStub), []byte(test.body))
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Len(t, serviceStub.calls, 1)
|
|
require.Equal(t, []int64{1, 2}, serviceStub.calls[0].userIDs)
|
|
require.Equal(t, test.expectedConcurrency, serviceStub.calls[0].concurrency)
|
|
require.Equal(t, test.expectedRPMLimit, serviceStub.calls[0].rpmLimit)
|
|
|
|
var response struct {
|
|
Data struct {
|
|
Affected int `json:"affected"`
|
|
} `json:"data"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
|
|
require.Equal(t, 2, response.Data.Affected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserHandlerBatchUpdateLimitsRejectsInvalidRequests(t *testing.T) {
|
|
tooManyIDs := make([]int64, 501)
|
|
for index := range tooManyIDs {
|
|
tooManyIDs[index] = int64(index + 1)
|
|
}
|
|
tooManyBody, err := json.Marshal(map[string]any{"user_ids": tooManyIDs, "rpm_limit": 10})
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
body []byte
|
|
}{
|
|
{name: "no limits", body: []byte(`{"user_ids":[1]}`)},
|
|
{name: "invalid json", body: []byte(`{"user_ids":`)},
|
|
{name: "missing user ids", body: []byte(`{"rpm_limit":10}`)},
|
|
{name: "more than 500 ids", body: tooManyBody},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
serviceStub := &batchLimitsAdminServiceStub{stubAdminService: newStubAdminService()}
|
|
recorder := postBatchLimits(t, setupBatchLimitsRouter(serviceStub), test.body)
|
|
|
|
require.Equal(t, http.StatusBadRequest, recorder.Code)
|
|
require.Empty(t, serviceStub.calls)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserHandlerBatchUpdateLimitsAllUsesEveryListedUser(t *testing.T) {
|
|
base := newStubAdminService()
|
|
base.users = []service.User{{ID: 11}, {ID: 12}, {ID: 13}}
|
|
serviceStub := &batchLimitsAdminServiceStub{stubAdminService: base}
|
|
recorder := postBatchLimits(
|
|
t,
|
|
setupBatchLimitsRouter(serviceStub),
|
|
[]byte(`{"all":true,"user_ids":[999],"rpm_limit":0}`),
|
|
)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
require.Len(t, serviceStub.calls, 1)
|
|
require.Equal(t, []int64{11, 12, 13}, serviceStub.calls[0].userIDs)
|
|
require.Equal(t, 1, base.lastListUsers.calls)
|
|
}
|
|
|
|
func pointerTo(value int) *int {
|
|
return &value
|
|
}
|