package service import ( "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/compose" "github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/mysql" "github.com/1Panel-dev/1Panel/agent/utils/mysql/client" "github.com/1Panel-dev/1Panel/agent/utils/re" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/copier" "github.com/pkg/errors" "gorm.io/gorm" ) type MysqlService struct{} type IMysqlService interface { SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) ListDBOption() ([]dto.MysqlOption, error) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) LoadFromRemote(req dto.MysqlLoadDB) error ChangeAccess(info dto.ChangeDBInfo) error ChangePassword(info dto.ChangeDBInfo) error UpdateVariables(req dto.MysqlVariablesUpdate) error UpdateDescription(req dto.UpdateDescription) error DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) Delete(ctx context.Context, req dto.MysqlDBDelete) error ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error) CreateUser(req dto.MysqlUserCreate) error UpdateUser(req dto.MysqlUserUpdate) error ChangeUserPassword(req dto.MysqlUserPassword) error SaveUserPassword(req dto.MysqlUserPassword) error DeleteUser(req dto.MysqlUserDelete) error GrantUser(req dto.MysqlGrantCreate) error RevokeGrant(req dto.MysqlGrantDelete) error LoadFormatOption(req dto.OperationWithName) []dto.MysqlFormatCollationOption LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) } func NewIMysqlService() IMysqlService { return &MysqlService{} } func normalizeDatabaseUserType(dbType string) string { if len(dbType) == 0 { return "mysql" } return dbType } func resolveDatabaseUserType(database string) (string, error) { databaseItem, err := databaseRepo.Get(repo.WithByName(database)) if err != nil { return "", err } return normalizeDatabaseUserType(databaseItem.Type), nil } func databaseUserKey(username, host string) string { return username + "@" + host } 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) } 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") } return nil } // 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", "healthcheck": return true default: return false } } func saveDatabaseUserCredential(dbType, database, username, host, password, description string) error { dbType = normalizeDatabaseUserType(dbType) user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(username, host)) if err != nil { user = model.DatabaseUser{ Type: dbType, Database: database, Username: username, Host: host, } } user.Password = password user.IsDelete = false if len(description) != 0 { user.Description = description } return databaseUserRepo.Save(&user) } func saveDatabaseUserCredentials(dbType, database, username, permission, password, description string) error { for _, host := range splitMysqlHosts(permission) { if err := saveDatabaseUserCredential(dbType, database, username, host, password, description); err != nil { return err } } return nil } type mysqlUserAppTarget struct { Key string Name string } func loadMysqlUserAppTargets(dbType, database, username, host string, dbNames ...string) ([]mysqlUserAppTarget, error) { if host != "%" { return nil, nil } dbNameSet := make(map[string]struct{}, len(dbNames)) for _, dbName := range dbNames { dbNameSet[dbName] = struct{}{} } dbItems, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(database)) if err != nil { return nil, err } localResourceIDs := make([]uint, 0, len(dbItems)) remoteResourceIDs := make([]uint, 0, len(dbItems)) for _, dbItem := range dbItems { if len(dbNameSet) != 0 { if _, ok := dbNameSet[dbItem.Name]; !ok { continue } } if dbItem.ID == 0 { continue } if dbItem.From == "local" { localResourceIDs = append(localResourceIDs, dbItem.ID) } else { remoteResourceIDs = append(remoteResourceIDs, dbItem.ID) } } appResources := make([]model.AppInstallResource, 0) if len(localResourceIDs) != 0 { app, err := appInstallRepo.LoadBaseInfo(dbType, database) if err != nil { return nil, err } resources, err := appInstallResourceRepo.GetBy( appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceIds(localResourceIDs), ) if err != nil { return nil, err } appResources = append(appResources, resources...) } if len(remoteResourceIDs) != 0 { resources, err := appInstallResourceRepo.GetBy( appInstallResourceRepo.WithResourceIds(remoteResourceIDs), appRepo.WithKey(dbType), ) if err != nil { return nil, err } appResources = append(appResources, resources...) } appInstallIDs := make([]uint, 0, len(appResources)) appInstallIDSet := make(map[uint]struct{}, len(appResources)) for _, appResource := range appResources { if appResource.AppInstallId == 0 { continue } if _, ok := appInstallIDSet[appResource.AppInstallId]; ok { continue } appInstallIDSet[appResource.AppInstallId] = struct{}{} appInstallIDs = append(appInstallIDs, appResource.AppInstallId) } if len(appInstallIDs) == 0 { return nil, nil } appInstalls, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(appInstallIDs)) if err != nil { return nil, err } targets := make([]mysqlUserAppTarget, 0, len(appInstalls)) for _, appInstall := range appInstalls { var envMap map[string]interface{} if err := json.Unmarshal([]byte(appInstall.Env), &envMap); err != nil { return nil, err } appUsername, ok := envMap["PANEL_DB_USER"].(string) if !ok || appUsername != username { continue } targets = append(targets, mysqlUserAppTarget{Key: appInstall.App.Key, Name: appInstall.Name}) } return targets, nil } func checkMysqlUserAppUsage(dbType, database, username, host string, dbNames ...string) error { targets, err := loadMysqlUserAppTargets(dbType, database, username, host, dbNames...) if err != nil { return err } if len(targets) == 0 { return nil } appNames := make([]string, 0, len(targets)) for _, target := range targets { appNames = append(appNames, target.Name) } sort.Strings(appNames) return buserr.WithDetail("ErrMysqlUserUsedByApps", strings.Join(appNames, ", "), nil) } func updateMysqlPasswordAppTargets(targets []mysqlUserAppTarget, password string) error { for _, target := range targets { global.LOG.Infof("start to update mysql password used by app %s-%s", target.Key, target.Name) if err := updateInstallInfoInDB(target.Key, target.Name, "user-password", password); err != nil { return err } } return nil } func syncDatabaseUserMetadata(dbType, database string, users []client.UserInfo) error { dbType = normalizeDatabaseUserType(dbType) metas, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database)) if err != nil { return err } metaMap := make(map[string]model.DatabaseUser, len(metas)) for _, item := range metas { metaMap[databaseUserKey(item.Username, item.Host)] = item } userMap := make(map[string]struct{}, len(users)) for _, item := range users { if isMysqlSystemUser(item.Username) { continue } key := databaseUserKey(item.Username, item.Host) userMap[key] = struct{}{} if meta, ok := metaMap[key]; ok { if meta.IsDelete { if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": false, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil { return err } } continue } if err := databaseUserRepo.Save(&model.DatabaseUser{ Type: dbType, Database: database, Username: item.Username, Host: item.Host, }); err != nil { return err } } for _, item := range metas { if isMysqlSystemUser(item.Username) { continue } if _, ok := userMap[databaseUserKey(item.Username, item.Host)]; ok { continue } if item.IsDelete { continue } if err := databaseUserRepo.Update(map[string]interface{}{"is_delete": true, "password": ""}, repo.WithByType(dbType), databaseUserRepo.WithByDatabase(database), databaseUserRepo.WithByUser(item.Username, item.Host)); err != nil { return err } } return nil } func saveDatabaseUserGrant(dbType, database, dbName, username, host string) error { dbType = normalizeDatabaseUserType(dbType) grant, err := databaseUserGrantRepo.Get( repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(database), databaseUserGrantRepo.WithByDBName(dbName), databaseUserGrantRepo.WithByUser(username, host), ) if err == nil && grant.ID != 0 { return nil } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } return databaseUserGrantRepo.Save(&model.DatabaseUserGrant{ Type: dbType, Database: database, DBName: dbName, Username: username, Host: host, }) } func syncDatabaseUserGrants(dbType, database string, grants []client.GrantInfo) error { dbType = normalizeDatabaseUserType(dbType) items := make([]model.DatabaseUserGrant, 0, len(grants)) grantMap := make(map[string]struct{}, len(grants)) for _, item := range grants { if isMysqlSystemUser(item.Username) || item.Database == "*" { continue } key := item.Database + "\x00" + item.Username + "\x00" + item.Host if _, ok := grantMap[key]; ok { continue } grantMap[key] = struct{}{} items = append(items, model.DatabaseUserGrant{ Type: dbType, Database: database, DBName: item.Database, Username: item.Username, Host: item.Host, }) } return databaseUserGrantRepo.Replace(dbType, database, items) } func (u *MysqlService) SearchWithPage(search dto.MysqlDBSearch) (int64, interface{}, error) { total, mysqls, err := mysqlRepo.Page(search.Page, search.PageSize, mysqlRepo.WithByMysqlName(search.Database), repo.WithByLikeName(search.Info), repo.WithOrderRuleBy(search.OrderBy, search.Order), ) var dtoMysqls []dto.MysqlDBInfo for _, mysql := range mysqls { var item dto.MysqlDBInfo if err := copier.Copy(&item, &mysql); err != nil { return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) } item.Username = "" item.Password = "" item.Permission = "" dtoMysqls = append(dtoMysqls, item) } return total, dtoMysqls, err } func (u *MysqlService) ListDBOption() ([]dto.MysqlOption, error) { mysqls, err := mysqlRepo.List() if err != nil { return nil, err } databases, err := databaseRepo.GetList(databaseRepo.WithTypeList("mysql,mariadb")) if err != nil { return nil, err } var dbs []dto.MysqlOption for _, mysql := range mysqls { var item dto.MysqlOption if err := copier.Copy(&item, &mysql); err != nil { return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) } item.Database = mysql.MysqlName for _, database := range databases { if database.Name == item.Database { item.Type = database.Type } } dbs = append(dbs, item) } return dbs, err } func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*model.DatabaseMysql, error) { if cmd.CheckIllegal(req.Name, req.Username, req.Password, req.Format, req.Collation, req.Permission) { return nil, buserr.New("ErrCmdIllegal") } 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 } mysql, _ := mysqlRepo.Get(repo.WithByName(req.Name), mysqlRepo.WithByMysqlName(req.Database), repo.WithByFrom(req.From)) if mysql.ID != 0 { return nil, buserr.New("ErrRecordExist") } var createItem model.DatabaseMysql if err := copier.Copy(&createItem, &req); err != nil { return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil) } createItem.Username = "" createItem.Password = "" createItem.Permission = "" if req.From == "local" && req.Username == "root" { return nil, errors.New("cannot set root as user name") } cli, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return nil, err } createItem.MysqlName = req.Database defer cli.Close() if err := cli.Create(client.CreateInfo{ Name: req.Name, Format: req.Format, Collation: req.Collation, Username: req.Username, Password: req.Password, Permission: req.Permission, Version: version, Timeout: 300, }); err != nil { return nil, err } if len(req.Username) != 0 { if err := saveDatabaseUserCredentials(dbType, req.Database, req.Username, req.Permission, req.Password, req.Description); err != nil { return nil, err } for _, host := range permissionHosts { if err := saveDatabaseUserGrant(dbType, req.Database, req.Name, req.Username, host); err != nil { return nil, err } } } global.LOG.Infof("create database %s successful!", req.Name) if err := mysqlRepo.Create(ctx, &createItem); err != nil { return nil, err } return &createItem, nil } func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) { dbType, err := resolveDatabaseUserType(req.Database) if err != nil { return nil, err } users, err := databaseUserRepo.List(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database)) if err != nil { return nil, err } res := make([]dto.MysqlUser, 0, len(users)) for _, user := range users { if isMysqlSystemUser(user.Username) { continue } res = append(res, dto.MysqlUser{ Username: user.Username, Host: user.Host, Password: user.Password, Description: user.Description, IsDelete: user.IsDelete, }) } return res, nil } func (u *MysqlService) ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error) { dbType, err := resolveDatabaseUserType(req.Database) if err != nil { return nil, err } grants, err := databaseUserGrantRepo.List(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database)) if err != nil { return nil, err } res := make([]dto.MysqlGrant, 0, len(grants)) for _, grant := range grants { res = append(res, dto.MysqlGrant{Database: grant.DBName, Username: grant.Username, Host: grant.Host}) } return res, nil } func (u *MysqlService) ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error) { res := make(map[string][]dto.MysqlUser, len(req.DBs)) dbMap := make(map[string]struct{}, len(req.DBs)) dbNames := make([]string, 0, len(req.DBs)) for _, item := range req.DBs { if item == "" { continue } if _, ok := dbMap[item]; ok { continue } dbMap[item] = struct{}{} dbNames = append(dbNames, item) res[item] = []dto.MysqlUser{} } if len(dbMap) == 0 { return res, nil } dbType, err := resolveDatabaseUserType(req.Database) if err != nil { return nil, err } grants, err := databaseUserGrantRepo.List( repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByDBNames(dbNames), ) if err != nil { return nil, err } userList := make([][2]string, 0, len(grants)) userSet := make(map[string]struct{}, len(grants)) for _, grant := range grants { key := databaseUserKey(grant.Username, grant.Host) if _, ok := userSet[key]; ok { continue } userSet[key] = struct{}{} userList = append(userList, [2]string{grant.Username, grant.Host}) } userMetas, err := databaseUserRepo.List( repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUserList(userList), ) if err != nil { return nil, err } metaMap := make(map[string]model.DatabaseUser, len(userMetas)) for _, item := range userMetas { metaMap[databaseUserKey(item.Username, item.Host)] = item } for _, grant := range grants { if _, ok := dbMap[grant.DBName]; !ok { continue } meta, ok := metaMap[databaseUserKey(grant.Username, grant.Host)] if !ok || meta.IsDelete { continue } item := dto.MysqlUser{Username: grant.Username, Host: grant.Host, Description: meta.Description} res[grant.DBName] = append(res[grant.DBName], item) } return res, nil } func (u *MysqlService) CreateUser(req dto.MysqlUserCreate) error { if cmd.CheckIllegal(req.Username, req.Password, req.Host) { return buserr.New("ErrCmdIllegal") } dbs := make([]string, 0, len(req.DBs)) dbSet := make(map[string]struct{}, len(req.DBs)) for _, db := range req.DBs { if db == "" { continue } if cmd.CheckIllegal(db) { return buserr.New("ErrCmdIllegal") } if db == "*" { return errors.New("global mysql privileges must be managed outside 1Panel") } if _, ok := dbSet[db]; ok { continue } dbSet[db] = struct{}{} dbs = append(dbs, db) } 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, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() 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) } } } savedHosts := make([]string, 0, len(hosts)) rollbackSavedHosts := func() { for i := len(savedHosts) - 1; i >= 0; i-- { host := savedHosts[i] if rollbackErr := databaseUserRepo.Delete( repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, host), ); rollbackErr != nil { global.LOG.Errorf("rollback mysql user record %s@%s failed, err: %v", req.Username, host, rollbackErr) } } } grantedItems := make([]client.GrantInfo, 0, len(dbs)*len(hosts)) rollbackGrantedItems := func() { for i := len(grantedItems) - 1; i >= 0; i-- { item := grantedItems[i] if rollbackErr := cli.RevokeGrant(item, 300); rollbackErr != nil { global.LOG.Errorf( "rollback mysql grant %s to %s@%s failed, err: %v", item.Database, item.Username, item.Host, rollbackErr, ) } if rollbackErr := databaseUserGrantRepo.Delete( repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByDBName(item.Database), databaseUserGrantRepo.WithByUser(item.Username, item.Host), ); rollbackErr != nil { global.LOG.Errorf( "rollback mysql grant record %s to %s@%s failed, err: %v", item.Database, item.Username, item.Host, rollbackErr, ) } } } rollbackAll := func() { rollbackGrantedItems() rollbackSavedHosts() rollbackCreatedHosts() } 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) } for _, host := range hosts { if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, host, req.Password, req.Description); err != nil { rollbackAll() return err } savedHosts = append(savedHosts, host) } for _, db := range dbs { for _, host := range hosts { item := client.GrantInfo{Database: db, Username: req.Username, Host: host} if err := cli.GrantUser(item, 300); err != nil { rollbackAll() return err } grantedItems = append(grantedItems, item) if err := saveDatabaseUserGrant(dbType, req.Database, db, req.Username, host); err != nil { rollbackAll() return err } } } return nil } func (u *MysqlService) UpdateUser(req dto.MysqlUserUpdate) error { if cmd.CheckIllegal(req.Username, req.Host, req.NewHost) { return buserr.New("ErrCmdIllegal") } 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 { return buserr.New("ErrRecordExist") } if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil { return err } cli, _, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() if err := cli.UpdateUser(client.UserUpdateInfo{Username: req.Username, Host: req.Host, NewHost: req.NewHost}, 300); 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, } } user.Host = req.NewHost user.IsDelete = false user.Description = req.Description if err := databaseUserRepo.Save(&user); err != nil { if rollbackErr := cli.UpdateUser(client.UserUpdateInfo{Username: req.Username, Host: req.NewHost, NewHost: req.Host}, 300); rollbackErr != nil { global.LOG.Errorf("rollback mysql user %s host from %s to %s failed, err: %v", req.Username, req.NewHost, req.Host, rollbackErr) } return err } if err := databaseUserGrantRepo.Update( map[string]interface{}{"host": req.NewHost}, repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByUser(req.Username, req.Host), ); err != nil { return err } return nil } 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.IsDelete = false user.Description = req.Description return databaseUserRepo.Save(&user) } func (u *MysqlService) ChangeUserPassword(req dto.MysqlUserPassword) error { if cmd.CheckIllegal(req.Username, req.Host, req.Password) { return buserr.New("ErrCmdIllegal") } 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 } cli, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() grants, err := cli.ListGrants(300) if err != nil { return err } grantDBs := make([]string, 0) for _, grant := range grants { if grant.Username == req.Username && grant.Host == req.Host { grantDBs = append(grantDBs, grant.Database) } } var appTargets []mysqlUserAppTarget if len(grantDBs) != 0 { appTargets, err = loadMysqlUserAppTargets(dbType, req.Database, req.Username, req.Host, grantDBs...) if err != nil { return err } } if err := cli.ChangePassword(client.PasswordChangeInfo{ Username: req.Username, Permission: req.Host, Password: req.Password, Version: version, Timeout: 300, }); err != nil { return err } if err := saveDatabaseUserCredential(dbType, req.Database, req.Username, req.Host, req.Password, ""); err != nil { return err } return updateMysqlPasswordAppTargets(appTargets, req.Password) } func (u *MysqlService) SaveUserPassword(req dto.MysqlUserPassword) error { if cmd.CheckIllegal(req.Username, req.Host, req.Password) { return buserr.New("ErrCmdIllegal") } 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 } user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host)) if err != nil { return err } if user.IsDelete { return errors.New("cannot save password for a deleted mysql user") } user.Password = req.Password return databaseUserRepo.Save(&user) } func (u *MysqlService) DeleteUser(req dto.MysqlUserDelete) error { if cmd.CheckIllegal(req.Username, req.Host) { return buserr.New("ErrCmdIllegal") } 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 } if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host); err != nil { return err } user, err := databaseUserRepo.Get(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host)) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } if err != nil || !user.IsDelete { cli, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() if err := cli.DeleteUser(client.UserInfo{Username: req.Username, Host: req.Host}, version, 300); err != nil { return err } } if err := databaseUserGrantRepo.Delete(repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByUser(req.Username, req.Host)); err != nil { return err } return databaseUserRepo.Delete(repo.WithByType(dbType), databaseUserRepo.WithByDatabase(req.Database), databaseUserRepo.WithByUser(req.Username, req.Host)) } func (u *MysqlService) GrantUser(req dto.MysqlGrantCreate) error { if cmd.CheckIllegal(req.DB, req.Username, req.Host) { return buserr.New("ErrCmdIllegal") } if req.DB == "*" { return errors.New("global mysql privileges must be managed outside 1Panel") } 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) if err != nil { return err } defer cli.Close() 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 nil } func (u *MysqlService) RevokeGrant(req dto.MysqlGrantDelete) error { if cmd.CheckIllegal(req.DB, req.Username, req.Host) { return buserr.New("ErrCmdIllegal") } if req.DB == "*" { return errors.New("global mysql privileges must be managed outside 1Panel") } 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 } if err := checkMysqlUserAppUsage(dbType, req.Database, req.Username, req.Host, req.DB); err != nil { return err } cli, _, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() if err := cli.RevokeGrant(client.GrantInfo{Database: req.DB, Username: req.Username, Host: req.Host}, 300); err != nil { return err } return databaseUserGrantRepo.Delete( repo.WithByType(dbType), databaseUserGrantRepo.WithByDatabase(req.Database), databaseUserGrantRepo.WithByDBName(req.DB), databaseUserGrantRepo.WithByUser(req.Username, req.Host), ) } func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error { dbType, err := resolveDatabaseUserType(req.Database) if err != nil { return err } client, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer client.Close() databases, err := mysqlRepo.List(mysqlRepo.WithByMysqlName(req.Database)) if err != nil { return err } datas, err := client.SyncDB(version) if err != nil { return err } users, err := client.ListUsers(300) if err != nil { return err } grants, err := client.ListGrants(300) if err != nil { return err } if err := syncDatabaseUserMetadata(dbType, req.Database, users); err != nil { return err } if err := syncDatabaseUserGrants(dbType, req.Database, grants); err != nil { return err } deleteList := databases for _, data := range datas { hasOld := false for i := 0; i < len(databases); i++ { if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MysqlName, data.MysqlName) { hasOld = true if databases[i].IsDelete { _ = mysqlRepo.Update(databases[i].ID, map[string]interface{}{"is_delete": false}) } deleteList = append(deleteList[:i], deleteList[i+1:]...) break } } if !hasOld { var createItem model.DatabaseMysql if err := copier.Copy(&createItem, &data); err != nil { return buserr.WithDetail("ErrStructTransform", err.Error(), nil) } createItem.Username = "" createItem.Password = "" createItem.Permission = "" if err := mysqlRepo.Create(context.Background(), &createItem); err != nil { return err } } } for _, delItem := range deleteList { _ = mysqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true}) } return nil } func (u *MysqlService) UpdateDescription(req dto.UpdateDescription) error { return mysqlRepo.Update(req.ID, map[string]interface{}{"description": req.Description}) } func loadMysqlDeleteTarget(id uint) (model.DatabaseMysql, string, error) { db, err := mysqlRepo.Get(repo.WithByID(id)) if err != nil { return db, "", err } dbType, err := resolveDatabaseUserType(db.MysqlName) if err != nil { return db, "", err } return db, dbType, nil } func (u *MysqlService) deleteCheck(db model.DatabaseMysql, dbType string) ([]dto.DBResource, error) { var res []dto.DBResource websites, err := websiteRepo.GetBy( websiteRepo.WithDBTypes([]string{constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster}), websiteRepo.WithDBID(db.ID), ) if err != nil { return res, err } for _, website := range websites { res = append(res, dto.DBResource{ Type: constant.TypeWebsite, Name: website.PrimaryDomain, }) } if db.From == "local" { app, err := appInstallRepo.LoadBaseInfo(dbType, db.MysqlName) if err != nil { return res, err } apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID)) if err != nil { return res, err } for _, app := range apps { appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { continue } return res, err } if appInstall.ID != 0 { res = append(res, dto.DBResource{ Type: constant.TypeApp, Name: appInstall.Name, }) } } } else { apps, err := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(dbType)) if err != nil { return res, err } for _, app := range apps { appInstall, err := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId)) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { continue } return res, err } if appInstall.ID != 0 { res = append(res, dto.DBResource{ Type: constant.TypeApp, Name: appInstall.Name, }) } } } return res, nil } func (u *MysqlService) DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error) { db, dbType, err := loadMysqlDeleteTarget(req.ID) if err != nil { return nil, err } return u.deleteCheck(db, dbType) } func (u *MysqlService) Delete(ctx context.Context, req dto.MysqlDBDelete) error { return u.delete(ctx, req, nil) } func (u *MysqlService) delete(ctx context.Context, req dto.MysqlDBDelete, exclusions []dto.DBResource) error { db, dbType, err := loadMysqlDeleteTarget(req.ID) if err != nil { return err } resources, err := u.deleteCheck(db, dbType) if err != nil { return err } if len(exclusions) != 0 { exclusionMap := make(map[string]struct{}, len(exclusions)) for _, exclusion := range exclusions { exclusionMap[exclusion.Type+"\x00"+exclusion.Name] = struct{}{} } filtered := resources[:0] for _, resource := range resources { if _, ok := exclusionMap[resource.Type+"\x00"+resource.Name]; ok { continue } filtered = append(filtered, resource) } resources = filtered } if len(resources) != 0 { names := make([]string, 0, len(resources)) for _, resource := range resources { names = append(names, resource.Name) } sort.Strings(names) return buserr.WithDetail("ErrInUsed", strings.Join(names, ", "), nil) } cli, version, err := LoadMysqlClientByFrom(db.MysqlName) if err != nil { return err } defer cli.Close() if err := cli.DeleteDatabase(client.DeleteInfo{ Name: db.Name, Version: version, Timeout: 300, }); err != nil && !req.ForceDelete { return err } if req.DeleteBackup { uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", dbType, db.MysqlName, db.Name)) if _, err := os.Stat(uploadDir); err == nil { _ = os.RemoveAll(uploadDir) } backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", dbType, db.MysqlName, db.Name)) if _, err := os.Stat(backupDir); err == nil { _ = os.RemoveAll(backupDir) } _ = backupRepo.DeleteRecord(ctx, repo.WithByType(dbType), repo.WithByName(db.MysqlName), repo.WithByDetailName(db.Name)) global.LOG.Infof("delete database %s-%s backups successful", db.MysqlName, db.Name) } _ = mysqlRepo.Delete(ctx, repo.WithByID(db.ID)) return nil } func deleteMysqlDatabaseForResourceOwner(ctx context.Context, req dto.MysqlDBDelete, owner dto.DBResource) error { return (&MysqlService{}).delete(ctx, req, []dto.DBResource{owner}) } func isMysqlDatabaseResourceInUseError(err error) bool { businessErr, ok := err.(buserr.BusinessError) return ok && businessErr.Msg == "ErrInUsed" } func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error { if cmd.CheckIllegal(req.Value) { return buserr.New("ErrCmdIllegal") } cli, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() if req.ID != 0 { return errors.New("mysql user password should be changed by user api") } passwordInfo := client.PasswordChangeInfo{ Username: "root", Password: req.Value, Timeout: 300, Version: version, } if err := cli.ChangePassword(passwordInfo); err != nil { return err } if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil { return err } if req.From == "local" { remote, err := databaseRepo.Get(repo.WithByName(req.Database)) if err != nil { return err } pass, err := encrypt.StringEncrypt(req.Value) if err != nil { return fmt.Errorf("decrypt database password failed, err: %v", err) } _ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass}) } return nil } func (u *MysqlService) ChangeAccess(req dto.ChangeDBInfo) error { if cmd.CheckIllegal(req.Value) { return buserr.New("ErrCmdIllegal") } cli, version, err := LoadMysqlClientByFrom(req.Database) if err != nil { return err } defer cli.Close() if req.ID != 0 { return errors.New("mysql user access should be changed by user api") } accessInfo := client.AccessChangeInfo{ Username: "root", Permission: req.Value, Timeout: 300, Version: version, } if err := cli.ChangeAccess(accessInfo); err != nil { return err } return nil } func (u *MysqlService) UpdateVariables(req dto.MysqlVariablesUpdate) error { app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database) if err != nil { return err } var files []string path := fmt.Sprintf("%s/%s/%s/conf/my.cnf", global.Dir.AppInstallDir, req.Type, app.Name) lineBytes, err := os.ReadFile(path) if err != nil { return err } files = strings.Split(string(lineBytes), "\n") group := "[mysqld]" for _, info := range req.Variables { if info.Param == "slow_query_log" && info.Value == "ON" { logFilePath := filepath.Join(global.Dir.DataDir, fmt.Sprintf("apps/%s/%s/data/1Panel-slow.log", app.Key, app.Name)) if req.Type == "mariadb" { logFilePath = filepath.Join(global.Dir.DataDir, fmt.Sprintf("apps/%s/%s/db/data/1Panel-slow.log", app.Key, app.Name)) } file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { return err } defer file.Close() } if !strings.HasPrefix(app.Version, "5.7") && !strings.HasPrefix(app.Version, "5.6") { if info.Param == "query_cache_size" { continue } } if _, ok := info.Value.(float64); ok { files = updateMyCnf(files, group, info.Param, common.LoadSizeUnit(info.Value.(float64))) } else { files = updateMyCnf(files, group, info.Param, info.Value) } } file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, constant.FilePerm) if err != nil { return err } defer file.Close() _, err = file.WriteString(strings.Join(files, "\n")) if err != nil { return err } if _, err := compose.Restart(fmt.Sprintf("%s/%s/%s/docker-compose.yml", global.Dir.AppInstallDir, req.Type, app.Name)); err != nil { return err } return nil } func (u *MysqlService) LoadRemoteAccess(req dto.OperationWithNameAndType) (bool, error) { app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) if err != nil { return false, err } hosts, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, "select host from mysql.user where user='root';") if err != nil { return false, err } for _, host := range hosts { if host == "%" { return true, nil } } return false, nil } func (u *MysqlService) LoadVariables(req dto.OperationWithNameAndType) (*dto.MysqlVariables, error) { app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) if err != nil { return nil, err } variableMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global variables;") if err != nil { return nil, err } var info dto.MysqlVariables arr, err := json.Marshal(variableMap) if err != nil { return nil, err } _ = json.Unmarshal(arr, &info) return &info, nil } func (u *MysqlService) LoadStatus(req dto.OperationWithNameAndType) (*dto.MysqlStatus, error) { app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name) if err != nil { return nil, err } statusMap, err := executeSqlForMaps(app.ContainerName, app.Key, app.Password, "show global status;") if err != nil { return nil, err } var info dto.MysqlStatus arr, err := json.Marshal(statusMap) if err != nil { return nil, err } _ = json.Unmarshal(arr, &info) if value, ok := statusMap["Run"]; ok { uptime, _ := strconv.Atoi(value) info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout) } else { if value, ok := statusMap["Uptime"]; ok { uptime, _ := strconv.Atoi(value) info.Run = time.Unix(time.Now().Unix()-int64(uptime), 0).Format(constant.DateTimeLayout) } } info.File = "OFF" info.Position = "OFF" masterStatus := "show master status;" if common.CompareAppVersion(app.Version, "8.4.0") && (req.Type == constant.AppMysql || req.Type == constant.AppMysqlCluster) { masterStatus = "show binary log status;" } rows, err := executeSqlForRows(app.ContainerName, app.Key, app.Password, masterStatus) if err != nil { return nil, err } if len(rows) > 2 { itemValue := strings.Split(rows[1], "\t") if len(itemValue) > 2 { info.File = itemValue[0] info.Position = itemValue[1] } } return &info, nil } func (u *MysqlService) LoadFormatOption(req dto.OperationWithName) []dto.MysqlFormatCollationOption { defaultList := []dto.MysqlFormatCollationOption{{Format: "utf8mb4"}, {Format: "utf8mb3"}, {Format: "gbk"}, {Format: "big5"}} client, _, err := LoadMysqlClientByFrom(req.Name) if err != nil { return defaultList } options, err := client.LoadFormatCollation(3) if err != nil { return defaultList } return options } func executeSqlForMaps(containerName, dbType, password, command string) (map[string]string, error) { if dbType == "mysql-cluster" { dbType = "mysql" } cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command) stdout, err := cmd.CombinedOutput() stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") if err != nil || strings.HasPrefix(stdStr, "ERROR ") { return nil, errors.New(stdStr) } rows := strings.Split(stdStr, "\n") rowMap := make(map[string]string) for _, v := range rows { itemRow := strings.Split(v, "\t") if len(itemRow) == 2 { rowMap[itemRow[0]] = itemRow[1] } } return rowMap, nil } func executeSqlForRows(containerName, dbType, password, command string) ([]string, error) { if dbType == "mysql-cluster" { dbType = "mysql" } cmd := exec.Command("docker", "exec", containerName, dbType, "-uroot", "-p"+password, "-e", command) stdout, err := cmd.CombinedOutput() stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "") if err != nil || strings.HasPrefix(stdStr, "ERROR ") { return nil, errors.New(stdStr) } return strings.Split(stdStr, "\n"), nil } func updateMyCnf(oldFiles []string, group string, param string, value interface{}) []string { isOn := false hasGroup := false hasKey := false regItem := re.GetRegex(re.MysqlGroupPattern) var newFiles []string i := 0 for _, line := range oldFiles { i++ if strings.HasPrefix(line, group) { isOn = true hasGroup = true newFiles = append(newFiles, line) continue } if !isOn { newFiles = append(newFiles, line) continue } if strings.HasPrefix(line, param+"=") || strings.HasPrefix(line, "# "+param+"=") { newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value)) hasKey = true continue } if regItem.Match([]byte(line)) || i == len(oldFiles) { isOn = false if !hasKey { newFiles = append(newFiles, fmt.Sprintf("%s=%v", param, value)) } newFiles = append(newFiles, line) continue } newFiles = append(newFiles, line) } if !hasGroup { newFiles = append(newFiles, group+"\n") newFiles = append(newFiles, fmt.Sprintf("%s=%v\n", param, value)) } return newFiles } func LoadMysqlClientByFrom(database string) (mysql.MysqlClient, string, error) { var ( dbInfo client.DBInfo version string err error ) dbInfo.Timeout = 300 databaseItem, err := databaseRepo.Get(repo.WithByName(database)) if err != nil { return nil, "", err } dbInfo.Type = databaseItem.Type dbInfo.From = databaseItem.From dbInfo.Database = database if dbInfo.From != "local" { dbInfo.Address = databaseItem.Address dbInfo.Port = databaseItem.Port dbInfo.Username = databaseItem.Username dbInfo.Password = databaseItem.Password dbInfo.SSL = databaseItem.SSL dbInfo.ClientKey = databaseItem.ClientKey dbInfo.ClientCert = databaseItem.ClientCert dbInfo.RootCert = databaseItem.RootCert dbInfo.SkipVerify = databaseItem.SkipVerify version = databaseItem.Version } else { app, err := appInstallRepo.LoadBaseInfo(databaseItem.Type, database) if err != nil { return nil, "", err } dbInfo.Address = app.ContainerName dbInfo.Username = "root" dbInfo.Password = app.Password version = app.Version } cli, err := mysql.NewMysqlClient(dbInfo) if err != nil { return nil, "", err } return cli, version, nil }