feat(ai-config): cifra la API key en reposo y la acota por tenant
Prepara el terreno para que el cliente cargue su propia cuenta de IA sin que su clave quede en texto plano ni que el selector le muestre las de los demás. - ClaveEnClaro() descifra con fallback a texto plano: las filas viejas se leen igual y quedan cifradas al primer guardado, sin script ni downtime. utils.Decrypt hace panic (no devuelve error) con entrada que no es un ciphertext válido, así que el fallback va sobre recover — eso mismo es el mecanismo de detección de "todavía está en claro". - Migrados TODOS los lectores: uMind (chat y embeddings), bot de Telegram, Whisper, Query Runner, streaming de IA y Landing Generator. Un lector sin migrar mandaría el ciphertext como API key. - AiConfig gana TenantID (null = global del staff) y GetAiConfigSelectPorTenants para acotar el selector. - Test de los 4 casos del fallback, incluido hex válido que no descifra. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c69b904b03
commit
96cd24f17d
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -29,10 +30,52 @@ type AiConfig struct {
|
||||
// Solo debe haber una config activa como agente a la vez.
|
||||
EsAgenteBot bool `gorm:"default:false" json:"es_agente_bot"`
|
||||
TelegramConfigID *uint `gorm:"index" json:"telegram_config_id"`
|
||||
// TenantID acota la config a un tenant de uMind: null = config global del
|
||||
// staff (el comportamiento histórico). Sin esto, el selector de IA le
|
||||
// mostraría a cada cliente las claves de todos los demás.
|
||||
TenantID *uint `gorm:"index" json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (AiConfig) TableName() string { return "ai_configs" }
|
||||
|
||||
// ClaveEnClaro devuelve la API key lista para usar. Las filas guardadas antes
|
||||
// de que se cifrara este campo están en texto plano y se devuelven tal cual;
|
||||
// al volver a guardarlas quedan cifradas, así que el parque se migra solo sin
|
||||
// script ni downtime.
|
||||
//
|
||||
// utils.Decrypt hace panic con entrada que no sea un ciphertext válido (no
|
||||
// devuelve error), de ahí el recover: es el mecanismo de detección de "esto
|
||||
// todavía está en texto plano".
|
||||
func (c *AiConfig) ClaveEnClaro() string {
|
||||
if c.ApiKey == "" || app.Http.Server.Key == "" {
|
||||
return c.ApiKey
|
||||
}
|
||||
return descifrarOTalCual(c.ApiKey)
|
||||
}
|
||||
|
||||
func descifrarOTalCual(valor string) (out string) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
out = valor
|
||||
}
|
||||
}()
|
||||
claro := utils.Decrypt(valor, app.Http.Server.Key)
|
||||
if claro == "" {
|
||||
return valor
|
||||
}
|
||||
return claro
|
||||
}
|
||||
|
||||
// CifrarClaveAi cifra una API key para guardarla. Si no hay APP_KEY
|
||||
// configurada devuelve el valor tal cual — preferible a romper el guardado en
|
||||
// un entorno sin la clave, y ClaveEnClaro lo lee igual.
|
||||
func CifrarClaveAi(clave string) string {
|
||||
if clave == "" || app.Http.Server.Key == "" {
|
||||
return clave
|
||||
}
|
||||
return utils.Encrypt(clave, app.Http.Server.Key)
|
||||
}
|
||||
|
||||
func GetAllAiConfigs(limit, offset int, search string) ([]AiConfig, int64, error) {
|
||||
var items []AiConfig
|
||||
var total int64
|
||||
@@ -111,6 +154,23 @@ func GetAiConfigSelect() ([]AiConfig, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetAiConfigSelectPorTenants acota el selector a las configs propias de esos
|
||||
// tenants más las globales del staff (tenant_id IS NULL), que son las que se
|
||||
// ofrecen a todos. Sin este filtro el cliente vería las claves de los demás.
|
||||
func GetAiConfigSelectPorTenants(tenantIDs []uint) ([]AiConfig, error) {
|
||||
var items []AiConfig
|
||||
db := app.Http.Database.DB.Model(&AiConfig{}).Select("id, nombre, provider, tenant_id")
|
||||
if len(tenantIDs) == 0 {
|
||||
db = db.Where("tenant_id IS NULL")
|
||||
} else {
|
||||
db = db.Where("tenant_id IS NULL OR tenant_id IN ?", tenantIDs)
|
||||
}
|
||||
if err := db.Order("nombre ASC").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetAgenteBotConfig retorna la config marcada como agente Telegram, con su TelegramConfig cargada.
|
||||
func GetAgenteBotConfig() (*AiConfig, *TelegramConfig, error) {
|
||||
var ai AiConfig
|
||||
|
||||
Reference in New Issue
Block a user