mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-07 20:10:14 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user