299 lines
12 KiB
Go
299 lines
12 KiB
Go
package sys
|
|
|
|
import (
|
|
"context"
|
|
"hotgo/internal/consts"
|
|
"hotgo/internal/dao"
|
|
"hotgo/internal/library/contexts"
|
|
"hotgo/internal/library/immessagecrypto"
|
|
"hotgo/internal/library/imtenant"
|
|
"hotgo/internal/model/entity"
|
|
"hotgo/internal/model/input/sysin"
|
|
"hotgo/internal/service"
|
|
"strings"
|
|
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
)
|
|
|
|
const (
|
|
imMessageEncryptionKeyTable = "im_message_encryption_keys"
|
|
imTenantMessageEncryptionColumn = "message_encryption_enabled"
|
|
)
|
|
|
|
type sSysImMessageEncryption struct{}
|
|
|
|
func NewSysImMessageEncryption() *sSysImMessageEncryption { return &sSysImMessageEncryption{} }
|
|
|
|
func init() {
|
|
service.RegisterSysImMessageEncryption(NewSysImMessageEncryption())
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) List(ctx context.Context, in *sysin.ImMessageEncryptionListInp) (list []*sysin.ImMessageEncryptionListModel, total int, err error) {
|
|
if err = in.Filter(ctx); err != nil {
|
|
return
|
|
}
|
|
if err = s.requireSuperAdmin(ctx); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
db := imtenant.RegistryDB()
|
|
mod := db.Model(dao.ImTenant.Table()+" t").Safe().Ctx(ctx).
|
|
LeftJoin(dao.ImTenantConfig.Table()+" c", "c.tenant_id=t.id").
|
|
Fields(
|
|
"t.id tenant_id", "t.code tenant_code", "t.name tenant_name",
|
|
"COALESCE(c."+imTenantMessageEncryptionColumn+",0) message_encryption_enabled",
|
|
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='active'),0) active_version",
|
|
"COALESCE((SELECT MAX(k.key_version) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id AND k.status='pending'),0) pending_version",
|
|
"(SELECT COUNT(1) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) key_count",
|
|
"(SELECT MAX(COALESCE(k.activated_at,k.created_at)) FROM "+imMessageEncryptionKeyTable+" k WHERE k.tenant_id=t.id) last_rotated_at",
|
|
)
|
|
if in.TenantId > 0 {
|
|
mod = mod.Where("t.id", in.TenantId)
|
|
}
|
|
if keyword := strings.TrimSpace(in.Keyword); keyword != "" {
|
|
mod = mod.Where("t.code LIKE ? OR t.name LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
|
}
|
|
if in.Enabled != nil {
|
|
mod = mod.Where("COALESCE(c."+imTenantMessageEncryptionColumn+",0)=?", *in.Enabled)
|
|
}
|
|
if err = mod.Page(in.Page, in.PerPage).OrderAsc("t.id").ScanAndCount(&list, &total, false); err != nil {
|
|
return nil, 0, gerror.Wrap(err, "获取商户消息加密配置失败")
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) Initialize(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
|
|
if err := in.Filter(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireSuperAdmin(ctx); err != nil {
|
|
return err
|
|
}
|
|
tenant, err := s.tenantByID(ctx, in.TenantId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dek, err := immessagecrypto.GenerateDEK()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ciphertext, nonce, err := immessagecrypto.WrapDEK(ctx, tenant.Id, 1, dek)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
count, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id).Count()
|
|
if queryErr != nil {
|
|
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
|
}
|
|
if count > 0 {
|
|
return gerror.New("该商户已经初始化消息密钥")
|
|
}
|
|
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
|
|
"tenant_id": tenant.Id, "key_version": 1, "algorithm": consts.ImMessageKeyAlgorithm,
|
|
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusActive,
|
|
"created_at": gtime.Now(), "activated_at": gtime.Now(), "created_by": contexts.GetUserId(ctx),
|
|
"remark": strings.TrimSpace(in.Remark),
|
|
}).Insert()
|
|
if insertErr != nil {
|
|
return gerror.Wrap(insertErr, "初始化商户消息密钥失败")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_initialize", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": 1})
|
|
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", 1)
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) Rotate(ctx context.Context, in *sysin.ImMessageEncryptionActionInp) error {
|
|
if err := in.Filter(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireSuperAdmin(ctx); err != nil {
|
|
return err
|
|
}
|
|
tenant, err := s.tenantByID(ctx, in.TenantId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var version uint
|
|
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
pending, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
|
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusPending).Count()
|
|
if queryErr != nil {
|
|
return gerror.Wrap(queryErr, "检查待激活消息密钥失败")
|
|
}
|
|
if pending > 0 {
|
|
return gerror.New("该商户已有待激活密钥,请等待IM Server确认")
|
|
}
|
|
value, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
|
Fields("COALESCE(MAX(key_version),0)").Where("tenant_id", tenant.Id).Value()
|
|
if queryErr != nil {
|
|
return gerror.Wrap(queryErr, "获取消息密钥版本失败")
|
|
}
|
|
if value.Uint() == 0 {
|
|
return gerror.New("请先初始化商户消息密钥")
|
|
}
|
|
version = value.Uint() + 1
|
|
dek, generateErr := immessagecrypto.GenerateDEK()
|
|
if generateErr != nil {
|
|
return generateErr
|
|
}
|
|
ciphertext, nonce, wrapErr := immessagecrypto.WrapDEK(ctx, tenant.Id, version, dek)
|
|
if wrapErr != nil {
|
|
return wrapErr
|
|
}
|
|
_, insertErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Data(g.Map{
|
|
"tenant_id": tenant.Id, "key_version": version, "algorithm": consts.ImMessageKeyAlgorithm,
|
|
"encrypted_dek": ciphertext, "dek_nonce": nonce, "status": consts.ImMessageKeyStatusPending,
|
|
"created_at": gtime.Now(), "created_by": contexts.GetUserId(ctx), "remark": strings.TrimSpace(in.Remark),
|
|
}).Insert()
|
|
if insertErr != nil {
|
|
return gerror.Wrap(insertErr, "创建待激活消息密钥失败")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = writeImAdminAudit(ctx, tenant.Id, "message_key_rotate", imMessageEncryptionKeyTable, 0, "success", g.Map{"version": version, "status": consts.ImMessageKeyStatusPending})
|
|
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.updated", "message_encryption", version)
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) Switch(ctx context.Context, in *sysin.ImMessageEncryptionSwitchInp) error {
|
|
if err := in.Filter(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := s.requireSuperAdmin(ctx); err != nil {
|
|
return err
|
|
}
|
|
tenant, err := s.tenantByID(ctx, in.TenantId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if in.Enabled == 1 {
|
|
count, queryErr := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
|
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).Count()
|
|
if queryErr != nil {
|
|
return gerror.Wrap(queryErr, "检查商户消息密钥失败")
|
|
}
|
|
if count == 0 {
|
|
return gerror.New("启用消息加密前必须先初始化有效密钥")
|
|
}
|
|
}
|
|
result, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
|
Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).
|
|
Data(g.Map{imTenantMessageEncryptionColumn: in.Enabled, dao.ImTenantConfig.Columns().UpdatedBy: contexts.GetUserId(ctx)}).Update()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "更新商户消息加密开关失败")
|
|
}
|
|
if affected, _ := result.RowsAffected(); affected == 0 {
|
|
return gerror.New("商户配置不存在")
|
|
}
|
|
_ = writeImAdminAudit(ctx, tenant.Id, "message_encryption_switch", "im_tenant_config", tenant.Id, "success", g.Map{"enabled": in.Enabled})
|
|
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "tenant.features.updated", "message_encryption", 0)
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) GetKey(ctx context.Context, in *sysin.ImMessageKeyGetInp) (*sysin.ImMessageKeyModel, error) {
|
|
tenant, err := s.tenantByCode(ctx, in.TenantCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
enabledValue, err := imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx).
|
|
Fields(imTenantMessageEncryptionColumn).Where(dao.ImTenantConfig.Columns().TenantId, tenant.Id).Value()
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "获取商户消息加密开关失败")
|
|
}
|
|
mod := imtenant.RegistryDB().Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).Where("tenant_id", tenant.Id)
|
|
if in.Version > 0 {
|
|
mod = mod.Where("key_version", in.Version).WhereIn("status", []string{consts.ImMessageKeyStatusPending, consts.ImMessageKeyStatusActive, consts.ImMessageKeyStatusRetired})
|
|
} else {
|
|
mod = mod.Where("status", consts.ImMessageKeyStatusActive).OrderDesc("key_version")
|
|
}
|
|
row, err := mod.One()
|
|
if err != nil {
|
|
return nil, gerror.Wrap(err, "获取商户消息密钥失败")
|
|
}
|
|
if row.IsEmpty() {
|
|
return nil, gerror.New("商户消息密钥不存在")
|
|
}
|
|
version := row["key_version"].Uint()
|
|
dek, err := immessagecrypto.UnwrapDEK(ctx, tenant.Id, version, row["encrypted_dek"].Bytes(), row["dek_nonce"].Bytes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &sysin.ImMessageKeyModel{
|
|
TenantId: tenant.Id, TenantCode: tenant.Code, Enabled: enabledValue.Int() == 1,
|
|
Version: version, Algorithm: row["algorithm"].String(), Status: row["status"].String(), Key: immessagecrypto.EncodeDEK(dek),
|
|
}, nil
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) Ack(ctx context.Context, in *sysin.ImMessageKeyAckInp) error {
|
|
tenant, err := s.tenantByCode(ctx, in.TenantCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if in.Version == 0 {
|
|
return gerror.New("消息密钥版本不能为空")
|
|
}
|
|
err = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
row, queryErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
|
Where("tenant_id", tenant.Id).Where("key_version", in.Version).One()
|
|
if queryErr != nil {
|
|
return gerror.Wrap(queryErr, "获取待激活消息密钥失败")
|
|
}
|
|
if row.IsEmpty() || row["status"].String() != consts.ImMessageKeyStatusPending {
|
|
return gerror.New("待激活消息密钥不存在")
|
|
}
|
|
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
|
Where("tenant_id", tenant.Id).Where("status", consts.ImMessageKeyStatusActive).
|
|
Data(g.Map{"status": consts.ImMessageKeyStatusRetired, "retired_at": gtime.Now()}).Update(); updateErr != nil {
|
|
return gerror.Wrap(updateErr, "退役旧消息密钥失败")
|
|
}
|
|
if _, updateErr := tx.Model(imMessageEncryptionKeyTable).Safe().Ctx(ctx).
|
|
Where("tenant_id", tenant.Id).Where("key_version", in.Version).
|
|
Data(g.Map{"status": consts.ImMessageKeyStatusActive, "activated_at": gtime.Now()}).Update(); updateErr != nil {
|
|
return gerror.Wrap(updateErr, "激活新消息密钥失败")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return publishImPolicyEvent(ctx, tenant.Id, tenant.Code, "message.keys.activated", "message_encryption", in.Version)
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) requireSuperAdmin(ctx context.Context) error {
|
|
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
|
return gerror.New("只有最高权限平台管理员可以管理消息密钥")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) tenantByID(ctx context.Context, tenantID int64) (*entity.ImTenant, error) {
|
|
var tenant *entity.ImTenant
|
|
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantID).Scan(&tenant); err != nil {
|
|
return nil, gerror.Wrap(err, "获取商户失败")
|
|
}
|
|
if tenant == nil || tenant.Id == 0 {
|
|
return nil, gerror.New("商户不存在")
|
|
}
|
|
return tenant, nil
|
|
}
|
|
|
|
func (s *sSysImMessageEncryption) tenantByCode(ctx context.Context, code string) (*entity.ImTenant, error) {
|
|
var tenant *entity.ImTenant
|
|
if err := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
|
Where(dao.ImTenant.Columns().Code, strings.TrimSpace(code)).Scan(&tenant); err != nil {
|
|
return nil, gerror.Wrap(err, "获取商户失败")
|
|
}
|
|
if tenant == nil || tenant.Id == 0 {
|
|
return nil, gerror.New("商户不存在")
|
|
}
|
|
return tenant, nil
|
|
}
|