From 78994b9719eddfc78090366cb3d3c2534ecc3ae1 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Fri, 4 Sep 2026 16:06:47 +0800 Subject: [PATCH] fix(admin): validate user credentials (fix #3563) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- service/admin/user.go | 33 ++++++++++--- service/admin/user_test.go | 96 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 service/admin/user_test.go diff --git a/service/admin/user.go b/service/admin/user.go index 8ae2aea9..fea859fb 100644 --- a/service/admin/user.go +++ b/service/admin/user.go @@ -13,6 +13,7 @@ import ( "github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/samber/lo" ) @@ -138,13 +139,31 @@ func (service *SingleUserService) CalibrateStorage(c *gin.Context) (*GetUserResp type ( UpsertUserService struct { User *ent.User `json:"user" binding:"required"` - Password string `json:"password"` + Password string `json:"password" binding:"omitempty,min=6,max=128"` TwoFA string `json:"two_fa"` } UpsertUserParamCtx struct{} ) +type adminUserEmailValidation struct { + Email string `binding:"required,email"` +} + +func (s *UpsertUserService) validateEmail() error { + if s.User == nil { + return serializer.NewError(serializer.CodeParamErr, "Email format error", nil) + } + if err := binding.Validator.ValidateStruct(adminUserEmailValidation{Email: s.User.Email}); err != nil { + return serializer.NewError(serializer.CodeParamErr, "Email format error", err) + } + return nil +} + func (s *UpsertUserService) Update(c *gin.Context) (*GetUserResponse, error) { + if err := s.validateEmail(); err != nil { + return nil, err + } + dep := dependency.FromContext(c) userClient := dep.UserClient() @@ -165,10 +184,6 @@ func (s *UpsertUserService) Update(c *gin.Context) (*GetUserResponse, error) { } - if s.Password != "" && len(s.Password) > 128 { - return nil, serializer.NewError(serializer.CodeParamErr, "Password too long", nil) - } - newUser, err := userClient.Upsert(ctx, s.User, s.Password, s.TwoFA) if err != nil { return nil, serializer.NewError(serializer.CodeDBError, "Failed to update user", err) @@ -179,13 +194,17 @@ func (s *UpsertUserService) Update(c *gin.Context) (*GetUserResponse, error) { } func (s *UpsertUserService) Create(c *gin.Context) (*GetUserResponse, error) { - dep := dependency.FromContext(c) - userClient := dep.UserClient() + if err := s.validateEmail(); err != nil { + return nil, err + } if s.Password == "" { return nil, serializer.NewError(serializer.CodeParamErr, "Password is required", nil) } + dep := dependency.FromContext(c) + userClient := dep.UserClient() + if s.User.ID != 0 { return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0", nil) } diff --git a/service/admin/user_test.go b/service/admin/user_test.go new file mode 100644 index 00000000..615360c5 --- /dev/null +++ b/service/admin/user_test.go @@ -0,0 +1,96 @@ +package admin + +import ( + "errors" + "strings" + "testing" + + "github.com/cloudreve/Cloudreve/v4/ent" + "github.com/cloudreve/Cloudreve/v4/pkg/serializer" + "github.com/gin-gonic/gin/binding" +) + +func TestUpsertUserServiceValidateEmail(t *testing.T) { + tests := []struct { + name string + email string + wantErr bool + }{ + {name: "empty", email: "", wantErr: true}, + {name: "missing top level domain", email: "admin@test", wantErr: true}, + {name: "localhost domain", email: "user@localhost", wantErr: true}, + {name: "surrounding whitespace", email: " user@example.com ", wantErr: true}, + {name: "regular address", email: "admin@test.com"}, + {name: "subaddress", email: "user+tag@example.co.uk"}, + {name: "internationalized address", email: "用户@例子.公司"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + service := &UpsertUserService{User: &ent.User{Email: test.email}} + err := service.validateEmail() + if test.wantErr { + if err == nil { + t.Fatal("expected email validation to fail") + } + var appErr serializer.AppError + if !errors.As(err, &appErr) { + t.Fatalf("expected AppError, got %T", err) + } + if appErr.Code != serializer.CodeParamErr { + t.Fatalf("unexpected error code: %d", appErr.Code) + } + return + } + if err != nil { + t.Fatalf("expected email validation to pass: %v", err) + } + }) + } +} + +func TestUpsertUserServiceValidateEmailRejectsMissingUser(t *testing.T) { + service := &UpsertUserService{} + err := service.validateEmail() + if err == nil { + t.Fatal("expected missing user validation to fail") + } + + var appErr serializer.AppError + if !errors.As(err, &appErr) || appErr.Code != serializer.CodeParamErr { + t.Fatalf("unexpected validation error: %v", err) + } +} + +func TestUpsertUserServicePasswordBinding(t *testing.T) { + tests := []struct { + name string + password string + wantErr bool + }{ + {name: "empty update", password: ""}, + {name: "short update", password: "12345", wantErr: true}, + {name: "minimum length update", password: "123456"}, + {name: "maximum length", password: strings.Repeat("a", 128)}, + {name: "too long", password: strings.Repeat("a", 129), wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + service := &UpsertUserService{ + User: &ent.User{Email: "admin@example.com"}, + Password: test.password, + } + err := binding.Validator.ValidateStruct(service) + if test.wantErr { + if err == nil { + t.Fatal("expected password validation to fail") + } + return + } + if err != nil { + t.Fatalf("expected password validation to pass: %v", err) + } + }) + } +}