Skip to content

refactor: unify SecretVar env/vault fields into single SecretRef/FromSecret - #4598

Closed
BearTS wants to merge 1 commit into
06-22-fix_add_missing_check_for_vaultfrom
06-22-refactor_secretvar
Closed

refactor: unify SecretVar env/vault fields into single SecretRef/FromSecret#4598
BearTS wants to merge 1 commit into
06-22-fix_add_missing_check_for_vaultfrom
06-22-refactor_secretvar

Conversation

@BearTS

@BearTS BearTS commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Unifies the SecretVar struct's dual-track secret sourcing (FromEnv/EnvVar + FromVault/VaultRef) into a single, source-agnostic pair of fields: FromSecret bool and SecretRef string. This eliminates branching throughout the codebase wherever env and vault references were handled separately, and establishes a cleaner abstraction for any future secret backend.

Changes

  • Replaced FromEnv/EnvVar/FromVault/VaultRef fields on SecretVar with FromSecret bool and SecretRef string
  • SecretRef now holds the full prefixed reference (e.g. env.MY_VAR or vault.path/to/secret) regardless of backend
  • IsFromEnv() and IsFromVault() methods removed; replaced by a single IsFromSecret() method
  • Backward-compatible deserialization added in UnmarshalJSON, NewSecretVar, and Scan so existing stored records using the old env_var/from_env JSON format continue to deserialize correctly
  • StoreVaultSecretVar now sets SecretRef to "vault." + path (previously it stored the raw path in VaultRef)
  • All call sites updated: proxy/TLS configuration, config store hashing, encryption hooks, GORM BeforeSave/AfterFind hooks, HTTP config handlers, and plugins
  • isSecretVarObject and marshalSecretVarObject in the HTTP plugin handler updated to recognize both the new secret_ref/from_secret keys and the legacy env_var/from_env keys
  • Tests updated throughout to use the new field names; TestSecretVar_UnmarshalJSON_FullStructure replaced with TestSecretVar_UnmarshalJSON_BackwardCompat to explicitly cover the old format

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./core/schemas/... ./core/providers/... ./framework/configstore/... ./transports/... ./plugins/...

Verify that existing configurations using env.VAR and vault.path/to/secret references continue to resolve correctly after the schema change. Confirm that records persisted with the old env_var/from_env JSON format deserialize into the new fields without data loss.

Breaking changes

  • Yes
  • No

The SecretVar struct's exported fields have changed. Any code outside this repository that directly reads or writes EnvVar, FromEnv, VaultRef, or FromVault will need to migrate to SecretRef and FromSecret. The JSON serialization format changes from env_var/from_env/vault_var/from_vault to secret_ref/from_secret; backward-compatible deserialization is provided for reading old records, but newly written records will use the new format.

Related issues

N/A

Security considerations

Secret reference metadata (SecretRef) is preserved through redaction and serialization paths so that env/vault references remain identifiable in API responses without exposing resolved values. The FullyRedacted and Redacted methods carry SecretRef/FromSecret through unchanged, maintaining the same security properties as before.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 11f7ef23-01ab-4ff6-aefe-e2e92f38fb77

📥 Commits

Reviewing files that changed from the base of the PR and between 9099811 and cf88c75.

📒 Files selected for processing (16)
  • core/providers/utils/utils.go
  • core/schemas/secretvar.go
  • core/schemas/secretvar_test.go
  • core/schemas/utils.go
  • core/schemas/vault.go
  • core/schemas/vault_test.go
  • framework/configstore/clientconfig.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/encryption.go
  • framework/configstore/tables/mcp.go
  • framework/configstore/tables/oauth.go
  • plugins/otel/main.go
  • plugins/telemetry/main.go
  • transports/bifrost-http/handlers/config.go
  • transports/bifrost-http/handlers/plugins.go
  • transports/bifrost-http/lib/config.go

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved validation for empty external secret references in proxy and TLS configurations to ensure invalid secrets are properly detected and logged.
  • Refactor

    • Unified internal representation of external secret sources for improved consistency in how secrets are validated, encrypted, redacted, and stored throughout the system.

Walkthrough

SecretVar's dual env/vault reference fields (EnvVar/FromEnv, VaultRef/FromVault) are replaced with a single unified model (SecretRef/FromSecret). All parsing, serialization, encryption, hashing, redaction, vault storage, proxy/TLS validation, and HTTP handler logic across the codebase is updated to use the new fields and the IsFromSecret() method.

Changes

SecretVar Unified Reference Model

