Access Control — Spec¶
Source of Truth: Discord Roles¶
RBAC is enforced via Discord roles read from the guild member object at interaction time — no Grist query needed for permission checks. The three bot tiers map directly to existing Discord roles:
| Bot Tier | Discord Role | Home Channel | Access Level |
|---|---|---|---|
CENTRAL_COMMITTEE |
Central Committee | #central-committee |
Full — all commands |
REPRESENTATIVE |
Chapter Representative | #representative-committee |
Standard admin |
ORGANIZER |
Chapter Organizer | #organizing-committee |
Operational |
Membership is inclusive upward — Central Committee members have all Chapter Representative and Chapter Organizer permissions; Chapter Representative members have all Chapter Organizer permissions.
Implementation¶
# config.py — order determines hierarchy (index 0 = highest)
ROLE_HIERARCHY: list[tuple[str, int]] = [
("Central Committee", int(os.environ["ROLE_ID_CENTRAL_COMMITTEE"])),
("Chapter Representative", int(os.environ["ROLE_ID_CHAPTER_REPRESENTATIVE"])),
("Chapter Organizer", int(os.environ["ROLE_ID_CHAPTER_ORGANIZER"])),
]
# utils/access_control.py
def get_permission_tier(member: discord.Member) -> str | None:
member_role_ids = {r.id for r in member.roles}
for name, role_id in ROLE_HIERARCHY:
if role_id in member_role_ids:
return name
return None
def requires_tier(required: str):
"""Decorator for app_commands. Sends ephemeral error if caller lacks required tier."""
...
Checks are by role ID (not name) to be resilient to role renames on the server.
Vetted Member Check¶
For self-service commands (/rsvp, /rsvp withdraw, /member me) accessible to any vetted member:
ROLE_ID_VETTED = int(os.environ["ROLE_ID_VETTED"])
def is_vetted(member: discord.Member) -> bool:
return any(r.id == ROLE_ID_VETTED for r in member.roles)
No Grist query — vetted status is read from the member's Discord roles at interaction time.
Command Permission Map¶
| Command | Required |
|---|---|
/event new |
Organizing Committee |
/event role |
Organizing Committee |
/event attendance |
Organizing Committee |
/rsvp (submit) |
Vetted member |
/rsvp withdraw |
Vetted member (self only) |
/rsvp list/accept/waitlist/info |
Chapter Organizer |
/member events |
Chapter Organizer |
/member tog |
Chapter Representative |
/member me info |
Vetted member (self only) |
/member me edit |
Vetted member (self only) |
/roster screener list/process/link |
Chapter Representative |
/roster status add |
Chapter Representative |
/roster discord link |
Chapter Representative |
/roster note |
Chapter Representative |
/onboard |
Chapter Representative |
/vetting session new/timeful |
Chapter Representative |
/donation list/link/info |
Chapter Representative |
/pssbot help |
Any server member |
/pssbot [command] help |
Any server member |
/pssbot feedback |
Any server member |
Channel Routing¶
The bot posts to these channels. #records-workstream is NDA/PII-ok — unmasked member information can be posted there. All other channels receive masked output only.
| Command family | Channel | Notes |
|---|---|---|
/event new, /rsvp organizer actions |
#firearms-instruction-workstream thread |
Auto-inferred from organizing thread context |
/roster, /donation |
#records-workstream |
NDA/PII-ok |
| Vetting Discord-linking, screener notifications | #records-workstream |
NDA/PII-ok |
/onboard |
#onboarding-workstream |
|
| Bot errors/unhandled exceptions | #bot-stuff |
|
| Moderator actions (arrears, role removals) | #moderator-action-log |
Existing moderation channel |
/pssbot |
Any |
#vetting-workstream is accessible to the vetting committee broadly and should not receive PII-sensitive bot output. All PII-sensitive vetting records ops go to #records-workstream.
Relationship to Grist Officer_roles¶
$Officer_roles in Grist is the organizational record of who holds what position. It is not used for RBAC enforcement. The bot reads Discord roles at interaction time. Discord role sync automation is in the parking lot.
Builder Notes¶
- Role IDs come from environment variables (
ROLE_ID_CENTRAL_COMMITTEE,ROLE_ID_CHAPTER_REPRESENTATIVE,ROLE_ID_CHAPTER_ORGANIZER,ROLE_ID_VETTED) — resilient to role renames on the server COMMANDSregistry dict drives both@requires_tierand/pssbot help— adding a command updates help automatically