package controller import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "strconv" "strings" "ezops/model" "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 model.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 model.SecretListQuery c.ShouldBindQuery(&query) db := h.db.Model(&model.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 []model.Credential db.Order("updated_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&creds) items := make([]model.SecretListResponse, 0, len(creds)) for _, cred := range creds { tags := []string{} if cred.Tags != "" { json.Unmarshal([]byte(cred.Tags), &tags) } items = append(items, model.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, model.Response{Code: 200, Data: model.ListResponse{ List: items, Total: total, }}) } // ────── Create ────── func (h *CredentialHandler) CreateCredential(c *gin.Context) { var req model.CreateSecretRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()}) return } plainContent, summary, err := buildContent(req) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } encrypted, err := h.crypt.Encrypt(plainContent) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "加密失败"}) return } tagsJSON, _ := json.Marshal(req.Tags) isActive := true if req.IsActive != nil { isActive = *req.IsActive } cred := model.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, model.Response{Code: 500, Message: "创建失败: " + err.Error()}) return } c.JSON(http.StatusOK, model.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 model.Credential if h.db.First(&cred, id).Error != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "凭据不存在"}) return } // Decrypt content plain, err := h.crypt.Decrypt(cred.Content) if err != nil { c.JSON(http.StatusInternalServerError, model.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, model.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 model.Credential if h.db.First(&cred, id).Error != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "凭据不存在"}) return } var req model.UpdateSecretRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.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, model.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, model.Response{Code: 200, Message: "更新成功"}) } func hasTypeFieldUpdate(req model.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 model.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(&model.Credential{}).Where("id = ?", id).Updates(updates) } c.JSON(http.StatusOK, model.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(&model.Credential{}, id).Error != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } // ────── Tags ────── func (h *CredentialHandler) GetTags(c *gin.Context) { var rows []string h.db.Model(&model.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, model.Response{Code: 200, Data: tags}) }