Layer / File(s) Summary
SecretVar struct, parsing, and core method rewrites
core/schemas/secretvar.go, core/schemas/utils.go
SecretVar fields changed from EnvVar/FromEnv/VaultRef/FromVault to SecretRef/FromSecret. NewSecretVar, UnmarshalJSON, IsFromSecret, IsRedacted, Equals, Redacted, FullyRedacted, Scan, Value, ShouldPreserveStored, IsSet, and SecretVarAsString are all rewritten against the new model with backward-compat JSON parsing.
Vault store and remove operations
core/schemas/vault.go, core/schemas/vault_test.go
removeOwnedVaultSecretVar now checks SecretRef for a vault.-prefixed value; StoreVaultSecretVar skips on IsFromSecret() and writes back SecretRef/FromSecret on success. All vault tests updated to assert the new fields.
SecretVar test suite updates
core/schemas/secretvar_test.go
All unmarshal, NewSecretVar, equality, redaction, IsSet, Scan, Value, and vault-related test assertions migrated from EnvVar/FromEnv/VaultRef/FromVault to SecretRef/FromSecret. TestSecretVar_UnmarshalJSON_BackwardCompat added for legacy JSON key coverage.
Config store encryption, hashing, and DB persistence
framework/configstore/tables/encryption.go, framework/configstore/tables/mcp.go, framework/configstore/tables/oauth.go, framework/configstore/rdb.go, framework/configstore/clientconfig.go
encryptSecretVar/decryptSecretVar, BeforeSave/AfterFind guards, header serialization, connection-string encryption, and all Generate*Hash and Redacted methods updated to use IsFromSecret()/SecretRef instead of env/vault-specific checks.
HTTP transport handlers and lib/config updates
transports/bifrost-http/handlers/config.go, transports/bifrost-http/handlers/plugins.go, transports/bifrost-http/lib/config.go
getConfig admin password redaction, updateConfig secret-backed credential validation, preserveSecretVar, loadAuthConfig, virtual key governance reconciliation, restoreRedactedValue, isSecretVarObject, and marshalSecretVarObject updated to use IsFromSecret/SecretRef/FromSecret.
Provider proxy/TLS validation and plugin redaction
core/providers/utils/utils.go, plugins/otel/main.go, plugins/telemetry/main.go
ConfigureProxy and ConfigureTLS empty-value checks switch to IsFromSecret() with SecretRef in error messages. hideResolvedEnvValue in both otel and telemetry plugins now gates redaction on IsFromSecret().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • danpiths
  • roroghost17

Poem

🐇 A rabbit hopped through fields of code so wide,
Where EnvVar and VaultRef once did hide.
Now SecretRef unites them, tidy and true,
FromSecret the flag that carries them through.
One field to rule them, one bool to say—
"This secret came from somewhere far away!" 🌿

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-22-refactor_secretvar

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@BearTS BearTS changed the title refactor: SecretVar refactor: unify SecretVar env/vault fields into single SecretRef/FromSecret Jun 22, 2026
@BearTS
BearTS marked this pull request as ready for review June 22, 2026 06:12
@BearTS BearTS closed this Jun 22, 2026
@coderabbitai
coderabbitai Bot requested review from danpiths and roroghost17 June 22, 2026 06:14
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 2/5

Not safe to merge — seven test files that were not included in the PR still reference removed SecretVar methods and fields, so go test ./framework/configstore/... ./transports/... will not compile. Additionally, the config schema is out of sync with the new field names.

The production code sweep is thorough and the unified abstraction is clean, but the test suite across framework/configstore and transports/bifrost-http will fail to compile because IsFromEnv(), IsFromVault(), .FromEnv, .EnvVar, and .VaultRef are all removed without updating the seven test files that rely on them. There is also a silent data-loss path for vault secrets stored in the old JSON object format, and the config schema has not been updated to document the new field names.

framework/configstore/tables/virtualkey_secretvar_test.go, framework/configstore/tables/encryption_test.go, framework/configstore/clientconfig_redaction_test.go, framework/configstore/encryption_test.go, transports/bifrost-http/lib/config_test.go, transports/bifrost-http/lib/ctx_test.go, scripts/bifrost-migration-cli/modelconformance_test.go — all reference removed fields/methods and will not compile. transports/config.schema.json needs new secret_ref/from_secret entries. core/schemas/secretvar.go needs vault_var/from_vault backward compat added to secretVarCompat.

Important Files Changed

