diff --git a/dbm-services/common/go-pubpkg/errno/50000_dbpriv_code.go b/dbm-services/common/go-pubpkg/errno/50000_dbpriv_code.go index f8f54d3786..e78f343d36 100644 --- a/dbm-services/common/go-pubpkg/errno/50000_dbpriv_code.go +++ b/dbm-services/common/go-pubpkg/errno/50000_dbpriv_code.go @@ -12,11 +12,6 @@ package errno var ( // dbpriv code start 50000 - - // PasswordNotConsistent TODO - PasswordNotConsistent = Errno{Code: 51008, - Message: "user is exist,but the new password is not consistent with the old password, should be consistent", - CNMessage: "账号已存在,但是新密码与旧密码不一致,需要保持一致"} // GrantPrivilegesFail TODO GrantPrivilegesFail = Errno{Code: 51009, Message: "Grant Privileges Fail", CNMessage: "授权执行失败"} // GrantPrivilegesSuccess TODO @@ -24,11 +19,6 @@ var ( // GrantPrivilegesParameterCheckFail TODO GrantPrivilegesParameterCheckFail = Errno{Code: 51010, Message: "Parameter of Grant Privileges Check Fail", CNMessage: "授权单据的参数检查失败"} - // ErrPswNotIdentical TODO - ErrPswNotIdentical = Errno{Code: 51000, - Message: "Password is not identical to the password of existed account rules, " + - "same accounts should use same password.", - CNMessage: "密码与已存在的账号规则中的密码不同,相同账号的密码需要保持一致!"} // AccountRuleExisted TODO AccountRuleExisted = Errno{Code: 51001, Message: "Account rule of user on this db is existed ", CNMessage: "用户对此DB授权的账号规则已存在"} @@ -57,10 +47,8 @@ var ( AccountRuleNotExisted = Errno{Code: 51004, Message: "Account rule not existed ", CNMessage: "账号规则不存在"} // OnlyOneDatabaseAllowed TODO OnlyOneDatabaseAllowed = Errno{Code: 51005, - Message: "Only one database allowed, database name should not contain space", CNMessage: "只允许填写一个数据库,数据库名称不能包含空格"} - // ErrMysqlInstanceStruct TODO - ErrMysqlInstanceStruct = Errno{Code: 51006, Message: "Not either tendbha or orphan structure", - CNMessage: "不符合tendbha或者orphan的集群结构"} + Message: "Only one database allowed, database name should not contain space", + CNMessage: "只允许填写一个数据库,数据库名称不能包含空格"} // GenerateEncryptedPasswordErr TODO GenerateEncryptedPasswordErr = Errno{Code: 51007, Message: "Generate Encrypted Password Err", CNMessage: "创建账号,生成加密的密码时发生错误"} @@ -70,11 +58,24 @@ var ( ClonePrivilegesCheckFail = Errno{Code: 51014, Message: "Clone privileges check fail", CNMessage: "克隆权限检查失败"} // NoPrivilegesNothingToDo TODO NoPrivilegesNothingToDo = Errno{Code: 51015, Message: "no privileges,nothing to do", CNMessage: "没有权限需要克隆"} - // IpPortFormatError TODO - IpPortFormatError = Errno{Code: 51017, Message: "format not in 'ip:port' format", - CNMessage: "格式不是ip:port的格式"} // CloudIdRequired TODO CloudIdRequired = Errno{Code: 51019, Message: "bk_cloud_id is required", CNMessage: "bk_cloud_id不能为空"} // ClusterTypeIsEmpty TODO - ClusterTypeIsEmpty = Errno{Code: 51021, Message: "Cluster type can't be empty", CNMessage: "cluster type不能为空"} + ClusterTypeIsEmpty = Errno{Code: 51021, Message: "Cluster type can't be empty", + CNMessage: "cluster type不能为空"} + ModifyUserPasswordFail = Errno{Code: 51022, Message: "modify user password fail", + CNMessage: "修改用户密码失败"} + IncludeCharTypesLargerThanLength = Errno{Code: 51023, Message: "include char types larger than length", + CNMessage: "要求包含的字符类型大于字符串长度"} + TryTooManyTimes = Errno{Code: 51024, Message: "try too many times", CNMessage: "尝试太多次"} + RuleIdNull = Errno{Code: 51025, Message: "Rule ID should not be empty", + CNMessage: "安全规则的id不能为空"} + RuleNameNull = Errno{Code: 51026, Message: "Rule name should not be empty", + CNMessage: "安全规则的名称不能为空"} + RuleExisted = Errno{Code: 51027, Message: "Rule already existed ", CNMessage: "规则已存在"} + RuleNotExisted = Errno{Code: 51028, Message: "Rule not existed ", CNMessage: "规则不存在"} + NotMeetComplexity = Errno{Code: 51030, Message: "Set Passwords must meet complexity requirements", + CNMessage: "设置的密码应该符合密码复杂度"} + NameNull = Errno{Code: 51031, Message: "username should not be empty ", + CNMessage: "用户名名称不能为空"} ) diff --git a/dbm-services/mysql/db-partition/service/check_partition_base_func.go b/dbm-services/mysql/db-partition/service/check_partition_base_func.go index 564d41411a..23fcb04518 100644 --- a/dbm-services/mysql/db-partition/service/check_partition_base_func.go +++ b/dbm-services/mysql/db-partition/service/check_partition_base_func.go @@ -619,7 +619,7 @@ func CreatePartitionTicket(check Checker, objects []PartitionObject, zoneOffset zone := fmt.Sprintf("%+03d:00", zoneOffset) ticketType := "MYSQL_PARTITION" if check.ClusterType == Tendbcluster { - ticketType = "SPIDER_PARTITION" + ticketType = "TENDBCLUSTER_PARTITION" } ticket := Ticket{BkBizId: check.BkBizId, TicketType: ticketType, Remark: "auto partition", Details: Detail{Infos: []Info{{check.ConfigId, check.ClusterId, check.ImmuteDomain, *check.BkCloudId, objects}}}} diff --git a/dbm-services/mysql/db-priv/.gitignore b/dbm-services/mysql/db-priv/.gitignore index ecc836345a..13c98800f7 100644 --- a/dbm-services/mysql/db-priv/.gitignore +++ b/dbm-services/mysql/db-priv/.gitignore @@ -13,4 +13,5 @@ pubkey.pem privkey.pem infile outfile -.code.yml \ No newline at end of file +.code.yml +*.log \ No newline at end of file diff --git a/dbm-services/mysql/db-priv/assests/migrations/000001_init.down.sql.sql b/dbm-services/mysql/db-priv/assests/migrations/000001_init.down.sql similarity index 100% rename from dbm-services/mysql/db-priv/assests/migrations/000001_init.down.sql.sql rename to dbm-services/mysql/db-priv/assests/migrations/000001_init.down.sql diff --git a/dbm-services/mysql/db-priv/assests/migrations/000003_init.down.sql.sql b/dbm-services/mysql/db-priv/assests/migrations/000003_init.down.sql similarity index 100% rename from dbm-services/mysql/db-priv/assests/migrations/000003_init.down.sql.sql rename to dbm-services/mysql/db-priv/assests/migrations/000003_init.down.sql diff --git a/dbm-services/mysql/db-priv/assests/migrations/000004_init.down.sql b/dbm-services/mysql/db-priv/assests/migrations/000004_init.down.sql new file mode 100644 index 0000000000..2aa3355148 --- /dev/null +++ b/dbm-services/mysql/db-priv/assests/migrations/000004_init.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS tb_security_rules; +DROP TABLE IF EXISTS tb_passwords; \ No newline at end of file diff --git a/dbm-services/mysql/db-priv/assests/migrations/000004_init.up.sql b/dbm-services/mysql/db-priv/assests/migrations/000004_init.up.sql new file mode 100644 index 0000000000..a67b03a49c --- /dev/null +++ b/dbm-services/mysql/db-priv/assests/migrations/000004_init.up.sql @@ -0,0 +1,30 @@ +SET NAMES utf8; +CREATE TABLE IF NOT EXISTS `tb_security_rules` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(200) NOT NULL COMMENT '规则名称', + `rule` json NOT NULL COMMENT '安全规则', + `creator` varchar(800) NOT NULL COMMENT '创建者', + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `operator` varchar(800) DEFAULT NULL COMMENT '最后一次变更者', + `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次变更时间', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `tb_passwords` ( + `ip` varchar(100) NOT NULL COMMENT '实例ip', + `port` int unsigned NOT NULL COMMENT '实例端口', + `password` varchar(800) NOT NULL COMMENT '加密后的密码', + `username` varchar(800) NOT NULL COMMENT '用户名称', + `lock_until` timestamp COMMENT '锁定到的时间', + `operator` varchar(800) DEFAULT NULL COMMENT '最后一次变更者', + `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次变更时间', + UNIQUE KEY `idx_ip_port` (ip, port, username), + KEY `idx_lock` (`lock_until`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `tb_randomize_exclude` ( + `username` varchar(800) NOT NULL COMMENT '用户名称', + `bk_biz_id` int(11) NOT NULL COMMENT '业务的 cmdb id', + `operator` varchar(800) DEFAULT NULL COMMENT '最后一次变更者', + `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次变更时间', + UNIQUE KEY `idx_username_bk_biz_id` (username, bk_biz_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/dbm-services/mysql/db-priv/handler/account_rule.go b/dbm-services/mysql/db-priv/handler/account_rule.go index 0ab11ef29c..2d5bb2ecde 100644 --- a/dbm-services/mysql/db-priv/handler/account_rule.go +++ b/dbm-services/mysql/db-priv/handler/account_rule.go @@ -24,7 +24,7 @@ func (m *PrivService) GetAccountRuleList(c *gin.Context) { return } - if err := json.Unmarshal(body, &input); err != nil { + if err = json.Unmarshal(body, &input); err != nil { slog.Error("msg", err) SendResponse(c, errno.ErrBind, err) return @@ -51,18 +51,14 @@ func (m *PrivService) AddAccountRule(c *gin.Context) { return } - if err := json.Unmarshal(body, &input); err != nil { + if err = json.Unmarshal(body, &input); err != nil { slog.Error("msg", err) SendResponse(c, errno.ErrBind, err) return } err = input.AddAccountRule(string(body)) - if err != nil { - SendResponse(c, err, nil) - return - } - SendResponse(c, nil, nil) + SendResponse(c, err, nil) return } @@ -102,17 +98,13 @@ func (m *PrivService) ModifyAccountRule(c *gin.Context) { return } - if err := json.Unmarshal(body, &input); err != nil { + if err = json.Unmarshal(body, &input); err != nil { slog.Error("msg", err) SendResponse(c, errno.ErrBind, err) return } err = input.ModifyAccountRule(string(body)) - if err != nil { - SendResponse(c, err, nil) - return - } - SendResponse(c, nil, nil) + SendResponse(c, err, nil) return } diff --git a/dbm-services/mysql/db-priv/handler/admin_password.go b/dbm-services/mysql/db-priv/handler/admin_password.go new file mode 100644 index 0000000000..673161f835 --- /dev/null +++ b/dbm-services/mysql/db-priv/handler/admin_password.go @@ -0,0 +1,81 @@ +package handler + +import ( + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/service" + "encoding/json" + "io/ioutil" + + "github.com/gin-gonic/gin" + "golang.org/x/exp/slog" +) + +// GetPassword 查询用户的密码 +func (m *PrivService) GetPassword(c *gin.Context) { + slog.Info("do GetPassword!") + var input service.GetPasswordPara + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + batch, err := input.GetPassword() + SendResponse(c, err, batch) + return + +} + +// ModifyPassword 新增或者修改密码 +func (m *PrivService) ModifyPassword(c *gin.Context) { + slog.Info("do ModifyMysqlAdminPassword!") + var input service.ModifyPasswordPara + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + err = input.ModifyPassword() + SendResponse(c, err, nil) + return +} + +// ModifyMysqlAdminPassword 新增或者修改mysql实例中管理用户的密码,可用于随机化密码 +func (m *PrivService) ModifyMysqlAdminPassword(c *gin.Context) { + slog.Info("do ModifyMysqlAdminPassword!") + var input service.ModifyAdminUserPasswordPara + + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 随机化定时任务异步返回,避免占用资源 + if input.Async == true { + SendResponse(c, nil, nil) + } + // 前端页面调用等同步返回,返回修改成功的实例以及没有修改成功的实例 + batch, err := input.ModifyMysqlAdminPassword() + if input.Async == false { + SendResponse(c, err, batch) + } + return +} diff --git a/dbm-services/mysql/db-priv/handler/generate_random_string.go b/dbm-services/mysql/db-priv/handler/generate_random_string.go new file mode 100644 index 0000000000..d989758476 --- /dev/null +++ b/dbm-services/mysql/db-priv/handler/generate_random_string.go @@ -0,0 +1,45 @@ +package handler + +import ( + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/service" + "encoding/base64" + "encoding/json" + "io/ioutil" + + "github.com/gin-gonic/gin" + "golang.org/x/exp/slog" +) + +// GenerateRandomString 生成随机化密码 +func (m *PrivService) GenerateRandomString(c *gin.Context) { + slog.Info("do GenerateRandomString!") + var input service.GenerateRandomStringPara + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + if input.SecurityRuleName == "" { + SendResponse(c, errno.RuleNameNull, nil) + return + } + // 获取安全规则 + security, err := service.GetSecurityRule(input.SecurityRuleName) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.RuleNameNull, nil) + return + } + password, err := service.GenerateRandomString(security) + // 传输base64,因为部分字符通过url传输会转义 + SendResponse(c, err, base64.StdEncoding.EncodeToString([]byte(password))) + return +} diff --git a/dbm-services/mysql/db-priv/handler/randomize_manage.go b/dbm-services/mysql/db-priv/handler/randomize_manage.go new file mode 100644 index 0000000000..a50246d826 --- /dev/null +++ b/dbm-services/mysql/db-priv/handler/randomize_manage.go @@ -0,0 +1,56 @@ +package handler + +import ( + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/service" + "encoding/json" + "io/ioutil" + + "github.com/gin-gonic/gin" + "golang.org/x/exp/slog" +) + +// GetRandomExclude 获取不参加随机化的业务 +func (m *PrivService) GetRandomExclude(c *gin.Context) { + slog.Info("do GetRandomExclude!") + var input service.RandomExcludePara + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 获取不参加随机化的业务 + exclude, err := input.GetRandomizeExclude() + SendResponse(c, err, exclude) + return +} + +// ModifyRandomExclude 修改不参与随机化的业务 +func (m *PrivService) ModifyRandomExclude(c *gin.Context) { + slog.Info("do ModifyRandomExclude!") + var input service.RandomExcludePara + + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 传入的业务列表替换当前业务列表 + err = input.ModifyRandomizeExclude(string(body)) + SendResponse(c, err, nil) + return +} diff --git a/dbm-services/mysql/db-priv/handler/register_routes.go b/dbm-services/mysql/db-priv/handler/register_routes.go index 0d9252e239..03671d74e0 100644 --- a/dbm-services/mysql/db-priv/handler/register_routes.go +++ b/dbm-services/mysql/db-priv/handler/register_routes.go @@ -46,6 +46,26 @@ func (m *PrivService) Routes() []*gin.RouteInfo { // 获取公钥,用于传输过程中加密密码 {Method: http.MethodPost, Path: "pub_key", HandlerFunc: m.GetPubKey}, + + // 修改实例中指定用户的密码 + {Method: http.MethodPost, Path: "modify_mysql_admin_password", HandlerFunc: m.ModifyMysqlAdminPassword}, + // 查询密码 + {Method: http.MethodPost, Path: "get_password", HandlerFunc: m.GetPassword}, + // 修改密码 + {Method: http.MethodPost, Path: "modify_password", HandlerFunc: m.ModifyPassword}, + + // 生成随机字符串 + {Method: http.MethodPost, Path: "get_random_string", HandlerFunc: m.GenerateRandomString}, + + // 安全规则 + {Method: http.MethodPost, Path: "get_security_rule", HandlerFunc: m.GetSecurityRule}, + {Method: http.MethodPost, Path: "add_security_rule", HandlerFunc: m.AddSecurityRule}, + {Method: http.MethodPost, Path: "modify_security_rule", HandlerFunc: m.ModifySecurityRule}, + {Method: http.MethodPost, Path: "delete_security_rule", HandlerFunc: m.DeleteSecurityRule}, + + // 不参与随机化的业务 + {Method: http.MethodPost, Path: "get_randomize_exclude", HandlerFunc: m.GetRandomExclude}, + {Method: http.MethodPost, Path: "modify_randomize_exclude", HandlerFunc: m.ModifyRandomExclude}, } } diff --git a/dbm-services/mysql/db-priv/handler/security_rule.go b/dbm-services/mysql/db-priv/handler/security_rule.go new file mode 100644 index 0000000000..fa7c73fbba --- /dev/null +++ b/dbm-services/mysql/db-priv/handler/security_rule.go @@ -0,0 +1,107 @@ +package handler + +import ( + "encoding/json" + "io/ioutil" + + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/service" + + "github.com/gin-gonic/gin" + "golang.org/x/exp/slog" +) + +// GetSecurityRule 获取安全规则 +func (m *PrivService) GetSecurityRule(c *gin.Context) { + slog.Info("do GetSecurityRule!") + + var input service.SecurityRulePara + + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 根据名称查询安全规则 + security, err := input.GetSecurityRule() + SendResponse(c, err, security) + return +} + +// AddSecurityRule 添加安全规则 +func (m *PrivService) AddSecurityRule(c *gin.Context) { + + slog.Info("do AddSecurityRule!") + var input service.SecurityRulePara + + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 添加安全规则,安全规则主要用于生成密码和检验密码复杂度 + err = input.AddSecurityRule(string(body)) + SendResponse(c, err, nil) + return +} + +// DeleteSecurityRule 删除安全规则 +func (m *PrivService) DeleteSecurityRule(c *gin.Context) { + slog.Info("do DeleteSecurityRule!") + + var input service.SecurityRulePara + + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 根据id删除安全规则 + err = input.DeleteSecurityRule(string(body)) + SendResponse(c, err, nil) + return +} + +// ModifySecurityRule 修改安全规则,修改安全规则的名称和内容 +func (m *PrivService) ModifySecurityRule(c *gin.Context) { + slog.Info("do ModifySecurityRule!") + var input service.SecurityRulePara + + body, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + + if err = json.Unmarshal(body, &input); err != nil { + slog.Error("msg", err) + SendResponse(c, errno.ErrBind, err) + return + } + // 根据id,修改安全规则,修改安全规则的名称和内容 + err = input.ModifySecurityRule(string(body)) + SendResponse(c, err, nil) + return +} diff --git a/dbm-services/mysql/db-priv/service/accout_rule.go b/dbm-services/mysql/db-priv/service/accout_rule.go index 1aa29b5e77..591fa76280 100644 --- a/dbm-services/mysql/db-priv/service/accout_rule.go +++ b/dbm-services/mysql/db-priv/service/accout_rule.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "golang.org/x/exp/slog" + "dbm-services/common/go-pubpkg/errno" "dbm-services/mysql/priv-service/util" @@ -20,6 +22,10 @@ func (m *BkBizId) QueryAccountRule() ([]*AccountRuleSplitUser, int64, error) { count int64 result *gorm.DB err error + rulesWhere string + accountsWhere string + ruleIds string + accountIds string ) if m.BkBizId == 0 { return nil, count, errno.BkBizIdIsEmpty @@ -29,15 +35,50 @@ func (m *BkBizId) QueryAccountRule() ([]*AccountRuleSplitUser, int64, error) { m.ClusterType = &ct // return nil, count, errno.ClusterTypeIsEmpty } - err = DB.Self.Model(&TbAccounts{}).Where(&TbAccounts{BkBizId: m.BkBizId, ClusterType: *m.ClusterType}).Select( - "id,bk_biz_id,user,creator,create_time").Scan(&accounts).Error - if err != nil { - return nil, count, err + + if len(m.RuleIds) > 0 { + var acountList []*AccountId + for _, id := range m.RuleIds { + ruleIds = fmt.Sprintf("%d,%s", id, ruleIds) + } + ruleIds = strings.TrimRight(ruleIds, ",") + rulesWhere = fmt.Sprintf("bk_biz_id=%d and cluster_type='%s' and id in (%s)", + m.BkBizId, *m.ClusterType, ruleIds) + err = DB.Self.Model(&TbAccountRules{}).Where(rulesWhere). + Select("distinct(account_id) as account_id").Scan(&acountList).Error + if err != nil { + return nil, count, err + } + for _, id := range acountList { + accountIds = fmt.Sprintf("%d,%s", id.AccountId, accountIds) + } + accountIds = strings.TrimRight(accountIds, ",") + slog.Info("msg", "accountIds", accountIds) + accountsWhere = fmt.Sprintf("bk_biz_id=%d and cluster_type='%s' and id in (%s)", + m.BkBizId, *m.ClusterType, accountIds) + slog.Info("msg", "accountsWhere", accountsWhere) + err = DB.Self.Model(&TbAccounts{}).Where(accountsWhere).Select( + "id,bk_biz_id,user,creator,create_time").Scan(&accounts).Error + if err != nil { + return nil, count, err + } + } else { + err = DB.Self.Model(&TbAccounts{}).Where(&TbAccounts{BkBizId: m.BkBizId, ClusterType: *m.ClusterType}).Select( + "id,bk_biz_id,user,creator,create_time").Scan(&accounts).Error + if err != nil { + return nil, count, err + } } accountRuleSplitUser = make([]*AccountRuleSplitUser, len(accounts)) for k, v := range accounts { - result = DB.Self.Model(&TbAccountRules{}).Where(&TbAccountRules{BkBizId: m.BkBizId, AccountId: (*v).Id, - ClusterType: *m.ClusterType}). + where := fmt.Sprintf("bk_biz_id=%d and cluster_type='%s' and account_id=%d ", + m.BkBizId, *m.ClusterType, (*v).Id) + slog.Info("msg", "where", where) + if len(m.RuleIds) > 0 { + where = fmt.Sprintf("%s and id in (%s)", where, ruleIds) + slog.Info("msg", "where", where) + } + result = DB.Self.Model(&TbAccountRules{}).Where(where). Select("id,account_id,bk_biz_id,dbname,priv,creator,create_time").Scan(&rules) accountRuleSplitUser[k] = &AccountRuleSplitUser{Account: v, Rules: rules} if err != nil { @@ -104,8 +145,10 @@ func (m *AccountRulePara) AddAccountRule(jsonPara string) error { return err } } - tx.Commit() - + err = tx.Commit().Error + if err != nil { + return err + } log := PrivLog{BkBizId: m.BkBizId, Operator: m.Operator, Para: jsonPara, Time: insertTime} AddPrivLog(log) @@ -225,7 +268,6 @@ func (m *DeleteAccountRuleById) DeleteAccountRule(jsonPara string) error { for k, v := range m.Id { temp[k] = fmt.Sprintf("%d", v) } - sql := fmt.Sprintf("delete from tb_account_rules where id in (%s) and bk_biz_id = %d and cluster_type = '%s'", strings.Join(temp, ","), m.BkBizId, *m.ClusterType) result := DB.Self.Exec(sql) diff --git a/dbm-services/mysql/db-priv/service/accout_rule_object.go b/dbm-services/mysql/db-priv/service/accout_rule_object.go index cf67268fe1..ff909448d4 100644 --- a/dbm-services/mysql/db-priv/service/accout_rule_object.go +++ b/dbm-services/mysql/db-priv/service/accout_rule_object.go @@ -20,6 +20,10 @@ type TbAccountRules struct { UpdateTime util.TimeFormat `gorm:"column:update_time" json:"update_time"` } +type AccountId struct { + AccountId int64 `gorm:"column:account_id;not_null" json:"account_id"` +} + // Rule 账号规则表中需要在前端展示的字段 type Rule struct { Id int64 `gorm:"column:id;primary_key;auto_increment" json:"id"` diff --git a/dbm-services/mysql/db-priv/service/add_priv_object.go b/dbm-services/mysql/db-priv/service/add_priv_object.go index eec7b5eb92..9b40171936 100644 --- a/dbm-services/mysql/db-priv/service/add_priv_object.go +++ b/dbm-services/mysql/db-priv/service/add_priv_object.go @@ -85,6 +85,7 @@ type Domain struct { // BkBizId QueryAccountRule 函数的入参 type BkBizId struct { BkBizId int64 `json:"bk_biz_id" url:"bk_biz_id"` + RuleIds []int64 `json:"ids" url:"ids"` ClusterType *string `json:"cluster_type" url:"cluster_type"` } diff --git a/dbm-services/mysql/db-priv/service/admin_password.go b/dbm-services/mysql/db-priv/service/admin_password.go new file mode 100644 index 0000000000..3668568978 --- /dev/null +++ b/dbm-services/mysql/db-priv/service/admin_password.go @@ -0,0 +1,259 @@ +package service + +import ( + "dbm-services/common/go-pubpkg/errno" + "encoding/base64" + "fmt" + "strings" + "sync" + + "golang.org/x/exp/slog" +) + +// GetPassword 查询密码 +func (m *GetPasswordPara) GetPassword() ([]*TbPasswords, error) { + var passwords []*TbPasswords + if m.UserName == nil { + return passwords, errno.ErrUserIsEmpty + } + where := fmt.Sprintf(" username='%s' ", *m.UserName) + var filter []string + for _, item := range m.Instances { + filter = append(filter, fmt.Sprintf("(ip='%s' and port=%d)", item.Ip, item.Port)) + } + filters := strings.Join(filter, " or ") + if filters != "" { + where = fmt.Sprintf(" %s and %s ", where, filters) + } + // mysql实例中ADMIN用户的密码,仅能查看人为修改密码且在有效期的密码,不可以查看随机化生成的密码 + if *m.UserName == "ADMIN" { + where = fmt.Sprintf(" %s and lock_until is not null", where) + } + err := DB.Self.Model(&TbPasswords{}).Where(where).Scan(&passwords).Error + if err != nil { + return passwords, err + } + err = DecodePassword(passwords) + if err != nil { + return passwords, err + } + return passwords, nil +} + +// ModifyPassword 修改tb_passwords表中密码 +func (m *ModifyPasswordPara) ModifyPassword() error { + if m.UserName == nil { + return errno.ErrUserIsEmpty + } + var psw, encrypt string + var security SecurityRule + plain, err := base64.StdEncoding.DecodeString(m.Psw) + if err != nil { + slog.Error("msg", "base64 decode error", err) + return err + } + m.Psw = string(plain) + if m.SecurityRuleName != "" { + security, err = GetSecurityRule(m.SecurityRuleName) + if err != nil { + slog.Error("msg", "GetSecurityRule", err) + return err + } + psw, err = CheckOrGetPassword(m.Psw, security) + if err != nil { + slog.Error("msg", "CheckOrGetPassword", err) + return err + } + } else { + if m.Psw == "" { + return errno.RuleNameNull + } else { + psw = m.Psw + } + } + encrypt, err = SM4Encrypt(psw) + tx := DB.Self.Begin() + for _, address := range m.Instances { + // 更新tb_passwords中实例的密码 + sql := fmt.Sprintf("replace into tb_passwords(ip,port,password,username,operator) values('%s',%d,'%s','%s','%s')", + address.Ip, address.Port, encrypt, *m.UserName, m.Operator) + err = tx.Debug().Exec(sql).Error + if err != nil { + slog.Error("msg", sql, err) + tx.Rollback() + return err + } + } + err = tx.Commit().Error + if err != nil { + return err + } + return nil +} + +// ModifyMysqlAdminPassword 修改mysql实例中用户的密码,可用于随机化密码 +func (m *ModifyAdminUserPasswordPara) ModifyMysqlAdminPassword() (BatchResult, error) { + var errMsg Err + var success Resource + var fail Resource + var batch BatchResult + var wg sync.WaitGroup + var security SecurityRule + var passwordInput string + var errCheck error + tokenBucket := make(chan int, 10) + + // 后台定时任务,1、randmize_daily比如每天执行一次,随机化没有被锁住的实例 2、randmize_expired比如每分钟执行一次随机化锁定过期的实例 + // 前台页面,单据已提示实例密码被锁定是否修改,用户确认修改,因此不检查是否锁定 + if m.Async && m.Range == "randmize_expired" { + errCheck = m.NeedToBeRandomized() + if errCheck != nil { + return batch, errCheck + } + } else if m.Async && m.Range == "randmize_daily" { + errCheck = m.RemoveLockedInstances() + if errCheck != nil { + return batch, errCheck + } + } else if m.Async { + return batch, fmt.Errorf("[ %s ] not supported randmize range", m.Range) + } + + if m.UserName == nil { + return batch, errno.ErrUserIsEmpty + } + + plain, errCheck := base64.StdEncoding.DecodeString(m.Psw) + if errCheck != nil { + slog.Error("msg", "base64 decode error", errCheck) + return batch, errCheck + } + m.Psw = string(plain) + + // 传入安全规则,1、如果传入密码,根据安全规则校验密码复杂度,2、如果没有传入密码,根据安全规则随机生成密码 + // 不允许没有不传入安全规则,并且不传入密码 + if m.SecurityRuleName != "" { + security, errCheck = GetSecurityRule(m.SecurityRuleName) + if errCheck != nil { + slog.Error("msg", "GetSecurityRule", errCheck) + return batch, errCheck + } + if m.Psw != "" { + passwordInput, errCheck = CheckOrGetPassword(m.Psw, security) + if errCheck != nil { + slog.Error("msg", "CheckOrGetPassword", errCheck) + return batch, errCheck + } + } + } else { + if m.Psw == "" { + return batch, errno.RuleNameNull + } else { + passwordInput = m.Psw + } + } + + for _, cluster := range m.Clusters { + if cluster.BkCloudId == nil { + return batch, errno.CloudIdRequired + } + if cluster.ClusterType == nil { + return batch, errno.ClusterTypeIsEmpty + } + var psw, encrypt string + var errOuter error + if passwordInput == "" { + psw, errOuter = CheckOrGetPassword("", security) + if errOuter != nil { + slog.Error("msg", "CheckOrGetPassword", errOuter) + return batch, errOuter + } + } else { + psw = passwordInput + } + // 加密 + encrypt, errOuter = SM4Encrypt(psw) + if errOuter != nil { + slog.Error("SM4Encrypt", "error", errOuter) + return batch, errOuter + } + for _, instanceList := range cluster.MultiRoleInstanceLists { + var base []string + role := instanceList.Role + if *cluster.ClusterType == tendbcluster && role == machineTypeSpider { + base = append(base, flushPriv, setBinlogOff, setDdlByCtlOFF) + } else if *cluster.ClusterType == tendbcluster && role == tdbctl { + base = append(base, flushPriv, setBinlogOff, setTcAdminOFF) + } else { + base = append(base, flushPriv, setBinlogOff) + } + for _, address := range instanceList.Addresses { + wg.Add(1) + tokenBucket <- 0 + go func(base []string, role, psw, encrypt string, address Address, cluster OneCluster) { + defer func() { + <-tokenBucket + wg.Done() + }() + // 获取修改密码的语句 + sqls := base + hostPort := fmt.Sprintf("%s:%d", address.Ip, address.Port) + mysqlVersion, err := GetMySQLVersion(hostPort, *cluster.BkCloudId) + if err != nil { + slog.Error("mysqlVersion", err) + AddError(&errMsg, hostPort, err) + return + } + userLocalhost := fmt.Sprintf("GRANT ALL PRIVILEGES ON *.* TO '%s'@'localhost' "+ + "IDENTIFIED BY '%s' WITH GRANT OPTION", *m.UserName, psw) + userIp := fmt.Sprintf("GRANT ALL PRIVILEGES ON *.* TO '%s'@'%s' "+ + "IDENTIFIED BY '%s' WITH GRANT OPTION", *m.UserName, address.Ip, psw) + if !(*cluster.ClusterType == tendbcluster && role == machineTypeSpider) && + MySQLVersionParse(mysqlVersion, "") >= + MySQLVersionParse("8.0.0", "") { + userLocalhost = fmt.Sprintf("ALTER USER '%s'@'localhost' "+ + "IDENTIFIED WITH mysql_native_password BY '%s'", *m.UserName, psw) + userIp = fmt.Sprintf("ALTER USER '%s'@'%s' "+ + "IDENTIFIED WITH mysql_native_password BY '%s'", *m.UserName, address.Ip, psw) + } + sqls = append(sqls, userLocalhost, userIp, setBinlogOn, flushPriv) + // 到实例更新密码 + var queryRequest = QueryRequest{[]string{hostPort}, sqls, true, 60, *cluster.BkCloudId} + _, err = OneAddressExecuteSql(queryRequest) + if err != nil { + AddResource(&fail, address) + slog.Error("OneAddressExecuteSql", err) + AddError(&errMsg, hostPort, err) + return + } + // 更新tb_passwords中实例的密码 + sql := fmt.Sprintf("replace into tb_passwords(ip,port,password,username,operator) "+ + "values('%s',%d,'%s','ADMIN','%s')", + address.Ip, address.Port, encrypt, m.Operator) + if m.LockUntil != "" { + sql = fmt.Sprintf("replace into tb_passwords(ip,port,password,username,"+ + "operator,lock_until) values('%s',%d,'%s','ADMIN','%s','%s')", + address.Ip, address.Port, encrypt, m.Operator, m.LockUntil) + } + result := DB.Self.Exec(sql) + if result.Error != nil { + AddResource(&fail, address) + AddError(&errMsg, hostPort, result.Error) + return + } + AddResource(&success, address) + return + }(base, role, psw, encrypt, address, cluster) + } + } + } + wg.Wait() + close(tokenBucket) + // 随机化成功的实例以及随机化失败的实例 + batch = BatchResult{Success: success.resources, Fail: fail.resources} + if len(errMsg.errs) > 0 { + errOuter := errno.ModifyUserPasswordFail.Add("\n" + strings.Join(errMsg.errs, "\n")) + return batch, errOuter + } + return batch, nil +} diff --git a/dbm-services/mysql/db-priv/service/admin_password_base_func.go b/dbm-services/mysql/db-priv/service/admin_password_base_func.go new file mode 100644 index 0000000000..ddfd1d9430 --- /dev/null +++ b/dbm-services/mysql/db-priv/service/admin_password_base_func.go @@ -0,0 +1,235 @@ +package service + +import ( + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/util" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "os" + "time" + + "github.com/spf13/viper" + "golang.org/x/exp/slog" +) + +func CheckOrGetPassword(psw string, security SecurityRule) (string, error) { + // 密码为空,根据security生成随机密码 + // 密码不为空,根据security检查密码复杂度,security为空则不检查负责度(迁移密码无法修改,且密码复杂度不足的情况) + var password string + var err error + if psw != "" { + password = psw + // 密码长度检查 + lenOk := len(psw) >= security.MinLength && len(psw) <= security.MaxLength + // 密码不可连续的字符规则、不可重复的字符规则 检查 + repeatOk := security.ExcludeContinuousRule.Repeats && CheckContinuousRepeats( + psw, security.ExcludeContinuousRule.Limit) || + security.ExcludeContinuousRule.Repeats == false + if !(CheckExcludeContinuousRule(psw, security.ExcludeContinuousRule) && repeatOk && lenOk) { + slog.Error("msg", "NotMeetComplexity", security) + return "", errno.NotMeetComplexity + } + } else { + // 没有传入密码,按照密码复杂度随机生成密码 + password, err = GenerateRandomString(security) + if err != nil { + slog.Error("GenerateRandomString", "error", err) + return "", err + } + } + return password, nil +} + +func GetSecurityRule(securityName string) (SecurityRule, error) { + var security SecurityRule + rule, err := (&SecurityRulePara{Name: securityName}).GetSecurityRule() + if err != nil { + slog.Error("msg", "GetSecurityRule", err) + return security, err + } + err = json.Unmarshal([]byte((*rule).Rule), &security) + if err != nil { + slog.Error("msg", "unmarshal error", err) + return security, err + } + return security, nil +} + +// RemoveLockedInstances 日常随机化剔除锁定的实例 +func (m *ModifyAdminUserPasswordPara) RemoveLockedInstances() error { + var locked []*Address + var clusters []OneCluster + err := DB.Self.Model(&TbPasswords{}).Where("lock_until is not null").Select("ip,port").Scan(&locked).Error + if err != nil { + slog.Error("msg", "get locked instances error", err) + return err + } + if len(locked) == 0 { + return nil + } + for _, cluster := range m.Clusters { + var roles []InstanceList + for _, role := range cluster.MultiRoleInstanceLists { + var addresses []Address + for _, address := range role.Addresses { + for k, lock := range locked { + if address.Ip == lock.Ip && address.Port == lock.Port { + break + } + if k == len(locked)-1 { + addresses = append(addresses, address) + } + } + } + if len(addresses) > 0 { + roles = append(roles, InstanceList{role.Role, addresses}) + } + } + if len(roles) > 0 { + clusters = append(clusters, OneCluster{cluster.BkCloudId, cluster.ClusterType, roles}) + } + } + m.Clusters = clusters + return nil +} + +// NeedToBeRandomized 锁定到期的实例实例随机化 +func (m *ModifyAdminUserPasswordPara) NeedToBeRandomized() error { + var needs []*Address + var clusters []OneCluster + err := DB.Self.Model(&TbPasswords{}).Where("lock_until <= now()").Select("ip,port").Scan(&needs).Error + if err != nil { + slog.Error("msg", "get locked instances error", err) + return err + } + for _, cluster := range m.Clusters { + var roles []InstanceList + for _, role := range cluster.MultiRoleInstanceLists { + var addresses []Address + for _, address := range role.Addresses { + for _, need := range needs { + if address.Ip == need.Ip && address.Port == need.Port { + addresses = append(addresses, address) + break + } + } + } + if len(addresses) > 0 { + roles = append(roles, InstanceList{role.Role, addresses}) + } + } + if len(roles) > 0 { + clusters = append(clusters, OneCluster{cluster.BkCloudId, cluster.ClusterType, roles}) + } + } + m.Clusters = clusters + return nil +} + +func DecodePassword(slice []*TbPasswords) error { + pswList := UniquePassword(slice) + pswMap := make(map[string]string, len(pswList)) + for _, item := range pswList { + // 避免在写入和读取数据库时乱码,存储hex进制 + bytes, err := hex.DecodeString(item) + if err != nil { + slog.Error("msg", "get hex decode error", err) + return err + } + pswMap[item], err = SM4Decrypt(string(bytes)) + if err != nil { + slog.Error("msg", "SM4Decrypt error", err) + return err + } + } + for k, lock := range slice { + slice[k].Password = pswMap[lock.Password] + } + return nil +} + +// SM4Encrypt 加密 +func SM4Encrypt(input string) (string, error) { + output, err := SM4(input, "encrypt") + if err != nil { + slog.Error("msg", "SM4Encrypt", err) + return "", err + } + return hex.EncodeToString(output), nil +} + +// SM4Decrypt 解密 +func SM4Decrypt(input string) (string, error) { + output, err := SM4(input, "decrypt") + if err != nil { + slog.Error("msg", "SM4Decrypt", err) + return "", err + } + return base64.StdEncoding.EncodeToString(output), nil +} + +// SM4 加密方式 +func SM4(input string, vtype string) ([]byte, error) { + rand.Seed(time.Now().UnixNano()) + r := rand.Intn(1000) + name := fmt.Sprintf("%d%03d", time.Now().UnixNano()/1e6, r) + inputName := fmt.Sprintf("%s.input", name) + outputName := fmt.Sprintf("%s.output", name) + //由于部分特殊字符可能导致命令行执行会失败,存入文件后解密或者机密 + //cmd := fmt.Sprintf(`echo -n '%s' | openssl enc -e -sm4-ctr -pbkdf2 -k %s`, plain, viper.GetString("bk_app_secret")) + //cmd := fmt.Sprintf(`echo -n '%s' | openssl enc -d -sm4-ctr -pbkdf2 -k %s`, cipher, viper.GetString("bk_app_secret")) + _, err := os.Stat(inputName) + if err == nil || !os.IsNotExist(err) { + slog.Error("msg", "check file exist", os.ErrExist) + return nil, os.ErrExist + } + inputFile, err := os.OpenFile(inputName, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + slog.Error("msg", "file", inputName, "create file error", err) + return nil, err + } + defer func() { + inputFile.Close() + _ = os.Remove(inputName) + _ = os.Remove(outputName) + }() + if _, err = inputFile.Write([]byte(input)); err != nil { + slog.Error("msg", "file", inputName, "write file error") + return nil, err + } + cmd := fmt.Sprintf(`openssl enc -in %s -out %s -sm4-ctr -pbkdf2 -k %s`, inputName, outputName, + viper.GetString("bk_app_secret")) + if vtype == "encrypt" { + cmd = fmt.Sprintf("%s -e ", cmd) + } else if vtype == "decrypt" { + cmd = fmt.Sprintf("%s -d ", cmd) + } + _, err = util.ExecShellCommand(false, cmd) + if err != nil { + slog.Error("msg", "exec command error", err) + return nil, err + } + output, err := ioutil.ReadFile(outputName) + if err != nil { + slog.Error("msg", "file", outputName, "read file error", err) + return nil, err + } + return output, nil +} + +// UniquePassword 获取distinct的数据 +func UniquePassword(slice []*TbPasswords) []string { + res := make([]string, 0) + mp := make(map[string]bool, len(slice)) + for _, e := range slice { + if mp[e.Password] == false { + mp[e.Password] = true + res = append(res, e.Password) + } + } + return res +} diff --git a/dbm-services/mysql/db-priv/service/admin_password_object.go b/dbm-services/mysql/db-priv/service/admin_password_object.go new file mode 100644 index 0000000000..11df274b7c --- /dev/null +++ b/dbm-services/mysql/db-priv/service/admin_password_object.go @@ -0,0 +1,73 @@ +package service + +import "dbm-services/mysql/priv-service/util" + +// ModifyAdminUserPasswordPara 函数的入参 +type ModifyAdminUserPasswordPara struct { + UserName *string `json:"username"` + Psw string `json:"password"` + LockUntil util.TimeFormat `json:"lock_until"` + Operator string `json:"operator"` + Clusters []OneCluster `json:"clusters"` + SecurityRuleName string `json:"security_rule_name"` + Range string `json:"range"` + Async bool `json:"async"` // 是否异步的方式执行 +} + +// ModifyPasswordPara 函数的入参 +type ModifyPasswordPara struct { + UserName *string `json:"username"` + Psw string `json:"password"` + Operator string `json:"operator"` + Instances []Address `json:"instances"` + SecurityRuleName string `json:"security_rule_name"` +} + +// GetPasswordPara 函数的入参 +type GetPasswordPara struct { + Instances []Address `json:"instances"` + UserName *string `json:"username"` +} + +type TbPasswords struct { + Id int64 `gorm:"column:id;primary_key;auto_increment" json:"id"` + Ip string `gorm:"column:ip;not_null" json:"ip"` + Port int64 `gorm:"column:port;not_null" json:"port"` + Password string `gorm:"column:password;not_null" json:"password"` + UserName string `gorm:"column:username;not_null" json:"username"` + LockUntil util.TimeFormat `gorm:"column:lock_until" json:"lock_until"` + Operator string `gorm:"column:operator" json:"operator"` + UpdateTime util.TimeFormat `gorm:"column:update_time" json:"update_time"` +} + +type OneCluster struct { + BkCloudId *int64 `json:"bk_cloud_id"` + ClusterType *string `json:"cluster_type"` + MultiRoleInstanceLists []InstanceList `json:"instances"` +} + +type InstanceList struct { + // 对于修改密码的接口,仅当集群为tendbcluster类型,需要再根据role判断实施方式 + // Role用于区分spider、tdbctl、remote + Role string `json:"role"` + Addresses []Address `json:"addresses"` +} + +type Address struct { + Ip string `gorm:"column:ip" json:"ip"` + Port int64 `gorm:"column:port" json:"port"` +} + +type BatchResult struct { + Success []Address `json:"success"` + Fail []Address `json:"fail"` +} + +type AdminPasswordResp struct { + Locked []*TbPasswords `json:"locked"` + Unlocked []*TbPasswords `json:"unlocked"` +} + +type Password struct { + Password string `gorm:"column:password;not_null" json:"password"` +} diff --git a/dbm-services/mysql/db-priv/service/clone_client_priv_base_func.go b/dbm-services/mysql/db-priv/service/clone_client_priv_base_func.go index 691ea657f0..ff7347cca2 100644 --- a/dbm-services/mysql/db-priv/service/clone_client_priv_base_func.go +++ b/dbm-services/mysql/db-priv/service/clone_client_priv_base_func.go @@ -166,3 +166,10 @@ func AddErrorOnly(errMsg *Err, err error) { errMsg.errs = append(errMsg.errs, err.Error()) errMsg.mu.Unlock() } + +// AddResource 并行时构建数组 +func AddResource(resources *Resource, resource Address) { + resources.mu.Lock() + resources.resources = append(resources.resources, resource) + resources.mu.Unlock() +} diff --git a/dbm-services/mysql/db-priv/service/clone_client_priv_object.go b/dbm-services/mysql/db-priv/service/clone_client_priv_object.go index 8ae86f2e7a..825cf94054 100644 --- a/dbm-services/mysql/db-priv/service/clone_client_priv_object.go +++ b/dbm-services/mysql/db-priv/service/clone_client_priv_object.go @@ -40,3 +40,9 @@ type Err struct { mu sync.RWMutex errs []string } + +// Resource 并行时共同维护数组 +type Resource struct { + mu sync.RWMutex + resources []Address +} diff --git a/dbm-services/mysql/db-priv/service/generate_random_string.go b/dbm-services/mysql/db-priv/service/generate_random_string.go new file mode 100644 index 0000000000..40d3f4a42c --- /dev/null +++ b/dbm-services/mysql/db-priv/service/generate_random_string.go @@ -0,0 +1,173 @@ +package service + +import ( + "dbm-services/common/go-pubpkg/errno" + "fmt" + "math" + "math/rand" + "strings" + "time" + + "golang.org/x/exp/slog" +) + +const lowercase = "abcdefghijklmnopqrstuvwxyz" +const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +const number = "0123456789" +const symbol = `!#$%&()*+,-./:;<=>?@[\]^_{|}~` + +// 为密码池添加连续的字母序,数字序,特殊字符序和键盘序 +const continuousSymbols = "~!@#$%^&*()_+" + +var continuousKeyboardCol = []string{"1qaz", "2wsx", "3edc", "4rfv", "5tgb", "6yhn", "7ujm", "8ik,", "9ol.", "0p;/"} +var continuousKeyboardRow = []string{"qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./"} + +// GenerateRandomStringPara 生成随机字符串的函数的入参 +type GenerateRandomStringPara struct { + SecurityRuleName string `json:"security_rule_name"` +} + +func GenerateRandomString(security SecurityRule) (string, error) { + length := rand.Intn(security.MaxLength-security.MinLength) + security.MinLength + var str []byte + // 字母与数字占比90%,符号占比10% + alphabetNumberStr := fmt.Sprintf("%s%s%s", lowercase, uppercase, number) + alphabetNumberPercent := 0.9 + rand.Seed(time.Now().UnixNano()) + // 必须包含的字符,至少有一个 + if security.IncludeRule.Lowercase { + index := rand.Intn(len(lowercase)) + str = append(str, lowercase[index]) + } + if security.IncludeRule.Uppercase { + index := rand.Intn(len(uppercase)) + str = append(str, uppercase[index]) + } + if security.IncludeRule.Symbols { + index := rand.Intn(len(symbol)) + str = append(str, symbol[index]) + } + if security.IncludeRule.Numbers { + index := rand.Intn(len(number)) + str = append(str, number[index]) + } + if len(str) > security.MinLength { + return string(str), errno.IncludeCharTypesLargerThanLength + } + remain := length - len(str) + if remain <= 0 { + return string(str), nil + } + alphabetNumberCnt := int(math.Ceil(alphabetNumberPercent * float64(remain))) + symbolCnt := remain - alphabetNumberCnt + //遍历,生成一个随机index索引 + for i := 0; i < alphabetNumberCnt; i++ { + index := rand.Intn(len(alphabetNumberStr)) + str = append(str, alphabetNumberStr[index]) + } + for i := 0; i < symbolCnt; i++ { + index := rand.Intn(len(symbol)) + str = append(str, symbol[index]) + } + for i := 0; i < 10; i++ { + RandShuffle(&str) + repeatOk := security.ExcludeContinuousRule.Repeats && + CheckContinuousRepeats(string(str), security.ExcludeContinuousRule.Limit) || + security.ExcludeContinuousRule.Repeats == false + // 连续规则以及重复规则不通过,打乱顺序 + if CheckExcludeContinuousRule(string(str), security.ExcludeContinuousRule) && repeatOk { + return string(str), nil + } + } + slog.Error("error", errno.TryTooManyTimes.AddBefore("GenerateRandomString")) + return "", errno.TryTooManyTimes.AddBefore("GenerateRandomString") +} + +// RandShuffle 随机打乱字符串中的字符顺序 +func RandShuffle(slice *[]byte) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len(*slice), func(i, j int) { + (*slice)[i], (*slice)[j] = (*slice)[j], (*slice)[i] + }) +} + +// CheckExcludeContinuousRule 检查密码不允许连续N位出现某个规则 +func CheckExcludeContinuousRule(str string, rule ExcludeContinuousRule) bool { + str = strings.ToLower(str) + // 连续的符号或者数字或者字母 + if rule.Symbols { + _, cnt := FindLongestCommonSubstr(str, continuousSymbols) + if cnt >= rule.Limit { + return false + } + } + if rule.Numbers { + _, cnt := FindLongestCommonSubstr(str, number) + if cnt >= rule.Limit { + return false + } + } + if rule.Letters { + _, cnt := FindLongestCommonSubstr(str, lowercase) + if cnt >= rule.Limit { + return false + } + } + // 连续的键盘序 + if rule.Keyboards { + for _, v := range continuousKeyboardRow { + _, cnt := FindLongestCommonSubstr(str, v) + if cnt >= rule.Limit { + return false + } + } + for _, v := range continuousKeyboardCol { + _, cnt := FindLongestCommonSubstr(str, v) + if cnt >= rule.Limit { + return false + } + } + } + return true +} + +// FindLongestCommonSubstr 找出最长的公共字符串子串以及其长度 +func FindLongestCommonSubstr(s1, s2 string) (string, int) { + var max, pos int + temp := make([][]int, len(s1)+1) + for i := range temp { + temp[i] = make([]int, len(s2)+1) + } + for i := 0; i < len(s1); i++ { + for j := 0; j < len(s2); j++ { + if s1[i] == s2[j] { + temp[i+1][j+1] = temp[i][j] + 1 + if temp[i+1][j+1] > max { + max = temp[i+1][j+1] + pos = i + 1 + } + } + } + } + return s1[pos-max : pos], max +} + +// CheckContinuousRepeats 字符连续重复出现的次数 +func CheckContinuousRepeats(str string, repeats int) bool { + str = strings.ToLower(str) + var max, j int + cnt := 1 + for i := 1; i < len(str); i++ { + if str[i] == str[j] { + cnt++ + } + if str[i] != str[j] || i == len(str)-1 { + if cnt > max { + max = cnt + } + j = i + cnt = 1 + } + } + return max < repeats +} diff --git a/dbm-services/mysql/db-priv/service/randomize_manager.go b/dbm-services/mysql/db-priv/service/randomize_manager.go new file mode 100644 index 0000000000..ffdbce002a --- /dev/null +++ b/dbm-services/mysql/db-priv/service/randomize_manager.go @@ -0,0 +1,75 @@ +package service + +import ( + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/util" + "fmt" + + "golang.org/x/exp/slog" +) + +// RandomExcludePara 对计划相关函数的入参 +type RandomExcludePara struct { + UserName string `json:"username"` + BkBizIds []int64 `json:"bk_biz_ids"` + Operator string `json:"operator"` +} + +// TbRandomExclude 不参与随机化的业务的表 +type TbRandomExclude struct { + UserName string `gorm:"column:username;not_null" json:"username"` + BkBizId int64 `gorm:"column:bk_biz_id;not_null" json:"bk_biz_id"` + Operator string `gorm:"column:operator" json:"operator"` + UpdateTime util.TimeFormat `gorm:"column:update_time" json:"update_time"` +} + +// ModifyRandomizeExclude 修改不参与随机化的业务 +func (m *RandomExcludePara) ModifyRandomizeExclude(jsonPara string) error { + if m.UserName == "" { + return errno.NameNull + } + tx := DB.Self.Begin() + // 传入的业务列表替换当前业务列表 + sql := fmt.Sprintf("delete from tb_randomize_exclude where username='%s'", m.UserName) + err := tx.Debug().Exec(sql).Error + if err != nil { + slog.Error("msg", sql, err) + tx.Rollback() + return err + } + for _, id := range m.BkBizIds { + // 更新tb_passwords中实例的密码 + sql = fmt.Sprintf("replace into tb_randomize_exclude(username,bk_biz_id,operator) values('%s',%d,'%s')", + m.UserName, id, m.Operator) + err = tx.Debug().Exec(sql).Error + if err != nil { + slog.Error("msg", sql, err) + tx.Rollback() + return err + } + } + err = tx.Commit().Error + if err != nil { + return err + } + log := PrivLog{BkBizId: 0, Operator: m.Operator, Para: jsonPara, Time: util.NowTimeFormat()} + AddPrivLog(log) + return nil +} + +// GetRandomizeExclude 查询不参与随机化的业务 +func (m *RandomExcludePara) GetRandomizeExclude() ([]int64, error) { + var ids []*TbRandomExclude + if m.UserName == "" { + return nil, errno.NameNull + } + err := DB.Self.Table("tb_randomize_exclude").Where(fmt.Sprintf("username = '%s'", m.UserName)).Scan(&ids).Error + if err != nil { + return nil, err + } + var list []int64 + for _, id := range ids { + list = append(list, id.BkBizId) + } + return list, nil +} diff --git a/dbm-services/mysql/db-priv/service/security_rule.go b/dbm-services/mysql/db-priv/service/security_rule.go new file mode 100644 index 0000000000..12a8a66980 --- /dev/null +++ b/dbm-services/mysql/db-priv/service/security_rule.go @@ -0,0 +1,93 @@ +package service + +import ( + "dbm-services/common/go-pubpkg/errno" + "dbm-services/mysql/priv-service/util" + "fmt" +) + +// ModifySecurityRule 修改安全规则 +func (m *SecurityRulePara) ModifySecurityRule(jsonPara string) error { + if m.Id == 0 { + return errno.RuleIdNull + } + updateTime := util.NowTimeFormat() + rule := TbSecurityRules{Name: m.Name, Rule: m.Rule, Operator: m.Operator, UpdateTime: updateTime} + id := TbSecurityRules{Id: m.Id} + result := DB.Self.Model(&id).Update(&rule) + if result.Error != nil { + return result.Error + } + // 是否更新 + if result.RowsAffected == 0 { + return errno.RuleNotExisted + } + log := PrivLog{BkBizId: 0, Operator: m.Operator, Para: jsonPara, Time: updateTime} + AddPrivLog(log) + return nil +} + +// AddSecurityRule 添加安全规则 +func (m *SecurityRulePara) AddSecurityRule(jsonPara string) error { + var count uint64 + // 检查是否已存在 + err := DB.Self.Model(&TbSecurityRules{}).Where(&TbSecurityRules{Name: m.Name}). + Count(&count).Error + if err != nil { + return err + } + if count != 0 { + return errno.RuleExisted.AddBefore(m.Name) + } + insertTime := util.NowTimeFormat() + // 添加规则 + rule := &TbSecurityRules{Name: m.Name, Rule: m.Rule, Creator: m.Operator, CreateTime: insertTime} + err = DB.Self.Model(&TbSecurityRules{}).Create(&rule).Error + if err != nil { + return err + } + log := PrivLog{BkBizId: 0, Operator: m.Operator, Para: jsonPara, Time: insertTime} + AddPrivLog(log) + return nil +} + +// GetSecurityRule 查询安全规则 +func (m *SecurityRulePara) GetSecurityRule() (*TbSecurityRules, error) { + var rules []*TbSecurityRules + if m.Name == "" { + return nil, errno.RuleNameNull + } + err := DB.Self.Model(&TbSecurityRules{}).Where(&TbSecurityRules{Name: m.Name}).Scan(&rules).Error + if err != nil { + return nil, err + } + if len(rules) > 0 { + return rules[0], nil + } + return nil, errno.RuleNotExisted.AddBefore(m.Name) +} + +// DeleteSecurityRule 删除安全规则 +func (m *SecurityRulePara) DeleteSecurityRule(jsonPara string) error { + var rules []*TbSecurityRules + if m.Id == 0 { + return errno.RuleIdNull + } + id := TbSecurityRules{Id: m.Id} + err := DB.Self.Model(&TbSecurityRules{}).Where(&id).Scan(&rules).Error + if err != nil { + return err + } + // 不允许删除密码规则,随机化默认使用的规则 + if len(rules) > 0 && rules[0].Name == "password" { + return fmt.Errorf("system config can not be delete") + } + // 根据id删除 + err = DB.Self.Model(&TbSecurityRules{}).Delete(&id).Error + if err != nil { + return err + } + log := PrivLog{BkBizId: 0, Operator: m.Operator, Para: jsonPara, Time: util.NowTimeFormat()} + AddPrivLog(log) + return nil +} diff --git a/dbm-services/mysql/db-priv/service/security_rule_object.go b/dbm-services/mysql/db-priv/service/security_rule_object.go new file mode 100644 index 0000000000..acc52bf6b6 --- /dev/null +++ b/dbm-services/mysql/db-priv/service/security_rule_object.go @@ -0,0 +1,47 @@ +package service + +import "dbm-services/mysql/priv-service/util" + +// SecurityRulePara 安全规则相关函数的入参 +type SecurityRulePara struct { + Id int64 `json:"id"` + Name string `json:"name"` + Rule string `json:"rule"` + Operator string `json:"operator"` +} + +// TbSecurityRules 安全规则表 +type TbSecurityRules struct { + Id int64 `gorm:"column:id;primary_key;auto_increment" json:"id"` + Name string `gorm:"column:name;not_null" json:"name"` + Rule string `gorm:"column:rule;not_null" json:"rule"` + Creator string `gorm:"column:creator;not_null;" json:"creator"` + CreateTime util.TimeFormat `gorm:"column:create_time" json:"create_time"` + Operator string `gorm:"column:operator" json:"operator"` + UpdateTime util.TimeFormat `gorm:"column:update_time" json:"update_time"` +} + +// SecurityRule 安全规则 +type SecurityRule struct { + ExcludeContinuousRule ExcludeContinuousRule `json:"exclude_continuous_rule"` // 连续字符的规则 + IncludeRule IncludeRule `json:"include_rule"` // 密码中必须包含某些字符 + MaxLength int `json:"max_length"` // 密码的最大长度 + MinLength int `json:"min_length"` // 密码的最小长度 +} + +// ExcludeContinuousRule 密码不允许连续N位出现 +type ExcludeContinuousRule struct { + Limit int `json:"limit"` //连续N位 + Letters bool `json:"letters"` //字母顺序 + Numbers bool `json:"numbers"` //数字顺序 + Symbols bool `json:"symbols"` //特殊符号顺序 + Keyboards bool `json:"keyboards"` //键盘顺序 + Repeats bool `json:"repeats"` //重复的字母、数字、特殊字符 +} + +type IncludeRule struct { + Numbers bool `json:"numbers"` //是否包含数字 + Symbols bool `json:"symbols"` //是否包含字符 + Lowercase bool `json:"lowercase"` //是否包含小写 + Uppercase bool `json:"uppercase"` //是否包含大写 +} diff --git a/dbm-services/mysql/db-priv/util/base_func.go b/dbm-services/mysql/db-priv/util/base_func.go index 5df9f0e592..2f1bd7db07 100644 --- a/dbm-services/mysql/db-priv/util/base_func.go +++ b/dbm-services/mysql/db-priv/util/base_func.go @@ -1,12 +1,16 @@ package util import ( + "bytes" "fmt" + "os/exec" "reflect" "regexp" "runtime" "strings" + "github.com/pkg/errors" + "github.com/asaskevich/govalidator" ) @@ -70,3 +74,25 @@ func HasElem(elem interface{}, slice interface{}) bool { } return false } + +func ExecShellCommand(isSudo bool, param string) ([]byte, error) { + if isSudo { + param = "sudo " + param + } + cmd := exec.Command("bash", "-c", param) + var stdout, stderr bytes.Buffer + var err error + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err = cmd.Run() + if err != nil { + // return stderr.String(), err + return stderr.Bytes(), errors.WithMessage(err, stderr.String()) + } + + if len(stderr.String()) > 0 { + err = fmt.Errorf("execute shell command(%s) has stderr:%s", param, stderr.String()) + return stderr.Bytes(), err + } + return stdout.Bytes(), nil +} diff --git a/dbm-ui/backend/components/mysql_priv_manager/client.py b/dbm-ui/backend/components/mysql_priv_manager/client.py index 97732c357b..489c254b34 100644 --- a/dbm-ui/backend/components/mysql_priv_manager/client.py +++ b/dbm-ui/backend/components/mysql_priv_manager/client.py @@ -83,7 +83,7 @@ def __init__(self): base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, url="/priv/modify_account", module=self.MODULE, - description=_("修改密码"), + description=_("修改账号的密码"), ) # 授权规则相关 @@ -145,6 +145,76 @@ def __init__(self): module=self.MODULE, description=_("mysql实例创建临时账号(切换专属接口)"), ) + self.modify_mysql_admin_password = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/modify_mysql_admin_password", + module=self.MODULE, + description=_("新增或者修改mysql实例中管理用户的密码"), + ) + self.get_password = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/get_password", + module=self.MODULE, + description=_("获取密码"), + ) + self.modify_password = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/modify_password", + module=self.MODULE, + description=_("新增或者修改密码"), + ) + self.get_random_string = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/get_random_string", + module=self.MODULE, + description=_("生成随机字符串"), + ) + self.get_security_rule = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/get_security_rule", + module=self.MODULE, + description=_("获取安全规则"), + ) + self.add_security_rule = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/add_security_rule", + module=self.MODULE, + description=_("添加安全规则"), + ) + self.modify_security_rule = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/modify_security_rule", + module=self.MODULE, + description=_("修改安全规则"), + ) + self.delete_security_rule = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/delete_security_rule", + module=self.MODULE, + description=_("删除安全规则"), + ) + self.get_randomize_exclude = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/get_randomize_exclude", + module=self.MODULE, + description=_("获取不参与随机化的业务"), + ) + self.modify_randomize_exclude = DataAPI( + method="POST", + base=MYSQL_PRIV_MANAGER_APIGW_DOMAIN, + url="/priv/modify_randomize_exclude", + module=self.MODULE, + description=_("修改不参与随机化的业务"), + ) MySQLPrivManagerApi = _MySQLPrivManagerApi() diff --git a/dbm-ui/backend/db_periodic_task/local_tasks/__init__.py b/dbm-ui/backend/db_periodic_task/local_tasks/__init__.py index afa727627e..8a78121103 100644 --- a/dbm-ui/backend/db_periodic_task/local_tasks/__init__.py +++ b/dbm-ui/backend/db_periodic_task/local_tasks/__init__.py @@ -13,6 +13,7 @@ from backend.db_periodic_task.local_tasks.db_monitor import * from backend.db_periodic_task.local_tasks.db_proxy import * from backend.db_periodic_task.local_tasks.redis_autofix import * +from backend.db_periodic_task.local_tasks.randomize_password import * from backend.db_periodic_task.local_tasks.ticket import * from backend.db_periodic_task.models import DBPeriodicTask diff --git a/dbm-ui/backend/db_periodic_task/local_tasks/randomize_password.py b/dbm-ui/backend/db_periodic_task/local_tasks/randomize_password.py new file mode 100644 index 0000000000..6b182f0499 --- /dev/null +++ b/dbm-ui/backend/db_periodic_task/local_tasks/randomize_password.py @@ -0,0 +1,137 @@ +import logging + +from celery.schedules import crontab +from django.db.models import Q +from django.utils.translation import ugettext as _ +from django_celery_beat.schedulers import ModelEntry + +from backend.components import MySQLPrivManagerApi +from backend.db_meta.enums import AccessLayer, ClusterType, MachineType, TenDBClusterSpiderRole +from backend.db_meta.models import Cluster +from backend.db_periodic_task.local_tasks import register_periodic_task +from backend.db_periodic_task.models import DBPeriodicTask +from backend.exceptions import ApiResultError +from backend.flow.consts import TDBCTL_USER + +logger = logging.getLogger("celery") + + +@register_periodic_task(run_every=crontab(day_of_week="*", hour="10", minute="3")) +def auto_randomize_password_daily(): + """每日随机化密码""" + randomize_admin_password(if_async=True, range_type="randmize_daily") + + +@register_periodic_task(run_every=crontab(minute="*")) +def auto_randomize_password_expired(): + """当密码锁定到期,随机化密码""" + randomize_admin_password(if_async=True, range_type="randmize_expired") + + +def randomize_admin_password(if_async: bool, range_type: str): + """密码随机化定时任务,只随机化mysql数据库""" + cluster_types = [ClusterType.TenDBCluster.value, ClusterType.TenDBHA.value, ClusterType.TenDBSingle.value] + cluster_ids = [] + if range_type == "randmize_daily": + # 获取不参与随机化的业务 + bk_biz_id_list = MySQLPrivManagerApi.get_randomize_exclude({"username": "ADMIN"}) + if bk_biz_id_list is None: + cluster_ids = [cluster.id for cluster in Cluster.objects.filter(cluster_type__in=cluster_types)] + else: + # 排除不随机化的业务 + cluster_ids = [ + cluster.id + for cluster in Cluster.objects.filter( + ~Q(bk_biz_id__in=[bk_biz_id_list]), cluster_type__in=cluster_types + ) + ] + elif range_type == "randmize_expired": + cluster_ids = [cluster.id for cluster in Cluster.objects.filter(cluster_type__in=cluster_types)] + clusters = [] + for cluster_id in cluster_ids: + clusters.append(get_mysql_instance(cluster_id)) + try: + MySQLPrivManagerApi.modify_mysql_admin_password( + params={ + "username": "ADMIN", # 管理用户 + "operator": range_type, + "clusters": clusters, + "security_rule_name": "password", # 用于生成随机化密码的安全规则 + "async": if_async, # 异步执行,避免占用资源 + "range": range_type, # 被锁定的密码到期,需要被随机化 + }, + raw=True, + ) + except ApiResultError as e: + # 捕获接口返回结果异常 + logger.error(_("「接口modify_mysql_admin_password返回结果异常」{}").format(e.message)) + except Exception as e: + # 捕获接口其他未知异常 + logger.error(_("「接口modify_mysql_admin_password调用异常」{}").format(e)) + return + + +def get_mysql_instance(cluster_id: int): + cluster = Cluster.objects.get(id=cluster_id) + instances = [ + { + "role": AccessLayer.STORAGE.value, + "addresses": [ + { + "ip": instance.machine.ip, + "port": instance.port, + "id": instance.id, + } + for instance in cluster.storageinstance_set.all() + ], + } + ] + # spider节点和tdbctl节点修改密码指令不同,需区别 + if cluster.cluster_type == ClusterType.TenDBCluster: + spiders = cluster.proxyinstance_set.all() + dbctls = cluster.proxyinstance_set.filter( + tendbclusterspiderext__spider_role=TenDBClusterSpiderRole.SPIDER_MASTER + ) + instances.append( + { + "role": MachineType.SPIDER.value, + "addresses": [ + { + "ip": instance.machine.ip, + "port": instance.port, + "id": instance.id, + } + for instance in spiders + ], + } + ) + instances.append( + { + "role": TDBCTL_USER, + "addresses": [ + { + "ip": instance.machine.ip, + "port": instance.admin_port, + "id": instance.id, + } + for instance in dbctls + ], + }, + ) + return {"bk_cloud_id": cluster.bk_cloud_id, "cluster_type": cluster.cluster_type, "instances": instances} + + +def modify_periodic_task_run_every(run_every, func_name): + """修改定时任务的运行周期""" + model_schedule, model_field = ModelEntry.to_model_schedule(run_every) + # 不存在抛出错误 + db_task = DBPeriodicTask.objects.get(name__contains=func_name) + celery_task = db_task.task + setattr(celery_task, model_field, model_schedule) + celery_task.save(update_fields=[model_field]) + + +def get_periodic_task_run_every(func_name): + """获取定时任务的运行周期""" + db_task = DBPeriodicTask.objects.get(name__contains=func_name) + return db_task.task.crontab.schedule diff --git a/dbm-ui/backend/tests/mock_data/components/mysql_priv_manager.py b/dbm-ui/backend/tests/mock_data/components/mysql_priv_manager.py index 66a53370d3..0a9d4f9e40 100644 --- a/dbm-ui/backend/tests/mock_data/components/mysql_priv_manager.py +++ b/dbm-ui/backend/tests/mock_data/components/mysql_priv_manager.py @@ -85,3 +85,8 @@ def clone_instance(cls, *args, **kwargs): @raw_response def clone_client(cls, *args, **kwargs): return True + + @classmethod + @raw_response + def modify_user_password(cls, *args, **kwargs): + return True