fix(admin): validate user credentials (fix #3563)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
master
Aaron Liu 7 days ago
parent 65f8c3f3ed
commit 78994b9719

@ -13,6 +13,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/serializer"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/samber/lo" "github.com/samber/lo"
) )
@ -138,13 +139,31 @@ func (service *SingleUserService) CalibrateStorage(c *gin.Context) (*GetUserResp
type ( type (
UpsertUserService struct { UpsertUserService struct {
User *ent.User `json:"user" binding:"required"` 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"` TwoFA string `json:"two_fa"`
} }
UpsertUserParamCtx struct{} 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) { func (s *UpsertUserService) Update(c *gin.Context) (*GetUserResponse, error) {
if err := s.validateEmail(); err != nil {
return nil, err
}
dep := dependency.FromContext(c) dep := dependency.FromContext(c)
userClient := dep.UserClient() 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) newUser, err := userClient.Upsert(ctx, s.User, s.Password, s.TwoFA)
if err != nil { if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to update user", err) 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) { func (s *UpsertUserService) Create(c *gin.Context) (*GetUserResponse, error) {
dep := dependency.FromContext(c) if err := s.validateEmail(); err != nil {
userClient := dep.UserClient() return nil, err
}
if s.Password == "" { if s.Password == "" {
return nil, serializer.NewError(serializer.CodeParamErr, "Password is required", nil) return nil, serializer.NewError(serializer.CodeParamErr, "Password is required", nil)
} }
dep := dependency.FromContext(c)
userClient := dep.UserClient()
if s.User.ID != 0 { if s.User.ID != 0 {
return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0", nil) return nil, serializer.NewError(serializer.CodeParamErr, "ID must be 0", nil)
} }

@ -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)
}
})
}
}
Loading…
Cancel
Save