Filename Overview
core/schemas/secretvar.go Core SecretVar refactor: replaces EnvVar/FromEnv/VaultRef/FromVault with SecretRef/FromSecret. Backward compat added for old env JSON format but missing for old vault JSON format — silent data loss for vault JSON objects.
framework/configstore/tables/virtualkey_secretvar_test.go NOT updated — still references IsFromEnv(), IsFromVault(), .FromEnv, .EnvVar which are removed from SecretVar. Will not compile.
framework/configstore/tables/encryption_test.go NOT updated — still references IsFromEnv(), .FromEnv, .EnvVar. Will not compile.
framework/configstore/clientconfig_redaction_test.go NOT updated — still references IsFromEnv(), .EnvVar, .FromEnv. Will not compile.
transports/bifrost-http/lib/config_test.go NOT updated — still references IsFromEnv() and .EnvVar at multiple sites in auth config tests. Will not compile.
transports/config.schema.json NOT updated — still documents env_var/from_env as the SecretVar object shape throughout; new secret_ref/from_secret fields are absent from the schema.
framework/configstore/clientconfig.go Hash functions updated to use unified SecretRef/FromSecret; hash prefix for env-backed headers changed from ':env:' to ':ref:', causing one-time hash invalidation on deploy.
core/schemas/secretvar_test.go Tests updated to use new field names; new backward compat test added for old env_var/from_env format. Old vault JSON format tests removed without replacement.
transports/bifrost-http/handlers/plugins.go isSecretVarObject and marshalSecretVarObject updated to recognize both new secret_ref/from_secret and legacy env_var/from_env; old vault_var/from_vault format not recognized.
transports/bifrost-http/handlers/config.go AdminPassword/AdminUserName handling simplified; old separate env/vault branches collapsed into single IsFromSecret() check. Logic is correct.
core/providers/utils/utils.go Proxy/TLS error paths simplified by removing env-vs-vault branching; now uses IsFromSecret() + SecretRef. Correct.
framework/configstore/rdb.go Header serialization and connection string encryption guard updated to use IsFromSecret(). Logic preserved correctly.
framework/configstore/tables/encryption.go encrypt/decryptSecretVar guards simplified to single IsFromSecret() check. Correct.
transports/bifrost-http/lib/config.go preserveSecretVar now copies SecretRef/FromSecret (previously only copied FromEnv/EnvVar, silently dropping vault references — this is a bug fix). Virtual key unresolved ref guard updated correctly.
core/schemas/vault.go StoreVaultSecretVar now sets SecretRef to 'vault.'+path. removeOwnedVaultSecretVar now uses HasPrefix check instead of IsFromVault(). Logic is correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Input: string or JSON"] --> B{JSON object?\nhas 'value' +\n'secret_ref' or 'env_var'}
    B -- Yes --> C{New format?\nsecret_ref != ''\nor from_secret=true}
    C -- Yes --> D["Set SecretRef=secret_ref\nFromSecret=true"]
    C -- No --> E{Backward compat?\nfrom_env=true\n&& env_var != ''}
    E -- Yes --> F["Set SecretRef='env.'+env_var\nFromSecret=true\nResolve env var"]
    E -- No --> G{Legacy format?\nvalue == env_var\nstarts with 'env.'}
    G -- Yes --> H["Set SecretRef=env_var\nFromSecret=true\nResolve env var"]
    G -- No --> I["Resolve SecretRef\nif vault. or env."]
    B -- No --> J{Plain string\nstarts with 'vault.'}
    J -- Yes --> K["SecretRef=val\nFromSecret=true\nLookupVault"]
    J -- No --> L{Plain string\nstarts with 'env.'}
    L -- Yes --> M["SecretRef=val\nFromSecret=true\nos.LookupEnv"]
    L -- No --> N["Literal value\nSecretRef=''\nFromSecret=false"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Input: string or JSON"] --> B{JSON object?\nhas 'value' +\n'secret_ref' or 'env_var'}
    B -- Yes --> C{New format?\nsecret_ref != ''\nor from_secret=true}
    C -- Yes --> D["Set SecretRef=secret_ref\nFromSecret=true"]
    C -- No --> E{Backward compat?\nfrom_env=true\n&& env_var != ''}
    E -- Yes --> F["Set SecretRef='env.'+env_var\nFromSecret=true\nResolve env var"]
    E -- No --> G{Legacy format?\nvalue == env_var\nstarts with 'env.'}
    G -- Yes --> H["Set SecretRef=env_var\nFromSecret=true\nResolve env var"]
    G -- No --> I["Resolve SecretRef\nif vault. or env."]
    B -- No --> J{Plain string\nstarts with 'vault.'}
    J -- Yes --> K["SecretRef=val\nFromSecret=true\nLookupVault"]
    J -- No --> L{Plain string\nstarts with 'env.'}
    L -- Yes --> M["SecretRef=val\nFromSecret=true\nos.LookupEnv"]
    L -- No --> N["Literal value\nSecretRef=''\nFromSecret=false"]
Loading

Comments Outside Diff (1)

  1. framework/configstore/clientconfig.go, line 1371-1383 (link)

    P2 Hash prefix change for MCP client headers invalidates all existing hashes

    The hash contribution for env-backed MCP client headers changed from k + ":env:" + val.EnvVar to k + ":ref:" + val.SecretRef. Any header hash computed before this deploy will mismatch after the deploy, which may trigger spurious reconnects or config-reload cycles for every MCP client whose headers include an env/vault reference. This is a one-time impact on first deploy, but it's worth documenting in the migration notes since operators may see unexpected reconnection bursts.

Reviews (1): Last reviewed commit: "refactor: SecretVar" | Re-trigger Greptile

Comment thread core/schemas/secretvar.go
Comment on lines +230 to +290
FromSecret: e.FromSecret,
}
}

