Reef runs in Docker. Use the VS Code Dev Container, or run docker/run for a
tmux-based session. Both bring up:
db- PostgreSQLapp- Django (runserveron 8001) and NGINX (8088) in front of it. NGINX also servesworker/public/at/and redirects/sto/survey/on Red's dev server on 3000, which hosts the survey runner, as the worker does in a deployment.celery- background workermq- RabbitMQ brokermailpit- captures outgoing email (web UI on 8025)pgadmin- database UI (under/pgadmin/)
The app image builds FROM ghcr.io/ietf-tools/reef-app-base, produced from
docker/base.Dockerfile. Until CI publishes it, build it locally and tag it
with that name.
REEF_DEPLOYMENT_MODE selects the settings module: development (default in the
dev containers), staging, production, or build (offline, for schema and
static generation during image builds).
Copy .env.example to .env. docker compose reads it automatically. The key
values:
REEF_ADMIN_OIDC_RP_CLIENT_ID/REEF_ADMIN_OIDC_RP_CLIENT_SECRET- the confidential Authentik client for the "reef-admin-staging" application, the only interactive login Reef performs (Django admin and the builder/analytics site nested under it at/admin/survey-builder/— the same administration functionality, gated the same way).REEF_SURVEYJS_LICENSE_KEY- required in production for Creator and Analytics.
Production adds environment-driven REEF_DJANGO_SECRET_KEY, REEF_ALLOWED_HOSTS,
REEF_DB_*, REEF_CORS_ALLOWED_ORIGINS, REEF_API_OIDC_ISSUERS,
REEF_API_OIDC_JWKS_ENDPOINTS and REEF_API_OIDC_AUDIENCES; see reef/settings/production.py. Red reaches the API
only once the last three are set, and each one fails in the browser rather than in
a server log:
REEF_CORS_ALLOWED_ORIGINS- newline-separated browser origins allowed to call the API, which is Red's origin for that environment (https://www.staging.rfc-editor.orgin staging,https://www.rfc-editor.orgin production). Unset means no cross-origin access: every request Red makes fails at the preflight with a missingAccess-Control-Allow-Originheader, and a system check warns at startup. Development hard-codes Red's dev server indevelopment.pyand ignores this variable.REEF_API_OIDC_ISSUERS/REEF_API_OIDC_JWKS_ENDPOINTS- comma-separated issuer URLs whose access tokens the API accepts, and the JWKS URL of each in the same order. One caller:rfc-editor(Red itself, including the survey runner it hosts), which is the default. Leaving it out refuses every signed-in survey-taker with a 401, even on an open survey that would have accepted them anonymously. A token'sissis matched against the list exactly, so include the trailing slash; the 401 a caller gets back names the issuer it presented. Staging and production share the Authentik instance and its applications, so the values are the same in both.REEF_API_OIDC_AUDIENCES- comma-separated client ids of those same applications, which is what Authentik puts in a token'saud. No default: a client id is opaque and per-deployment, so unlike the issuers above there's nothing sensible to name automatically — an empty list disables audience verification, so every real deployment must set this. List Red's client id (a token with an unlistedaudis refused with a 401), which Red commits as theoidcClientIddefault in itswebsite/nuxt.config.tsand Authentik shows under the rfc-editor application's provider. To confirm what a caller actually sends, decode the JWT in itsAuthorizationheader and readaud. These are public client ids, not secrets.
Staging is the production module unchanged. Anything that differs between staging
and production is an environment value, and those live in the reef-secrets-env
Kubernetes secret that the ietf-tools/infra-k8s repository defines per cluster,
not in a settings file here.
base.py sets the dummy backend, so a mode that has not configured mail sends
nothing rather than sending it somewhere unintended. Development points SMTP at the
mailpit container (web UI on 8025) and production configures it from
REEF_EMAIL_HOST / REEF_EMAIL_PORT, falling back to a mailpit Kubernetes service.
reef.mail is where the project's mail defaults live, so that no caller has to
remember them: the from address, a Message-ID in REEF_MESSAGE_ID_DOMAIN rather than
in the local hostname, and the headers that mark a notification as automatically
generated. Message bodies are plain-text Django templates under templates/, one per
message with the subject composed in Python, both conventions from Purple. There is
one template per message, not per subscription kind: all six kinds produce the same
digest and differ only in an opening sentence.
REEF_DEFAULT_FROM_EMAIL- from address. Defaults toreef@ietf.org.REEF_MESSAGE_ID_DOMAIN- domain for generated Message-IDs. Defaults toietf.org. Without it Django would use the pod hostname.REEF_SUBSCRIPTIONS_URL- Red's subscription management page. A notification links to it and setsList-Unsubscribeto it. Empty leaves both out rather than linking nowhere, so set it in any deployment that sends notifications.REEF_ADMINS- newline-separated addresses told when a task gives up after its retries. Optional; without it the failure is only logged.
Reef itself registers one application in Authentik (account.ietf.org):
reef-admin-staging— the Django admin and its nested/admin/survey-builder/builder/analytics, the same administration functionality gated the same way. One confidential client against it, redirect URIhttp://localhost:8088/oidc/callback/for development, plus the staging and production equivalents.
Survey-takers sign in to Red's rfc-editor application. Reef keys its users on
sub alone, so rfc-editor and any other application that calls the API must
give the same person the same sub (the same subject mode on each provider);
a mismatch splits one person into several users. Red's access tokens must also
carry the name and email claims, since Reef overwrites a user's profile
from every token and a missing claim blanks it.
The admin login's OIDC URLs are each their own variable, none derived from
another, and default in reef/settings/base.py to the reef-admin-staging
application: REEF_ADMIN_OIDC_ISSUER, REEF_ADMIN_OIDC_AUTHORIZATION_ENDPOINT,
REEF_ADMIN_OIDC_TOKEN_ENDPOINT, REEF_ADMIN_OIDC_USERINFO_ENDPOINT,
REEF_ADMIN_OIDC_JWKS_ENDPOINT and REEF_ADMIN_OIDC_END_SESSION_ENDPOINT.
Only credentials and the redirect URI need configuring.
- Django
/admin/and its nested/admin/survey-builder/builder/analytics: the same interactive, server-side OIDC login (session), via thereef-admin-stagingapplication — follow the "Sign in with Authentik" link on/admin/login/, or go directly to/oidc/authenticate/?next=/admin/. - Survey runner: hosted by Red, which signs the survey-taker in against
rfc-editorand calls the API with that bearer token; required only for surveys whose visibility isauthenticated. - API: Reef validates Authentik bearer tokens as a resource server; the open-survey list treats a token as optional (it adds user-specific surveys when present) and popularity and open surveys are anonymous.
Logging staff into Reef and validating API callers' tokens are two separate
roles, configured separately. The OIDC_* settings (from
REEF_ADMIN_OIDC_*) are the one relying-party login, shared by
/admin/ and its nested /admin/survey-builder/ alike. Which callers the
API accepts is REEF_API_OIDC_ISSUERS, REEF_API_OIDC_JWKS_ENDPOINTS,
REEF_API_OIDC_AUDIENCES and
REEF_API_OIDC_ALGORITHMS — rfc-editor, with its own issuer, JWKS, client
id and signing key, independent of the admin login's. A caller
whose issuer is not listed is rejected with a message naming the
issuer it presented, and each token is verified against its own issuer's JWKS.
Note the algorithm is per application and is not OIDC_RP_SIGN_ALGO: Authentik
signs with whatever key the application was given, and rfc-editor uses ES256
where RS256 is the more usual default. Symmetric algorithms are refused whatever
is configured, since the verifying key is a published public key and accepting
HS* would let a caller sign its own tokens with it.
To see what an application actually signs with, read its Authentik metadata:
curl -s https://account.ietf.org/application/o/<slug>/.well-known/openid-configuration \
| jq .id_token_signing_alg_values_supported
drf-spectacular is the source of truth. Regenerate and validate the schema:
REEF_DEPLOYMENT_MODE=build ./manage.py spectacular \
--urlconf reef.urls_contract --file reef_api.yaml --validate
reef.urls_contract, not the served reef.urls: the precomputed subject payloads
are rendered from views nothing routes, so they are in no served urlconf and would
be in no contract. That file is reef.urls plus those views and nothing serves it.
A test checks the committed file still matches the code, because nothing else does:
spectacular --validate says the schema is well formed, not that reef_api.yaml is
current, and the file is what Red generates its types from.
reef_api.yaml is committed. Red generates its own client from it.
The Django builder and analytics pages load self-hosted SurveyJS bundles, not a
CDN, so the Content Security Policy can stay strict. vendor/ pins the packages
and vendor/sync.sh copies their dist files into the Django static tree. This
runs during container init and the statics image build. See vendor/README.md.
./manage.py test
ruff check .
ruff format --check .
cd worker && npm test