Skip to content

Pretix using Discourse Auth - #1

Draft
gundalow wants to merge 39 commits into
mainfrom
feat-initial-plugin
Draft

gundalow wants to merge 39 commits into
mainfrom
feat-initial-plugin

Conversation

@gundalow

@gundalow gundalow commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

This Pretix plugin authenticates users through DiscourseConnect and grants only the Pretix access supported by verified Forum claims and Pretix-owned account state.

Access policy

  • Any valid, non-suspended, non-silenced, non-anonymized Forum account can sign in as an attendee and obtain tickets.
  • Only exact meetup-organisers-{city-slug} claims listed in the plugin's explicit city-to-team mapping grant membership in the corresponding city organizer team. A missing or ambiguous mapped team blocks that organizer login before user provisioning; ordinary attendee logins do not depend on the city-team configuration.
  • Members of city organizer teams, the Pretix-only Ansible Meetup Staff event team, and Pretix accounts with is_staff=true must have a signed DiscourseConnect confirmed_2fa=true assertion at session creation. The plugin always sends require_2fa=true; there is no 2FA configuration switch.
  • Pretix admins alone manage membership in Ansible Meetup Staff and grant or revoke site-wide is_staff. The plugin ignores Forum staff groups and never changes is_staff.
  • Every login requires working Discourse Admin API credentials so silenced and suspended accounts fail closed. Missing or rejected API credentials block login.

Security and maintenance

  • SSO payloads are strictly parsed, size-limited, HMAC-verified, and protected with an atomically consumed ten-minute nonce.
  • Discourse moderation responses are validated before user provisioning. Organizer memberships are reconciled from exact claims; stale city-team memberships are removed when Pretix organizer configuration is valid.
  • Sessions use a three-hour idle expiry, with Pretix's absolute session limit applying and “keep me logged in” disabled.
  • The city-to-team mapping must stay aligned with the explicit city registry in the meetup repository.

gundalow and others added 8 commits October 5, 2026 18:11
Critical fixes (were exploitable):
- Fix wrong Discourse API field names: silenced/suspended → silenced_till/suspended_till
  (old fields returned None, allowing silenced/suspended users through)
- Fix RTBF email domain: @example.invalid → @anonymized.invalid (verified in source)
- Remove RTBF username/name heuristics (false positives); email domain only
- Decouple is_staff from Discourse admin flag; use dedicated staff_group
- Add require_2fa=true to SSO request for protocol-layer 2FA enforcement
- Scope team queries to configured organizer (Team names not globally unique)

High-priority hardening:
- All API errors now fail secure (no silent continue with defaults)
- API key required; backend invisible without it
- Add nonce time expiry (10 min) alongside single-use pop
- Handle JSONDecodeError on API response
- Parse resp.json() once (was called twice)
- Add startup config validation (HTTPS, secret length, host_prefix, template)
- Warn on staff_group collision with Discourse automatic groups

Spec & docs:
- SPECIFICATION.md v2: terminology section, rejection matrix, verified
  assumptions (cross-referenced against Discourse and pretix source),
  all 5 design decisions resolved, deployment checklist added
- FUTURE_WORK.md: security hardening, robustness, feature ideas
- Add requests to pyproject.toml dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Team template: default changed from "Ansible Meetup Staff - {city}"
to "Ansible Meetup Organisers - {city}" to match main project.

Config validation: host_prefix empty check moved to backend.py
_config_errors so backend becomes invisible (matching spec §5.1).

Enrichment API: raw_data.get('user', raw_data) simplified to raw_data
(Discourse Admin API uses root: false, returns user object directly).

Logging: R5 nonce rejection now distinguishes missing vs mismatch vs
expired with age details. R7 2FA rejection now includes username.

i18n: verbose_name wrapped in gettext_lazy().

Version bump: 1.0.0 → 2.0.0 (matches spec version).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- team_template: "Staff" → "Organisers" throughout (7 occurrences)
- Step 9: document root: false (no wrapper needed), add warning about
  second_factor_enabled suppression in SSO-enabled Discourse
