mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix: require Feishu webhook authentication (#1638)
* fix: require Feishu webhook authentication * fix: handle Feishu v2 webhook token auth * fix: skip empty verification token write, consistent with app_id/app_secret Address zmanian review nit #4: only write verification_token to workspace when present, matching the if-let pattern used for app_id and app_secret. Functionally identical (the auth check filters empty strings), but consistent. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7234700c78
commit
30db07c58e
@@ -317,6 +317,14 @@ impl LoadedChannel {
|
||||
.map(|f| f.webhook_secret_name())
|
||||
.unwrap_or_else(|| format!("{}_webhook_secret", self.channel.channel_name()))
|
||||
}
|
||||
|
||||
/// Whether the host should enforce generic webhook-secret validation.
|
||||
pub fn webhook_secret_managed_by_host(&self) -> bool {
|
||||
self.capabilities_file
|
||||
.as_ref()
|
||||
.map(|f| f.webhook_secret_managed_by_host())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Results from loading multiple channels.
|
||||
|
||||
@@ -185,6 +185,19 @@ impl ChannelCapabilitiesFile {
|
||||
.and_then(|w| w.secret_name.clone())
|
||||
.unwrap_or_else(|| format!("{}_webhook_secret", self.name))
|
||||
}
|
||||
|
||||
/// Whether the host should enforce generic webhook-secret validation.
|
||||
///
|
||||
/// Defaults to true. Channels can opt out when they validate the shared
|
||||
/// secret themselves using provider-specific request body fields.
|
||||
pub fn webhook_secret_managed_by_host(&self) -> bool {
|
||||
self.capabilities
|
||||
.channel
|
||||
.as_ref()
|
||||
.and_then(|c| c.webhook.as_ref())
|
||||
.and_then(|w| w.managed_by_host)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema for channel capabilities.
|
||||
@@ -302,6 +315,14 @@ pub struct WebhookSchema {
|
||||
/// Secret name in secrets store for HMAC-SHA256 signing (Slack-style).
|
||||
#[serde(default)]
|
||||
pub hmac_secret_name: Option<String>,
|
||||
|
||||
/// Whether the host/router should enforce generic webhook-secret
|
||||
/// validation before the channel sees the request.
|
||||
///
|
||||
/// Default: true. Set to false when the provider sends the shared secret
|
||||
/// in a provider-specific request field rather than the configured header.
|
||||
#[serde(default)]
|
||||
pub managed_by_host: Option<bool>,
|
||||
}
|
||||
|
||||
/// Setup configuration schema.
|
||||
@@ -611,6 +632,25 @@ mod tests {
|
||||
Some("X-Telegram-Bot-Api-Secret-Token")
|
||||
);
|
||||
assert_eq!(file.webhook_secret_name(), "telegram_webhook_secret");
|
||||
assert!(file.webhook_secret_managed_by_host());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webhook_schema_can_disable_host_managed_secret_validation() {
|
||||
let json = r#"{
|
||||
"name": "feishu",
|
||||
"capabilities": {
|
||||
"channel": {
|
||||
"webhook": {
|
||||
"secret_name": "feishu_verification_token",
|
||||
"managed_by_host": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let file = ChannelCapabilitiesFile::from_json(json).unwrap();
|
||||
assert!(!file.webhook_secret_managed_by_host());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -139,13 +139,18 @@ async fn register_channel(
|
||||
};
|
||||
|
||||
let secret_header = loaded.webhook_secret_header().map(|s| s.to_string());
|
||||
let host_webhook_secret = if loaded.webhook_secret_managed_by_host() {
|
||||
webhook_secret.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let webhook_path = format!("/webhook/{}", channel_name);
|
||||
let endpoints = vec![RegisteredEndpoint {
|
||||
channel_name: channel_name.clone(),
|
||||
path: webhook_path,
|
||||
methods: vec!["POST".to_string()],
|
||||
require_secret: webhook_secret.is_some(),
|
||||
require_secret: host_webhook_secret.is_some(),
|
||||
}];
|
||||
|
||||
let channel_arc = Arc::new(loaded.channel.with_owner_actor_id(owner_actor_id.clone()));
|
||||
@@ -205,7 +210,7 @@ async fn register_channel(
|
||||
|
||||
tracing::info!(
|
||||
channel = %channel_name,
|
||||
has_webhook_secret = webhook_secret.is_some(),
|
||||
has_webhook_secret = host_webhook_secret.is_some(),
|
||||
secret_header = ?secret_header,
|
||||
"Registering channel with router"
|
||||
);
|
||||
@@ -214,7 +219,7 @@ async fn register_channel(
|
||||
.register(
|
||||
Arc::clone(&channel_arc),
|
||||
endpoints,
|
||||
webhook_secret.clone(),
|
||||
host_webhook_secret.clone(),
|
||||
secret_header,
|
||||
)
|
||||
.await;
|
||||
@@ -392,8 +397,9 @@ pub async fn inject_channel_credentials(
|
||||
/// placeholders in URLs and headers, so this function fills config fields
|
||||
/// that map to secret names.
|
||||
///
|
||||
/// Mapping: for a channel named "feishu", secrets `feishu_app_id` and
|
||||
/// `feishu_app_secret` are injected as config keys `app_id` and `app_secret`.
|
||||
/// Mapping: for a channel named "feishu", secrets `feishu_app_id`,
|
||||
/// `feishu_app_secret`, and `feishu_verification_token` are injected as config
|
||||
/// keys `app_id`, `app_secret`, and `verification_token`.
|
||||
async fn inject_channel_secrets_into_config(
|
||||
channel_name: &str,
|
||||
secrets_store: &Option<Arc<dyn SecretsStore + Send + Sync>>,
|
||||
@@ -404,6 +410,7 @@ async fn inject_channel_secrets_into_config(
|
||||
"feishu" => &[
|
||||
("app_id", "feishu_app_id"),
|
||||
("app_secret", "feishu_app_secret"),
|
||||
("verification_token", "feishu_verification_token"),
|
||||
],
|
||||
_ => return,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user