fix(database): support multiple MySQL user hosts (#13342)

This commit is contained in:
ssongliu
2026-07-23 10:01:39 +08:00
committed by GitHub
parent d330f60b2d
commit f6ed11ac55
20 changed files with 273 additions and 107 deletions
+6
View File
@@ -1,6 +1,7 @@
package repo
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
@@ -16,6 +17,7 @@ type IDatabaseUserRepo interface {
List(opts ...DBOption) ([]model.DatabaseUser, error)
Save(user *model.DatabaseUser) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByUser(username, host string) DBOption
@@ -81,6 +83,10 @@ func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
return db.Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
+7
View File
@@ -1,6 +1,8 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
@@ -14,6 +16,7 @@ type IDatabaseUserGrantRepo interface {
Save(grant *model.DatabaseUserGrant) error
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByDBName(dbName string) DBOption
@@ -69,6 +72,10 @@ func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
return db.Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
+14
View File
@@ -412,6 +412,20 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
switch install.App.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
if err = databaseUserGrantRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserGrantRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
if err = databaseUserRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppMongodb:
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
+112 -26
View File
@@ -89,19 +89,32 @@ func databaseUserKey(username, host string) string {
func splitMysqlHosts(permission string) []string {
hosts := strings.Split(permission, ",")
res := make([]string, 0, len(hosts))
seen := make(map[string]struct{}, len(hosts))
for _, host := range hosts {
host = strings.TrimSpace(host)
if len(host) == 0 {
continue
}
if _, ok := seen[host]; ok {
continue
}
seen[host] = struct{}{}
res = append(res, host)
}
if len(res) == 0 {
return []string{"%"}
}
return res
}
func parseMysqlHosts(permission string, allowMultiple bool) ([]string, error) {
hosts := splitMysqlHosts(permission)
if len(hosts) == 0 {
return nil, errors.New("mysql user host is required")
}
if !allowMultiple && len(hosts) > 1 {
return nil, errors.New("multiple mysql user hosts are not supported for this operation")
}
return hosts, nil
}
func checkMysqlNormalUser(username string) error {
if isMysqlSystemUser(username) {
return errors.New("mysql system user does not support this operation")
@@ -109,14 +122,16 @@ func checkMysqlNormalUser(username string) error {
return nil
}
// isMysqlSystemUser identifies built-in accounts which must only be managed by
// MySQL itself. The root account is managed through the dedicated root API.
// isMysqlSystemUser identifies internal accounts which must only be managed by
// MySQL or its container runtime. The root account is managed through the
// dedicated root API.
func isMysqlSystemUser(username string) bool {
switch strings.ToLower(username) {
case "root",
"mysql.session", "mysql.sys", "mysql.infoschema", "mysqlxsys",
"mariadb.sys", "mariadb-sys",
"debian-sys-maint":
"debian-sys-maint",
"healthcheck":
return true
default:
return false
@@ -424,6 +439,15 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
if len(req.Username) != 0 && len(req.Password) == 0 {
return nil, errors.New("password is required when creating mysql user")
}
permissionHosts := make([]string, 0)
if len(req.Username) != 0 {
var err error
permissionHosts, err = parseMysqlHosts(req.Permission, true)
if err != nil {
return nil, err
}
req.Permission = strings.Join(permissionHosts, ",")
}
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return nil, err
@@ -468,7 +492,7 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil {
return nil, err
}
for _, host := range splitMysqlHosts(req.Permission) {
for _, host := range permissionHosts {
if err := saveDatabaseUserGrant(dbType, req.Database, req.Name, req.Username, host); err != nil {
return nil, err
}
@@ -597,19 +621,53 @@ func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
hosts, err := parseMysqlHosts(req.Host, true)
if err != nil {
return err
}
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
}
cli, _, err := LoadMysqlClientByFrom(req.Database)
cli, version, err := LoadMysqlClientByFrom(req.Database)
if err != nil {
return err
}
defer cli.Close()
if err := cli.CreateUserOnly(client.UserInfo{Username: req.Username, Host: req.Host}, req.Password, 300); err != nil {
return err
createdHosts := make([]string, 0, len(hosts))
rollbackCreatedHosts := func() {
for i := len(createdHosts) - 1; i >= 0; i-- {
host := createdHosts[i]
if rollbackErr := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: host}, version, 300); rollbackErr != nil {
global.LOG.Errorf("rollback mysql user %s@%s failed, err: %v", req.Username, host, rollbackErr)
}
}
}
return saveDatabaseUserCredential(dbType, req.Database, req.Username, req.Host, req.Password, req.Description)
for _, host := range hosts {
if err := cli.CreateUserOnly(client.UserInfo{Username: req.Username, Host: host}, req.Password, 300); err != nil {
rollbackCreatedHosts()
return err
}
createdHosts = append(createdHosts, host)
}
savedHosts := make([]string, 0, len(hosts))
for _, host := range hosts {
if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, host, req.Password, req.Description); err != nil {
for _, savedHost := range savedHosts {
if rollbackErr := databaseUserRepo.Delete(
repo.WithByType(dbType),
databaseUserRepo.WithByDatabase(req.Database),
databaseUserRepo.WithByUser(req.Username, savedHost),
); rollbackErr != nil {
global.LOG.Errorf("rollback mysql user record %s@%s failed, err: %v", req.Username, savedHost, rollbackErr)
}
}
rollbackCreatedHosts()
return err
}
savedHosts = append(savedHosts, host)
}
return nil
}
func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
@@ -619,10 +677,20 @@ func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
oldHosts, err := parseMysqlHosts(req.Host, false)
if err != nil {
return err
}
newHosts, err := parseMysqlHosts(req.NewHost, false)
if err != nil {
return err
}
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
}
req.Host = oldHosts[0]
req.NewHost = newHosts[0]
if req.Host != req.NewHost {
targetUser, _ := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.NewHost))
if targetUser.ID != 0 {
@@ -687,6 +755,11 @@ func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
hosts, err := parseMysqlHosts(req.Host, false)
if err != nil {
return err
}
req.Host = hosts[0]
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
@@ -722,18 +795,7 @@ func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error {
}); err != nil {
return err
}
user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host))
if err != nil {
user = model.DatabaseUser{
Type: dbType,
Database: req.Database,
Username: req.Username,
Host: req.Host,
}
}
user.Password = req.Password
user.IsDelete = false
if err := databaseUserRepo.Save(&user); err != nil {
if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, req.Host, req.Password, ""); err != nil {
return err
}
return updateMysqlPasswordAppTargets(appTargets, req.Password)
@@ -746,6 +808,11 @@ func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
hosts, err := parseMysqlHosts(req.Host, false)
if err != nil {
return err
}
req.Host = hosts[0]
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
@@ -768,6 +835,11 @@ func (u *MysqlService) DeleteUser(req dto.MysqlUserDelete) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
hosts, err := parseMysqlHosts(req.Host, false)
if err != nil {
return err
}
req.Host = hosts[0]
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
@@ -805,6 +877,10 @@ func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
hosts, err := parseMysqlHosts(req.Host, true)
if err != nil {
return err
}
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
@@ -814,10 +890,15 @@ func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error {
return err
}
defer cli.Close()
if err := cli.GrantUser(client.GrantInfo{Database: req.DB, Username: req.Username, Host: req.Host}, 300); err != nil {
return err
for _, host := range hosts {
if err := cli.GrantUser(client.GrantInfo{Database: req.DB, Username: req.Username, Host: host}, 300); err != nil {
return err
}
if err := saveDatabaseUserGrant(dbType, req.Database, req.DB, req.Username, host); err != nil {
return err
}
}
return saveDatabaseUserGrant(dbType, req.Database, req.DB, req.Username, req.Host)
return nil
}
func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
@@ -830,6 +911,11 @@ func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error {
if err := checkMysqlNormalUser(req.Username); err != nil {
return err
}
hosts, err := parseMysqlHosts(req.Host, false)
if err != nil {
return err
}
req.Host = hosts[0]
dbType, err := resolveDatabaseUserType(req.Database)
if err != nil {
return err
-2
View File
@@ -558,10 +558,8 @@ const message = {
addUserAuthorization: 'Add user authorization',
revokeAuthorization: 'Revoke authorization',
revokeAuthorizationHelper: "Revoke user {0}'s access to database {1}?",
passwordPendingSupplement: 'Pending',
supplementPassword: 'Add password',
supplementPasswordHelper: 'This only saves the existing password to 1Panel and does not change it in MySQL.',
deleteUserRecordHelper: 'This user has been deleted from MySQL. This only removes the local 1Panel record.',
noUserBind: 'No binding',
pgBindHelper:
'This operation is used to create a new user and bind it to the target database. Currently, selecting users already existing in the database is not supported.',
+1 -4
View File
@@ -531,7 +531,7 @@ const message = {
hostSelect: 'Server Selection',
selectHelper: 'Estas seguro de importar el archivo de backup {0}?',
remoteAccess: 'Acceso remoto',
remoteHelper: 'Múltiples IP separadas por comas, ejemplo: 172.16.10.111, 172.16.10.112',
remoteHelper: 'Múltiples IP separadas por comas, ejemplo: 172.16.10.111,172.16.10.112',
remoteConnHelper:
'Conectarse remotamente como usuario root en MySQL puede presentar riesgos de seguridad. Por lo tanto, realice esta operación con precaución.',
changePassword: 'Cambiar contraseña',
@@ -555,11 +555,8 @@ const message = {
addUserAuthorization: 'Añadir autorización de usuario',
revokeAuthorization: 'Revocar autorización',
revokeAuthorizationHelper: '¿Revocar el acceso del usuario {0} a la base de datos {1}?',
passwordPendingSupplement: 'Pendiente',
supplementPassword: 'Añadir contraseña',
supplementPasswordHelper: 'Solo guarda la contraseña existente en 1Panel; no cambia la contraseña en MySQL.',
deleteUserRecordHelper:
'El usuario ya se eliminó de MySQL. Esta acción solo elimina el registro local de 1Panel.',
pgBindHelper:
'Esta operación se utiliza para crear un nuevo usuario y vincularlo a la base de datos destino. Actualmente no se admite seleccionar usuarios ya existentes en la base de datos.',
pgSuperUser: 'Superusuario',
-2
View File
@@ -546,10 +546,8 @@ const message = {
addUserAuthorization: 'افزودن مجوز کاربر',
revokeAuthorization: 'لغو مجوز',
revokeAuthorizationHelper: 'دسترسی کاربر {0} به پایگاه داده {1} لغو شود؟',
passwordPendingSupplement: 'در انتظار تکمیل',
supplementPassword: 'افزودن گذرواژه',
supplementPasswordHelper: 'فقط گذرواژه فعلی را در 1Panel ذخیره می‌کند و گذرواژه MySQL را تغییر نمی‌دهد.',
deleteUserRecordHelper: 'این کاربر از MySQL حذف شده است. این عملیات فقط رکورد محلی 1Panel را حذف می‌کند.',
pgBindHelper:
'این عملیات برای ایجاد کاربر جدید و اتصال آن به پایگاه داده هدف استفاده می‌شود. در حال حاضر انتخاب کاربرانی که از قبل در پایگاه داده وجود دارند پشتیبانی نمی‌شود.',
pgSuperUser: 'کاربر فوق‌العاده',
+1 -3
View File
@@ -529,7 +529,7 @@ const message = {
hostSelect: 'サーバー選択',
selectHelper: 'バックアップファイル {0} をインポートしてもよろしいですか',
remoteAccess: 'リモートアクセス',
remoteHelper: '複数のIP Comma delimited:172.16.10.111172.16.10.112',
remoteHelper: '複数のIP Comma delimited:172.16.10.111,172.16.10.112',
remoteConnHelper:
'ユーザールートとしてのMySQLへのリモート接続にはセキュリティリスクがある場合がありますしたがってこの操作を慎重に実行します',
changePassword: 'パスワード',
@@ -553,10 +553,8 @@ const message = {
addUserAuthorization: 'ユーザー権限を追加',
revokeAuthorization: '権限を取り消す',
revokeAuthorizationHelper: 'ユーザー {0} のデータベース {1} へのアクセス権を取り消しますか',
passwordPendingSupplement: '未登録',
supplementPassword: 'パスワードを登録',
supplementPasswordHelper: '既存のパスワードを 1Panel に保存するだけでMySQL のパスワードは変更しません',
deleteUserRecordHelper: 'このユーザーは MySQL から削除済みです1Panel のローカル記録のみ削除します',
pgBindHelper:
'この操作は新しいユーザーを作成しターゲットデータベースにバインドするために使用されます現在データベースに既存のユーザーを選択することはサポートされていません',
pgSuperUser: 'スーパーユーザー',
+1 -3
View File
@@ -524,7 +524,7 @@ const message = {
hostSelect: '서버 선택',
selectHelper: '백업 파일 {0}() 가져오시겠습니까?',
remoteAccess: '원격 접근',
remoteHelper: '여러 IP 쉼표로 구분하여 입력, : 172.16.10.111, 172.16.10.112',
remoteHelper: '여러 IP 쉼표로 구분하여 입력, : 172.16.10.111,172.16.10.112',
remoteConnHelper:
'MySQL root 사용자로 원격 접속은 보안 위험을 초래할 있습니다. 따라서 작업은 신중히 수행해야 합니다.',
changePassword: '비밀번호',
@@ -547,10 +547,8 @@ const message = {
addUserAuthorization: '사용자 권한 추가',
revokeAuthorization: '권한 취소',
revokeAuthorizationHelper: '사용자 {0} 데이터베이스 {1} 접근 권한을 취소하시겠습니까?',
passwordPendingSupplement: '입력 필요',
supplementPassword: '비밀번호 추가',
supplementPasswordHelper: '기존 비밀번호를 1Panel에만 저장하며 MySQL 비밀번호는 변경하지 않습니다.',
deleteUserRecordHelper: ' 사용자는 MySQL에서 이미 삭제되었습니다. 1Panel의 로컬 기록만 삭제합니다.',
pgBindHelper:
' 작업은 사용자를 생성하여 대상 데이터베이스에 바인딩하는 사용됩니다. 현재 데이터베이스에 이미 존재하는 사용자 선택은 지원되지 않습니다.',
pgSuperUser: '슈퍼 사용자',
-2
View File
@@ -554,10 +554,8 @@ const message = {
addUserAuthorization: 'ເພີ່ມສິດຜູ້ໃຊ້',
revokeAuthorization: 'ຖອນສິດ',
revokeAuthorizationHelper: 'ຖອນສິດຜູ້ໃຊ້ {0} ໃນຖານຂໍ້ມູນ {1} ຫຼືບໍ່?',
passwordPendingSupplement: 'ລໍຖ້າເພີ່ມ',
supplementPassword: 'ເພີ່ມລະຫັດຜ່ານ',
supplementPasswordHelper: 'ບັນທຶກລະຫັດຜ່ານປັດຈຸບັນໄວ້ໃນ 1Panel ເທົ່ານັ້ນ ແລະບໍ່ປ່ຽນລະຫັດ MySQL.',
deleteUserRecordHelper: 'ຜູ້ໃຊ້ນີ້ຖືກລຶບຈາກ MySQL ແລ້ວ. ຈະລຶບສະເພາະຂໍ້ມູນທ້ອງຖິ່ນໃນ 1Panel.',
noUserBind: 'ບໍ່ມີການຜູກມັດ',
pgBindHelper:
'ການດຳເນີນການນີ້ໃຊ້ເພື່ອສ້າງຜູ້ໃຊ້ໃໝ່ ແລະ ຜູກມັດກັບຖານຂໍ້ມູນເປົ້າໝາຍ. ໃນປັດຈຸບັນ, ຍັງບໍ່ຮອງຮັບການເລືອກຜູ້ໃຊ້ທີ່ມີຢູ່ແລ້ວໃນຖານຂໍ້ມູນ.',
+1 -4
View File
@@ -534,7 +534,7 @@ const message = {
hostSelect: 'Pemilihan Pelayan',
selectHelper: 'Adakah anda pasti ingin mengimport fail sandaran {0}?',
remoteAccess: 'Akses jauh',
remoteHelper: 'Berbilang IP dipisahkan dengan koma, contoh: 172.16.10.111, 172.16.10.112',
remoteHelper: 'Berbilang IP dipisahkan dengan koma, contoh: 172.16.10.111,172.16.10.112',
remoteConnHelper:
'Sambungan jauh ke MySQL sebagai pengguna root mungkin mempunyai risiko keselamatan. Oleh itu, lakukan operasi ini dengan berhati-hati.',
changePassword: 'Kata laluan',
@@ -558,11 +558,8 @@ const message = {
addUserAuthorization: 'Tambah kebenaran pengguna',
revokeAuthorization: 'Tarik balik kebenaran',
revokeAuthorizationHelper: 'Tarik balik akses pengguna {0} kepada pangkalan data {1}?',
passwordPendingSupplement: 'Belum dilengkapkan',
supplementPassword: 'Tambah kata laluan',
supplementPasswordHelper: 'Hanya menyimpan kata laluan sedia ada ke 1Panel dan tidak mengubahnya dalam MySQL.',
deleteUserRecordHelper:
'Pengguna ini telah dipadam daripada MySQL. Tindakan ini hanya memadam rekod setempat 1Panel.',
pgBindHelper:
'Operasi ini digunakan untuk mencipta pengguna baharu dan mengaitkannya dengan pangkalan data sasaran. Pada masa ini, memilih pengguna yang sudah wujud dalam pangkalan data tidak disokong.',
pgSuperUser: 'Pengguna Super',
+1 -4
View File
@@ -528,7 +528,7 @@ const message = {
hostSelect: 'Seleção de Servidor',
selectHelper: 'Tem certeza de que deseja importar o arquivo de backup {0}?',
remoteAccess: 'Acesso remoto',
remoteHelper: 'Vários IPs separados por vírgula, exemplo: 172.16.10.111, 172.16.10.112',
remoteHelper: 'Vários IPs separados por vírgula, exemplo: 172.16.10.111,172.16.10.112',
remoteConnHelper:
'Conectar-se ao MySQL como usuário root pode representar riscos de segurança. Realize esta operação com cautela.',
changePassword: 'Senha',
@@ -552,11 +552,8 @@ const message = {
addUserAuthorization: 'Adicionar autorização de usuário',
revokeAuthorization: 'Revogar autorização',
revokeAuthorizationHelper: 'Revogar o acesso do usuário {0} ao banco de dados {1}?',
passwordPendingSupplement: 'Pendente',
supplementPassword: 'Adicionar senha',
supplementPasswordHelper: 'Apenas salva a senha existente no 1Panel; não altera a senha no MySQL.',
deleteUserRecordHelper:
'O usuário foi excluído do MySQL. Esta ação remove apenas o registro local do 1Panel.',
pgBindHelper:
'Esta operação cria um novo usuário e o vincula ao banco de dados alvo. A seleção de usuários existentes no banco de dados não é suportada.',
pgSuperUser: 'Superusuário',
+1 -3
View File
@@ -523,7 +523,7 @@ const message = {
hostSelect: 'Выбор сервера',
selectHelper: 'Вы уверены, что хотите импортировать файл резервной копии {0}?',
remoteAccess: 'Удаленный доступ',
remoteHelper: 'Несколько IP через запятую, например: 172.16.10.111, 172.16.10.112',
remoteHelper: 'Несколько IP через запятую, например: 172.16.10.111,172.16.10.112',
remoteConnHelper:
'Удаленное подключение к MySQL как пользователь root может иметь риски безопасности. Поэтому выполняйте эту операцию с осторожностью.',
changePassword: 'Пароль',
@@ -546,10 +546,8 @@ const message = {
addUserAuthorization: 'Добавить доступ пользователю',
revokeAuthorization: 'Отозвать доступ',
revokeAuthorizationHelper: 'Отозвать у пользователя {0} доступ к базе данных {1}?',
passwordPendingSupplement: 'Требуется пароль',
supplementPassword: 'Добавить пароль',
supplementPasswordHelper: 'Сохраняет текущий пароль только в 1Panel и не изменяет пароль в MySQL.',
deleteUserRecordHelper: 'Пользователь уже удалён из MySQL. Будет удалена только локальная запись 1Panel.',
pgBindHelper:
'Эта операция используется для создания нового пользователя и привязки его к целевой базе данных. В настоящее время выбор уже существующих пользователей в базе данных не поддерживается.',
pgSuperUser: 'Суперпользователь',
+1 -3
View File
@@ -529,7 +529,7 @@ const message = {
hostSelect: 'Sunucu Seçimi',
selectHelper: '{0} yedek dosyasını içe aktarmak istediğinizden emin misiniz?',
remoteAccess: 'Uzaktan erişim',
remoteHelper: 'Birden fazla IP virgülle ayrılır, örnek: 172.16.10.111, 172.16.10.112',
remoteHelper: 'Birden fazla IP virgülle ayrılır, örnek: 172.16.10.111,172.16.10.112',
remoteConnHelper:
'Root kullanıcısı olarak MySQLe uzaktan bağlantı güvenlik riski oluşturabilir. Bu nedenle bu işlemi dikkatli yapın.',
changePassword: 'Şifre değiştir',
@@ -553,10 +553,8 @@ const message = {
addUserAuthorization: 'Kullanıcı yetkisi ekle',
revokeAuthorization: 'Yetkiyi kaldır',
revokeAuthorizationHelper: '{0} kullanıcısının {1} veritabanına erişimi kaldırılsın mı?',
passwordPendingSupplement: 'Bekliyor',
supplementPassword: 'Parola ekle',
supplementPasswordHelper: 'Yalnızca mevcut parolayı 1Panel içine kaydeder; MySQL parolasını değiştirmez.',
deleteUserRecordHelper: 'Bu kullanıcı MySQL üzerinden silinmiş. Bu işlem yalnızca yerel 1Panel kaydını siler.',
pgBindHelper:
'Bu işlem yeni bir kullanıcı oluşturmak ve hedef veritabanına bağlamak için kullanılır. Şu anda veritabanında mevcut olan kullanıcıları seçmek desteklenmiyor.',
pgSuperUser: 'Süper Kullanıcı',
-2
View File
@@ -530,10 +530,8 @@ const message = {
addUserAuthorization: '新增使用者授權',
revokeAuthorization: '撤銷授權',
revokeAuthorizationHelper: '確定撤銷使用者 {0} 對資料庫 {1} 的存取授權嗎',
passwordPendingSupplement: '待補充',
supplementPassword: '補充密碼',
supplementPasswordHelper: '僅將現有密碼儲存到 1Panel不會修改 MySQL 使用者密碼',
deleteUserRecordHelper: '該使用者已從 MySQL 中刪除此操作僅刪除 1Panel 中的本機記錄',
pgBindHelper: '此操作用於建立新使用者並將其綁定到目標資料庫暫不支援選擇已存在於資料庫中的使用者',
pgSuperUser: '超級使用者',
loadFromRemoteHelper: '此操作將同步伺服器上的資料庫使用者及授權資訊到 1Panel是否繼續',
-2
View File
@@ -522,10 +522,8 @@ const message = {
addUserAuthorization: '添加用户授权',
revokeAuthorization: '撤销授权',
revokeAuthorizationHelper: '确定撤销用户 {0} 对数据库 {1} 的访问授权吗',
passwordPendingSupplement: '待补充',
supplementPassword: '补充密码',
supplementPasswordHelper: '仅将现有密码保存到 1Panel不会修改 MySQL 用户密码',
deleteUserRecordHelper: '该用户已从 MySQL 中删除此操作仅删除 1Panel 中的本地记录',
noUserBind: '不绑定',
pgBindHelper: '该操作用于创建新用户并将其绑定到目标数据库暂不支持选择已存在于数据库中的用户',
pgSuperUser: '超级用户',
@@ -1,5 +1,5 @@
<template>
<DialogPro v-model="dialogVisible" :title="$t('database.authorizationManagement')" size="small">
<DialogPro v-model="dialogVisible" :title="$t('database.authorizationManagement')" size="large">
<div v-loading="loading">
<div class="authorization-toolbar">
<el-button type="primary" @click="openAddDialog">
@@ -8,7 +8,31 @@
</div>
<el-table :data="authorizedUsers" :empty-text="$t('commons.msg.noneData')">
<el-table-column :label="$t('commons.login.username')" min-width="140">
<template #default="{ row }">{{ row.username }}@{{ row.host }}</template>
<template #default="{ row }">{{ row.username }}</template>
</el-table-column>
<el-table-column :label="$t('commons.login.password')" prop="password" min-width="180">
<template #default="{ row }">
<span v-if="!row.password">-</span>
<div v-else class="password-cell">
<span v-if="!row.showPassword" class="password-text">**********</span>
<span v-else class="password-text">{{ row.password }}</span>
<el-button
v-if="!row.showPassword"
link
@click="row.showPassword = true"
icon="View"
class="password-action"
/>
<el-button
v-else
link
@click="row.showPassword = false"
icon="Hide"
class="password-action"
/>
<CopyButton class="password-copy" :content="row.password" />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('database.permission')" min-width="110">
<template #default="{ row }">{{ permissionLabel(row.host) }}</template>
@@ -162,11 +186,19 @@ const form = reactive({
password: '',
description: '',
});
const checkMysqlHosts = (_rule: unknown, value: string, callback: (error?: Error) => void) => {
const hosts = value.split(',').map((item) => item.trim());
if (hosts.length === 0 || hosts.some((item) => !item)) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
return;
}
callback();
};
const rules = reactive({
userKey: [Rules.requiredSelect],
username: [Rules.requiredInput, Rules.name],
permission: [Rules.requiredSelect],
host: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
host: [Rules.requiredInput, Rules.noSpace, Rules.illegal, { validator: checkMysqlHosts, trigger: 'blur' }],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
});
@@ -189,13 +221,13 @@ const availableUsers = computed(() =>
);
const loadContext = async () => {
if (!form.database) {
const database = form.database;
if (!database) {
users.value = [];
grants.value = [];
return;
}
const [userRes, grantRes] = await Promise.all([
searchMysqlUsers({ database: form.database }),
searchMysqlGrants({ database: form.database }),
]);
const [userRes, grantRes] = await Promise.all([searchMysqlUsers({ database }), searchMysqlGrants({ database })]);
users.value = userRes.data || [];
grants.value = grantRes.data || [];
};
@@ -333,6 +365,25 @@ defineExpose({
display: flex;
margin-bottom: 12px;
}
.password-cell {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
}
.password-text {
overflow: hidden;
max-width: 96px;
text-overflow: ellipsis;
white-space: nowrap;
}
.password-action {
min-height: 20px;
padding: 0;
}
.password-copy {
margin-left: -2px;
}
:deep(.user-select-option) {
height: auto;
min-height: 48px;
@@ -169,6 +169,14 @@ const form = reactive({
permissionIPs: '',
description: '',
});
const checkMysqlHosts = (_rule: unknown, value: string, callback: (error?: Error) => void) => {
const hosts = value.split(',').map((item) => item.trim());
if (hosts.length === 0 || hosts.some((item) => !item)) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
return;
}
callback();
};
const rules = reactive({
name: [Rules.requiredInput, Rules.dbName],
format: [Rules.requiredSelect],
@@ -177,7 +185,7 @@ const rules = reactive({
username: [Rules.requiredInput, Rules.name],
password: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
permission: [Rules.requiredSelect],
permissionIPs: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
permissionIPs: [Rules.requiredInput, Rules.noSpace, Rules.illegal, { validator: checkMysqlHosts, trigger: 'blur' }],
});
type FormInstance = InstanceType<typeof ElForm>;
@@ -234,12 +242,22 @@ const loadCollations = async () => {
};
const loadUsers = async () => {
const res = await searchMysqlUsers({ database: form.database });
const database = form.database;
if (!database) {
users.value = [];
return;
}
const res = await searchMysqlUsers({ database });
users.value = res.data || [];
};
const loadGrants = async () => {
const res = await searchMysqlGrants({ database: form.database });
const database = form.database;
if (!database) {
grants.value = [];
return;
}
const res = await searchMysqlGrants({ database });
grants.value = res.data || [];
};
+1 -1
View File
@@ -442,7 +442,7 @@ const openUserDrawer = async () => {
};
const openAuthorizationManagement = (row: Database.MysqlDBInfo) => {
if (!currentDB.value) {
if (!currentDB.value?.database) {
return;
}
bindRef.value!.acceptParams({
@@ -8,7 +8,7 @@
<ComplexTable :data="users" :heightDiff="260">
<el-table-column :label="$t('commons.login.username')" prop="username" min-width="150">
<template #default="{ row }">
<span>{{ row.username }}@{{ row.host }}</span>
<span>{{ row.username }}</span>
<el-tag v-if="row.isDelete" round type="info" class="ml-1" size="small">
{{ $t('database.isDelete') }}
</el-tag>
@@ -18,7 +18,6 @@
<template #default="{ row }">
<span v-if="row.isDelete">-</span>
<div v-else-if="!row.password" class="password-cell">
<el-tag type="warning" size="small">{{ $t('database.passwordPendingSupplement') }}</el-tag>
<el-button link type="primary" @click="openSupplementPasswordDialog(row)">
{{ $t('database.supplementPassword') }}
</el-button>
@@ -44,7 +43,7 @@
</div>
</template>
</el-table-column>
<el-table-column :label="$t('database.permission')" prop="host" min-width="120">
<el-table-column :label="$t('database.permission')" show-overflow-tooltip prop="host" min-width="120">
<template #default="{ row }">
{{ permissionLabel(row.host) }}
</template>
@@ -88,8 +87,11 @@
</el-form-item>
<el-form-item v-if="userForm.permission === 'ip'" prop="host">
<el-input v-model.trim="userForm.host" />
<span v-if="userDialogMode === 'create'" class="input-help">
{{ $t('database.remoteHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-form-item :label="$t('commons.login.password')" prop="password" :required="userDialogMode === 'create'">
<el-input type="password" clearable show-password v-model.trim="userForm.password" />
<span v-if="userDialogMode === 'edit'" class="input-help">
{{ $t('setting.passwordEmptyTip') }}
@@ -140,14 +142,13 @@
</template>
</DialogPro>
<DialogPro v-model="deleteDialogVisible" :title="$t('commons.button.delete')" size="small">
<DialogPro
v-model="deleteDialogVisible"
:title="$t('commons.button.delete') + ' - ' + deleteUserIdentity"
size="small"
>
<div v-loading="loading">
<div class="delete-user-title">
<template v-if="deleteUserDbs.length > 0">
{{ deleteUser.username }} {{ $t('database.userBoundDatabases') }}
</template>
<template v-else>{{ deleteUser.username }}</template>
</div>
<div class="delete-user-title">{{ deleteUserIdentity }} {{ $t('database.userBoundDatabases') }}</div>
<div class="delete-user-section">
<div v-if="deleteUserDbs.length === 0" class="delete-user-empty">-</div>
<div v-else class="bind-db-list delete-user-dbs">
@@ -158,18 +159,13 @@
</div>
<div class="delete-user-section">
<div>
<template v-if="deleteUser.isDelete">
<span style="font-size: 12px">{{ $t('database.deleteUserRecordHelper') }}</span>
</template>
<template v-else>
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
<span style="font-size: 12px; color: red; font-weight: 500">
{{ deleteUser.username }}
</span>
<span style="font-size: 12px">{{ $t('database.deleteUserHelper') }}</span>
</template>
<span style="font-size: 12px">{{ $t('database.delete') }}</span>
<span style="font-size: 12px; color: red; font-weight: 500">
{{ deleteUserIdentity }}
</span>
<span style="font-size: 12px">{{ $t('database.deleteUserHelper') }}</span>
</div>
<el-input v-model="deleteConfirmInput" :placeholder="deleteUser.username" />
<el-input v-model="deleteConfirmInput" :placeholder="deleteUserIdentity" />
</div>
</div>
<template #footer>
@@ -179,7 +175,7 @@
<el-button
type="primary"
@click="submitDeleteUser()"
:disabled="deleteConfirmInput !== deleteUser.username || loading"
:disabled="deleteConfirmInput !== deleteUserIdentity || loading"
>
{{ $t('commons.button.confirm') }}
</el-button>
@@ -239,6 +235,7 @@ const deleteUser = reactive({
host: '',
isDelete: false,
});
const deleteUserIdentity = computed(() => `${deleteUser.username}@${deleteUser.host}`);
const deleteUserDbs = ref<string[]>([]);
const supplementDialogVisible = ref(false);
const supplementFormRef = ref();
@@ -269,10 +266,22 @@ const checkPassword = (_rule: unknown, value: string, callback: (error?: Error)
}
callback();
};
const checkUserHosts = (_rule: unknown, value: string, callback: (error?: Error) => void) => {
const hosts = value.split(',').map((item) => item.trim());
if (hosts.length === 0 || hosts.some((item) => !item)) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
return;
}
if (userDialogMode.value === 'edit' && hosts.length > 1) {
callback(new Error(i18n.global.t('commons.rule.illegalInput')));
return;
}
callback();
};
const userRules = reactive({
username: [Rules.requiredInput, Rules.name],
permission: [Rules.requiredSelect],
host: [Rules.requiredInput, Rules.noSpace, Rules.illegal],
host: [Rules.requiredInput, Rules.noSpace, Rules.illegal, { validator: checkUserHosts, trigger: 'blur' }],
password: [{ validator: checkPassword, trigger: 'blur' }],
});
@@ -290,12 +299,22 @@ const acceptParams = async (params: DialogProps) => {
};
const loadUsers = async () => {
const res = await searchMysqlUsers({ database: database.value });
const databaseName = database.value;
if (!databaseName) {
users.value = [];
return;
}
const res = await searchMysqlUsers({ database: databaseName });
users.value = res.data || [];
};
const loadGrants = async () => {
const res = await searchMysqlGrants({ database: database.value });
const databaseName = database.value;
if (!databaseName) {
grants.value = [];
return;
}
const res = await searchMysqlGrants({ database: databaseName });
grants.value = res.data || [];
};
@@ -416,15 +435,7 @@ const submitUser = async () => {
const selectedDbs = userForm.dbs || [];
const addDbs = selectedDbs.filter((item) => !originalDbs.value.includes(item));
const removeDbs = originalDbs.value.filter((item) => !selectedDbs.includes(item));
if (userForm.host !== originalHost.value) {
await updateMysqlUser({
database: database.value,
username: userForm.username,
host: originalHost.value,
newHost: userForm.host,
description: userForm.description,
});
} else if (userForm.description !== originalDescription.value) {
if (userForm.host !== originalHost.value || userForm.description !== originalDescription.value) {
await updateMysqlUser({
database: database.value,
username: userForm.username,