Przeglądaj źródła

fix(user-migration): 补齐主节点保护与导入校验

- 为 user-migrations 管理路由增加仅 master 节点可访问的保护
- 为导入创建用户补充用户名、显示名、邮箱长度校验及测试
- 调整测试环境部署脚本,自动写入 CN/OV 的 NODE_TYPE

Co-Authored-By: Codex <noreply@anthropic.com>
master
fengsilin 2 dni temu
rodzic
commit
2f7a71d61e
6 zmienionych plików z 118 dodań i 16 usunięć
  1. +14
    -0
      middleware/auth.go
  2. +36
    -0
      model/region_migration_user_test.go
  3. +23
    -0
      model/user.go
  4. +1
    -1
      router/api-router.go
  5. +28
    -2
      router/user_migration_router_test.go
  6. +16
    -13
      scripts/deploy-test.sh

+ 14
- 0
middleware/auth.go Wyświetl plik

@@ -175,6 +175,20 @@ func RootAuth() func(c *gin.Context) {
}
}

func MasterNodeOnly() func(c *gin.Context) {
return func(c *gin.Context) {
if !common.IsMasterNode {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "only master node can access this endpoint",
})
c.Abort()
return
}
c.Next()
}
}

func WssAuth(c *gin.Context) {

}


+ 36
- 0
model/region_migration_user_test.go Wyświetl plik

@@ -74,6 +74,42 @@ func TestInsertImportedUserWithHashedPassword_RejectsExplicitId(t *testing.T) {
require.Contains(t, err.Error(), "Id must not be set")
}

func TestInsertImportedUserWithHashedPassword_RejectsOverlongFields(t *testing.T) {
db := setupRegionMigrationUserDB(t)
orig := DB
DB = db
defer func() {
DB = orig
}()

user := &User{
Username: "abcdefghijklmnopqrstu",
Password: "already-hashed-value",
Email: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@example.com",
DisplayName: "DDDDDDDDDDDDDDDDDDDDDDDDD",
Status: common.UserStatusEnabled,
Role: common.RoleCommonUser,
Group: "default",
Quota: 123,
AffCode: "A002",
CreatedAt: 1710000000,
}

err := InsertImportedUserWithHashedPassword(user)
require.Error(t, err)
require.Contains(t, err.Error(), "username exceeds max length")

user.Username = "valid-user"
err = InsertImportedUserWithHashedPassword(user)
require.Error(t, err)
require.Contains(t, err.Error(), "display_name exceeds max length")

user.DisplayName = "Valid Name"
err = InsertImportedUserWithHashedPassword(user)
require.Error(t, err)
require.Contains(t, err.Error(), "email exceeds max length")
}

