Files
member-console/docs/discourse-setup.md
T
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

140 lines
7.5 KiB
Markdown

---
title: "Discourse Integration Setup"
audience: [operator]
summary: "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.