[Backend][Privacy] Every campaign member can read every other member's email address #677

Closed
opened 2026-09-16 17:01:56 +00:00 by claude-bot · 3 comments
Collaborator

Found alongside #671's audit, during #672. Reported, not changed — it is a different class from that issue (PII, not a credential) and it needs a product decision rather than a redaction.

What happens

MemberResponse (webapp/backend/app/schemas/campaign.py) carries email, and it is populated in campaign_service.list_members. The member list is readable by any campaign member, so every member gets every other member's email address.

Nothing displays it — the member list UI shows display names, pronouns and characters — so this is a response-body fact, not a visible one. Same mechanism as #671: the field is there because the model that serves the list is the model that has everything.

Why it deserves a decision rather than a reflex

This one is genuinely arguable, which is why it is its own issue:

  • For keeping it: a campaign is a group of friends who mostly know each other's contact details already. A GM organising a session offline has an obvious use for it. Quest Board is self-hosted, so the operator is usually one of the players.
  • For removing it: people join by invite link, and #643 added an approval flow precisely because membership is not always tightly held. Someone who joins a public-ish game to try it out has not agreed to hand their email to five strangers — and would have no way to know they had. An email address is also the thing most reused across services.

The middle option: keep it for the GM, drop it for players. That matches how the rest of the campaign surfaces are gated, and preserves the only concrete use anyone has named.

Where it lands in the redesign

CAMP-8 (Members & characters) is drawn in lane 2 (#659) and shows no email at all for anyone — display name, the person's pronouns, their characters. So the redesign does not need this field for any member-facing screen. If it stays, it stays for the API's sake, not the UI's.

Suggested shape

  1. Decide: everyone / GM only / nobody.
  2. If it narrows, redact in list_members rather than adding a second schema — the same reasoning as #672, where putting it in the one builder covered all the call sites.
  3. Add a test asserting a player's member list has no email, in the shape of test_admin_campaign_inventory.py:395-427, which already does exactly this for the admin inventory and is the house pattern.

Found by reading the code; not exploited or tested against any running instance.

Found alongside #671's audit, during #672. **Reported, not changed** — it is a different class from that issue (PII, not a credential) and it needs a product decision rather than a redaction. ## What happens `MemberResponse` (`webapp/backend/app/schemas/campaign.py`) carries `email`, and it is populated in `campaign_service.list_members`. The member list is readable by any campaign member, so every member gets every other member's email address. Nothing displays it — the member list UI shows display names, pronouns and characters — so this is a response-body fact, not a visible one. Same mechanism as #671: the field is there because the model that serves the list is the model that has everything. ## Why it deserves a decision rather than a reflex This one is genuinely arguable, which is why it is its own issue: - **For keeping it:** a campaign is a group of friends who mostly know each other's contact details already. A GM organising a session offline has an obvious use for it. Quest Board is self-hosted, so the operator is usually one of the players. - **For removing it:** people join by invite link, and #643 added an approval flow precisely because membership is not always tightly held. Someone who joins a public-ish game to try it out has not agreed to hand their email to five strangers — and would have no way to know they had. An email address is also the thing most reused across services. The middle option: **keep it for the GM, drop it for players.** That matches how the rest of the campaign surfaces are gated, and preserves the only concrete use anyone has named. ## Where it lands in the redesign `CAMP-8` (Members & characters) is drawn in lane 2 (#659) and shows **no email at all** for anyone — display name, the person's pronouns, their characters. So the redesign does not need this field for any member-facing screen. If it stays, it stays for the API's sake, not the UI's. ## Suggested shape 1. Decide: everyone / GM only / nobody. 2. If it narrows, redact in `list_members` rather than adding a second schema — the same reasoning as #672, where putting it in the one builder covered all the call sites. 3. Add a test asserting a player's member list has no `email`, in the shape of `test_admin_campaign_inventory.py:395-427`, which already does exactly this for the admin inventory and is the house pattern. Found by reading the code; not exploited or tested against any running instance.
Author
Collaborator

Owner's answer to Q147 (S14), 2026-10-04: (b), as recommended. A member's email address is readable by the campaign's GMs, and by each person for their own. Other players no longer receive it. To be recorded as D275. Being built now: the members list, and any other campaign-scoped response that carries another member's address, leaves it out unless the reader is a GM of that campaign or it is their own.

**Owner's answer to Q147 (S14), 2026-10-04: (b), as recommended.** A member's email address is readable by **the campaign's GMs, and by each person for their own**. Other players no longer receive it. To be recorded as D275. Being built now: the members list, and any other campaign-scoped response that carries another member's address, leaves it out unless the reader is a GM of that campaign or it is their own.
Author
Collaborator

Done in PR #1061, merged to main on 2026-10-04 with CI green (full backend and frontend suites). D275 is recorded (PR #1060).

  • The rule. In a campaign's member responses, another member's email is present only for a reader with the GM role in that campaign. A reader always gets their own. Everyone else gets null.
  • Where it lives. campaign_service.list_members takes a required viewer_id, and all five routes that return members go through it.
  • Audit. MemberResponse.email was the only campaign-scoped response carrying another person's address. Join requests, invites, ownership offers, rosters, attendance, discussions, wiki owners, search, notes and the bot API carry none. /api/me and the admin pages are unchanged.
  • An owner who only plays does not see other members' addresses, following the decision's wording ("GMs"). Say if owners should count: it is a one-line change.
  • The Privacy page now says who sees your address.

It reaches dev with the next prerelease.

Done in PR #1061, merged to main on 2026-10-04 with CI green (full backend and frontend suites). D275 is recorded (PR #1060). - **The rule.** In a campaign's member responses, another member's `email` is present only for a reader with the GM role in that campaign. A reader always gets their own. Everyone else gets `null`. - **Where it lives.** `campaign_service.list_members` takes a required `viewer_id`, and all five routes that return members go through it. - **Audit.** `MemberResponse.email` was the only campaign-scoped response carrying another person's address. Join requests, invites, ownership offers, rosters, attendance, discussions, wiki owners, search, notes and the bot API carry none. `/api/me` and the admin pages are unchanged. - **An owner who only plays** does not see other members' addresses, following the decision's wording ("GMs"). Say if owners should count: it is a one-line change. - **The Privacy page** now says who sees your address. It reaches dev with the next prerelease.
Author
Collaborator

D285 (the owner's answer to Q157, 2026-10-05), built in PR #1097 and merged. CI was green on the PR's head.

A campaign's owner now reads members' emails too, even when someone else runs the game. This amends D275 (GMs only, and each person their own).

  • campaign_service.list_members checks campaigns.owner_id at request time, so a former owner loses it once ownership moves.
  • An instance admin who is neither a GM nor the owner gets nothing new.
  • The Privacy page says "the GMs and the owner".

Reaches dev with the next prerelease.

**D285 (the owner's answer to Q157, 2026-10-05), built in PR #1097 and merged. CI was green on the PR's head.** A campaign's owner now reads members' emails too, even when someone else runs the game. This amends D275 (GMs only, and each person their own). - `campaign_service.list_members` checks `campaigns.owner_id` at request time, so a former owner loses it once ownership moves. - An instance admin who is neither a GM nor the owner gets nothing new. - The Privacy page says "the GMs and the owner". Reaches dev with the next prerelease.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#677
No description provided.