- R5: split into R5a/R5b/R5c for missing/mismatch/expired with age
- R7: add username to log fields
- Section 5.1: host_prefix validation now disables backend
- Deployment checklist: add discourse_connect_provider_claims check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gundalow gundalow changed the title Feat initial plugin Pretix using Discourse Auth Oct 6, 2026
@gundalow
gundalow marked this pull request as draft October 6, 2026 13:15
Comment thread SPECIFICATION.md Outdated
| `host_prefix` | No | `meetup-host` | Group name prefix identifying meetup hosts. **Must not be empty.** |
| `staff_group` | No | `meetup-admin` | Discourse group name that grants pretix `is_staff` (exact match, case-insensitive). **Must not collide with Discourse automatic group names** (`staff`, `admins`, `moderators`, `trust_level_*`, `everyone`, etc.). |
| `team_template` | No | `Ansible Meetup Organisers - {city}` | Python format string. **Must contain `{city}`.** |
| `enforce_2fa_privileged` | No | `true` | Require 2FA for privileged users. Enforced at two layers: DiscourseConnect protocol (`require_2fa`) and enrichment API (`second_factor_enabled`). |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should simplify 2FA. If we miss configured something we could have a subtle access bug here.

  • 2FA is a hard requirement for organisers and staff
  • No check for the rest
  • No configuration option

Comment thread SPECIFICATION.md Outdated
| `api_username` | No | `system` | Discourse username for API requests. |
| `organizer` | Yes | — | Pretix organizer slug. Team queries are scoped to this organizer. Required because pretix Team names are not globally unique. |
| `host_prefix` | No | `meetup-host` | Group name prefix identifying meetup hosts. **Must not be empty.** |
| `staff_group` | No | `meetup-admin` | Discourse group name that grants pretix `is_staff` (exact match, case-insensitive). **Must not collide with Discourse automatic group names** (`staff`, `admins`, `moderators`, `trust_level_*`, `everyone`, etc.). |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard code staff_group (meetup-staff)
We don't need this to be configurable, prevent miss configuration that would give everybody admin (and therefore being able to see all real names & email addresses)

Also, I think we are inconsistent between staff and admin, need to check what term Prefix prefers.

Comment thread pretix_discourse_auth/views.py Outdated
return redirect(reverse(_LOGIN_URL))

# Step 6: Group parsing
groups_set = {g.strip() for g in parsed_sso.get('groups', '').split(',') if g.strip()}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to pre-filter this to only include meetup-organisers- and meetup-staff?

Comment thread pretix_discourse_auth/views.py Outdated
API_KEY = config.get('discourse_auth', 'api_key', fallback='')
API_USER = config.get('discourse_auth', 'api_username', fallback='system')
HOST_PREFIX = config.get('discourse_auth', 'host_prefix', fallback='meetup-host').lower()
STAFF_GROUP = config.get('discourse_auth', 'staff_group', fallback='meetup-admin').lower()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcode staff group

Comment thread pretix_discourse_auth/views.py Outdated
TEAM_TEMPLATE = config.get('discourse_auth', 'team_template', fallback='Ansible Meetup Organisers - {city}')
ENFORCE_2FA = config.get('discourse_auth', 'enforce_2fa_privileged', fallback='true').lower() == 'true'
API_TIMEOUT = int(config.get('discourse_auth', 'api_timeout', fallback='10'))
ORGANIZER_SLUG = config.get('discourse_auth', 'organizer', fallback='')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard code organizers prefix

Comment thread SPECIFICATION.md Outdated
api_key = <discourse-admin-api-key>
api_username = system
organizer = ansible-meetups
host_prefix = meetup-host

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the wrong term
Also needs validating it matches ^meetup-organisers-[a-z]+$

Comment thread SPECIFICATION.md Outdated
@@ -0,0 +1,781 @@
# Technical Specification: pretix-discourse-auth

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to look at Prefix Session times, and set via scripts/provision_environment.py in ansible-community/meetup-pretix-discourse-tooling#1

Comment thread SPECIFICATION.md Outdated
- User identity (email, username, display name)
- Group memberships (which cities a host manages, and whether they hold pretix staff access)
- Moderation status (silenced, suspended)
- 2FA enforcement (via `require_2fa` in the DiscourseConnect protocol)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs work, and likely simplification

  • Remove all code related to 2FA as a configuration option
  • At the time pretix establishes the session, Discourse MUST attest that the authentication satisfied the required second-factor policy (ie admin & organisers)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant