Files
cgalo5758 a94ff08336 Add Discourse integration
Deliver forum posting entitlements through managed group membership with
identity linkage, periodic reconciliation, webhook handling, and an
operator mapping surface.

Include fake and live test environments, setup documentation,
migrations,
and end-to-end coverage.
2026-07-20 19:49:34 -07:00

7.5 KiB

title, audience, summary
title audience summary
Discourse Integration Setup
operator
Connecting the member console to a Discourse forum: forum-side prerequisites, linkage-mode selection (email / oidc / discourseconnect), group mapping, webhooks, and the settings that must stay off.

Discourse integration — operator setup

The Discourse integration delivers the org-held discourse_posting entitlement as membership in Discourse groups you choose. The console computes who should be in each mapped group (everyone with an active membership in any org that currently holds the entitlement), converges the forum to that set on a periodic sweep, and corrects drift. What those groups gate (category permissions, posting rights) stays entirely in your Discourse admin — the console manages membership, never permissions.

Works against any Discourse instance. Wiki.cafe's deployment is the tested reference, not an assumption.

1. Forum-side prerequisites

  1. A group to manage. Create a manually-managed (non-automatic) group per mapped entitlement (e.g. members). Automatic groups (trust_level_*, staff, ...) are refused at mapping time — Discourse computes their membership itself. Treat mapped groups as console-owned: membership changes made by forum admins are corrected on the next sweep (and logged console-side).
  2. An admin API key. Discourse admin → API → New API Key (global key; note the key and the acting username, usually system). Rate-limit note: Discourse's admin API budget (default 60 requests/min) is one site-wide bucket shared with human admins. The console caps itself well below it (30/min) and backs off on 429.
  3. (Recommended) A webhook. Admin → API → Webhooks: payload URL https://<console-host>/webhooks/discourse, a shared secret, events User (created) and Group User (added/removed). Webhooks only reduce latency — a missed event costs at most one sweep interval.
  4. Never enable discourse_connect_overrides_groups. It full-replaces a user's non-automatic groups at every login, which fights any external group management (this integration's and your admins' alike).

2. Choose a linkage mode

The console must map its people to forum users. discourse-linkage-mode selects how:

mode forum-side requirement linkage guarantee
email none verified primary email = forum primary email (exact, case-insensitive). Fragile if emails drift.
oidc (default) enable the bundled OpenID Connect plugin against the same IdP the console uses (openid_connect_enabled + discovery URL + client credentials) deterministic: forum association ↔ OIDC subject. Existing forum accounts self-link at first SSO login (match_by_email, default on). Additive — local logins keep working.
discourseconnect DiscourseConnect SSO with the console's IdP as provider deterministic via external_id. Warning: exclusive — enabling DiscourseConnect disables local and all other login methods for every forum user, and takes over existing accounts by email match. A forum-wide auth decision, not just integration plumbing.

Members without a resolvable forum account appear on the operator page as entitled but unlinked — a normal state, not an error. In oidc mode they link automatically seconds after their first forum SSO login (via the user_created webhook) or at the next sweep.

discourse-auto-create-users=true additionally creates forum accounts (active, approved, identity-bound in oidc mode) for entitled persons who have none. Leave it off unless your deployment wants forum accounts to exist before first visit. Not supported in discourseconnect mode.

3. Console configuration

discourse-base-url          https://forum.example.com     (required)
discourse-api-key           <admin API key>               (required, secret; -file variant available)
discourse-api-username      system                        (default)
discourse-linkage-mode      oidc                          (default; email | oidc | discourseconnect)
discourse-auto-create-users false                         (default)
discourse-webhook-secret    <shared secret>               (secret, optional; empty = webhook endpoint unmounted)
discourse-sync-interval     15m                           (default; sweep cadence)

discourse-base-url and discourse-api-key must be set together (or both left empty: the integration stays dormant — registered, migrations applied, no forum calls, and the operator page says so).

4. Map entitlements to groups

Operator panel → Integrations → Discourse:

  • Map group: choose the entitlement (discourse_posting) and enter the forum group's name. The console validates against the forum (group must exist and not be automatic) and stores the mapping; the group is now managed and converges on the next sweep.
  • Remove mapping: stops management. Forum-side membership is left as-is — removal never bulk-kicks members.
  • The page also shows the last sweep's outcome, entitled-but-unlinked members, and any link conflicts (one forum account claimed by two identities — the console freezes the link and never reassigns; resolve forum-side or in member data, then the next sweep relinks).

5. Test stack / development

Two forum options exist; unit/DB tests always use the fake.

Fake (default — fast, no containers). An in-process fake implementing the consumed API surface lives at internal/integrations/discourse/discoursetest:

go run ./internal/integrations/discourse/discoursetest/fakeserver \
    -addr 127.0.0.1:9457 -api-key test-key -seed-group walkthrough-members
cd test && go run .. start --config mc-config.yaml \
    --discourse-base-url http://127.0.0.1:9457 --discourse-api-key test-key

Live Discourse (opt-in compose profile). A real Discourse — the official discourse/discourse image with discourse/postgres + redis sidecars, mirroring the Co-op Cloud recipe's topology — runs behind the discourse profile so the default stack stays fast (~1.5 GB image, minutes-long first boot):

cd test && docker compose --profile discourse up -d
# discourse-seed migrates the DB, creates admin (admin@example.com /
# discourse-admin-pw1), enables the bundled OIDC authenticator, creates the
# walkthrough-members group, registers a webhook pointed at the app on the
# host (secret test-only-webhook-secret; the discourse service maps
# member-console.localhost to the docker host-gateway), and writes the
# master API key to testdata/discourse/api-key. Then, per test/.env's
# commented lines:
#   MC_DISCOURSE_BASE_URL=http://localhost:${DISCOURSE_PORT}
#   MC_DISCOURSE_API_KEY_FILE=testdata/discourse/api-key
#   MC_DISCOURSE_WEBHOOK_SECRET=test-only-webhook-secret
go run .. start --config mc-config.yaml

Re-running the seed (any --profile discourse up) re-mints the API key — restart the app afterwards so it reads the fresh key file.

test/seed/discourse/seed-discourse.sh seeds an org conferred discourse_posting so the sweep has someone to deliver. The browser walkthrough (test/e2e/operator-walkthroughs/discourse_test.go) exercises the mapping surface end-to-end against either forum (both seed the walkthrough-members group) and skips when Discourse is unconfigured. Browser SSO into the live forum additionally needs a discourse client in the Keycloak realm — not seeded yet; the OIDC authenticator is enabled for API-level identity binding (external_ids) regardless.