func TestCopyOAuthBindingsIfMissing_Idempotent(t *testing.T) {
db := setupRegionMigrationUserDB(t)
orig := DB


+ 23
- 0
model/user.go Wyświetl plik

@@ -7,6 +7,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/dto"
@@ -579,6 +580,25 @@ func InsertImportedUserWithHashedPassword(user *User) error {
return InsertImportedUserWithHashedPasswordTx(DB, user)
}

func validateImportedUserFields(user *User) error {
if user == nil {
return errors.New("user is nil")
}
if strings.TrimSpace(user.Username) == "" {
return errors.New("username is required")
}
if utf8.RuneCountInString(user.Username) > UserNameMaxLength {
return fmt.Errorf("username exceeds max length %d", UserNameMaxLength)
}
if utf8.RuneCountInString(user.DisplayName) > 20 {
return errors.New("display_name exceeds max length 20")
}
if utf8.RuneCountInString(user.Email) > 50 {
return errors.New("email exceeds max length 50")
}
return nil
}

func InsertImportedUserWithHashedPasswordTx(tx *gorm.DB, user *User) error {
if user == nil {
return errors.New("user is nil")
@@ -586,6 +606,9 @@ func InsertImportedUserWithHashedPasswordTx(tx *gorm.DB, user *User) error {
if user.Id != 0 {
return errors.New("InsertImportedUserWithHashedPassword: Id must not be set, let DB auto-assign")
}
if err := validateImportedUserFields(user); err != nil {
return err
}
if user.AffCode == "" {
user.AffCode = common.GetRandomString(4)
}


+ 1
- 1
router/api-router.go Wyświetl plik

@@ -417,7 +417,7 @@ func SetApiRouter(router *gin.Engine) {
}

userMigrationRoute := apiRouter.Group("/user-migrations")
userMigrationRoute.Use(middleware.RootAuth())
userMigrationRoute.Use(middleware.RootAuth(), middleware.MasterNodeOnly())
{
userMigrationRoute.GET("/candidate-users", controller.ListMigrationCandidateUsers)
userMigrationRoute.POST("/batches", controller.CreateUserMigrationBatch)


+ 28
- 2
router/user_migration_router_test.go Wyświetl plik

@@ -61,7 +61,7 @@ func setupUserMigrationRootRouter() *gin.Engine {
session.Save()
c.Request.Header.Set("New-Api-User", "1")
c.Next()
}, middleware.RootAuth())
}, middleware.RootAuth(), middleware.MasterNodeOnly())
{
userMigrationRoute.GET("/batches", controller.ListUserMigrationBatches)
userMigrationRoute.POST("/batches/:id/execute", controller.ExecuteUserMigrationBatch)
@@ -93,7 +93,7 @@ func setupUserMigrationAdminRouter() *gin.Engine {
session.Save()
c.Request.Header.Set("New-Api-User", "2")
c.Next()
}, middleware.RootAuth())
}, middleware.RootAuth(), middleware.MasterNodeOnly())
{
userMigrationRoute.GET("/batches", controller.ListUserMigrationBatches)
userMigrationRoute.POST("/batches/:id/execute", controller.ExecuteUserMigrationBatch)
@@ -106,10 +106,13 @@ func setupUserMigrationAdminRouter() *gin.Engine {
func TestUserMigrationRoutes_RootCanAccess(t *testing.T) {
db := setupUserMigrationRouterDB(t)
orig := model.DB
origMaster := common.IsMasterNode
model.DB = db
defer func() {
model.DB = orig
common.IsMasterNode = origMaster
}()
common.IsMasterNode = true

require.NoError(t, db.Create(&model.UserMigrationBatch{
Id: 1,
@@ -141,10 +144,13 @@ func TestUserMigrationRoutes_RootCanAccess(t *testing.T) {
func TestUserMigrationRoutes_AdminForbidden(t *testing.T) {
db := setupUserMigrationRouterDB(t)
orig := model.DB
origMaster := common.IsMasterNode
model.DB = db
defer func() {
model.DB = orig
common.IsMasterNode = origMaster
}()
common.IsMasterNode = true

router := setupUserMigrationAdminRouter()

@@ -166,3 +172,23 @@ func TestUserMigrationRoutes_AdminForbidden(t *testing.T) {
require.Equal(t, http.StatusOK, w.Code)
require.Contains(t, w.Body.String(), "success\":false")
}

func TestUserMigrationRoutes_RootForbiddenOnSlaveNode(t *testing.T) {
db := setupUserMigrationRouterDB(t)
orig := model.DB
origMaster := common.IsMasterNode
model.DB = db
defer func() {
model.DB = orig
common.IsMasterNode = origMaster
}()
common.IsMasterNode = false

router := setupUserMigrationRootRouter()

req := httptest.NewRequest(http.MethodGet, "/api/user-migrations/batches", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusForbidden, w.Code)
require.Contains(t, w.Body.String(), "only master node")
}

+ 16
- 13
scripts/deploy-test.sh Wyświetl plik

@@ -10,15 +10,12 @@

set -euo pipefail

# ==================== 配置 ====================
DOCKER_REGISTRY="registry.cn-hangzhou.aliyuncs.com/fengsilin/new-api"
PROJECT_DIR="/mnt/d/code/new-api"

# 测试节点
CN_DIR="/root/new-api-cn"
OV_DIR="/root/new-api-ov"

# ==================== 颜色 ====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
@@ -38,14 +35,13 @@ section() {
echo -e " ${BOLD}=========================================${NC}"
}

# ==================== 参数解析 ====================
TARGET="all"
NO_BUILD=false

for arg in "$@"; do
case "$arg" in
cn) TARGET="cn" ;;
ov) TARGET="ov" ;;
cn) TARGET="cn" ;;
ov) TARGET="ov" ;;
--no-build) NO_BUILD=true ;;
-h|--help)
echo "用法: $0 [cn|ov] [--no-build]"
@@ -62,7 +58,6 @@ for arg in "$@"; do
esac
done

# ==================== Step 1: 构建并推送 ====================
if [ "$NO_BUILD" = false ]; then
section "Step 1/2: 构建并推送镜像"

@@ -99,7 +94,6 @@ else
ok "使用镜像: ${IMAGE}"
fi

# ==================== Step 2: 部署到测试节点 ====================
section "Step 2/2: 部署到测试环境"

FAILED=()
@@ -107,6 +101,7 @@ FAILED=()
deploy_local() {
local NAME="$1"
local DIR="$2"
local NODE_TYPE_VALUE="$3"

echo ""
info "部署到 ${NAME}..."
@@ -123,6 +118,11 @@ deploy_local() {

echo " [1/3] 更新 image tag..."
sed -i "s|${DOCKER_REGISTRY}:.*|${DOCKER_REGISTRY}:${TAG}|" docker-compose.yml
if grep -q "NODE_TYPE=" docker-compose.yml; then
sed -i "s|NODE_TYPE=.*|NODE_TYPE=${NODE_TYPE_VALUE}|" docker-compose.yml
else
sed -i "/SESSION_NAME=/a\\ - NODE_TYPE=${NODE_TYPE_VALUE}" docker-compose.yml
fi

echo " [2/3] 拉取新镜像..."
docker compose pull new-api
@@ -136,15 +136,18 @@ deploy_local() {
}

case "$TARGET" in
cn) deploy_local "国内测试(CN)" "$CN_DIR" ;;
ov) deploy_local "国外测试(OV)" "$OV_DIR" ;;
cn)
deploy_local "国内测试(CN)" "$CN_DIR" "master"
;;
ov)
deploy_local "国外测试(OV)" "$OV_DIR" "slave"
;;
all)
deploy_local "国内测试(CN)" "$CN_DIR"
deploy_local "国外测试(OV)" "$OV_DIR"
deploy_local "国内测试(CN)" "$CN_DIR" "master"
deploy_local "国外测试(OV)" "$OV_DIR" "slave"
;;
esac

# ==================== 总结 ====================
echo ""
section "部署总结"
echo -e " 镜像: ${GREEN}${IMAGE}${NC}"


Ładowanie…
Anuluj
Zapisz