mirror of
https://github.com/dsec-hub/dsec-discord-bot.git
synced 2026-09-22 15:53:56 +00:00
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
75 lines
3.2 KiB
Markdown
75 lines
3.2 KiB
Markdown
# Security runbook
|
|
|
|
Operational notes for moderators and maintainers. This file is for the people who
|
|
run the bot, not for contributors setting up a dev environment (that is `README.md`).
|
|
|
|
## Undoing a verification link (`/unlink`)
|
|
|
|
The bot links a Discord account to a student id in the `dsec_discord_members` table
|
|
when someone verifies. Because verification only checks a **name + student id** pair —
|
|
both semi-public — a person can verify as someone else. When that happens, undo it.
|
|
|
|
### Preferred: the `/unlink` command
|
|
|
|
Run `/unlink @member` in the server. It requires the **Manage Roles** permission and:
|
|
|
|
1. deletes the member's `dsec_discord_members` row, **then**
|
|
2. removes the verified role from them,
|
|
3. replies to you privately (ephemeral) with what it did, and
|
|
4. writes a log entry to the logs channel naming you as the moderator who ran it.
|
|
|
|
It does the delete first and the role removal second **on purpose**: the reverse order
|
|
can leave someone un-roled but still linked, which permanently breaks `/member_info`
|
|
for them. If the role removal half fails, the reply and the log both say so — finish it
|
|
by hand (next section) and the row is already gone.
|
|
|
|
### Manual removal (when `/unlink` cannot be used)
|
|
|
|
You need the Supabase project credentials for this. **Who holds them:** the club
|
|
committee — ask in the committee Discord. (Historically the bot maintainer; TODO: name
|
|
the current holder here.) Do **not** paste real credentials into a chat or a ticket.
|
|
|
|
Find the link row(s) for a student id:
|
|
|
|
```sql
|
|
select * from dsec_discord_members where student_id = 's123456789';
|
|
```
|
|
|
|
Delete a specific link by hand:
|
|
|
|
```sql
|
|
delete from dsec_discord_members where discord_id = '<discord user id>';
|
|
```
|
|
|
|
**Deleting the row does NOT revoke the Discord role.** Removing the row and stripping
|
|
the verified role are two separate actions — that is exactly why `/unlink` does both.
|
|
After a manual delete, also remove the verified role from the member in Discord (Server
|
|
Settings → Members, or right-click the member → Roles), or they keep their access with
|
|
no link.
|
|
|
|
## Owner-only database step (SEC-19)
|
|
|
|
`dsec_discord_members.student_id` should have a `UNIQUE` constraint so one student id
|
|
cannot be claimed by two Discord accounts. The application already refuses the second
|
|
claimant, but the durable fix is the constraint. It is **not** applied by any migration
|
|
in this repo — it touches live data and must be run by a maintainer.
|
|
|
|
Sweep for existing duplicates first; the DDL fails if any exist:
|
|
|
|
```sql
|
|
select student_id, count(*) from dsec_discord_members
|
|
group by student_id having count(*) > 1;
|
|
```
|
|
|
|
Resolve any duplicates by hand (decide which Discord account keeps each link), then:
|
|
|
|
```sql
|
|
alter table dsec_discord_members add constraint dsec_discord_members_student_id_key unique (student_id);
|
|
```
|
|
|
|
There is no staging Supabase project — do the sweep and the `ALTER` with a second
|
|
maintainer watching. Expect it to start rejecting inserts that used to succeed.
|
|
|
|
> The real fix for the weak identity check is a possession proof — email a one-time
|
|
> code to the roster address (dsec-app already owns OTP machinery; the bot would call
|
|
> dsec-api). That is feature-sized work tracked under SEC-19, not covered here.
|