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)
This commit is contained in:
chanx
2026-03-11 14:04:00 +08:00
committed by GitHub
parent 2028e895fd
commit 9ca2bac984
4 changed files with 332 additions and 20 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}