Skip to content

Commit 9d425b2

Browse files
authored
feat: add allowed_users config for per-user access control (#108)
* feat: add allowed_users config for per-user access control - Add allowed_users field to DiscordConfig (serde default = empty) - Add user ID check in discord message handler; react 🚫 if denied - Extract parse_id_set() helper to DRY up channel/user ID parsing - Fail closed when all configured IDs are invalid - Log tracing::warn on 🚫 reaction failure and invalid ID entries - Helm: use toJson for both allowedChannels and allowedUsers - Helm: add regexMatch validation for allowedUsers (--set mangling) - Consistent rendering: both lists always rendered (no if-condition) Closes #107 * refactor: improve logging and style for allowed_users - Upgrade denied user log from debug to info (security audit) - Add parsed allowlist count log after parse_id_set - Simplify ReactionType::Unicode path via import * docs: add allowed_users setup guide and config examples - discord-bot-howto.md: add User ID setup (step 7), allowed_users config example, access control behavior table - README.md: add allowed_users to Quick Start and config reference * fix: add trailing newline to main.rs --------- Co-authored-by: masami-agent <masami-agent@users.noreply.github.com>
1 parent df911c1 commit 9d425b2

8 files changed

Lines changed: 69 additions & 10 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Edit `config.toml`:
5050
[discord]
5151
bot_token = "${DISCORD_BOT_TOKEN}"
5252
allowed_channels = ["YOUR_CHANNEL_ID"]
53+
# allowed_users = ["YOUR_USER_ID"] # optional: restrict who can use the bot
5354

5455
[agent]
5556
command = "kiro-cli"
@@ -164,6 +165,7 @@ env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" }
164165
[discord]
165166
bot_token = "${DISCORD_BOT_TOKEN}" # supports env var expansion
166167
allowed_channels = ["123456789"] # channel ID allowlist
168+
# allowed_users = ["987654321"] # user ID allowlist (empty = all users)
167169

168170
[agent]
169171
command = "kiro-cli" # CLI command

charts/openab/templates/configmap.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ data:
1717
{{- fail (printf "discord.allowedChannels contains a mangled ID: %s — use --set-string instead of --set for channel IDs" (toString .)) }}
1818
{{- end }}
1919
{{- end }}
20-
allowed_channels = [{{ range $i, $ch := $cfg.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}]
20+
allowed_channels = {{ $cfg.discord.allowedChannels | default list | toJson }}
21+
{{- range $cfg.discord.allowedUsers }}
22+
{{- if regexMatch "e\\+|E\\+" (toString .) }}
23+
{{- fail (printf "discord.allowedUsers contains a mangled ID: %s — use --set-string instead of --set for user IDs" (toString .)) }}
24+
{{- end }}
25+
{{- end }}
26+
allowed_users = {{ $cfg.discord.allowedUsers | default list | toJson }}
2127
2228
[agent]
2329
command = "{{ $cfg.command }}"

charts/openab/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ agents:
2828
# # ⚠️ Use --set-string for channel IDs to avoid float64 precision loss
2929
# allowedChannels:
3030
# - "YOUR_CHANNEL_ID"
31+
# allowedUsers: []
3132
# workingDir: /home/agent
3233
# env: {}
3334
# envFrom: []
@@ -57,6 +58,8 @@ agents:
5758
# ⚠️ Use --set-string for channel IDs to avoid float64 precision loss
5859
allowedChannels:
5960
- "YOUR_CHANNEL_ID"
61+
# ⚠️ Use --set-string for user IDs to avoid float64 precision loss
62+
allowedUsers: [] # empty = allow all users (default)
6063
workingDir: /home/agent
6164
env: {}
6265
envFrom: []

config.toml.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[discord]
22
bot_token = "${DISCORD_BOT_TOKEN}"
33
allowed_channels = ["1234567890"]
4+
# allowed_users = ["<YOUR_DISCORD_USER_ID>"] # empty or omitted = allow all users
45

56
[agent]
67
command = "kiro-cli"

docs/discord-bot-howto.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ Step-by-step guide to create and configure a Discord bot for openab.
4747
3. Click **Copy Channel ID**
4848
4. Use this ID in `allowed_channels` in your config
4949

50-
## 7. Configure openab
50+
## 7. Get Your User ID (optional)
51+
52+
1. Make sure **Developer Mode** is enabled (see step 6)
53+
2. Right-click your own username (in a message or the member list)
54+
3. Click **Copy User ID**
55+
4. Use this ID in `allowed_users` to restrict who can interact with the bot
56+
57+
## 8. Configure openab
5158

5259
Set the bot token and channel ID:
5360

@@ -61,15 +68,28 @@ In `config.toml`:
6168
[discord]
6269
bot_token = "${DISCORD_BOT_TOKEN}"
6370
allowed_channels = ["your-channel-id-from-step-6"]
71+
# allowed_users = ["your-user-id-from-step-7"] # optional: restrict who can use the bot
6472
```
6573

74+
### Access control behavior
75+
76+
| `allowed_channels` | `allowed_users` | Result |
77+
|---|---|---|
78+
| empty | empty | All users, all channels (default) |
79+
| set | empty | Only these channels, all users |
80+
| empty | set | All channels, only these users |
81+
| set | set | **AND** — must be in allowed channel AND allowed user |
82+
83+
- Empty `allowed_users` (default) = no user filtering, fully backward compatible
84+
- Denied users get a 🚫 reaction and no reply
85+
6686
For Kubernetes:
6787
```bash
6888
kubectl create secret generic openab-secret \
6989
--from-literal=discord-bot-token="your-token-from-step-3"
7090
```
7191

72-
## 8. Test
92+
## 9. Test
7393

7494
In the allowed channel, mention the bot:
7595

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pub struct DiscordConfig {
1818
pub bot_token: String,
1919
#[serde(default)]
2020
pub allowed_channels: Vec<String>,
21+
#[serde(default)]
22+
pub allowed_users: Vec<String>,
2123
}
2224

2325
#[derive(Debug, Deserialize)]

src/discord.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::config::ReactionsConfig;
33
use crate::format;
44
use crate::reactions::StatusReactionController;
55
use serenity::async_trait;
6-
use serenity::model::channel::Message;
6+
use serenity::model::channel::{Message, ReactionType};
77
use serenity::model::gateway::Ready;
88
use serenity::model::id::{ChannelId, MessageId};
99
use serenity::prelude::*;
@@ -15,6 +15,7 @@ use tracing::{error, info};
1515
pub struct Handler {
1616
pub pool: Arc<SessionPool>,
1717
pub allowed_channels: HashSet<u64>,
18+
pub allowed_users: HashSet<u64>,
1819
pub reactions_config: ReactionsConfig,
1920
}
2021

@@ -64,6 +65,14 @@ impl EventHandler for Handler {
6465
return;
6566
}
6667

68+
if !self.allowed_users.is_empty() && !self.allowed_users.contains(&msg.author.id.get()) {
69+
tracing::info!(user_id = %msg.author.id, "denied user, ignoring");
70+
if let Err(e) = msg.react(&ctx.http, ReactionType::Unicode("🚫".into())).await {
71+
tracing::warn!(error = %e, "failed to react with 🚫");
72+
}
73+
return;
74+
}
75+
6776
let prompt = if is_mentioned {
6877
strip_mention(&msg.content)
6978
} else {

src/main.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,22 @@ async fn main() -> anyhow::Result<()> {
2929
agent_cmd = %cfg.agent.command,
3030
pool_max = cfg.pool.max_sessions,
3131
channels = ?cfg.discord.allowed_channels,
32+
users = ?cfg.discord.allowed_users,
3233
reactions = cfg.reactions.enabled,
3334
"config loaded"
3435
);
3536

3637
let pool = Arc::new(acp::SessionPool::new(cfg.agent, cfg.pool.max_sessions));
3738
let ttl_secs = cfg.pool.session_ttl_hours * 3600;
3839

39-
let allowed_channels: HashSet<u64> = cfg
40-
.discord
41-
.allowed_channels
42-
.iter()
43-
.filter_map(|s| s.parse().ok())
44-
.collect();
40+
let allowed_channels = parse_id_set(&cfg.discord.allowed_channels, "allowed_channels")?;
41+
let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?;
42+
info!(channels = allowed_channels.len(), users = allowed_users.len(), "parsed allowlists");
4543

4644
let handler = discord::Handler {
4745
pool: pool.clone(),
4846
allowed_channels,
47+
allowed_users,
4948
reactions_config: cfg.reactions,
5049
};
5150

@@ -84,3 +83,20 @@ async fn main() -> anyhow::Result<()> {
8483
info!("openab shut down");
8584
Ok(())
8685
}
86+
87+
fn parse_id_set(raw: &[String], label: &str) -> anyhow::Result<HashSet<u64>> {
88+
let set: HashSet<u64> = raw
89+
.iter()
90+
.filter_map(|s| match s.parse() {
91+
Ok(id) => Some(id),
92+
Err(_) => {
93+
tracing::warn!(value = %s, label = label, "ignoring invalid entry");
94+
None
95+
}
96+
})
97+
.collect();
98+
if !raw.is_empty() && set.is_empty() {
99+
anyhow::bail!("all {label} entries failed to parse — refusing to start with an empty allowlist");
100+
}
101+
Ok(set)
102+
}

0 commit comments

Comments
 (0)