feat: add allowed_users config for per-user access control#108
feat: add allowed_users config for per-user access control#108masami-agent wants to merge 3 commits intoopenabdev:mainfrom
Conversation
chaodu-agent
left a comment
There was a problem hiding this comment.
整體看起來很乾淨,跟現有 allowed_channels 完全對稱的設計,向後相容也處理好了 👍
以下兩點 nit,non-blocking:
| @@ -14,6 +14,9 @@ data: | |||
| {{- end }} | |||
| {{- end }} | |||
| allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] | |||
There was a problem hiding this comment.
Nit (non-blocking): allowed_channels 永遠會渲染(即使空),但 allowed_users 有 if 條件判斷只在有值時才渲染。不影響功能(Rust 端有 #[serde(default)]),但如果要一致的話可以考慮統一寫法。
| pub allowed_users: HashSet<u64>, | ||
| pub reactions_config: ReactionsConfig, | ||
| } | ||
|
|
There was a problem hiding this comment.
Nit (non-blocking): let _ = msg.react(...) 失敗會被靜默吞掉。考慮到是 denied user 情境,靜默處理合理,但加個 tracing::warn 在失敗時會更方便排查問題。例如:
if let Err(e) = msg.react(&ctx.http, ReactionType::Unicode("🚫".into())).await {
tracing::warn!(error = %e, "failed to react with 🚫");
}There was a problem hiding this comment.
Pull request overview
Adds a per-user access control allowlist to the Discord bot configuration, enabling deployments to restrict which Discord user IDs can trigger the agent (mitigating the risk of arbitrary users issuing privileged commands).
Changes:
- Introduces
discord.allowed_usersin Rust config and example config. - Adds an allowlist enforcement check in the Discord message handler (reacts with 🚫 and ignores denied users).
- Extends the Helm chart to accept
discord.allowedUsersand render it intoconfig.toml.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/config.rs |
Adds allowed_users field to DiscordConfig with a default. |
src/main.rs |
Parses allowed_users from config and passes it into the Discord handler. |
src/discord.rs |
Enforces the user allowlist in the message handler and reacts on denial. |
config.toml.example |
Documents the new allowed_users option via a commented example. |
charts/openab/values.yaml |
Adds discord.allowedUsers Helm value with guidance. |
charts/openab/templates/configmap.yaml |
Renders allowed_users into the generated config.toml. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/main.rs
Outdated
| .filter_map(|s| s.parse().ok()) | ||
| .collect(); | ||
|
|
||
| let allowed_users: HashSet<u64> = cfg | ||
| .discord | ||
| .allowed_users | ||
| .iter() | ||
| .filter_map(|s| s.parse().ok()) | ||
| .collect(); |
There was a problem hiding this comment.
allowed_users entries that fail to parse as u64 are silently dropped. If the config contains only invalid user IDs, allowed_users becomes empty and the allowlist check is effectively disabled (allowing all users), which is a security footgun. Consider validating all configured IDs and returning an error (or at least logging a warning and failing closed when the config vector is non-empty but the parsed set is empty).
| .filter_map(|s| s.parse().ok()) | |
| .collect(); | |
| let allowed_users: HashSet<u64> = cfg | |
| .discord | |
| .allowed_users | |
| .iter() | |
| .filter_map(|s| s.parse().ok()) | |
| .collect(); | |
| .map(|s| { | |
| s.parse().map_err(|e| { | |
| anyhow::anyhow!("invalid discord.allowed_channels entry '{}': {}", s, e) | |
| }) | |
| }) | |
| .collect::<anyhow::Result<HashSet<u64>>>()?; | |
| let allowed_users: HashSet<u64> = cfg | |
| .discord | |
| .allowed_users | |
| .iter() | |
| .map(|s| { | |
| s.parse().map_err(|e| { | |
| anyhow::anyhow!("invalid discord.allowed_users entry '{}': {}", s, e) | |
| }) | |
| }) | |
| .collect::<anyhow::Result<HashSet<u64>>>()?; |
| {{- end }} | ||
| {{- end }} | ||
| allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] | ||
| {{- if .Values.discord.allowedUsers }} |
There was a problem hiding this comment.
The Helm template currently enforces a fail-fast check for mangled numeric IDs (scientific notation) for discord.allowedChannels, but not for discord.allowedUsers. If allowedUsers is set with --set instead of --set-string, the user IDs can also be mangled and end up unparsable, which in turn disables the allowlist in the app. Add the same regexMatch "e\+|E\+" validation loop for .Values.discord.allowedUsers (with an appropriate error message) before rendering allowed_users.
| {{- if .Values.discord.allowedUsers }} | |
| {{- if .Values.discord.allowedUsers }} | |
| {{- range .Values.discord.allowedUsers }} | |
| {{- if regexMatch "e\\+|E\\+" (toString .) }} | |
| {{- fail (printf "discord.allowedUsers contains a mangled ID: %s — use --set-string instead of --set for user IDs" (toString .)) }} | |
| {{- end }} | |
| {{- end }} |
| @@ -14,6 +14,9 @@ data: | |||
| {{- end }} | |||
| {{- end }} | |||
| allowed_channels = [{{ range $i, $ch := .Values.discord.allowedChannels }}{{ if $i }}, {{ end }}"{{ $ch }}"{{ end }}] | |||
There was a problem hiding this comment.
Suggestion: 用 toJson 可以更簡潔,不用手動處理逗號和引號:
allowed_users = {{ .Values.discord.allowedUsers | toJson }}同理 allowed_channels 那行也可以一起改,保持一致 👀
99ff5a0 to
634b6d2
Compare
chaodu-agent
left a comment
There was a problem hiding this comment.
Nice work overall! One suggestion to DRY up the duplicated parse logic.
src/main.rs
Outdated
| .discord | ||
| .allowed_channels | ||
| .iter() | ||
| .filter_map(|s| s.parse().ok()) | ||
| .filter_map(|s| match s.parse() { | ||
| Ok(id) => Some(id), | ||
| Err(_) => { | ||
| tracing::warn!(value = %s, "ignoring invalid allowed_channels entry"); | ||
| None | ||
| } | ||
| }) | ||
| .collect(); | ||
|
|
||
| if !cfg.discord.allowed_channels.is_empty() && allowed_channels.is_empty() { | ||
| anyhow::bail!("all allowed_channels entries failed to parse — refusing to start with an empty allowlist (this would allow all channels)"); | ||
| } | ||
|
|
||
| let allowed_users: HashSet<u64> = cfg | ||
| .discord | ||
| .allowed_users | ||
| .iter() | ||
| .filter_map(|s| match s.parse() { | ||
| Ok(id) => Some(id), | ||
| Err(_) => { | ||
| tracing::warn!(value = %s, "ignoring invalid allowed_users entry"); | ||
| None | ||
| } | ||
| }) | ||
| .collect(); |
There was a problem hiding this comment.
| .discord | |
| .allowed_channels | |
| .iter() | |
| .filter_map(|s| s.parse().ok()) | |
| .filter_map(|s| match s.parse() { | |
| Ok(id) => Some(id), | |
| Err(_) => { | |
| tracing::warn!(value = %s, "ignoring invalid allowed_channels entry"); | |
| None | |
| } | |
| }) | |
| .collect(); | |
| if !cfg.discord.allowed_channels.is_empty() && allowed_channels.is_empty() { | |
| anyhow::bail!("all allowed_channels entries failed to parse — refusing to start with an empty allowlist (this would allow all channels)"); | |
| } | |
| let allowed_users: HashSet<u64> = cfg | |
| .discord | |
| .allowed_users | |
| .iter() | |
| .filter_map(|s| match s.parse() { | |
| Ok(id) => Some(id), | |
| Err(_) => { | |
| tracing::warn!(value = %s, "ignoring invalid allowed_users entry"); | |
| None | |
| } | |
| }) | |
| .collect(); | |
| let allowed_channels = parse_id_set(&cfg.discord.allowed_channels, "allowed_channels")?; | |
| let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?; |
Add this helper before main():
fn parse_id_set(raw: &[String], label: &str) -> anyhow::Result<HashSet<u64>> {
let set: HashSet<u64> = raw
.iter()
.filter_map(|s| match s.parse() {
Ok(id) => Some(id),
Err(_) => {
tracing::warn!(value = %s, "ignoring invalid {label} entry");
None
}
})
.collect();
if !raw.is_empty() && set.is_empty() {
anyhow::bail!("all {label} entries failed to parse — refusing to start with an empty allowlist");
}
Ok(set)
}
chaodu-agent
left a comment
There was a problem hiding this comment.
Looks good overall — security concerns are addressed and the feature is solid. Two remaining nits from earlier reviews before we merge:
-
DRY up parse logic in
main.rs—allowed_channelsandallowed_usersparsing is nearly identical (~15 lines duplicated). Please extract aparse_id_set()helper as suggested earlier. -
Simplify configmap template — use
{{ .Values.discord.allowedUsers | toJson }}instead of the manual range loop. Same forallowedChannelsfor consistency.
Both are small changes. Once addressed, good to merge 👍
|
Thanks for the review feedback! I've pushed a fix in 9821f3f:
The |
Testing Results for
|
- 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 openabdev#107
9821f3f to
5531b23
Compare
|
Rebased on main and addressed all review feedback in a single clean commit (5531b23): Conflicts resolved:
Review feedback addressed:
Ready for re-review. |
|
@chaodu-agent All review feedback has been addressed and rebased on main. Could you take another look when you get a chance? Thanks! 🙏 |
|
@pahud Could you review and approve this PR when you have a moment? All feedback from the previous review has been addressed. Thanks! 🙏 |
- 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
thepagent
left a comment
There was a problem hiding this comment.
Clean implementation overall, security design and backward compatibility are solid 👍
Please update docs before merging:
-
docs/discord-bot-howto.md— Currently only covers Channel ID setup. Please add:- How to get a Discord User ID (Developer Mode → right-click → Copy User ID)
allowed_usersconfig example- Explanation of the AND behavior when both
allowed_channelsandallowed_usersare set
-
README.md— The Quick Start config example only showsallowed_channels. Please add a commentedallowed_usersline so users know the option exists.
Good to merge once docs are updated 🙏
- 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
|
Thanks for the review @thepagent! Docs updated in 16f445d:
Please take another look 🙏 |
Summary
Adds
allowed_usersconfig option to restrict which Discord users can trigger the bot.Closes #107
Motivation
Currently anyone in an
allowed_channelcan interact with the bot. This is a security concern when the agent has access to sensitive credentials (e.g.GH_TOKEN). Any user in the channel could instruct the agent to push code, delete branches, etc.Changes
src/config.rsallowed_users: Vec<String>toDiscordConfigsrc/discord.rssrc/main.rsallowed_usersand pass to Handlercharts/openab/values.yamldiscord.allowedUserscharts/openab/templates/configmap.yamlallowed_usersin configconfig.toml.exampleBehavior
allowed_users(default) = no filtering, fully backward compatibleUsage
helm install openab openab/openab \ --set-string discord.allowedUsers[0]="<YOUR_DISCORD_USER_ID>"