Files
aiops/backend/internal/api/handlers/credential.go
T
attacker 24f955832e feat: 发布权限 RBAC + PipelineEditor 修复 + 菜单优化
- 新增 DeployRBAC 页面: Jenkins-style Item Role 权限管理
  - 正则匹配流水线 code,实时命中预览
  - 内置用户分配面板,无需跳转系统管理
  - 支持创建/编辑/删除/启用禁用 Item Role
- 修复 PipelineEditor 成员管理 http 双解包 bug
- 修复 PipelineManager 前端 code 路由迁移(delete/trigger/stop/loadRuns)
- 新增批量删除流水线运行记录 API
- Sidebar: 应用发布下新增「发布权限」, 仓库认证→发布配置, 移除用户组
- 恢复系统管理菜单(不受发布权限改动影响)
- 后端 ItemRole CRUD handler + 正则匹配中间件已就绪
2026-06-21 05:26:01 +00:00

401 lines
13 KiB
Go

package handlers
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"ezops/internal/models"
"ezops/pkg/crypto"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// CredentialHandler 凭据管理处理器 v2(多类型动态字段)
type CredentialHandler struct {
db *gorm.DB
crypt *crypto.SecretBox
}
func NewCredentialHandler(db *gorm.DB, masterKey string) *CredentialHandler {
return &CredentialHandler{db: db, crypt: crypto.NewSecretBox(masterKey)}
}
// ────── Helpers ──────
func genEnvPrefix() string {
b := make([]byte, 4)
rand.Read(b)
return hex.EncodeToString(b)[:8]
}
func buildContent(req models.CreateSecretRequest) (string, string, error) {
switch req.SecretType {
case "ssh":
data := map[string]interface{}{
"private_key": req.SSHPrivateKey,
"public_key": req.SSHPublicKey,
"ssh_user": req.SSHUser,
"ssh_port": req.SSHPort,
"password": req.SSHPassword,
}
b, _ := json.Marshal(data)
return string(b), fmt.Sprintf("SSH %s@%d", req.SSHUser, req.SSHPort), nil
case "docker":
data := map[string]string{
"server": req.DockerServer,
"username": req.DockerUsername,
"password": req.DockerPassword,
}
b, _ := json.Marshal(data)
return string(b), req.DockerServer, nil
case "tls":
data := map[string]string{"cert": req.TLSCert, "key": req.TLSKey, "ca": req.TLSCA}
b, _ := json.Marshal(data)
return string(b), "TLS 证书已配置", nil
case "aksk":
data := map[string]string{
"provider": req.AKSKProvider,
"access_key": req.AKSKAccessKey,
"secret_key": req.AKSKSecretKey,
"region": req.AKSKRegion,
}
b, _ := json.Marshal(data)
return string(b), req.AKSKProvider, nil
case "database":
data := map[string]interface{}{
"db_type": req.DBType,
"host": req.DBHost,
"port": req.DBPort,
"db_name": req.DBName,
"username": req.DBUsername,
"password": req.DBPassword,
}
b, _ := json.Marshal(data)
return string(b), fmt.Sprintf("%s://%s:%d/%s", req.DBType, req.DBHost, req.DBPort, req.DBName), nil
case "api":
data := map[string]string{"api_key": req.APIKey, "endpoint": req.APIEndpoint}
b, _ := json.Marshal(data)
return string(b), req.APIEndpoint, nil
case "generic":
data := map[string]string{"generic_key": req.GenericKey, "generic_value": req.GenericValue}
b, _ := json.Marshal(data)
return string(b), req.GenericKey, nil
}
return "{}", "", fmt.Errorf("unknown type: %s", req.SecretType)
}
// ────── List ──────
func (h *CredentialHandler) ListCredentials(c *gin.Context) {
var query models.SecretListQuery
c.ShouldBindQuery(&query)
db := h.db.Model(&models.Credential{})
if query.SecretType != "" {
db = db.Where("secret_type = ?", query.SecretType)
}
if query.Search != "" {
s := "%" + query.Search + "%"
db = db.Where("name LIKE ? OR description LIKE ? OR variable_name LIKE ?", s, s, s)
}
if query.Tags != "" {
for _, tag := range strings.Split(query.Tags, ",") {
tag = strings.TrimSpace(tag)
if tag != "" {
db = db.Where("tags LIKE ?", "%\""+tag+"\"%")
}
}
}
var total int64
db.Count(&total)
page := query.Page
if page < 1 { page = 1 }
pageSize := query.PageSize
if pageSize < 1 || pageSize > 100 { pageSize = 20 }
var creds []models.Credential
db.Order("updated_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&creds)
items := make([]models.SecretListResponse, 0, len(creds))
for _, cred := range creds {
tags := []string{}
if cred.Tags != "" {
json.Unmarshal([]byte(cred.Tags), &tags)
}
items = append(items, models.SecretListResponse{
ID: cred.ID,
Name: cred.Name,
VariableName: cred.VariableName,
SecretType: cred.SecretType,
Summary: cred.Summary,
Tags: tags,
Description: cred.Description,
ExpiresAt: cred.ExpiresAt,
IsActive: cred.IsActive,
EnvPrefix: cred.EnvPrefix,
CreatedAt: cred.CreatedAt,
UpdatedAt: cred.UpdatedAt,
})
}
c.JSON(http.StatusOK, models.Response{Code: 200, Data: models.ListResponse{
List: items, Total: total,
}})
}
// ────── Create ──────
func (h *CredentialHandler) CreateCredential(c *gin.Context) {
var req models.CreateSecretRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
return
}
plainContent, summary, err := buildContent(req)
if err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: err.Error()})
return
}
encrypted, err := h.crypt.Encrypt(plainContent)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "加密失败"})
return
}
tagsJSON, _ := json.Marshal(req.Tags)
isActive := true
if req.IsActive != nil { isActive = *req.IsActive }
cred := models.Credential{
Name: req.Name,
VariableName: req.VariableName,
SecretType: req.SecretType,
Summary: summary,
Content: encrypted,
Tags: string(tagsJSON),
Description: req.Description,
ExpiresAt: req.ExpiresAt,
IsActive: isActive,
EnvPrefix: genEnvPrefix(),
}
if err := h.db.Create(&cred).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "创建成功", Data: cred.ID})
}
// ────── Get Detail ──────
func (h *CredentialHandler) GetCredential(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
var cred models.Credential
if h.db.First(&cred, id).Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"})
return
}
// Decrypt content
plain, err := h.crypt.Decrypt(cred.Content)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "解密失败"})
return
}
var contentData map[string]interface{}
json.Unmarshal([]byte(plain), &contentData)
tags := []string{}
if cred.Tags != "" { json.Unmarshal([]byte(cred.Tags), &tags) }
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{
"id": cred.ID,
"name": cred.Name,
"variable_name": cred.VariableName,
"secret_type": cred.SecretType,
"content": contentData,
"tags": tags,
"description": cred.Description,
"expires_at": cred.ExpiresAt,
"is_active": cred.IsActive,
"env_prefix": cred.EnvPrefix,
"created_at": cred.CreatedAt,
"updated_at": cred.UpdatedAt,
}})
}
// ────── Update ──────
func (h *CredentialHandler) UpdateCredential(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
var cred models.Credential
if h.db.First(&cred, id).Error != nil {
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "凭据不存在"})
return
}
var req models.UpdateSecretRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
return
}
updates := map[string]interface{}{}
if req.Name != nil { updates["name"] = *req.Name }
if req.VariableName != nil { updates["variable_name"] = *req.VariableName }
if req.Description != nil { updates["description"] = *req.Description }
if req.ExpiresAt != nil { updates["expires_at"] = req.ExpiresAt }
if req.IsActive != nil { updates["is_active"] = *req.IsActive }
if len(req.Tags) > 0 {
b, _ := json.Marshal(req.Tags)
updates["tags"] = string(b)
}
// Rebuild content if any type-specific field changed
if hasTypeFieldUpdate(req) {
// Decrypt old content
oldPlain, _ := h.crypt.Decrypt(cred.Content)
var old map[string]interface{}
json.Unmarshal([]byte(oldPlain), &old)
if old == nil { old = map[string]interface{}{} }
mergeUpdate(req, old)
newPlain, _ := json.Marshal(old)
enc, err := h.crypt.Encrypt(string(newPlain))
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "加密失败"})
return
}
updates["content"] = enc
// Update summary
updates["summary"] = buildSummaryFromRaw(old, cred.SecretType)
}
if len(updates) > 0 {
h.db.Model(&cred).Updates(updates)
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"})
}
func hasTypeFieldUpdate(req models.UpdateSecretRequest) bool {
return req.SSHPrivateKey != nil || req.SSHPublicKey != nil || req.SSHUser != nil || req.SSHPort != nil || req.SSHPassword != nil ||
req.DockerServer != nil || req.DockerUsername != nil || req.DockerPassword != nil ||
req.TLSCert != nil || req.TLSKey != nil || req.TLSCA != nil ||
req.AKSKProvider != nil || req.AKSKAccessKey != nil || req.AKSKSecretKey != nil || req.AKSKRegion != nil ||
req.DBType != nil || req.DBHost != nil || req.DBPort != nil || req.DBName != nil || req.DBUsername != nil || req.DBPassword != nil ||
req.APIKey != nil || req.APIEndpoint != nil ||
req.GenericKey != nil || req.GenericValue != nil
}
func mergeUpdate(req models.UpdateSecretRequest, old map[string]interface{}) {
if req.SSHPrivateKey != nil { old["private_key"] = *req.SSHPrivateKey }
if req.SSHPublicKey != nil { old["public_key"] = *req.SSHPublicKey }
if req.SSHUser != nil { old["ssh_user"] = *req.SSHUser }
if req.SSHPort != nil { old["ssh_port"] = *req.SSHPort }
if req.SSHPassword != nil { old["password"] = *req.SSHPassword }
if req.DockerServer != nil { old["server"] = *req.DockerServer }
if req.DockerUsername != nil { old["username"] = *req.DockerUsername }
if req.DockerPassword != nil { old["password"] = *req.DockerPassword }
if req.TLSCert != nil { old["cert"] = *req.TLSCert }
if req.TLSKey != nil { old["key"] = *req.TLSKey }
if req.TLSCA != nil { old["ca"] = *req.TLSCA }
if req.AKSKProvider != nil { old["provider"] = *req.AKSKProvider }
if req.AKSKAccessKey != nil { old["access_key"] = *req.AKSKAccessKey }
if req.AKSKSecretKey != nil { old["secret_key"] = *req.AKSKSecretKey }
if req.AKSKRegion != nil { old["region"] = *req.AKSKRegion }
if req.DBType != nil { old["db_type"] = *req.DBType }
if req.DBHost != nil { old["host"] = *req.DBHost }
if req.DBPort != nil { old["port"] = *req.DBPort }
if req.DBName != nil { old["db_name"] = *req.DBName }
if req.DBUsername != nil { old["username"] = *req.DBUsername }
if req.DBPassword != nil { old["password"] = *req.DBPassword }
if req.APIKey != nil { old["api_key"] = *req.APIKey }
if req.APIEndpoint != nil { old["endpoint"] = *req.APIEndpoint }
if req.GenericKey != nil { old["generic_key"] = *req.GenericKey }
if req.GenericValue != nil { old["generic_value"] = *req.GenericValue }
}
func buildSummaryFromRaw(data map[string]interface{}, stype string) string {
switch stype {
case "ssh":
return fmt.Sprintf("SSH %v@%v", data["ssh_user"], data["ssh_port"])
case "docker":
return fmt.Sprintf("%v", data["server"])
case "aksk":
return fmt.Sprintf("%v", data["provider"])
case "database":
return fmt.Sprintf("%v://%v:%v/%v", data["db_type"], data["host"], data["port"], data["db_name"])
case "api":
return fmt.Sprintf("%v", data["endpoint"])
case "generic":
return fmt.Sprintf("%v", data["generic_key"])
case "tls":
return "TLS 证书已配置"
}
return ""
}
// ────── Patch (toggle is_active) ──────
func (h *CredentialHandler) PatchCredential(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
var body map[string]interface{}
c.ShouldBindJSON(&body)
updates := map[string]interface{}{}
if v, ok := body["is_active"]; ok {
updates["is_active"] = v
}
if len(updates) > 0 {
h.db.Model(&models.Credential{}).Where("id = ?", id).Updates(updates)
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "ok"})
}
// ────── Delete ──────
func (h *CredentialHandler) DeleteCredential(c *gin.Context) {
id, _ := strconv.ParseUint(c.Param("id"), 10, 32)
if h.db.Delete(&models.Credential{}, id).Error != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"})
}
// ────── Tags ──────
func (h *CredentialHandler) GetTags(c *gin.Context) {
var rows []string
h.db.Model(&models.Credential{}).Where("tags != ''").Pluck("tags", &rows)
tagSet := map[string]bool{}
for _, r := range rows {
var tags []string
if json.Unmarshal([]byte(r), &tags) == nil {
for _, t := range tags { tagSet[t] = true }
}
}
tags := make([]string, 0, len(tagSet))
for t := range tagSet { tags = append(tags, t) }
c.JSON(http.StatusOK, models.Response{Code: 200, Data: tags})
}