// UnmarshalJSON unmarshals the value from JSON.
func (e *SecretVar) UnmarshalJSON(data []byte) error {
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
}
// Check if the incoming data is a valid JSON object matching the SecretVar schema.
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 secretVarAlias SecretVar
var secretVar secretVarAlias
if err := sonic.Unmarshal(data, &secretVar); err == nil {
e.Val = secretVar.Val
e.FromEnv = secretVar.FromEnv
e.EnvVar = secretVar.EnvVar
e.FromVault = secretVar.FromVault
e.VaultRef = secretVar.VaultRef
secretRefNode, _ := sonic.Get(data, "secret_ref")
if valueNode.Exists() && (envNode.Exists() || secretRefNode.Exists()) {
type secretVarCompat struct {
Val string `json:"value"`
SecretRef string `json:"secret_ref"`
FromSecret bool `json:"from_secret"`
// backward compat: env_var/from_env (shipped)
EnvVar string `json:"env_var"`
FromEnv bool `json:"from_env"`
}
var raw secretVarCompat
if err := sonic.Unmarshal(data, &raw); err == nil {
e.Val = raw.Val

// Explicit vault reference: {from_vault: true, vault_var: "vault.path"}
if e.FromVault && e.VaultRef != "" {
if !strings.HasPrefix(e.VaultRef, "vault.") {
e.VaultRef = "vault." + e.VaultRef
// New format
if raw.SecretRef != "" || raw.FromSecret {
e.SecretRef = raw.SecretRef
e.FromSecret = raw.FromSecret
} else if raw.FromEnv && raw.EnvVar != "" {
// Backward compat: env
ref := raw.EnvVar
if !strings.HasPrefix(ref, "env.") {
ref = "env." + ref
}
e.Val = e.VaultRef
if vaultValue, ok := LookupVault(e.VaultRef); ok {
e.Val = vaultValue
e.SecretRef = ref
e.FromSecret = true
if envValue, ok := os.LookupEnv(strings.TrimPrefix(ref, "env.")); ok {
e.Val = envValue
} else {
e.Val = ""
}
return nil
}
// Old format: value == env_var == "env.XXX"
if strings.HasPrefix(e.Val, "env.") && e.Val == e.EnvVar {
} else if strings.HasPrefix(raw.Val, "env.") && raw.Val == raw.EnvVar {
// Old format: value == env_var == "env.XXX"
e.SecretRef = raw.EnvVar
e.FromSecret = true
e.Val = ""
envValue, ok := os.LookupEnv(strings.TrimPrefix(e.EnvVar, "env."))
if ok {
if envValue, ok := os.LookupEnv(strings.TrimPrefix(raw.EnvVar, "env.")); ok {
e.Val = envValue
}
e.FromEnv = true
return nil
}
// New format: value is empty, from_env=true, env_var holds the reference
if e.Val == "" && e.FromEnv && strings.HasPrefix(e.EnvVar, "env.") {
if envValue, ok := os.LookupEnv(strings.TrimPrefix(e.EnvVar, "env.")); ok {

// Resolve references
if e.FromSecret && strings.HasPrefix(e.SecretRef, "vault.") {
e.Val = e.SecretRef
if vaultValue, ok := LookupVault(e.SecretRef); ok {
e.Val = vaultValue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing backward compat for old vault JSON format silently drops vault references

The secretVarCompat struct includes EnvVar/FromEnv for old env-format backward compat, but has no VaultRef/FromVault fields. A record stored in the old format {"value":"","env_var":"","from_env":false,"vault_var":"vault.bifrost/key","from_vault":true} will match the valueNode.Exists() && envNode.Exists() guard (because env_var is present, even if empty) and enter the unmarshal block, but every condition thereafter falls through with an empty SecretRef and FromSecret=false. The vault reference is silently lost, causing the field to behave as an empty literal value rather than resolving the secret.

The old test cases for this exact input ("struct form with from_vault") were removed without a replacement. Adding VaultVar string and FromVault bool to secretVarCompat and mapping them to SecretRef/FromSecret would close this gap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant