Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ allowed_channels = ["1234567890"] # ↑ omitted + non-empty list → auto-
# allowed_users = ["U1234567890abcdef0123456789abcdef"] # LINE user IDs (U…, 33 chars)
# # env fallback: LINE_ALLOWED_USERS (comma-separated)

# --- WeCom / Google Chat / MS Teams (first-class trust sections) ---
# Same shape as [line]: L3 identity trust per platform, replacing the uniform
# GATEWAY_ALLOW_ALL_USERS / GATEWAY_ALLOWED_USERS env for that platform
# (deprecated: warns at startup; becomes an error in Phase 2). Platform
# credentials stay on their gateway env vars (WECOM_CORP_ID/WECOM_SECRET,
# GOOGLE_CHAT_*, TEAMS_APP_ID/TEAMS_APP_SECRET).
# [wecom]
# allow_all_users = false # env fallback: WECOM_ALLOW_ALL_USERS
# allowed_users = ["zhangsan", "lisi"] # WeCom UserIDs (tenant-assigned, freeform strings)
# # env fallback: WECOM_ALLOWED_USERS (comma-separated)
# [googlechat]
# allow_all_users = false # env fallback: GOOGLE_CHAT_ALLOW_ALL_USERS
# allowed_users = ["users/123456789"] # Chat user resource names (users/<id>)
# # env fallback: GOOGLE_CHAT_ALLOWED_USERS (comma-separated)
# [teams]
# allow_all_users = false # env fallback: TEAMS_ALLOW_ALL_USERS
# allowed_users = ["29:1abc..."] # Bot Framework activity.from.id values (29:…)
# # env fallback: TEAMS_ALLOWED_USERS (comma-separated)

# --- AgentCore Runtime (alternative to [agent]) ---
# When [agentcore] is set and [agent].command is not explicitly provided,
# OAB auto-spawns the bundled agentcore-acp adapter. No manual wiring needed.
Expand Down
193 changes: 193 additions & 0 deletions crates/openab-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ pub struct Config {
pub gateway: Option<GatewayConfig>,
pub telegram: Option<TelegramConfig>,
pub line: Option<LineConfig>,
pub wecom: Option<PlatformTrustConfig>,
pub googlechat: Option<PlatformTrustConfig>,
pub teams: Option<PlatformTrustConfig>,
pub agentcore: Option<AgentCoreConfig>,
#[serde(default)]
pub agent: AgentConfig,
Expand Down Expand Up @@ -833,6 +836,76 @@ impl LineConfig {
}
}

/// Shared first-class trust section for gateway platforms whose Phase 1 needs
/// exactly the two L3 identity fields (identity-trust-none ADR): `[wecom]`,
/// `[googlechat]`, `[teams]`. Same shape and resolution order as
/// [`LineConfig`] / [`TelegramConfig`]: `[section].field` (with `${}`
/// expansion) → `{PREFIX}_*` env var → deny-all default.
///
/// Trust-only by design — platform credentials stay on the gateway env vars
/// the webhook adapters read (`WECOM_CORP_ID`/`WECOM_SECRET`,
/// `GOOGLE_CHAT_*`, `TEAMS_APP_ID`/`TEAMS_APP_SECRET`). Platforms that later
/// need extra trust fields (e.g. `trusted_bot_ids`) graduate to their own
/// struct, as LINE will for group policy.
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default)]
pub struct PlatformTrustConfig {
/// Explicit flag: true = allow all users, false = check `allowed_users`.
/// When not set, defaults to `false` (deny-all, per identity-trust-none ADR).
/// Env fallback: `{PREFIX}_ALLOW_ALL_USERS` (empty string treated as unset).
///
/// **Note:** When this resolves to `true`, the `allowed_users` list is
/// bypassed entirely — all users are permitted regardless of list contents.
pub allow_all_users: Option<bool>,
/// Platform user IDs allowed to interact with the bot. Only checked when
/// `allow_all_users` resolves to `false`. Env fallback:
/// `{PREFIX}_ALLOWED_USERS` (comma-separated).
/// `None` = not set (fall back to env); `Some([])` = explicit empty (deny all).
pub allowed_users: Option<Vec<String>>,
}

/// Fully resolved platform trust settings (config → env → default applied).
#[derive(Debug, Clone)]
pub struct ResolvedPlatformTrust {
pub allow_all_users: bool,
pub allowed_users: Vec<String>,
}

impl PlatformTrustConfig {
/// Resolve both fields against `{prefix}_ALLOW_ALL_USERS` /
/// `{prefix}_ALLOWED_USERS` env fallbacks (e.g. prefix `"WECOM"`,
/// `"GOOGLE_CHAT"`, `"TEAMS"`).
pub fn resolve_with_env(&self, prefix: &str) -> ResolvedPlatformTrust {
let allowed_users: Vec<String> = match &self.allowed_users {
Some(list) => list.clone(),
None => std::env::var(format!("{prefix}_ALLOWED_USERS"))
.unwrap_or_default()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
};
ResolvedPlatformTrust {
allow_all_users: self.allow_all_users.unwrap_or_else(|| {
std::env::var(format!("{prefix}_ALLOW_ALL_USERS"))
.ok()
.filter(|v| !v.is_empty())
.map(|v| v != "0" && !v.eq_ignore_ascii_case("false"))
.unwrap_or(false)
}),
allowed_users,
}
}

/// Whether first-class trust env vars exist for `prefix` — used by the
/// binary to decide whether the platform's trust still rides on the
/// deprecated uniform `GATEWAY_*` seed (and to warn when it does).
pub fn env_trust_present(prefix: &str) -> bool {
std::env::var(format!("{prefix}_ALLOW_ALL_USERS")).is_ok()
|| std::env::var(format!("{prefix}_ALLOWED_USERS")).is_ok()
}
}

/// Raw intermediate struct for serde — uses `Option` to detect explicit fields.
#[derive(Debug, Deserialize)]
#[serde(default)]
Expand Down Expand Up @@ -1987,6 +2060,126 @@ allowed_users = ["U1234567890abcdef0123456789abcdef"]
std::env::remove_var("LINE_ALLOWED_USERS");
}

#[test]
fn platform_trust_sections_parse_from_toml() {
let toml_str = r#"
[discord]
bot_token = "x"

[wecom]
allowed_users = ["zhangsan", "lisi"]

[googlechat]
allowed_users = ["users/123456789"]

[teams]
allow_all_users = true
"#;
let cfg = parse_config_str(toml_str, "test").unwrap();
let wecom = cfg.wecom.expect("wecom section");
assert_eq!(
wecom.allowed_users.as_deref(),
Some(&["zhangsan".to_string(), "lisi".to_string()][..])
);
assert_eq!(wecom.allow_all_users, None);
let gc = cfg.googlechat.expect("googlechat section");
assert_eq!(
gc.allowed_users.as_deref(),
Some(&["users/123456789".to_string()][..])
);
let teams = cfg.teams.expect("teams section");
assert_eq!(teams.allow_all_users, Some(true));

// Absent sections → None (trust falls back to legacy GATEWAY_* seed).
let cfg = parse_config_str("[discord]\nbot_token = \"x\"\n", "test").unwrap();
assert!(cfg.wecom.is_none());
assert!(cfg.googlechat.is_none());
assert!(cfg.teams.is_none());
}

/// All `WECOM_*` env scenarios in ONE test — std::env is process-global and
/// cargo runs tests in parallel (same pattern as
/// `line_resolve_all_scenarios`). The WECOM prefix stands in for all
/// `PlatformTrustConfig` users; the prefix is a plain format argument, so
/// GOOGLE_CHAT/TEAMS behave identically by construction.
#[test]
fn platform_trust_resolve_all_scenarios() {
// --- Scenario 1: defaults — deny-all per identity-trust-none ADR ---
std::env::remove_var("WECOM_ALLOW_ALL_USERS");
std::env::remove_var("WECOM_ALLOWED_USERS");
let r = PlatformTrustConfig::default().resolve_with_env("WECOM");
assert!(!r.allow_all_users);
assert!(r.allowed_users.is_empty());
assert!(!PlatformTrustConfig::env_trust_present("WECOM"));

// --- Scenario 2: config wins over env ---
std::env::set_var("WECOM_ALLOW_ALL_USERS", "true");
std::env::set_var("WECOM_ALLOWED_USERS", "mallory"); // ignored — config list wins
let cfg = PlatformTrustConfig {
allow_all_users: Some(false),
allowed_users: Some(vec!["zhangsan".into()]),
};
let r = cfg.resolve_with_env("WECOM");
assert!(!r.allow_all_users);
assert_eq!(r.allowed_users, vec!["zhangsan".to_string()]);

// --- Scenario 3: env fallback (comma-separated, trimmed, empties dropped) ---
std::env::set_var("WECOM_ALLOWED_USERS", " zhangsan , lisi,,wangwu ");
let r = PlatformTrustConfig::default().resolve_with_env("WECOM");
assert!(r.allow_all_users); // from WECOM_ALLOW_ALL_USERS=true
assert_eq!(
r.allowed_users,
vec![
"zhangsan".to_string(),
"lisi".to_string(),
"wangwu".to_string()
]
);
assert!(PlatformTrustConfig::env_trust_present("WECOM"));

// --- Scenario 4: empty-string env flag treated as unset → deny-all ---
std::env::set_var("WECOM_ALLOW_ALL_USERS", "");
std::env::remove_var("WECOM_ALLOWED_USERS");
assert!(
!PlatformTrustConfig::default()
.resolve_with_env("WECOM")
.allow_all_users
);

// --- Scenario 5: "0"/"false" env values resolve false ---
std::env::set_var("WECOM_ALLOW_ALL_USERS", "0");
assert!(
!PlatformTrustConfig::default()
.resolve_with_env("WECOM")
.allow_all_users
);
std::env::set_var("WECOM_ALLOW_ALL_USERS", "false");
assert!(
!PlatformTrustConfig::default()
.resolve_with_env("WECOM")
.allow_all_users
);

// --- Scenario 6: explicit empty config list = deny-all, ignores env ---
std::env::set_var("WECOM_ALLOWED_USERS", "mallory");
let cfg = PlatformTrustConfig {
allow_all_users: None,
allowed_users: Some(vec![]),
};
assert!(cfg.resolve_with_env("WECOM").allowed_users.is_empty());

// --- Scenario 7: prefixes are independent — WECOM env must not bleed
// into another prefix ---
assert!(!PlatformTrustConfig::env_trust_present("TEAMS"));
assert!(PlatformTrustConfig::default()
.resolve_with_env("TEAMS")
.allowed_users
.is_empty());

std::env::remove_var("WECOM_ALLOW_ALL_USERS");
std::env::remove_var("WECOM_ALLOWED_USERS");
}

#[test]
fn hooks_any_configured_false_when_empty() {
let h = HooksConfig::default();
Expand Down
37 changes: 37 additions & 0 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ Custom Gateway adapter for platforms like Telegram, LINE, Feishu/Lark, and Googl

First-class L3 identity trust for the LINE adapter (identity-trust-none ADR, Phase 1). Replaces the uniform `GATEWAY_ALLOW_ALL_USERS` / `GATEWAY_ALLOWED_USERS` env vars for LINE — relying on those for LINE is deprecated and warns at startup. Channel credentials remain on the `LINE_CHANNEL_SECRET` / `LINE_CHANNEL_ACCESS_TOKEN` env vars.

> **Mode scoping:** takes effect on the **embedded/unified adapter path** (see the note under `[wecom]` / `[googlechat]` / `[teams]` below — the same applies here).

Each field resolves: config value → `LINE_*` env var → default (deny-all).

| Key | Type | Default | Description |
Expand All @@ -154,6 +156,41 @@ allowed_users = ["U1234567890abcdef0123456789abcdef"]

---

## `[wecom]` / `[googlechat]` / `[teams]`

First-class L3 identity trust for the remaining gateway platforms — same shape and semantics as `[line]`. Each section replaces the uniform `GATEWAY_ALLOW_ALL_USERS` / `GATEWAY_ALLOWED_USERS` env vars for its platform (deprecated: warns at startup, becomes an error in Phase 2). Platform credentials remain on the gateway env vars (`WECOM_CORP_ID`/`WECOM_SECRET`, `GOOGLE_CHAT_*`, `TEAMS_APP_ID`/`TEAMS_APP_SECRET`).

> **Mode scoping:** these sections (like `[line]`) take effect on the **embedded/unified adapter path**, where events pass the shared ingress trust gate. Deployments using the standalone `openab-gateway` companion over WebSocket enforce trust via `[gateway].allow_all_users` / `allowed_users` instead; Phase 1c consolidates the two paths.

Each field resolves: config value → `{PREFIX}_*` env var → default (deny-all). Env prefixes: `WECOM`, `GOOGLE_CHAT`, `TEAMS`.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `allow_all_users` | bool \| omit | `false` (deny-all) | `true` = any user may interact (bypasses `allowed_users` entirely). Env fallback: `{PREFIX}_ALLOW_ALL_USERS`. |
| `allowed_users` | string[] | `[]` | Platform user IDs allowed to interact. Only checked when `allow_all_users` resolves to false. Env fallback: `{PREFIX}_ALLOWED_USERS` (comma-separated). |

Sender ID formats per platform:

| Platform | Sender ID format | Example |
|----------|-----------------|---------|
| WeCom | Tenant-assigned UserID (freeform string) | `"zhangsan"` |
| Google Chat | User resource name | `"users/123456789"` |
| MS Teams | Bot Framework `activity.from.id` | `"29:1abc..."` |

```toml
[wecom]
allowed_users = ["zhangsan", "lisi"]

[googlechat]
allowed_users = ["users/123456789"]

[teams]
allowed_users = ["29:1abc..."]
# allow_all_users = true # explicit opt-in only
```

---

## `[agent]`

The AI agent subprocess that OpenAB spawns to handle messages via ACP.
Expand Down
16 changes: 16 additions & 0 deletions docs/google-chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ allow_all_users = true
[agent]
```

### User Trust (`[googlechat]` section)

> **Mode scoping:** the `[googlechat]` section applies when the Google Chat adapter is **embedded in the OAB binary** (unified mode, `GOOGLE_CHAT_ENABLED=true` env set on the OAB container). In the standalone-gateway mode shown above, trust is enforced by `[gateway].allow_all_users` / `allowed_users` instead — the `[googlechat]` section has no effect on that path yet (Phase 1c consolidates the two).

Identity trust defaults to **deny-all** (identity-trust-none ADR): unknown senders are rejected until explicitly admitted. Configure trust with a first-class `[googlechat]` section:

```toml
[googlechat]
allowed_users = ["users/123456789"] # Chat user resource names (users/<id>)
# allow_all_users = true # explicit opt-in only — any user can drive the agent
```

Each field falls back to its `GOOGLE_CHAT_ALLOW_ALL_USERS` / `GOOGLE_CHAT_ALLOWED_USERS` env var when unset.

> ⚠️ **Deprecated:** driving Google Chat trust through the uniform `GATEWAY_ALLOW_ALL_USERS` / `GATEWAY_ALLOWED_USERS` env vars still works but logs a startup warning; it will become a startup error in a later phase. Migrate to `[googlechat]` (or `GOOGLE_CHAT_*` env vars).

## Features

### Supported
Expand Down
2 changes: 2 additions & 0 deletions docs/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ platform = "line"

### User Trust (`[line]` section)

> **Mode scoping:** the `[line]` section applies when the LINE adapter is **embedded in the OAB binary** (unified mode, `LINE_CHANNEL_SECRET` env set on the OAB container). In the standalone-gateway mode shown above, trust is enforced by `[gateway].allow_all_users` / `allowed_users` instead — the `[line]` section has no effect on that path yet (Phase 1c consolidates the two).

Identity trust defaults to **deny-all** (identity-trust-none ADR): unknown senders are rejected until explicitly admitted. Configure trust with a first-class `[line]` section:

```toml
Expand Down
16 changes: 16 additions & 0 deletions docs/msteams-selfhosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ tables = "off"

Set `TEAMS_APP_ID` and `TEAMS_APP_SECRET` on the container. No `[gateway]` needed.

### User Trust (`[teams]` section)

Identity trust defaults to **deny-all** (identity-trust-none ADR): unknown senders are rejected until explicitly admitted. Configure trust with a first-class `[teams]` section:

```toml
[teams]
allowed_users = ["29:1abc..."] # Bot Framework activity.from.id values
# allow_all_users = true # explicit opt-in only — any user can drive the agent
```

Each field falls back to its `TEAMS_ALLOW_ALL_USERS` / `TEAMS_ALLOWED_USERS` env var when unset. To find a user's `29:…` ID, check the OAB logs — the sender ID is logged for each incoming message.

> **Mode scoping:** the `[teams]` section applies to this unified (embedded) mode. In the legacy standalone-gateway mode below, trust is enforced by `[gateway].allow_all_users` / `allowed_users` instead — the `[teams]` section has no effect on that path yet (Phase 1c consolidates the two).

> ⚠️ **Deprecated:** driving Teams trust through the uniform `GATEWAY_ALLOW_ALL_USERS` / `GATEWAY_ALLOWED_USERS` env vars still works but logs a startup warning; it will become a startup error in a later phase. Migrate to `[teams]` (or `TEAMS_*` env vars).

---

## Standalone Gateway Mode (Legacy)
Expand Down
16 changes: 16 additions & 0 deletions docs/wecom.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ max_sessions = 10
| `allow_all_channels` | No | Allow messages from all channels (default: `false`) |
| `allow_all_users` | No | Allow messages from all users (default: `false`) |

### User Trust (`[wecom]` section)

> **Mode scoping:** the `[wecom]` section applies when the WeCom adapter is **embedded in the OAB binary** (unified mode, `WECOM_CORP_ID` env set on the OAB container). In the standalone-gateway mode shown above, trust is enforced by `[gateway].allow_all_users` / `allowed_users` instead — the `[wecom]` section has no effect on that path yet (Phase 1c consolidates the two).

Identity trust defaults to **deny-all** (identity-trust-none ADR): unknown senders are rejected until explicitly admitted. Configure trust with a first-class `[wecom]` section:

```toml
[wecom]
allowed_users = ["zhangsan", "lisi"] # WeCom UserIDs (tenant-assigned, freeform strings)
# allow_all_users = true # explicit opt-in only — any user can drive the agent
```

Each field falls back to its `WECOM_ALLOW_ALL_USERS` / `WECOM_ALLOWED_USERS` env var when unset.

> ⚠️ **Deprecated:** driving WeCom trust through the uniform `GATEWAY_ALLOW_ALL_USERS` / `GATEWAY_ALLOWED_USERS` env vars still works but logs a startup warning; it will become a startup error in a later phase. Migrate to `[wecom]` (or `WECOM_*` env vars).

## 6. Expose the Gateway (HTTPS)

WeCom requires a publicly accessible HTTPS URL for callbacks.
Expand Down
Loading
Loading