From 9ca2bac98451881842107844e10e57fc71e6ace1 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Wed, 11 Mar 2026 14:04:00 +0800 Subject: [PATCH] Feat: Implement user creation, deletion, and permission management functionality. (#13519) ### What problem does this PR solve? Feat: Implement user creation, deletion, and permission management functionality. - Added the `ListByEmail` method to `user.go` to query users by email address. - Updated the user activation status handling logic in `handler.go`, adding input validation. - Added RSA password decryption functionality to `password.go`. - Implemented complete user management functionality in `service.go`, including user creation, deletion, password modification, activation status, and permission management. - Added input validation and error handling logic. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- internal/admin/handler.go | 22 ++-- internal/admin/password.go | 76 ++++++++++++ internal/admin/service.go | 237 +++++++++++++++++++++++++++++++++++-- internal/dao/user.go | 17 ++- 4 files changed, 332 insertions(+), 20 deletions(-) diff --git a/internal/admin/handler.go b/internal/admin/handler.go index a47628f64..542bf491a 100644 --- a/internal/admin/handler.go +++ b/internal/admin/handler.go @@ -289,7 +289,7 @@ func (h *Handler) ChangePassword(c *gin.Context) { // UpdateActivateStatusHTTPRequest update activate status request type UpdateActivateStatusHTTPRequest struct { - ActivateStatus bool `json:"activate_status" binding:"required"` + ActivateStatus string `json:"activate_status" binding:"required"` } // UpdateUserActivateStatus handle update user activate status @@ -306,7 +306,13 @@ func (h *Handler) UpdateUserActivateStatus(c *gin.Context) { return } - if err := h.service.UpdateUserActivateStatus(username, req.ActivateStatus); err != nil { + if req.ActivateStatus != "on" && req.ActivateStatus != "off" { + errorResponse(c, "Activation status must be 'on' or 'off'", 400) + return + } + + isActive := req.ActivateStatus == "on" + if err := h.service.UpdateUserActivateStatus(username, isActive); err != nil { errorResponse(c, err.Error(), 500) return } @@ -322,9 +328,9 @@ func (h *Handler) GrantAdmin(c *gin.Context) { return } - // Get current user from context - currentUser, _ := c.Get("user") - if currentUser != nil && currentUser.(string) == username { + // Get current user email from context + email, _ := c.Get("email") + if email != nil && email.(string) == username { errorResponse(c, "can't grant current user: "+username, 409) return } @@ -345,9 +351,9 @@ func (h *Handler) RevokeAdmin(c *gin.Context) { return } - // Get current user from context - currentUser, _ := c.Get("user") - if currentUser != nil && currentUser.(string) == username { + // Get current user email from context + email, _ := c.Get("email") + if email != nil && email.(string) == username { errorResponse(c, "can't revoke current user: "+username, 409) return } diff --git a/internal/admin/password.go b/internal/admin/password.go index ab81b169b..d1cd9ec05 100644 --- a/internal/admin/password.go +++ b/internal/admin/password.go @@ -18,11 +18,16 @@ package admin import ( "crypto/rand" + "crypto/rsa" "crypto/sha256" + "crypto/x509" "encoding/base64" "encoding/hex" + "encoding/pem" + "errors" "fmt" "hash" + "os" "strconv" "strings" @@ -109,3 +114,74 @@ func GenerateWerkzeugPasswordHash(password string, iterations int) (string, erro return fmt.Sprintf("pbkdf2:sha256:%d$%s$%s", iterations, saltB64, hashB64), nil } + +// DecryptPassword decrypts the password using RSA private key +// The password is expected to be base64 encoded RSA encrypted data +// If decryption fails, the original password is returned (assumed to be plain text) +func DecryptPassword(encryptedPassword string) (string, error) { + // Try to decode base64 + ciphertext, err := base64.StdEncoding.DecodeString(encryptedPassword) + if err != nil { + // If base64 decoding fails, assume it's already a plain password + return encryptedPassword, nil + } + + // Load private key + privateKey, err := loadPrivateKey() + if err != nil { + return "", err + } + + // Decrypt using PKCS#1 v1.5 + plaintext, err := rsa.DecryptPKCS1v15(nil, privateKey, ciphertext) + if err != nil { + // If decryption fails, assume it's already a plain password + return encryptedPassword, nil + } + + return string(plaintext), nil +} + +// loadPrivateKey loads and decrypts the RSA private key from conf/private.pem +func loadPrivateKey() (*rsa.PrivateKey, error) { + // Read private key file + keyData, err := os.ReadFile("conf/private.pem") + if err != nil { + return nil, fmt.Errorf("failed to read private key file: %w", err) + } + + // Parse PEM block + block, _ := pem.Decode(keyData) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + // Decrypt the PEM block if it's encrypted + var privateKey interface{} + if block.Headers["Proc-Type"] == "4,ENCRYPTED" { + // Decrypt using password "Welcome" + decryptedData, err := x509.DecryptPEMBlock(block, []byte("Welcome")) + if err != nil { + return nil, fmt.Errorf("failed to decrypt private key: %w", err) + } + + // Parse the decrypted key + privateKey, err = x509.ParsePKCS1PrivateKey(decryptedData) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + } else { + // Not encrypted, parse directly + privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + } + + rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("not an RSA private key") + } + + return rsaPrivateKey, nil +} diff --git a/internal/admin/service.go b/internal/admin/service.go index 555cd2817..dc1fb7406 100644 --- a/internal/admin/service.go +++ b/internal/admin/service.go @@ -32,6 +32,7 @@ import ( "ragflow/internal/model" "ragflow/internal/server" "ragflow/internal/utility" + "regexp" "strconv" "time" ) @@ -113,11 +114,75 @@ func (s *Service) ListUsers() ([]map[string]interface{}, error) { } // CreateUser create a new user +// Parameters: +// - username: email address of the user +// - password: encrypted password (base64 encoded RSA encrypted) +// - role: user role ("user" or "admin") +// +// Returns: +// - map[string]interface{}: user information without password +// - error: error message func (s *Service) CreateUser(username, password, role string) (map[string]interface{}, error) { - // TODO: Implement user creation with proper password hashing + emailRegex := regexp.MustCompile(`^[\w\._-]+@([\w_-]+\.)+[\w-]{2,}$`) + if !emailRegex.MatchString(username) { + return nil, fmt.Errorf("Invalid email address: %s!", username) + } + + existUser, _ := s.userDAO.GetByEmail(username) + if existUser != nil { + return nil, fmt.Errorf("User '%s' already exists", username) + } + + decryptedPassword, err := DecryptPassword(password) + if err != nil { + return nil, fmt.Errorf("failed to decrypt password: %w", err) + } + + hashedPassword, err := GenerateWerkzeugPasswordHash(decryptedPassword, 150000) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + + userID := utility.GenerateToken() + accessToken := utility.GenerateToken() + status := "1" + loginChannel := "password" + isSuperuser := role == "admin" + + now := time.Now().Unix() + nowDate := time.Now() + + user := &model.User{ + ID: userID, + AccessToken: &accessToken, + Email: username, + Nickname: "", + Password: &hashedPassword, + Status: &status, + IsActive: "1", + IsAuthenticated: "1", + IsAnonymous: "0", + LoginChannel: &loginChannel, + IsSuperuser: &isSuperuser, + BaseModel: model.BaseModel{ + CreateTime: &now, + CreateDate: &nowDate, + UpdateTime: &now, + UpdateDate: &nowDate, + }, + } + + if err := s.userDAO.Create(user); err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + return map[string]interface{}{ - "username": username, - "role": role, + "id": user.ID, + "email": user.Email, + "nickname": user.Nickname, + "is_active": user.IsActive, + "is_superuser": isSuperuser, + "create_date": user.CreateDate, }, nil } @@ -141,32 +206,188 @@ func (s *Service) GetUserDetails(username string) (map[string]interface{}, error } // DeleteUser delete user +// Parameters: +// - username: email address of the user to delete +// +// Returns: +// - error: error message func (s *Service) DeleteUser(username string) error { - // TODO: Implement user deletion + userList, err := s.userDAO.ListByEmail(username) + if err != nil || len(userList) == 0 { + return fmt.Errorf("User '%s' not found", username) + } + + if len(userList) > 1 { + return fmt.Errorf("Exist more than 1 user: %s!", username) + } + + user := userList[0] + + // Check if user is active - cannot delete active users + if user.IsActive == "1" { + return fmt.Errorf("User '%s' is active and can't be deleted. Please deactivate the user first", username) + } + + // Check if user is superuser - cannot delete admin accounts + if user.IsSuperuser != nil && *user.IsSuperuser { + return fmt.Errorf("Cannot delete admin account") + } + + if err := s.userDAO.DeleteByID(user.ID); err != nil { + return fmt.Errorf("failed to delete user: %w", err) + } + return nil } // ChangePassword change user password +// Parameters: +// - username: email address of the user +// - newPassword: new encrypted password (base64 encoded RSA encrypted) +// +// Returns: +// - error: error message func (s *Service) ChangePassword(username, newPassword string) error { - // TODO: Implement password change + userList, err := s.userDAO.ListByEmail(username) + if err != nil || len(userList) == 0 { + return fmt.Errorf("User '%s' not found", username) + } + + if len(userList) > 1 { + return fmt.Errorf("Exist more than 1 user: %s!", username) + } + + user := userList[0] + + decryptedPassword, err := DecryptPassword(newPassword) + if err != nil { + return fmt.Errorf("failed to decrypt password: %w", err) + } + + if user.Password != nil && CheckWerkzeugPassword(decryptedPassword, *user.Password) { + return nil + } + + hashedPassword, err := GenerateWerkzeugPasswordHash(decryptedPassword, 150000) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + user.Password = &hashedPassword + now := time.Now().Unix() + user.UpdateTime = &now + + if err := s.userDAO.Update(user); err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + return nil } // UpdateUserActivateStatus update user activate status +// Parameters: +// - username: email address of the user +// - isActive: true to activate, false to deactivate +// +// Returns: +// - error: error message func (s *Service) UpdateUserActivateStatus(username string, isActive bool) error { - // TODO: Implement activate status update + userList, err := s.userDAO.ListByEmail(username) + if err != nil || len(userList) == 0 { + return fmt.Errorf("User '%s' not found", username) + } + + if len(userList) > 1 { + return fmt.Errorf("Exist more than 1 user: %s!", username) + } + + user := userList[0] + + targetStatus := "0" + if isActive { + targetStatus = "1" + } + + if user.IsActive == targetStatus { + return nil + } + + user.IsActive = targetStatus + now := time.Now().Unix() + user.UpdateTime = &now + + if err := s.userDAO.Update(user); err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + return nil } // GrantAdmin grant admin privileges +// Parameters: +// - username: email address of the user +// +// Returns: +// - error: error message func (s *Service) GrantAdmin(username string) error { - // TODO: Implement grant admin + userList, err := s.userDAO.ListByEmail(username) + if err != nil || len(userList) == 0 { + return fmt.Errorf("User '%s' not found", username) + } + + if len(userList) > 1 { + return fmt.Errorf("Exist more than 1 user: %s!", username) + } + + user := userList[0] + + if user.IsSuperuser != nil && *user.IsSuperuser { + return nil + } + + isSuperuser := true + user.IsSuperuser = &isSuperuser + now := time.Now().Unix() + user.UpdateTime = &now + + if err := s.userDAO.Update(user); err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + return nil } // RevokeAdmin revoke admin privileges +// Parameters: +// - username: email address of the user +// +// Returns: +// - error: error message func (s *Service) RevokeAdmin(username string) error { - // TODO: Implement revoke admin + userList, err := s.userDAO.ListByEmail(username) + if err != nil || len(userList) == 0 { + return fmt.Errorf("User '%s' not found", username) + } + + if len(userList) > 1 { + return fmt.Errorf("Exist more than 1 user: %s!", username) + } + + user := userList[0] + + if user.IsSuperuser == nil || !*user.IsSuperuser { + return nil + } + + isSuperuser := false + user.IsSuperuser = &isSuperuser + now := time.Now().Unix() + user.UpdateTime = &now + + if err := s.userDAO.Update(user); err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + return nil } diff --git a/internal/dao/user.go b/internal/dao/user.go index 7fb5b10b3..abbf79c2f 100644 --- a/internal/dao/user.go +++ b/internal/dao/user.go @@ -84,16 +84,17 @@ func (dao *UserDAO) UpdateAccessToken(user *model.User, token string) error { return DB.Model(user).Update("access_token", token).Error } -// List list users +// List list users (only active users with status != "0") func (dao *UserDAO) List(offset, limit int) ([]*model.User, int64, error) { var users []*model.User var total int64 - if err := DB.Model(&model.User{}).Count(&total).Error; err != nil { + // Only count users with status != "0" (not deleted) + if err := DB.Model(&model.User{}).Where("status != ? OR status IS NULL", "0").Count(&total).Error; err != nil { return nil, 0, err } - query := DB.Model(&model.User{}) + query := DB.Model(&model.User{}).Where("status != ? OR status IS NULL", "0") if offset > 0 { query = query.Offset(offset) } @@ -109,7 +110,15 @@ func (dao *UserDAO) Delete(id uint) error { return DB.Delete(&model.User{}, id).Error } -// DeleteByID delete user by string ID +// DeleteByID delete user by string ID (soft delete - set status to 0) func (dao *UserDAO) DeleteByID(id string) error { return DB.Model(&model.User{}).Where("id = ?", id).Update("status", "0").Error } + +// ListByEmail list users by email (only active users with status != "0") +// Returns all users matching the given email address +func (dao *UserDAO) ListByEmail(email string) ([]*model.User, error) { + var users []*model.User + err := DB.Where("email = ? AND (status != ? OR status IS NULL)", email, "0").Find(&users).Error + return users, err +}