* SEC-19: harden Discord verification (identical failures, attempt limits, id uniqueness) Collapse both verification failure paths into one shared verification_failed_embed() constructor so "not found" and "name mismatch" are byte-identical and can no longer be used as an oracle for whether a student id holds an Active membership. Remove the stale in-memory student_cache entirely: it was consulted before the only query carrying membership_status = "Active", had no TTL and was never evicted, making it a stale-membership bypass in a long-lived container. fetch_student now runs on every verification attempt before any role grant. Add a per-Discord-user failed-attempt counter in AppState (Mutex<HashMap<UserId, (u32, Instant)>>). After 5 failures in 15 minutes an attempt is refused with the generic embed and no database round trip. Each failure is logged to the logs channel with the Discord user id and a timestamp only -- never the submitted name or student id. Add an application-level student-id uniqueness check in add_dsec_discord_table: if a different discord_id already holds the student id, refuse with the generic embed and log the conflict to the logs channel with both discord ids (not the student id). The UNIQUE constraint DDL and duplicate sweep on live Supabase are an owner step (see SECURITY.md); the emailed-OTP possession proof is noted as a follow-up TODO. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE * COL-BOT-01: add /unlink to undo hijacked verifications + manual runbook Add a moderator-gated /unlink <user> slash command (required_permissions = "MANAGE_ROLES", same gate as embed()). It deletes the dsec_discord_members row FIRST, then removes the verified role -- that order avoids leaving a member un-roled but still linked, the state that permanently breaks /member_info. The delete uses .returning(...) so PostgREST returns the row body (the COR-03 empty-204 gotcha) and so we can tell whether a link actually existed. It replies ephemerally with what it did and reports clearly if the role-removal half fails so a human can finish it, then writes a log_embed entry to the logs channel naming the acting moderator. Registered in main.rs. Add SECURITY.md: the moderator runbook for undoing a link (via /unlink and by hand), who holds the Supabase credentials, the fact that deleting the row does NOT revoke the Discord role (why /unlink does both), and the owner-only SEC-19 UNIQUE-constraint step. A member-facing /unverify is deliberately not added. The /member_info leave/trim/remove policy call is an owner decision and is intentionally not implemented here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE * SEC-19: fix 7 review defects in the verification hardening Address adversarial-review findings on the verify path: - Already-linked caller bypass (HIGH): add_dsec_discord_table no longer early-returns Linked on any existing row. It now checks the SUBMITTED id's owner first (idempotent only when that owner is the caller) and refuses a caller already linked to a DIFFERENT student id -- closing the path where a stale COR-03 partial-insert row let an account claim someone else's id. - Check-then-insert race (HIGH): the insert now catches a UNIQUE(student_id) violation (SQLSTATE 23505), re-queries the owner and converts it to the generic refusal + audit. SECURITY.md now marks the live UNIQUE constraint (after the dup sweep) as a merge/deploy gate, not optional. - PII leak in error prints (HIGH): raw supabase/reqwest errors (which embed student_id=eq.<id> and a 23505 Key detail) are never printed on the verify path. New redact_digits() masks 7+ digit runs; the interaction id is used as an opaque correlation ref. student_id_owner now selects only discord_id. - Concurrent-attempt rate-limit bypass (HIGH): a per-UserId tokio Mutex (AppState. verify_locks) serializes a whole verification attempt so concurrent modal submits cannot each slip under the 5-in-15min limit. Requires tokio "sync" feature. - Ownership conflict never counted (MED): a refused link now records exactly one failed attempt (success/infra-error record none), so a stolen-but-claimed credential can no longer loop the query set forever. - Expired-interaction mutate-then-skip (MED): defer_ephemeral is now sent the instant the modal arrives, before any DB/logs work; every later reply edits the deferred response. - Log oracle (MED): the failed-attempt audit line is one fixed generic string, so "not found" vs "name mismatch" are indistinguishable in the logs channel too. Also: on_error no longer forwards a handled FrameworkError::Command to poise's builtin (whose Command arm does a non-ephemeral ctx.say(raw_error), leaking DB error text); mutex locks recover from poisoning instead of panicking; and the attempt/lock maps evict stale/idle entries so they cannot grow unbounded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE * COL-BOT-01: fix /unlink auth, deferral and delete-failure audit (review) Address adversarial-review findings on /unlink: - Self-service-in-DM / cross-guild destructive (CRITICAL): the MANAGE_ROLES gate is not trusted for auth -- poise 0.6 treats a DM invoker as Permissions::all(), so it passed in a DM and let /unlink @self delete the row before any guild check. The command is now guild_only and, as the FIRST thing before any DB op, asserts ctx.guild_id() is the DSEC guild specifically (which also stops a moderator of another guild the bot is in from deleting DSEC rows). The gate is kept for command visibility only. - Expired-interaction mutate-then-skip-audit (MED): defer_ephemeral is sent before any DB/HTTP work; the moderator reply is best-effort so a failed reply cannot skip the audit log, which now always runs. - Delete failure skipped audit + could leak (MED): the row delete no longer uses `?`. A delete error is handled in-command with a sanitized ephemeral (no raw error) and a distinct failure audit line; the raw error is redacted (redact_digits) before it reaches stderr. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE * SEC-19: close cross-guild verify exploit + 4 remaining review defects MERGE-BLOCKER — verify flow was cross-guild exploitable: /verify was globally registered and the button/modal handler accepted ANY guild, so a foreign guild's copy of the button could reach live Supabase (insert a link row over real PII, then fail the role grant) and hand an outsider a valid/invalid oracle while blocking the real student. Fix: /verify is now guild_only, and handle_verify asserts the interaction's guild == the configured DSEC guild as its FIRST action, before any query — anything else is bounced with no DB round trip. #3 fail-closed: after a 23505 the owner re-query now grants ONLY when the resolved owner is the caller. A different owner is a conflict; an unresolved owner (winning row vanished, or a violation from another constraint) is a new RefusedUnresolved outcome — an infrastructure refusal, audited and never a silent grant. #4 no submitted id in any log, in any format: normalise_student_id now reduces the id to digits only, so no punctuated form (123-456-789) can survive into a query URL; and redact_digits now masks separator-joined digit tokens (>=7 digits) as one unit, not just contiguous runs. Two layers. #6 a refused link always counts: record_failure moved INTO link_and_grant, before the fallible Discord edit, so a lost/failed reply can no longer make a refusal count zero. #7a verify defer failure now ABORTS before any DB/role mutation, so an expired modal can never mutate state with no acknowledged interaction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE * COL-BOT-01: /unlink edits its deferred reply + disambiguates delete failures #7b — one coherent reply: /unlink now takes ApplicationContext, defers via defer_response(true), and EDITs that deferred ephemeral (create/edit at the serenity layer). Previously ctx.send after a defer posted a followup in this poise version, leaving the "thinking…" placeholder dangling. Defer failure now aborts before any DB op (mirrors the verify fix). new #A — no false "nothing changed": a DELETE whose HTTP call errors may still have committed server-side. On a delete error /unlink now reads the row back: row present -> genuinely failed, nothing changed; row gone -> the delete took effect, proceed to role removal; read-back also fails -> report status UNKNOWN and do NOT touch the role, telling the moderator to verify by hand. It never asserts "no change" on a bare error. new #B — runbook SQL corrected: the bot stores student ids as digits only (strips the leading s and punctuation), so the find-link query now searches '123456789', not 's123456789', with a note on the stored format. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE * SEC-19: serialize /unlink against verify + floor supabase logging config-independently 1. Cross-command race: /unlink deleted the row and removed the role without the per-user lock that verification holds across its attempt, so the two could interleave into role-with-no-row (or the inverse) and /unlink could audit a removal the race had undone. Fix: /unlink now acquires the SAME per-user async lock (user_attempt_lock / verify_locks), keyed on the TARGET user's id, held across the delete + role removal — so it serializes against that user's own verification. user_attempt_lock is now pub(crate); it is a tokio Mutex (safe across awaits) and the std map guard is still released before the await, so no deadlock and no std lock held across an await. 2. "No submitted id in logs" made config-independent: supabase-lib-rs logs the full SELECT URL (student_id=eq.<id>) via tracing::debug! on target `supabase` (its [lib] name), which bypasses our redact_digits if an operator sets RUST_LOG=debug. The EnvFilter default now appends `supabase=info` via add_directive, which replaces any same-target directive from RUST_LOG — so supabase debug lines never emit even under RUST_LOG=debug or RUST_LOG=supabase=debug, while our own modules keep their level. Left to the owner (documented deploy gate, non-blocking): the live UNIQUE(student_id) constraint + dup sweep on Supabase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .github/workflows | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| SECURITY.md | ||
DSEC Discord Bot
Deakin Software Engineering Club Discord Bot project. To encourage students to learn Rust and how to work in a practical and collaborative project.
Setup
Setup Rust
Installing Rustup will also install cargo
Linux & MacOS:
curl https://sh.rustup.rs -sSf | sh
Windows:
Download and run rustup-init.exe
Setup Discord Bot Profile on Discord Developers
Note: To contribute, you need to create your own Discord Bot profile and test it yourself in another server.
-
Open Discord Developers and click on "Get Started"
-
Create a New Application, with any name you like
-
Navigate to
Boton the left sidebar- Note down the
Token, the code of your bot will require it. - Enable all the Intents
Presence,Server Members,Message Content- This is required by Discord to ensure popular discord bots do not scrape server message contents without permission.
- Note down the
-
Generate a Discord Bot URL:
- Navigate to
OAuth2on the left sidebar - Scroll down to
OAuth2 URL Generator - Under Scopes, select
bot - Scroll down to
Bot Permissions - Select permissions, or later override it in the invite link.
- DSEC Bot's Permission integer is
4235288712703990.
- Copy the Generated URL, and invite your bot to your Discord server.
OR
https://discord.com/oauth2/authorize?client_id=DISCORD_BOT_ID&permissions=4235288712703990&integration_type=0&scope=bot
- Navigate to
Setup Discord Bot on your machine
Rust with Cargo
- Navigate to directory on your machine
git clone https://github.com/liyunze-coding/DSEC-Discord-Bot- Create
.envfile according to.env.example
- You can ask the committee (or Ryan) for the environment variables on Discord.
- Run
cargo run - You may need to reload Discord to see changes to slash commands.
Or Use Docker
- Make sure Docker engine is running.
- On Windows, open Docker Desktop.
- Run the commands
docker-compose build
docker-compose up
Logging
The bot logs at info by default. Do not set RUST_LOG to debug or trace
on the VPS or in the DOT_ENV secret: at debug the Supabase client logs the
generated query URLs (which contain student IDs) and the service-account
email. Adjust the level with RUST_LOG locally only (e.g. RUST_LOG=warn).
Rules
General Rules
- Follow DSEC Server Rules
- Follow Deakin Code of Conduct
- Follow Discord Terms of Services
Programming Rules
- Do not test in Production
- Do not write malicious code (unless you have obtained permission for white hat hacking)
- Do not spam pull requests
- Do not add your own code formatter, affecting the whole files you edit
To-do
- Membership verification command
- Unit information command
Information
What is Rust?
Rust is memory safe yet performant, making it the ideal programming language for systems programming.
C and C++ require developers to manage memory allocation, which can lead to memory unsafe programs.
Python, Java, C# and Go use the garbage collector so that developers don't need to manually manage memory, but can slow down the program significantly due to lack of low level control.
Rust takes a unique approach, by using an "ownership" and "borrowing" system to prevent memory bugs at compile time.
Hence, Rust is performant and memory safe (when you write it well).
Why Rust?
This is a good opportunity for students at Deakin to learn Rust.
At Deakin, Software Engineering, Computer Science and IT students mostly touch on high level languages such as Python, C# and low level languages such as C++.
More and more developer tools are being written in Rust, including Rolldown, Rspack, Tauri, SWC and many more (Ryan is a web developer, he's only aware of these tools written in Rust).
Rust has its own unique concepts and challenges such as the ownership model and the borrow checker. Its strict rules help prevent programming errors such as data races and memory leaks. The strict rules also help students learn how to think about writing efficient code coming from high level languages.