Skip to content

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
  • COMMANDS registry dict drives both @requires_tier and /pssbot help — adding a command updates help automatically