You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
405 lines
13 KiB
405 lines
13 KiB
package console
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
cryptorand "crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/sys/postgres"
|
|
"yunyan/pb"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ============================ 依赖服务配置域 (svcconfig) ============================
|
|
//
|
|
// 移植自 deep_server modules/console/api_svcconfig.go,适配 yunyan-sas 的 postgres 封装与本地 struct。
|
|
// 数据存 console 主库(postgres.GetSys()),与 globalconfig/mcp/会议模板同库。
|
|
// 服务字段(fields)用 jsonb 列(serializer:json)存储;加密字段值 AES-256-GCM 加密落库,
|
|
// 返回前端时脱敏为 "[encrypted]"。区域覆盖:默认值 + 各区域覆盖,缺失字段继承默认值。
|
|
|
|
// ============================== AES-256-GCM 字段加密 ==============================
|
|
|
|
// svcEncrypt 用 AES-256-GCM 加密明文,返回 base64(nonce‖ciphertext)。空明文返回空串。
|
|
func svcEncrypt(key, plaintext string) (string, error) {
|
|
if plaintext == "" {
|
|
return "", nil
|
|
}
|
|
block, err := aes.NewCipher(normAESKey(key))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err = io.ReadFull(cryptorand.Reader, nonce); err != nil {
|
|
return "", err
|
|
}
|
|
ct := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
|
return base64.StdEncoding.EncodeToString(ct), nil
|
|
}
|
|
|
|
// svcDecrypt 解密 svcEncrypt 产出的密文,返回明文。供业务侧内部调用(不暴露给 console 前端)。
|
|
func svcDecrypt(key, ciphertext string) (string, error) {
|
|
if ciphertext == "" {
|
|
return "", nil
|
|
}
|
|
raw, err := base64.StdEncoding.DecodeString(ciphertext)
|
|
if err != nil {
|
|
return "", fmt.Errorf("base64 解码失败: %w", err)
|
|
}
|
|
block, err := aes.NewCipher(normAESKey(key))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ns := gcm.NonceSize()
|
|
if len(raw) < ns {
|
|
return "", fmt.Errorf("密文过短")
|
|
}
|
|
plain, err := gcm.Open(nil, raw[:ns], raw[ns:], nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("GCM 解密失败: %w", err)
|
|
}
|
|
return string(plain), nil
|
|
}
|
|
|
|
// normAESKey 把任意字符串规整为 32 字节 AES-256 密钥(不足补零,超出截断)。
|
|
func normAESKey(key string) []byte {
|
|
k := make([]byte, 32)
|
|
copy(k, []byte(key))
|
|
return k
|
|
}
|
|
|
|
// ============================== 脱敏工具 ==============================
|
|
|
|
// maskSvcFields 把返回给前端的字段列表中 encrypted=true 的 def_value 替换为 "[encrypted]"。
|
|
func maskSvcFields(fields []SvcField) {
|
|
for i := range fields {
|
|
if fields[i].Encrypted && fields[i].DefValue != "" {
|
|
fields[i].DefValue = "[encrypted]"
|
|
}
|
|
}
|
|
}
|
|
|
|
// encryptFieldsSlice 对字段列表中 encrypted=true 的字段做加密处理:
|
|
// oldMap 不为 nil 时,空值/占位符保留 oldMap[key] 密文;oldMap 为 nil 时直接加密非空值。
|
|
func encryptFieldsSlice(key string, fields []SvcField, oldMap map[string]string) error {
|
|
for i := range fields {
|
|
if !fields[i].Encrypted {
|
|
continue
|
|
}
|
|
if fields[i].DefValue == "" || fields[i].DefValue == "[encrypted]" {
|
|
if oldMap != nil {
|
|
fields[i].DefValue = oldMap[fields[i].Key]
|
|
} else {
|
|
fields[i].DefValue = ""
|
|
}
|
|
continue
|
|
}
|
|
ct, err := svcEncrypt(key, fields[i].DefValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fields[i].DefValue = ct
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ============================== 依赖服务配置 CRUD ==============================
|
|
|
|
// getSvcConfigs 列出全部依赖服务,加密字段值脱敏。
|
|
func (this *serverComp) getSvcConfigs(c *gin.Context) {
|
|
items := make([]*ThirdSvcConfig, 0)
|
|
if err := postgres.Find(comm.TableThirdSvcConfig, &items, ""); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
for _, item := range items {
|
|
maskSvcFields(item.Fields)
|
|
}
|
|
writeOK(c, gin.H{"items": items})
|
|
}
|
|
|
|
// addSvcConfig 新增依赖服务。encrypted=true 的字段值加密存储。
|
|
func (this *serverComp) addSvcConfig(c *gin.Context) {
|
|
var m ThirdSvcConfig
|
|
if err := c.ShouldBindJSON(&m); err != nil {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(m.Id) == "" || strings.TrimSpace(m.Name) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "服务ID 和 名称 必填")
|
|
return
|
|
}
|
|
// 主键重复检查
|
|
exist := &ThirdSvcConfig{}
|
|
if err := postgres.FindOne(comm.TableThirdSvcConfig, exist, "id=?", m.Id); err == nil {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "服务ID 已存在: "+m.Id)
|
|
return
|
|
}
|
|
// 加密字段
|
|
if err := encryptFieldsSlice(this.options.EncryptKey, m.Fields, nil); err != nil {
|
|
writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error())
|
|
return
|
|
}
|
|
now := time.Now().UnixMilli()
|
|
m.Createtime = now
|
|
m.Updatetime = now
|
|
if err := postgres.Insert(comm.TableThirdSvcConfig, &m); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
broadcastConfigChanged(comm.ConfigKindThirdSvc, "add", 0)
|
|
maskSvcFields(m.Fields)
|
|
writeOK(c, &m)
|
|
}
|
|
|
|
// updateSvcConfig 更新依赖服务。encrypted 字段:传 "" 或 "[encrypted]" 保留原密文,否则重新加密。
|
|
func (this *serverComp) updateSvcConfig(c *gin.Context) {
|
|
var m ThirdSvcConfig
|
|
if err := c.ShouldBindJSON(&m); err != nil {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(m.Id) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "服务ID 必填")
|
|
return
|
|
}
|
|
// 读旧记录,取已存储的加密字段密文
|
|
old := &ThirdSvcConfig{}
|
|
if err := postgres.FindOne(comm.TableThirdSvcConfig, old, "id=?", m.Id); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+m.Id)
|
|
return
|
|
}
|
|
oldMap := make(map[string]string, len(old.Fields))
|
|
for _, f := range old.Fields {
|
|
oldMap[f.Key] = f.DefValue
|
|
}
|
|
if err := encryptFieldsSlice(this.options.EncryptKey, m.Fields, oldMap); err != nil {
|
|
writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error())
|
|
return
|
|
}
|
|
m.Createtime = old.Createtime
|
|
m.Updatetime = time.Now().UnixMilli()
|
|
if err := postgres.Save(comm.TableThirdSvcConfig, &m); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
broadcastConfigChanged(comm.ConfigKindThirdSvc, "update", 0)
|
|
maskSvcFields(m.Fields)
|
|
writeOK(c, &m)
|
|
}
|
|
|
|
// delSvcConfig 删除服务及其所有区域覆盖。
|
|
func (this *serverComp) delSvcConfig(c *gin.Context) {
|
|
var req struct {
|
|
Id string `json:"id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Id) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填")
|
|
return
|
|
}
|
|
if err := postgres.Delete(comm.TableThirdSvcConfig, "id=?", req.Id); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
_ = postgres.Delete(comm.TableThirdSvcRegionOverride, "svc_id=?", req.Id)
|
|
broadcastConfigChanged(comm.ConfigKindThirdSvc, "delete", 0)
|
|
writeOK(c, gin.H{"id": req.Id})
|
|
}
|
|
|
|
// ============================== 区域字段覆盖 ==============================
|
|
|
|
// svcRegionFieldView 是返回给前端的合并视图(字段定义 + 有效值 + 是否被覆盖)。
|
|
type svcRegionFieldView struct {
|
|
Key string `json:"key"`
|
|
Description string `json:"description"`
|
|
Encrypted bool `json:"encrypted"`
|
|
IsOverridden bool `json:"is_overridden"`
|
|
Value string `json:"value"` // 加密字段显示 [encrypted]
|
|
}
|
|
|
|
// getSvcRegionOverride 返回某服务在指定区域的字段有效值(覆盖优先,否则默认值,加密字段脱敏)。
|
|
func (this *serverComp) getSvcRegionOverride(c *gin.Context) {
|
|
var req struct {
|
|
SvcId string `json:"svc_id"`
|
|
Region int32 `json:"region"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.SvcId == "" || req.Region == 0 {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 和 region 必填")
|
|
return
|
|
}
|
|
cfg := &ThirdSvcConfig{}
|
|
if err := postgres.FindOne(comm.TableThirdSvcConfig, cfg, "id=?", req.SvcId); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+req.SvcId)
|
|
return
|
|
}
|
|
ovr := &SvcRegionOverride{}
|
|
_ = postgres.FindOne(comm.TableThirdSvcRegionOverride, ovr, "svc_id=? AND region=?", req.SvcId, req.Region)
|
|
if ovr.Overrides == nil {
|
|
ovr.Overrides = make(map[string]string)
|
|
}
|
|
views := make([]*svcRegionFieldView, 0, len(cfg.Fields))
|
|
for _, f := range cfg.Fields {
|
|
val, overridden := ovr.Overrides[f.Key]
|
|
if !overridden {
|
|
val = f.DefValue
|
|
}
|
|
if f.Encrypted && val != "" {
|
|
val = "[encrypted]"
|
|
}
|
|
views = append(views, &svcRegionFieldView{
|
|
Key: f.Key, Description: f.Description,
|
|
Encrypted: f.Encrypted, IsOverridden: overridden, Value: val,
|
|
})
|
|
}
|
|
writeOK(c, gin.H{"svc_id": req.SvcId, "region": req.Region, "override_id": ovr.Id, "fields": views})
|
|
}
|
|
|
|
// saveSvcRegionOverride 保存(upsert)某服务在指定区域的字段覆盖。
|
|
// overrides map 中加密字段:空值/占位符保留原密文,非空重新加密;非加密字段直接存。
|
|
// 空值表示继承(不写入 newOverrides,即删除该字段覆盖)。
|
|
func (this *serverComp) saveSvcRegionOverride(c *gin.Context) {
|
|
var req struct {
|
|
SvcId string `json:"svc_id"`
|
|
Region int32 `json:"region"`
|
|
Overrides map[string]string `json:"overrides"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.SvcId == "" || req.Region == 0 {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 和 region 必填")
|
|
return
|
|
}
|
|
if req.Overrides == nil {
|
|
req.Overrides = make(map[string]string)
|
|
}
|
|
// 读服务字段定义,确定哪些字段需要加密
|
|
cfg := &ThirdSvcConfig{}
|
|
if err := postgres.FindOne(comm.TableThirdSvcConfig, cfg, "id=?", req.SvcId); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+req.SvcId)
|
|
return
|
|
}
|
|
encFields := make(map[string]bool, len(cfg.Fields))
|
|
for _, f := range cfg.Fields {
|
|
if f.Encrypted {
|
|
encFields[f.Key] = true
|
|
}
|
|
}
|
|
// 读旧覆盖,用于保留旧密文
|
|
old := &SvcRegionOverride{}
|
|
_ = postgres.FindOne(comm.TableThirdSvcRegionOverride, old, "svc_id=? AND region=?", req.SvcId, req.Region)
|
|
if old.Overrides == nil {
|
|
old.Overrides = make(map[string]string)
|
|
}
|
|
key := this.options.EncryptKey
|
|
newOverrides := make(map[string]string, len(req.Overrides))
|
|
for k, v := range req.Overrides {
|
|
if encFields[k] {
|
|
if v == "" || v == "[encrypted]" {
|
|
// 加密字段留空/占位:保留旧密文(若有),否则视为继承(不写入)
|
|
if oldVal, ok := old.Overrides[k]; ok {
|
|
newOverrides[k] = oldVal
|
|
}
|
|
} else {
|
|
ct, err := svcEncrypt(key, v)
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_SystemError, "字段加密失败: "+err.Error())
|
|
return
|
|
}
|
|
newOverrides[k] = ct
|
|
}
|
|
} else {
|
|
// 非加密字段:空值表示继承/删除覆盖,不写入 newOverrides
|
|
if v != "" {
|
|
newOverrides[k] = v
|
|
}
|
|
}
|
|
}
|
|
ovr := &SvcRegionOverride{
|
|
Id: old.Id, SvcId: req.SvcId,
|
|
Region: req.Region, Overrides: newOverrides,
|
|
Updatetime: time.Now().UnixMilli(),
|
|
}
|
|
var err error
|
|
if ovr.Id == 0 {
|
|
err = postgres.Insert(comm.TableThirdSvcRegionOverride, ovr)
|
|
} else {
|
|
err = postgres.Save(comm.TableThirdSvcRegionOverride, ovr)
|
|
}
|
|
if err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
broadcastConfigChanged(comm.ConfigKindThirdSvc, "update", req.Region)
|
|
writeOK(c, gin.H{"ok": true, "override_id": ovr.Id})
|
|
}
|
|
|
|
// syncSvcToRegions 把某服务的字段值(来自默认或指定区域覆盖)批量推送到多个目标区域。
|
|
// src_region=0 时使用字段 def_value;>0 时使用该区域的覆盖值。
|
|
func (this *serverComp) syncSvcToRegions(c *gin.Context) {
|
|
var req struct {
|
|
SvcId string `json:"svc_id"`
|
|
SrcRegion int32 `json:"src_region"`
|
|
TargetRegions []int32 `json:"target_regions"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.SvcId == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 必填")
|
|
return
|
|
}
|
|
if len(req.TargetRegions) == 0 {
|
|
writeOK(c, gin.H{"synced": 0})
|
|
return
|
|
}
|
|
// 确定同步源 overrides map
|
|
var srcOverrides map[string]string
|
|
if req.SrcRegion == 0 {
|
|
cfg := &ThirdSvcConfig{}
|
|
if err := postgres.FindOne(comm.TableThirdSvcConfig, cfg, "id=?", req.SvcId); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "服务不存在: "+req.SvcId)
|
|
return
|
|
}
|
|
srcOverrides = make(map[string]string, len(cfg.Fields))
|
|
for _, f := range cfg.Fields {
|
|
srcOverrides[f.Key] = f.DefValue
|
|
}
|
|
} else {
|
|
src := &SvcRegionOverride{}
|
|
if err := postgres.FindOne(comm.TableThirdSvcRegionOverride, src, "svc_id=? AND region=?", req.SvcId, req.SrcRegion); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "源区域覆盖不存在")
|
|
return
|
|
}
|
|
srcOverrides = src.Overrides
|
|
}
|
|
now := time.Now().UnixMilli()
|
|
synced := 0
|
|
for _, tgt := range req.TargetRegions {
|
|
old := &SvcRegionOverride{}
|
|
_ = postgres.FindOne(comm.TableThirdSvcRegionOverride, old, "svc_id=? AND region=?", req.SvcId, tgt)
|
|
ovr := &SvcRegionOverride{
|
|
Id: old.Id, SvcId: req.SvcId,
|
|
Region: tgt, Overrides: srcOverrides, Updatetime: now,
|
|
}
|
|
var err error
|
|
if ovr.Id == 0 {
|
|
err = postgres.Insert(comm.TableThirdSvcRegionOverride, ovr)
|
|
} else {
|
|
err = postgres.Save(comm.TableThirdSvcRegionOverride, ovr)
|
|
}
|
|
if err == nil {
|
|
synced++
|
|
broadcastConfigChanged(comm.ConfigKindThirdSvc, "update", tgt)
|
|
}
|
|
}
|
|
writeOK(c, gin.H{"synced": synced})
|
|
}
|
|
|