Skip to content

Commit bcd03ff

Browse files
authored
feat(trust): gateway echo-on-deny (Phase 2) + trust-none default (Phase 3) (#1273)
* feat(trust): gateway echo-on-deny (Phase 2) + deny-all default (Phase 3) - Phase 2: on Decision::DenyIdentity, echo the sender their ID via adapter.send_message so they can request access (request-access UX). Throttled to 1 echo per (platform,sender) per 5min (LazyLock map) to prevent amplification. DenyScope stays silent (not a security boundary). - Phase 3 (gateway): flip GATEWAY_ALLOW_ALL_USERS default true→false, so gateway L3 is trust-none by default. L2 (channels) stays open. Admit via GATEWAY_ALLOWED_USERS or GATEWAY_ALLOW_ALL_USERS=true. Ships in pre-beta (self-use). emilie unaffected (own image + WS path, which doesn't use this registry/echo). Discord/Slack registry entries unchanged (still behavior-preserving allow-all). Refs #1264 #1269 * test(trust): echo throttle regression test --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
1 parent c72c86a commit bcd03ff

2 files changed

Lines changed: 76 additions & 11 deletions

File tree

crates/openab-core/src/gateway.rs

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,28 @@ pub struct GatewayEventContext {
11401140
///
11411141
/// This is the core event-handling logic extracted from the WebSocket handler,
11421142
/// made available for the unified binary to call directly from axum webhook handlers.
1143+
/// Throttle for request-access echoes: at most one echo per (platform, sender)
1144+
/// per [`ECHO_WINDOW`], to prevent an untrusted spammer from being amplified by
1145+
/// the bot's replies.
1146+
static ECHO_THROTTLE: std::sync::LazyLock<
1147+
std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1148+
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
1149+
1150+
const ECHO_WINDOW: std::time::Duration = std::time::Duration::from_secs(300);
1151+
1152+
/// Returns true if an echo to `key` is allowed now (and records the timestamp).
1153+
fn echo_allowed(key: &str) -> bool {
1154+
let now = std::time::Instant::now();
1155+
let mut map = ECHO_THROTTLE.lock().unwrap();
1156+
match map.get(key) {
1157+
Some(prev) if now.duration_since(*prev) < ECHO_WINDOW => false,
1158+
_ => {
1159+
map.insert(key.to_string(), now);
1160+
true
1161+
}
1162+
}
1163+
}
1164+
11431165
pub async fn process_gateway_event(
11441166
event_json: &str,
11451167
ctx: &GatewayEventContext,
@@ -1174,16 +1196,47 @@ pub async fn process_gateway_event(
11741196
let decision =
11751197
ctx.router
11761198
.gate_incoming(&event.platform, &event.channel.id, false, &event.sender.id);
1177-
if !decision.is_allowed() {
1178-
tracing::info!(
1179-
platform = %event.platform,
1180-
sender = %event.sender.id,
1181-
channel = %event.channel.id,
1182-
?decision,
1183-
"gateway event denied by trust gate"
1184-
);
1185-
// Phase 2 will echo the sender their ID on Decision::DenyIdentity.
1186-
return Ok(false);
1199+
match decision {
1200+
crate::trust::Decision::Allow => {}
1201+
crate::trust::Decision::DenyIdentity => {
1202+
// L3 identity deny → echo the sender their ID so they can request
1203+
// access (throttled to avoid amplification). Bots never reach here
1204+
// (should_skip_event handles bot admission; L3 is human-only).
1205+
tracing::info!(
1206+
platform = %event.platform,
1207+
sender = %event.sender.id,
1208+
channel = %event.channel.id,
1209+
"gateway event denied (identity); echoing request-access"
1210+
);
1211+
let throttle_key = format!("{}:{}", event.platform, event.sender.id);
1212+
if echo_allowed(&throttle_key) {
1213+
let echo_channel = ChannelRef {
1214+
platform: event.platform.clone(),
1215+
channel_id: event.channel.id.clone(),
1216+
thread_id: event.channel.thread_id.clone(),
1217+
parent_id: None,
1218+
origin_event_id: Some(event.event_id.clone()),
1219+
};
1220+
let echo = format!(
1221+
"⚠️ You are not on this bot's trusted list.\nYour ID: {}\nAsk the admin to add it to allowed_users.",
1222+
event.sender.id
1223+
);
1224+
let _ = ctx.adapter.send_message(&echo_channel, &echo).await;
1225+
}
1226+
return Ok(false);
1227+
}
1228+
// DenyScope (and any future variant) → silent drop (scope is not a
1229+
// security boundary; no request-access echo).
1230+
_ => {
1231+
tracing::info!(
1232+
platform = %event.platform,
1233+
sender = %event.sender.id,
1234+
channel = %event.channel.id,
1235+
?decision,
1236+
"gateway event denied (scope); silent"
1237+
);
1238+
return Ok(false);
1239+
}
11871240
}
11881241

11891242
tracing::info!(
@@ -1420,6 +1473,15 @@ mod tests {
14201473
use super::*;
14211474
use std::collections::HashSet;
14221475

1476+
#[test]
1477+
fn echo_allowed_throttles_repeat_within_window() {
1478+
// Unique key so we don't collide with other tests touching the global map.
1479+
let key = "test-platform:test-sender-echo-throttle";
1480+
assert!(echo_allowed(key), "first echo should be allowed");
1481+
assert!(!echo_allowed(key), "immediate repeat should be throttled");
1482+
assert!(!echo_allowed(key), "still throttled within the window");
1483+
}
1484+
14231485
fn make_event(is_bot: bool, sender_id: &str, channel_id: &str, channel_type: &str, thread_id: Option<&str>, mentions: Vec<&str>) -> GatewayEvent {
14241486
serde_json::from_value(serde_json::json!({
14251487
"schema": "openab.gateway.event.v1",

src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,10 @@ async fn main() -> anyhow::Result<()> {
278278
};
279279
let allow_all_channels = env_bool("GATEWAY_ALLOW_ALL_CHANNELS", true);
280280
let allowed_channels = env_set("GATEWAY_ALLOWED_CHANNELS");
281-
let allow_all_users = env_bool("GATEWAY_ALLOW_ALL_USERS", true);
281+
// L3 identity: trust-none by default (Phase 3). Was `true` in #1267
282+
// (behavior-preserving); now defaults deny-all — set GATEWAY_ALLOW_ALL_USERS=true
283+
// or list GATEWAY_ALLOWED_USERS to admit senders. L2 (channels) stays open.
284+
let allow_all_users = env_bool("GATEWAY_ALLOW_ALL_USERS", false);
282285
let allowed_users = env_set("GATEWAY_ALLOWED_USERS");
283286
let mut reg = PlatformTrustConfigs::new();
284287
for platform in ["telegram", "line", "feishu", "wecom", "googlechat", "teams"] {

0 commit comments

Comments
 (0)