Files
sub2api/backend/internal/handler/admin/user_handler_role_stepup_test.go
T
shaw 539bfc8bad feat(security): 敏感操作 step-up 2FA 开关化,安全开关默认关闭
新增系统设置 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 扁平化错误对象。
2026-07-18 10:46:42 +08:00

87 lines
2.8 KiB
Go

package admin
import (
"bytes"
"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"
)
// 角色提升为管理员的 step-up 门控条件测试。
// 测试环境不注入认证上下文,因此门控一旦触发会以 401 中止;
// 借此区分「触发了 step-up 校验」与「直接放行到业务层(200)」。
func setupRoleStepUpRouter(t *testing.T) (*gin.Engine, *stubAdminService) {
t.Helper()
gin.SetMode(gin.TestMode)
router := gin.New()
adminSvc := newStubAdminService()
// 追加一个已是管理员的目标用户,验证「目标已是 admin 不触发门控」。
adminSvc.users = append(adminSvc.users, service.User{
ID: 2,
Email: "admin@example.com",
Role: service.RoleAdmin,
Status: service.StatusActive,
})
h := NewUserHandler(adminSvc, nil, nil, nil, nil, nil, nil)
router.POST("/api/v1/admin/users", h.Create)
router.PUT("/api/v1/admin/users/:id", h.Update)
return router, adminSvc
}
func doJSON(t *testing.T, router *gin.Engine, method, path string, payload map[string]any) *httptest.ResponseRecorder {
t.Helper()
body, err := json.Marshal(payload)
require.NoError(t, err)
rec := httptest.NewRecorder()
req := httptest.NewRequest(method, path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(rec, req)
return rec
}
func TestUpdateUserPromoteToAdminRequiresStepUp(t *testing.T) {
router, _ := setupRoleStepUpRouter(t)
rec := doJSON(t, router, http.MethodPut, "/api/v1/admin/users/1", map[string]any{"role": "admin"})
require.Equal(t, http.StatusUnauthorized, rec.Code)
}
func TestUpdateUserKeepAdminRoleSkipsStepUp(t *testing.T) {
router, _ := setupRoleStepUpRouter(t)
rec := doJSON(t, router, http.MethodPut, "/api/v1/admin/users/2", map[string]any{"role": "admin"})
require.Equal(t, http.StatusOK, rec.Code)
}
func TestUpdateUserRegularRoleSkipsStepUp(t *testing.T) {
router, _ := setupRoleStepUpRouter(t)
rec := doJSON(t, router, http.MethodPut, "/api/v1/admin/users/1", map[string]any{"role": "user", "email": "u@example.com"})
require.Equal(t, http.StatusOK, rec.Code)
}
func TestCreateAdminUserRequiresStepUp(t *testing.T) {
router, _ := setupRoleStepUpRouter(t)
rec := doJSON(t, router, http.MethodPost, "/api/v1/admin/users", map[string]any{
"email": "new-admin@example.com", "password": "pass123", "role": "admin",
})
require.Equal(t, http.StatusUnauthorized, rec.Code)
}
func TestCreateRegularUserSkipsStepUp(t *testing.T) {
router, _ := setupRoleStepUpRouter(t)
rec := doJSON(t, router, http.MethodPost, "/api/v1/admin/users", map[string]any{
"email": "new-user@example.com", "password": "pass123", "role": "user",
})
require.Equal(t, http.StatusOK, rec.Code)
}