kra-new/internal/initialize/configuration.go

365 lines
12 KiB
Go

package initialize
import (
"context"
"encoding/json"
"kra/internal/conf"
"kra/pkg/protoutil"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
)
type configurationEnvelope struct {
Data json.RawMessage `json:"data"`
Admin json.RawMessage `json:"admin"`
Email *struct {
To string `json:"to"`
From string `json:"from"`
Host string `json:"host"`
Secret string `json:"secret"`
Nickname string `json:"nickname"`
Port int32 `json:"port"`
IsSSL bool `json:"is-ssl"`
IsLoginAuth bool `json:"is-loginauth"`
} `json:"email"`
}
func (r *Repo) ConfigurationJSON() (json.RawMessage, error) {
dataConfig, adminConfig := r.backend.RuntimeValues()
admin := map[string]any{"routerPrefix": ""}
email := map[string]any{}
if adminConfig != nil {
admin["routerPrefix"] = adminConfig.RouterPrefix
if adminConfig.System != nil {
admin["system"] = map[string]any{
"useRedis": adminConfig.System.UseRedis,
"useMultipoint": adminConfig.System.UseMultipoint,
"useStrictAuth": adminConfig.System.UseStrictAuth,
"disableAutoMigrate": adminConfig.System.DisableAutoMigrate,
"useMongo": adminConfig.System.UseMongo,
"addr": adminConfig.System.Addr,
"iplimitCount": adminConfig.System.IplimitCount,
"iplimitTime": adminConfig.System.IplimitTime,
}
}
if adminConfig.Jwt != nil {
admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer}
}
if adminConfig.Captcha != nil {
admin["captcha"] = map[string]any{"keyLong": adminConfig.Captcha.KeyLong, "imgWidth": adminConfig.Captcha.ImgWidth, "imgHeight": adminConfig.Captcha.ImgHeight, "storeExpiration": durationString(adminConfig.Captcha.StoreExpiration)}
}
if adminConfig.Local != nil {
admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix}
}
if adminConfig.Media != nil {
chunkDir := adminConfig.Media.ChunkDir
if chunkDir == "" {
chunkDir = "uploads/chunks"
}
admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize, "chunkDir": chunkDir}
}
if adminConfig.Email != nil {
secret := ""
if adminConfig.Email.Secret != "" {
secret = "******"
}
email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": secret, "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth}
}
if adminConfig.Storage != nil {
storage := proto.Clone(adminConfig.Storage).(*conf.AdminBackend_Storage)
maskStorageSecrets(storage)
admin["storage"] = storage
}
disks := make([]map[string]any, 0, len(adminConfig.DiskList))
for _, disk := range adminConfig.DiskList {
if disk != nil {
disks = append(disks, map[string]any{"mount_point": disk.MountPoint})
}
}
admin["disk_list"] = disks
if adminConfig.Zap != nil {
admin["zap"] = adminConfig.Zap
}
if adminConfig.Cors != nil {
admin["cors"] = adminConfig.Cors
}
if adminConfig.App != nil {
admin["app"] = adminConfig.App
}
if adminConfig.Websocket != nil {
admin["websocket"] = adminConfig.Websocket
}
if adminConfig.Mq != nil {
mqConfig := proto.Clone(adminConfig.Mq).(*conf.AdminBackend_MQ)
if mqConfig.Password != "" {
mqConfig.Password = "******"
}
admin["mq"] = mqConfig
}
}
// Never mask secrets on the live runtime object. ConfigurationJSON is a
// read-only operation; mutating dataConfig here would replace the actual
// database/Redis/Mongo credentials with "******" and make the next reload
// fail. Work on a clone, just as we already do for adminConfig.
safeData := cloneDataConfig(dataConfig)
maskDataSecrets(safeData)
safeAdmin := cloneAdminConfig(adminConfig)
if safeAdmin != nil {
if safeAdmin.Jwt != nil {
safeAdmin.Jwt.SigningKey = "******"
}
if safeAdmin.Email != nil && safeAdmin.Email.Secret != "" {
safeAdmin.Email.Secret = "******"
}
maskStorageSecrets(safeAdmin.Storage)
if safeAdmin.Mq != nil && safeAdmin.Mq.Password != "" {
safeAdmin.Mq.Password = "******"
}
}
dataMap := map[string]any{}
if safeData != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(safeData)
if err != nil {
return nil, err
}
if err = json.Unmarshal(raw, &dataMap); err != nil {
return nil, err
}
}
config := compatibleConfiguration(safeData, safeAdmin)
config["admin"], config["email"], config["data"] = admin, email, dataMap
return json.Marshal(map[string]any{"config": config})
}
func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
currentData, currentAdmin := r.backend.RuntimeValues()
if currentAdmin == nil {
return nil
}
nextData := cloneDataConfig(currentData)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
if err := applyCompatibleConfiguration(raw, nextData, nextAdmin); err != nil {
return err
}
var value configurationEnvelope
if err := json.Unmarshal(raw, &value); err != nil {
return err
}
options := protojson.UnmarshalOptions{DiscardUnknown: true}
if len(value.Data) > 0 && string(value.Data) != "null" {
if err := protoutil.MergeProtoJSON(nextData, value.Data, options); err != nil {
return err
}
}
if len(value.Admin) > 0 && string(value.Admin) != "null" {
if err := protoutil.MergeProtoJSON(nextAdmin, value.Admin, options); err != nil {
return err
}
}
preserveDataSecrets(nextData, currentData)
preserveAdminSecrets(nextAdmin, currentAdmin)
if value.Email != nil {
if nextAdmin.Email == nil {
nextAdmin.Email = &conf.AdminBackend_Email{}
}
nextAdmin.Email.To, nextAdmin.Email.From, nextAdmin.Email.Host = value.Email.To, value.Email.From, value.Email.Host
nextAdmin.Email.Nickname, nextAdmin.Email.Port = value.Email.Nickname, value.Email.Port
nextAdmin.Email.IsSsl, nextAdmin.Email.IsLoginAuth = value.Email.IsSSL, value.Email.IsLoginAuth
if value.Email.Secret != "" && value.Email.Secret != "******" {
nextAdmin.Email.Secret = value.Email.Secret
}
}
if err := r.backend.RefreshDatabaseSources(nextData); err != nil {
return err
}
nextAdmin.ConfigPath = currentAdmin.ConfigPath
dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData)
if err != nil {
return err
}
adminRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextAdmin)
if err != nil {
return err
}
return r.PersistRuntimeConfig(ctx, dataRaw, adminRaw)
}
func (r *Repo) DiskMountPoints() []string {
config := r.backend.RuntimeAdmin()
if config == nil {
return nil
}
points := make([]string, 0, len(config.DiskList))
for _, item := range config.DiskList {
if item != nil && item.MountPoint != "" {
points = append(points, item.MountPoint)
}
}
return points
}
func cloneDataConfig(value *conf.Data) *conf.Data {
if value == nil {
return &conf.Data{}
}
return proto.Clone(value).(*conf.Data)
}
func cloneAdminConfig(value *conf.AdminBackend) *conf.AdminBackend {
if value == nil {
return &conf.AdminBackend{}
}
return proto.Clone(value).(*conf.AdminBackend)
}
func durationString(value *durationpb.Duration) string {
if value == nil {
return "0s"
}
return value.AsDuration().String()
}
func maskDataSecrets(value *conf.Data) {
if value == nil {
return
}
if value.Database != nil {
value.Database.Password = "******"
// Source may contain an embedded password (for example a MySQL DSN).
// The management response must never expose it. Structured database
// fields are retained and refreshDatabaseSources rebuilds Source when
// the configuration is saved.
value.Database.Source = ""
}
if value.Redis != nil {
value.Redis.Password = "******"
}
if value.Mongo != nil {
value.Mongo.Password = "******"
}
for _, item := range value.DatabaseList {
if item != nil {
item.Password = "******"
item.Source = ""
}
}
for _, item := range value.RedisList {
if item != nil {
item.Password = "******"
}
}
}
func preserveDataSecrets(next, current *conf.Data) {
if next == nil || current == nil {
return
}
if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) {
next.Database.Password = current.Database.Password
}
if next.Database != nil && current.Database != nil && next.Database.Source == "" {
next.Database.Source = current.Database.Source
}
if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) {
next.Redis.Password = current.Redis.Password
}
if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) {
next.Mongo.Password = current.Mongo.Password
}
currentDatabases := make(map[string]*conf.Data_Database, len(current.DatabaseList))
for _, item := range current.DatabaseList {
if item != nil && item.AliasName != "" {
currentDatabases[item.AliasName] = item
}
}
for i, item := range next.DatabaseList {
var previous *conf.Data_Database
if item != nil && item.AliasName != "" {
previous = currentDatabases[item.AliasName]
}
if previous == nil && i < len(current.DatabaseList) {
previous = current.DatabaseList[i]
}
if item != nil && previous != nil {
if maskedSecret(item.Password) {
item.Password = previous.Password
}
if item.Source == "" {
item.Source = previous.Source
}
}
}
currentRedis := make(map[string]*conf.Data_Redis, len(current.RedisList))
for _, item := range current.RedisList {
if item != nil && item.Name != "" {
currentRedis[item.Name] = item
}
}
for i, item := range next.RedisList {
var previous *conf.Data_Redis
if item != nil && item.Name != "" {
previous = currentRedis[item.Name]
}
if previous == nil && i < len(current.RedisList) {
previous = current.RedisList[i]
}
if item != nil && previous != nil && maskedSecret(item.Password) {
item.Password = previous.Password
}
}
}
func preserveAdminSecrets(next, current *conf.AdminBackend) {
if next.Jwt != nil && current.Jwt != nil && maskedSecret(next.Jwt.SigningKey) {
next.Jwt.SigningKey = current.Jwt.SigningKey
}
if next.Email != nil && current.Email != nil && maskedSecret(next.Email.Secret) {
next.Email.Secret = current.Email.Secret
}
preserveStorageSecrets(next.Storage, current.Storage)
if next.Mq != nil && current.Mq != nil && maskedSecret(next.Mq.Password) {
next.Mq.Password = current.Mq.Password
}
}
func maskedSecret(value string) bool { return value == "" || value == "******" }
func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore {
if storage == nil {
return nil
}
return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio}
}
func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
if storage == nil {
return
}
if storage.Qiniu != nil && storage.Qiniu.SecretKey != "" {
storage.Qiniu.SecretKey = "******"
}
for _, item := range objectStores(storage) {
if item != nil && item.SecretKey != "" {
item.SecretKey = "******"
}
}
}
func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) {
if next == nil || current == nil {
return
}
if next.Qiniu != nil && current.Qiniu != nil && maskedSecret(next.Qiniu.SecretKey) {
next.Qiniu.SecretKey = current.Qiniu.SecretKey
}
nextItems, currentItems := objectStores(next), objectStores(current)
for i := range nextItems {
if nextItems[i] != nil && currentItems[i] != nil && maskedSecret(nextItems[i].SecretKey) {
nextItems[i].SecretKey = currentItems[i].SecretKey
}
}
}