mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(provider): fallback to config.api_key when auth-profiles.json has no key (#2724)
Co-authored-by: root <root@10.0.0.3> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
root
Steven Enamakel
parent
45d1d2105b
commit
fb708b92ae
@@ -608,6 +608,36 @@ fn legacy_custom_inference_provider_string(config: &Config) -> Option<String> {
|
||||
.map(|entry| cloud_entry_provider_string(entry, config))
|
||||
}
|
||||
|
||||
/// Resolve the slug of the cloud-provider entry that represents the legacy
|
||||
/// direct-inference route — the entry whose endpoint matches the configured
|
||||
/// custom `inference_url`.
|
||||
///
|
||||
/// Top-level `config.api_key` was historically paired with `inference_url`
|
||||
/// for direct endpoint routing, so it is scoped to this single provider. The
|
||||
/// `lookup_key_for_slug` fallback uses this to avoid leaking the global key to
|
||||
/// any other provider slug whose auth-profile lookup returned empty.
|
||||
fn legacy_inference_slug(config: &Config) -> Option<&str> {
|
||||
let inference_url = config
|
||||
.inference_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|url| !url.is_empty())?;
|
||||
|
||||
if looks_like_openhuman_backend(inference_url) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let normalized_inference = normalize_endpoint_for_compare(inference_url);
|
||||
config
|
||||
.cloud_providers
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
!is_openhuman_cloud_entry(entry)
|
||||
&& normalize_endpoint_for_compare(&entry.endpoint) == normalized_inference
|
||||
})
|
||||
.map(|entry| entry.slug.as_str())
|
||||
}
|
||||
|
||||
fn cloud_entry_provider_string(
|
||||
entry: &crate::openhuman::config::schema::cloud_providers::CloudProviderCreds,
|
||||
config: &Config,
|
||||
@@ -949,6 +979,28 @@ pub fn lookup_key_for_slug(slug: &str, config: &Config) -> anyhow::Result<String
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: read from top-level config.api_key (direct config.toml api_key).
|
||||
// This handles the case where a key was set in config.toml but not saved
|
||||
// through the UI into auth-profiles.json.
|
||||
//
|
||||
// Scoped to the legacy direct-inference provider only — the cloud-provider
|
||||
// slug whose endpoint matches `config.inference_url`. `config.api_key` was
|
||||
// historically paired with `inference_url` for direct endpoint routing, so
|
||||
// an unscoped fallback would leak this global key to any other provider
|
||||
// whose auth-profile lookup returned empty (cross-provider credential leak
|
||||
// flagged by CodeRabbit + maintainers on #2724).
|
||||
if legacy_inference_slug(config) == Some(slug) {
|
||||
if let Some(config_key) = config.api_key.as_ref() {
|
||||
if !config_key.trim().is_empty() {
|
||||
log::debug!(
|
||||
"[providers][chat-factory] auth lookup slug={} key_present=true (config.toml fallback for legacy inference_url)",
|
||||
slug
|
||||
);
|
||||
return Ok(config_key.trim().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"[providers][chat-factory] auth lookup slug={} key_present=false",
|
||||
slug
|
||||
|
||||
@@ -1581,3 +1581,66 @@ fn nvidia_nim_falls_back_to_default_model_when_no_model_in_string() {
|
||||
"should fall back to default_model from config entry"
|
||||
);
|
||||
}
|
||||
|
||||
// ── config.api_key fallback scoping (PR #2724) ───────────────────────────
|
||||
|
||||
/// Build a tempdir-backed Config with a global `config.api_key`, a custom
|
||||
/// `inference_url`, and two cloud providers: one whose endpoint matches the
|
||||
/// inference_url (the legacy direct-inference slug) and one that does not.
|
||||
///
|
||||
/// The tempdir workspace has no stored auth-profiles, so `lookup_key_for_slug`
|
||||
/// exhausts the standard auth path and reaches the `config.api_key` fallback.
|
||||
fn config_for_api_key_fallback(tmp: &TempDir) -> Config {
|
||||
let mut custom = openai_entry("p_custom", "custom");
|
||||
custom.endpoint = "https://inference.example.com/v1".to_string();
|
||||
let config = config_with_providers_in_tempdir(
|
||||
tmp,
|
||||
vec![custom, anthropic_entry("p_anthropic", "anthropic")],
|
||||
);
|
||||
let mut config = config;
|
||||
config.api_key = Some("global-key".to_string());
|
||||
config.inference_url = Some("https://inference.example.com/v1".to_string());
|
||||
config
|
||||
}
|
||||
|
||||
/// The legacy direct-inference slug — the provider whose endpoint matches
|
||||
/// `config.inference_url` — inherits the global `config.api_key`.
|
||||
#[test]
|
||||
fn config_api_key_fallback_applies_to_legacy_inference_slug() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let config = config_for_api_key_fallback(&tmp);
|
||||
assert_eq!(
|
||||
lookup_key_for_slug("custom", &config).expect("lookup must succeed"),
|
||||
"global-key",
|
||||
"legacy direct-inference slug must inherit config.api_key fallback",
|
||||
);
|
||||
}
|
||||
|
||||
/// Load-bearing negative assertion: a provider whose endpoint does NOT match
|
||||
/// `config.inference_url` must NOT inherit the global `config.api_key`.
|
||||
/// Without this guard the fallback would leak one provider's credential to
|
||||
/// every other provider (cross-provider credential leak, PR #2724).
|
||||
#[test]
|
||||
fn config_api_key_fallback_does_not_leak_to_other_slugs() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let config = config_for_api_key_fallback(&tmp);
|
||||
assert_eq!(
|
||||
lookup_key_for_slug("anthropic", &config).expect("lookup must succeed"),
|
||||
"",
|
||||
"non-matching slug must NOT inherit config.api_key — would leak credentials",
|
||||
);
|
||||
}
|
||||
|
||||
/// When `inference_url` itself is unset, the `config.api_key` fallback never
|
||||
/// fires (no legacy direct-inference slug to scope to), so no slug inherits it.
|
||||
#[test]
|
||||
fn config_api_key_fallback_inert_without_inference_url() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let mut config = config_for_api_key_fallback(&tmp);
|
||||
config.inference_url = None;
|
||||
assert_eq!(
|
||||
lookup_key_for_slug("custom", &config).expect("lookup must succeed"),
|
||||
"",
|
||||
"without inference_url there is no legacy slug — fallback must stay inert",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user