354 lines
9.3 KiB
Go
354 lines
9.3 KiB
Go
package schemas
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/bytedance/sonic"
|
|
)
|
|
|
|
// EnvVar is a wrapper around a value that can be sourced from an environment variable.
|
|
type EnvVar struct {
|
|
Val string `json:"value"`
|
|
EnvVar string `json:"env_var"`
|
|
FromEnv bool `json:"from_env"`
|
|
}
|
|
|
|
// NewEnvVar creates a new EnvValue from a string.
|
|
func NewEnvVar(value string) *EnvVar {
|
|
// Cleanup string if required
|
|
// Use strconv.Unquote to properly handle JSON string escape sequences
|
|
// This converts "\"{\\\"key\\\":\\\"value\\\"}\"" to "{\"key\":\"value\"}"
|
|
val := value
|
|
if unquoted, err := strconv.Unquote(value); err == nil {
|
|
val = unquoted
|
|
}
|
|
// Here we will need to check if the incoming data is a valid JSON object
|
|
// If it's a valid JSON object and follows the EnvVar schema, then we will unmarshal it into an EnvVar object
|
|
if sonic.Valid([]byte(value)) {
|
|
valueNode, _ := sonic.Get([]byte(val), "value")
|
|
envNode, _ := sonic.Get([]byte(val), "env_var")
|
|
if valueNode.Exists() && envNode.Exists() {
|
|
// Use a type alias to avoid infinite recursion (alias doesn't inherit methods)
|
|
type envVarAlias EnvVar
|
|
var envVar envVarAlias
|
|
if err := sonic.Unmarshal([]byte(value), &envVar); err == nil {
|
|
e := &EnvVar{
|
|
Val: envVar.Val,
|
|
FromEnv: envVar.FromEnv,
|
|
EnvVar: envVar.EnvVar,
|
|
}
|
|
// Here we will check if the Val starts with env and is same as the EnvVar
|
|
if strings.HasPrefix(e.Val, "env.") && e.Val == e.EnvVar {
|
|
e.Val = ""
|
|
// Load the environment variable value
|
|
envValue, ok := os.LookupEnv(strings.TrimPrefix(e.EnvVar, "env."))
|
|
if ok {
|
|
e.Val = envValue
|
|
}
|
|
e.FromEnv = true
|
|
}
|
|
return e
|
|
}
|
|
}
|
|
}
|
|
if envKey, ok := strings.CutPrefix(val, "env."); ok {
|
|
if envValue, ok := os.LookupEnv(envKey); ok {
|
|
return &EnvVar{
|
|
Val: envValue,
|
|
FromEnv: true,
|
|
EnvVar: val,
|
|
}
|
|
}
|
|
return &EnvVar{
|
|
Val: "",
|
|
FromEnv: true,
|
|
EnvVar: val,
|
|
}
|
|
}
|
|
return &EnvVar{
|
|
Val: val,
|
|
FromEnv: false,
|
|
EnvVar: "",
|
|
}
|
|
}
|
|
|
|
// IsRedacted returns true if the value is redacted.
|
|
func (e *EnvVar) IsRedacted() bool {
|
|
if e.Val == "" && !e.FromEnv {
|
|
return false
|
|
}
|
|
// Check if it's an environment variable reference
|
|
if e.FromEnv {
|
|
return true
|
|
}
|
|
if len(e.Val) <= 8 {
|
|
return strings.Count(e.Val, "*") == len(e.Val)
|
|
}
|
|
// Check for exact redaction pattern: 4 chars + 24 asterisks + 4 chars
|
|
if len(e.Val) == 32 {
|
|
middle := e.Val[4:28]
|
|
if middle == strings.Repeat("*", 24) {
|
|
return true
|
|
}
|
|
}
|
|
// Check if its string <redacted>
|
|
if e.Val == "<redacted>" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Equals checks if two SecretKeys are equal.
|
|
func (e *EnvVar) Equals(other *EnvVar) bool {
|
|
if e == nil && other == nil {
|
|
return true
|
|
}
|
|
if e == nil || other == nil {
|
|
return false
|
|
}
|
|
return e.Val == other.Val &&
|
|
e.EnvVar == other.EnvVar &&
|
|
e.FromEnv == other.FromEnv
|
|
}
|
|
|
|
// Redacted returns a new SecretKey with the value redacted.
|
|
func (e *EnvVar) Redacted() *EnvVar {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
if e.Val == "" {
|
|
return &EnvVar{
|
|
Val: "",
|
|
FromEnv: e.FromEnv,
|
|
EnvVar: e.EnvVar,
|
|
}
|
|
}
|
|
// If key is 8 characters or less, just return all asterisks
|
|
if len(e.Val) <= 8 {
|
|
return &EnvVar{
|
|
Val: strings.Repeat("*", len(e.Val)),
|
|
FromEnv: e.FromEnv,
|
|
EnvVar: e.EnvVar,
|
|
}
|
|
}
|
|
// Show first 4 and last 4 characters, replace middle with asterisks
|
|
prefix := e.Val[:4]
|
|
suffix := e.Val[len(e.Val)-4:]
|
|
middle := strings.Repeat("*", 24)
|
|
|
|
return &EnvVar{
|
|
Val: prefix + middle + suffix,
|
|
FromEnv: e.FromEnv,
|
|
EnvVar: e.EnvVar,
|
|
}
|
|
}
|
|
|
|
// MarshalJSON serializes the EnvVar to JSON.
|
|
// SECURITY: When the value was sourced from an environment variable, the resolved
|
|
// value is automatically redacted before being serialized. This ensures that secrets
|
|
// injected via env vars are never leaked through any JSON API response, regardless
|
|
// of whether the surrounding code remembered to call Redacted() explicitly.
|
|
//
|
|
// Plain (non-env) values are still emitted as-is — callers that want to mask those
|
|
// must continue using Redacted() at the field level (this matches the existing
|
|
// per-provider redaction logic).
|
|
//
|
|
// This does NOT affect:
|
|
// - GORM persistence (uses the Value() driver method, not JSON)
|
|
// - Encryption (operates on the Val field directly)
|
|
// - Internal LLM request paths (use GetValue() directly)
|
|
func (e EnvVar) MarshalJSON() ([]byte, error) {
|
|
type envVarAlias EnvVar
|
|
out := envVarAlias(e)
|
|
if e.FromEnv {
|
|
// Redact the resolved value but keep the env var reference and from_env flag
|
|
// so the UI still knows which env var backs this field.
|
|
redacted := e.Redacted()
|
|
if redacted != nil {
|
|
out = envVarAlias(*redacted)
|
|
}
|
|
}
|
|
return sonic.Marshal(out)
|
|
}
|
|
|
|
// UnmarshalJSON unmarshals the value from JSON.
|
|
func (e *EnvVar) UnmarshalJSON(data []byte) error {
|
|
// This is always going to be value
|
|
// Here we will first considering this as value
|
|
// if it has env. then we will process it and set the FromEnv to true
|
|
// if it doesn't have env. then we will set the FromEnv to false
|
|
// if it has env. then we will process it and set the FromEnv to true
|
|
val := string(data)
|
|
// Cleanup string if required
|
|
// Use strconv.Unquote to properly handle JSON string escape sequences
|
|
// This converts "\"{\\\"key\\\":\\\"value\\\"}\"" to "{\"key\":\"value\"}"
|
|
if unquoted, err := strconv.Unquote(val); err == nil {
|
|
val = unquoted
|
|
}
|
|
// Here we will need to check if the incoming data is a valid JSON object
|
|
// If it's a valid JSON object and follows the EnvVar schema, then we will unmarshal it into an EnvVar object
|
|
if sonic.Valid(data) {
|
|
valueNode, _ := sonic.Get(data, "value")
|
|
envNode, _ := sonic.Get(data, "env_var")
|
|
if valueNode.Exists() && envNode.Exists() {
|
|
// Use a type alias to avoid infinite recursion (alias doesn't inherit methods)
|
|
type envVarAlias EnvVar
|
|
var envVar envVarAlias
|
|
if err := sonic.Unmarshal(data, &envVar); err == nil {
|
|
e.Val = envVar.Val
|
|
e.FromEnv = envVar.FromEnv
|
|
e.EnvVar = envVar.EnvVar
|
|
// Here we will check if the Val starts with env and is same as the EnvVar
|
|
if strings.HasPrefix(e.Val, "env.") && e.Val == e.EnvVar {
|
|
e.Val = ""
|
|
// Load the environment variable value
|
|
envValue, ok := os.LookupEnv(strings.TrimPrefix(e.EnvVar, "env."))
|
|
if ok {
|
|
e.Val = envValue
|
|
}
|
|
e.FromEnv = true
|
|
}
|
|
return nil
|
|
}
|
|
// Else the value is JSON, so we will treat this as a normal value
|
|
}
|
|
}
|
|
if envKey, ok := strings.CutPrefix(val, "env."); ok {
|
|
if envValue, ok := os.LookupEnv(envKey); ok {
|
|
e.Val = envValue
|
|
e.FromEnv = true
|
|
e.EnvVar = val
|
|
return nil
|
|
}
|
|
e.Val = ""
|
|
e.FromEnv = true
|
|
e.EnvVar = val
|
|
return nil
|
|
}
|
|
e.Val = val
|
|
e.FromEnv = false
|
|
e.EnvVar = ""
|
|
return nil
|
|
}
|
|
|
|
// String returns the value as a string.
|
|
func (e *EnvVar) String() string {
|
|
return e.Val
|
|
}
|
|
|
|
// Scan scans the value from the database.
|
|
func (e *EnvVar) Scan(value any) error {
|
|
if value == nil {
|
|
e.Val = ""
|
|
e.FromEnv = false
|
|
e.EnvVar = ""
|
|
return nil
|
|
}
|
|
switch v := value.(type) {
|
|
case []byte:
|
|
return e.Scan(string(v))
|
|
case string:
|
|
// Cleanup string if required
|
|
// The string may have "\"env.TEST\"", "env.TEST" or "env.TEST\"", we need to clean it up to "env.TEST"
|
|
val := strings.Trim(v, "\"")
|
|
if envKey, ok := strings.CutPrefix(val, "env."); ok {
|
|
if envValue, ok := os.LookupEnv(envKey); ok {
|
|
e.Val = envValue
|
|
e.FromEnv = true
|
|
e.EnvVar = val
|
|
return nil
|
|
}
|
|
e.Val = ""
|
|
e.FromEnv = true
|
|
e.EnvVar = val
|
|
return nil
|
|
}
|
|
e.Val = val
|
|
e.FromEnv = false
|
|
e.EnvVar = ""
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to scan value: %v", value)
|
|
}
|
|
|
|
// Value implements driver.Valuer for database storage.
|
|
// It stores the original env reference (e.g., "env.API_KEY") if FromEnv is true,
|
|
// otherwise stores the raw value.
|
|
func (e EnvVar) Value() (driver.Value, error) {
|
|
if e.FromEnv {
|
|
return e.EnvVar, nil
|
|
}
|
|
return e.Val, nil
|
|
}
|
|
|
|
// IsFromEnv returns true if the value is sourced from an environment variable.
|
|
func (e *EnvVar) IsFromEnv() bool {
|
|
return e.FromEnv
|
|
}
|
|
|
|
// IsSet returns true if the EnvVar has a resolved value or an environment variable reference.
|
|
// This should be used instead of GetValue() != "" when checking whether a field was configured,
|
|
// because env var references may have an empty Val before resolution (e.g., when the env var
|
|
// is not available in the current environment).
|
|
func (e *EnvVar) IsSet() bool {
|
|
if e == nil {
|
|
return false
|
|
}
|
|
return e.Val != "" || e.EnvVar != ""
|
|
}
|
|
|
|
// GetValue returns the value.
|
|
func (e *EnvVar) GetValue() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
return e.Val
|
|
}
|
|
|
|
// GetValuePtr returns a pointer to the value.
|
|
func (e *EnvVar) GetValuePtr() *string {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return &e.Val
|
|
}
|
|
|
|
// CoerceInt coerces value to int
|
|
func (e *EnvVar) CoerceInt(defaultValue int) int {
|
|
if e == nil {
|
|
return defaultValue
|
|
}
|
|
val, err := strconv.Atoi(e.GetValue())
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return val
|
|
}
|
|
|
|
// CoerceBool coerces value to bool
|
|
func (e *EnvVar) CoerceBool(defaultValue bool) bool {
|
|
if e == nil {
|
|
return defaultValue
|
|
}
|
|
val, err := strconv.ParseBool(e.GetValue())
|
|
if err != nil {
|
|
return defaultValue
|
|
}
|
|
return val
|
|
}
|
|
|
|
// IsDefined returns true if the EnvVar has a source (static value or env key)
|
|
func (e *EnvVar) IsDefined() bool {
|
|
if e == nil {
|
|
return false
|
|
}
|
|
if e.IsFromEnv() {
|
|
return e.EnvVar != ""
|
|
}
|
|
return e.Val != ""
|
|
}
|