fix: persist runtime proxy token
This commit is contained in:
+115
-2
@@ -966,6 +966,8 @@ async fn change_auth_token() -> Json<Value> {
|
||||
|
||||
async fn runtime_settings(State(state): State<AppState>) -> Json<Value> {
|
||||
Json(json!({
|
||||
"proxyTokenMasked": state.runtime_auth.proxy_token_masked(),
|
||||
"proxyTokenSource": if state.runtime_auth.using_default_proxy_token() { "config" } else { "runtime" },
|
||||
"server": {
|
||||
"host": state.config.server.host,
|
||||
"port": state.config.server.port,
|
||||
@@ -989,8 +991,34 @@ async fn runtime_settings(State(state): State<AppState>) -> Json<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn update_runtime_settings() -> Json<Value> {
|
||||
Json(json!({ "success": true }))
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RuntimeSettingsUpdateRequest {
|
||||
proxy_token: Option<String>,
|
||||
}
|
||||
|
||||
async fn update_runtime_settings(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<RuntimeSettingsUpdateRequest>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
if let Some(proxy_token) = req.proxy_token {
|
||||
let updated = state
|
||||
.runtime_auth
|
||||
.set_proxy_token(&state.db, proxy_token)
|
||||
.await
|
||||
.map_err(|err| AppError::BadRequest(err.to_string()))?;
|
||||
return Ok(Json(json!({
|
||||
"success": true,
|
||||
"proxyTokenMasked": crate::services::runtime_auth::mask_token(&updated),
|
||||
"proxyTokenSource": if state.runtime_auth.using_default_proxy_token() { "config" } else { "runtime" },
|
||||
})));
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"success": true,
|
||||
"proxyTokenMasked": state.runtime_auth.proxy_token_masked(),
|
||||
"proxyTokenSource": if state.runtime_auth.using_default_proxy_token() { "config" } else { "runtime" },
|
||||
})))
|
||||
}
|
||||
|
||||
async fn brand_list() -> Json<Value> {
|
||||
@@ -1166,3 +1194,88 @@ async fn clear_usage_data(State(state): State<AppState>) -> AppResult<Json<Value
|
||||
async fn test_notification() -> Json<Value> {
|
||||
Json(json!({ "success": true, "message": "notification endpoint available" }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
config::{AppConfig, AuthConfig, DbConfig, ProxyConfig, ServerConfig},
|
||||
services::{builtin_keys::BuiltinKeys, runtime_auth::RuntimeAuthSettings},
|
||||
};
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
|
||||
async fn test_state() -> AppState {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("CREATE TABLE settings (key TEXT PRIMARY KEY NOT NULL, value TEXT)")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let runtime_auth = RuntimeAuthSettings::load(&pool, "sk-default-token".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
AppState::new(
|
||||
AppConfig {
|
||||
server: ServerConfig {
|
||||
host: "127.0.0.1".into(),
|
||||
port: 0,
|
||||
log_level: "info".into(),
|
||||
cors_origins: vec![],
|
||||
},
|
||||
db: DbConfig {
|
||||
url: "sqlite::memory:".into(),
|
||||
max_connections: 1,
|
||||
ssl: false,
|
||||
runtime_db_url: None,
|
||||
runtime_db_dialect: None,
|
||||
runtime_db_ssl: None,
|
||||
},
|
||||
auth: AuthConfig {
|
||||
jwt_secret: "secret".into(),
|
||||
jwt_expiry_hours: 1,
|
||||
auth_token: "auth".into(),
|
||||
proxy_token: "sk-default-token".into(),
|
||||
},
|
||||
proxy: ProxyConfig {
|
||||
upstream_timeout_secs: 30,
|
||||
first_byte_timeout_secs: 30,
|
||||
max_retries: 3,
|
||||
proxy_log_retention_days: 7,
|
||||
template_proxy_url: None,
|
||||
},
|
||||
comfyui: Default::default(),
|
||||
},
|
||||
pool,
|
||||
BuiltinKeys {
|
||||
comfyui_agent: (1, "sk-comfyui-test".into()),
|
||||
},
|
||||
runtime_auth,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_settings_reports_and_updates_proxy_token() {
|
||||
let state = test_state().await;
|
||||
let Json(initial) = runtime_settings(State(state.clone())).await;
|
||||
assert_eq!(initial["proxyTokenMasked"], "••••oken");
|
||||
assert_eq!(initial["proxyTokenSource"], "config");
|
||||
|
||||
let Json(updated) = update_runtime_settings(
|
||||
State(state.clone()),
|
||||
Json(RuntimeSettingsUpdateRequest {
|
||||
proxy_token: Some("sk-new-runtime-token".into()),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated["success"], true);
|
||||
assert_eq!(updated["proxyTokenMasked"], "••••oken");
|
||||
assert_eq!(updated["proxyTokenSource"], "runtime");
|
||||
assert_eq!(state.runtime_proxy_token(), "sk-new-runtime-token");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +417,7 @@ mod tests {
|
||||
BuiltinKeys {
|
||||
comfyui_agent: (1, "sk-comfyui-abcdef0123456789".into()),
|
||||
},
|
||||
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -217,6 +217,7 @@ const HIDDEN_SETTINGS_PREFIX: &str = "builtin_key.";
|
||||
|
||||
fn is_hidden_setting_key(key: &str) -> bool {
|
||||
key.starts_with(HIDDEN_SETTINGS_PREFIX)
|
||||
|| key == crate::services::runtime_auth::PROXY_TOKEN_SETTING_KEY
|
||||
}
|
||||
|
||||
/// GET /api/settings — return all settings as a key-value map.
|
||||
@@ -313,6 +314,12 @@ mod tests {
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO settings (key, value) VALUES ('runtime.auth.proxy_token', 'sk-runtime-secret')",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
AppState::new(
|
||||
AppConfig {
|
||||
@@ -349,11 +356,12 @@ mod tests {
|
||||
BuiltinKeys {
|
||||
comfyui_agent: (1, "sk-comfyui-secret".into()),
|
||||
},
|
||||
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
|
||||
)
|
||||
}
|
||||
|
||||
/// 5.2 — `GET /api/settings` returns regular settings but never any
|
||||
/// `builtin_key.*` rows.
|
||||
/// 5.2 — `GET /api/settings` returns regular settings but never protected
|
||||
/// secret rows.
|
||||
#[tokio::test]
|
||||
async fn list_settings_filters_builtin_key_prefix() {
|
||||
let state = settings_state().await;
|
||||
@@ -364,6 +372,10 @@ mod tests {
|
||||
!data.contains_key("builtin_key.comfyui_agent.secret"),
|
||||
"builtin_key.* rows must not appear in /api/settings response"
|
||||
);
|
||||
assert!(
|
||||
!data.contains_key("runtime.auth.proxy_token"),
|
||||
"runtime proxy token must not appear in /api/settings response"
|
||||
);
|
||||
}
|
||||
|
||||
/// 5.3 — `PUT /api/settings` rejects writes targeting `builtin_key.*`
|
||||
|
||||
@@ -49,8 +49,9 @@ async fn check_auth(state: &AppState, headers: &HeaderMap) -> Result<(), AppErro
|
||||
|
||||
// Downstream API key
|
||||
if auth.starts_with("sk-") {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
let result =
|
||||
authorize_downstream_token(auth, &state.db, Some(&state.config.auth.proxy_token))
|
||||
authorize_downstream_token(auth, &state.db, Some(&proxy_token))
|
||||
.await
|
||||
.map_err(AppError::Unauthorized)?;
|
||||
|
||||
|
||||
@@ -243,11 +243,12 @@ async fn resolve_agent_auth(
|
||||
)
|
||||
})?;
|
||||
|
||||
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
if token == state.config.auth.auth_token || token == proxy_token {
|
||||
return Ok(AgentAuthContext {
|
||||
user_id: None,
|
||||
role: "admin".to_string(),
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: proxy_token,
|
||||
key_name: "global-proxy".to_string(),
|
||||
});
|
||||
}
|
||||
@@ -282,7 +283,7 @@ async fn resolve_agent_auth(
|
||||
Err(_) if claims.role == "admin" => Ok(AgentAuthContext {
|
||||
user_id: Some(user_id),
|
||||
role: claims.role,
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: state.runtime_proxy_token(),
|
||||
key_name: "global-proxy".to_string(),
|
||||
}),
|
||||
Err(message) => Err((
|
||||
@@ -1245,6 +1246,7 @@ mod tests {
|
||||
BuiltinKeys {
|
||||
comfyui_agent: (1, "sk-comfyui-test".into()),
|
||||
},
|
||||
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -104,11 +104,12 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<DigitalHu
|
||||
let token = extract_bearer_token(headers)
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
|
||||
|
||||
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
if token == state.config.auth.auth_token || token == proxy_token {
|
||||
return Ok(DigitalHumanAuth {
|
||||
user_id: None,
|
||||
role: "admin".to_string(),
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: proxy_token,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +132,7 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<DigitalHu
|
||||
Err(_) if claims.role == "admin" => Ok(DigitalHumanAuth {
|
||||
user_id: Some(user_id),
|
||||
role: claims.role,
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: state.runtime_proxy_token(),
|
||||
}),
|
||||
Err(message) => Err(AppError::Forbidden(message)),
|
||||
}
|
||||
|
||||
@@ -88,11 +88,12 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<MoneyPrin
|
||||
let token = extract_bearer_token(headers)
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
|
||||
|
||||
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
if token == state.config.auth.auth_token || token == proxy_token {
|
||||
return Ok(MoneyPrinterAuth {
|
||||
user_id: None,
|
||||
role: "admin".to_string(),
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: proxy_token,
|
||||
key_name: "global-proxy".to_string(),
|
||||
});
|
||||
}
|
||||
@@ -117,7 +118,7 @@ async fn resolve_auth(headers: &HeaderMap, state: &AppState) -> Result<MoneyPrin
|
||||
Err(_) if claims.role == "admin" => Ok(MoneyPrinterAuth {
|
||||
user_id: Some(user_id),
|
||||
role: claims.role,
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: state.runtime_proxy_token(),
|
||||
key_name: "global-proxy".to_string(),
|
||||
}),
|
||||
Err(message) => Err(AppError::Forbidden(message)),
|
||||
|
||||
@@ -123,11 +123,12 @@ async fn resolve_picture_agent_auth(
|
||||
let token = extract_bearer_token(headers)
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
|
||||
|
||||
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
if token == state.config.auth.auth_token || token == proxy_token {
|
||||
return Ok(PictureAgentAuth {
|
||||
user_id: None,
|
||||
role: "admin".to_string(),
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: proxy_token,
|
||||
key_name: "global-proxy".to_string(),
|
||||
});
|
||||
}
|
||||
@@ -153,7 +154,7 @@ async fn resolve_picture_agent_auth(
|
||||
Err(_) if claims.role == "admin" => Ok(PictureAgentAuth {
|
||||
user_id: Some(user_id),
|
||||
role: claims.role,
|
||||
outbound_key: state.config.auth.proxy_token.clone(),
|
||||
outbound_key: state.runtime_proxy_token(),
|
||||
key_name: "global-proxy".to_string(),
|
||||
}),
|
||||
Err(message) => Err(AppError::Forbidden(message)),
|
||||
|
||||
@@ -610,7 +610,8 @@ async fn check_auth(state: &AppState, headers: &HeaderMap) -> Result<Option<i64>
|
||||
.and_then(|v| v.split_whitespace().next())
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
|
||||
|
||||
let result = authorize_downstream_token(auth, &state.db, Some(&state.config.auth.proxy_token))
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
let result = authorize_downstream_token(auth, &state.db, Some(&proxy_token))
|
||||
.await
|
||||
.map_err(AppError::Unauthorized)?;
|
||||
|
||||
@@ -1693,6 +1694,7 @@ mod tests {
|
||||
crate::services::builtin_keys::BuiltinKeys {
|
||||
comfyui_agent: (0, "sk-comfyui-test".to_string()),
|
||||
},
|
||||
crate::services::runtime_auth::RuntimeAuthSettings::from_token_for_test("proxy"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ async fn call_llm(state: &AppState, prompt: &str, system: &str) -> AppResult<Str
|
||||
|
||||
let resp = client
|
||||
.post(url)
|
||||
.bearer_auth(&state.config.auth.proxy_token)
|
||||
.bearer_auth(state.runtime_proxy_token())
|
||||
.json(&json!({
|
||||
"model": &state.config.comfyui.agent_model,
|
||||
"messages": [
|
||||
@@ -319,7 +319,7 @@ async fn call_image_gen(state: &AppState, prompt: &str) -> AppResult<String> {
|
||||
|
||||
let resp = client
|
||||
.post(url)
|
||||
.bearer_auth(&state.config.auth.proxy_token)
|
||||
.bearer_auth(state.runtime_proxy_token())
|
||||
.json(&json!({
|
||||
"model": "gpt-image-2",
|
||||
"prompt": prompt,
|
||||
@@ -362,7 +362,7 @@ async fn call_tts(state: &AppState, text: &str, voice: &str) -> AppResult<Vec<u8
|
||||
|
||||
let resp = client
|
||||
.post(url)
|
||||
.bearer_auth(&state.config.auth.proxy_token)
|
||||
.bearer_auth(state.runtime_proxy_token())
|
||||
.json(&json!({
|
||||
"model": "tts-1",
|
||||
"input": text,
|
||||
|
||||
@@ -307,7 +307,8 @@ fn caller_from_headers(state: &AppState, headers: &HeaderMap) -> AppResult<Workf
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing authorization token".into()))?;
|
||||
|
||||
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
if token == state.config.auth.auth_token || token == proxy_token {
|
||||
return Ok(WorkflowCaller::Admin);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ fn resolve_caller(
|
||||
let token = bearer(headers)
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing Authorization header".into()))?;
|
||||
|
||||
if token == state.config.auth.auth_token || token == state.config.auth.proxy_token {
|
||||
let proxy_token = state.runtime_proxy_token();
|
||||
if token == state.config.auth.auth_token || token == proxy_token {
|
||||
return Ok(WorkspaceCaller::Admin);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user