mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 06:21:10 +00:00
201 lines
5.2 KiB
Go
201 lines
5.2 KiB
Go
package store
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
SecureCLIEnvKindSensitive = "sensitive"
|
|
SecureCLIEnvKindValue = "value"
|
|
)
|
|
|
|
// SecureCLIEnvEntry is the stored per-key env representation; legacy KEY:string maps decode as sensitive.
|
|
type SecureCLIEnvEntry struct {
|
|
Kind string `json:"kind"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// SecureCLIEnvResponseEntry is safe to serialize in admin API responses.
|
|
type SecureCLIEnvResponseEntry struct {
|
|
Kind string `json:"kind"`
|
|
Value *string `json:"value"`
|
|
Masked bool `json:"masked"`
|
|
}
|
|
|
|
func ParseSecureCLIEnv(raw []byte) (map[string]SecureCLIEnvEntry, error) {
|
|
if len(bytes.TrimSpace(raw)) == 0 {
|
|
return map[string]SecureCLIEnvEntry{}, nil
|
|
}
|
|
|
|
var payload map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
env := make(map[string]SecureCLIEnvEntry, len(payload))
|
|
for key, item := range payload {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
entry, err := parseSecureCLIEnvEntry(item)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", key, err)
|
|
}
|
|
env[key] = entry
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
func parseSecureCLIEnvEntry(raw json.RawMessage) (SecureCLIEnvEntry, error) {
|
|
var legacy string
|
|
if err := json.Unmarshal(raw, &legacy); err == nil {
|
|
return SecureCLIEnvEntry{Kind: SecureCLIEnvKindSensitive, Value: legacy}, nil
|
|
}
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) > 0 && trimmed[0] != '{' {
|
|
value, err := secureCLIEnvValueAsString(raw)
|
|
if err != nil {
|
|
return SecureCLIEnvEntry{}, err
|
|
}
|
|
return SecureCLIEnvEntry{Kind: SecureCLIEnvKindSensitive, Value: value}, nil
|
|
}
|
|
|
|
var obj struct {
|
|
Kind string `json:"kind"`
|
|
Value json.RawMessage `json:"value"`
|
|
}
|
|
if err := json.Unmarshal(raw, &obj); err != nil {
|
|
return SecureCLIEnvEntry{}, err
|
|
}
|
|
|
|
kind := strings.ToLower(strings.TrimSpace(obj.Kind))
|
|
if kind == "" {
|
|
kind = SecureCLIEnvKindSensitive
|
|
}
|
|
if kind != SecureCLIEnvKindSensitive && kind != SecureCLIEnvKindValue {
|
|
return SecureCLIEnvEntry{}, fmt.Errorf("invalid env kind %q", obj.Kind)
|
|
}
|
|
|
|
value, err := secureCLIEnvValueAsString(obj.Value)
|
|
if err != nil {
|
|
return SecureCLIEnvEntry{}, err
|
|
}
|
|
return SecureCLIEnvEntry{Kind: kind, Value: value}, nil
|
|
}
|
|
|
|
func secureCLIEnvValueAsString(raw json.RawMessage) (string, error) {
|
|
if len(bytes.TrimSpace(raw)) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
|
|
return "", nil
|
|
}
|
|
var s string
|
|
if err := json.Unmarshal(raw, &s); err == nil {
|
|
return s, nil
|
|
}
|
|
var b bool
|
|
if err := json.Unmarshal(raw, &b); err == nil {
|
|
if b {
|
|
return "true", nil
|
|
}
|
|
return "false", nil
|
|
}
|
|
var f float64
|
|
if err := json.Unmarshal(raw, &f); err == nil {
|
|
return fmt.Sprint(f), nil
|
|
}
|
|
return "", fmt.Errorf("env value must be string, number, bool, or null")
|
|
}
|
|
|
|
func SerializeSecureCLIEnv(env map[string]SecureCLIEnvEntry) ([]byte, error) {
|
|
normalized := make(map[string]SecureCLIEnvEntry, len(env))
|
|
for key, entry := range env {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
kind := strings.ToLower(strings.TrimSpace(entry.Kind))
|
|
if kind == "" {
|
|
kind = SecureCLIEnvKindSensitive
|
|
}
|
|
if kind != SecureCLIEnvKindSensitive && kind != SecureCLIEnvKindValue {
|
|
return nil, fmt.Errorf("%s: invalid env kind %q", key, entry.Kind)
|
|
}
|
|
normalized[key] = SecureCLIEnvEntry{Kind: kind, Value: entry.Value}
|
|
}
|
|
return json.Marshal(normalized)
|
|
}
|
|
|
|
func FlattenSecureCLIEnv(raw []byte) (map[string]string, error) {
|
|
entries, err := ParseSecureCLIEnv(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
flat := make(map[string]string, len(entries))
|
|
for key, entry := range entries {
|
|
flat[key] = entry.Value
|
|
}
|
|
return flat, nil
|
|
}
|
|
|
|
func MergeSecureCLIEnv(existingJSON []byte, incoming json.RawMessage) ([]byte, error) {
|
|
existing, err := ParseSecureCLIEnv(existingJSON)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse existing env: %w", err)
|
|
}
|
|
incomingEntries, err := ParseSecureCLIEnv(incoming)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse incoming env: %w", err)
|
|
}
|
|
|
|
out := make(map[string]SecureCLIEnvEntry, len(incomingEntries))
|
|
for key, entry := range incomingEntries {
|
|
if entry.Kind == SecureCLIEnvKindSensitive && entry.Value == "" {
|
|
if prev, ok := existing[key]; ok {
|
|
prev.Kind = SecureCLIEnvKindSensitive
|
|
out[key] = prev
|
|
continue
|
|
}
|
|
}
|
|
out[key] = entry
|
|
}
|
|
return SerializeSecureCLIEnv(out)
|
|
}
|
|
|
|
func SecureCLIEnvKeys(raw []byte) []string {
|
|
env, err := ParseSecureCLIEnv(raw)
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
keys := make([]string, 0, len(env))
|
|
for key := range env {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func SanitizeSecureCLIEnv(env map[string]SecureCLIEnvEntry) map[string]SecureCLIEnvResponseEntry {
|
|
out := make(map[string]SecureCLIEnvResponseEntry, len(env))
|
|
for key, entry := range env {
|
|
if entry.Kind == SecureCLIEnvKindValue {
|
|
value := entry.Value
|
|
out[key] = SecureCLIEnvResponseEntry{Kind: SecureCLIEnvKindValue, Value: &value, Masked: false}
|
|
continue
|
|
}
|
|
out[key] = SecureCLIEnvResponseEntry{Kind: SecureCLIEnvKindSensitive, Value: nil, Masked: true}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func SanitizeSecureCLIEnvJSON(raw []byte) map[string]SecureCLIEnvResponseEntry {
|
|
env, err := ParseSecureCLIEnv(raw)
|
|
if err != nil {
|
|
return map[string]SecureCLIEnvResponseEntry{}
|
|
}
|
|
return SanitizeSecureCLIEnv(env)
|
|
}
|