Skip to content

Latest commit

 

History

History
194 lines (157 loc) · 9.79 KB

development.md

File metadata and controls

194 lines (157 loc) · 9.79 KB

Development

Running the stack

Reef runs in Docker. Use the VS Code Dev Container, or run docker/run for a tmux-based session. Both bring up:

  • db - PostgreSQL
  • app - Django (runserver on 8001) and NGINX (8088) in front of it. NGINX also serves worker/public/ at / and redirects /s to /survey/ on Red's dev server on 3000, which hosts the survey runner, as the worker does in a deployment.
  • celery - background worker
  • mq - RabbitMQ broker
  • mailpit - 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.

Deployment modes

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).

Settings and environment

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.org in staging, https://www.rfc-editor.org in production). Unset means no cross-origin access: every request Red makes fails at the preflight with a missing Access-Control-Allow-Origin header, and a system check warns at startup. Development hard-codes Red's dev server in development.py and 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's iss is 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's aud. 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 unlisted aud is refused with a 401), which Red commits as the oidcClientId default in its website/nuxt.config.ts and Authentik shows under the rfc-editor application's provider. To confirm what a caller actually sends, decode the JWT in its Authorization header and read aud. 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.

Email

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 to reef@ietf.org.
  • REEF_MESSAGE_ID_DOMAIN - domain for generated Message-IDs. Defaults to ietf.org. Without it Django would use the pod hostname.
  • REEF_SUBSCRIPTIONS_URL - Red's subscription management page. A notification links to it and sets List-Unsubscribe to 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.

Authentik setup

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 URI http://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.

Authentication summary

  • Django /admin/ and its nested /admin/survey-builder/ builder/analytics: the same interactive, server-side OIDC login (session), via the reef-admin-staging application — 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-editor and calls the API with that bearer token; required only for surveys whose visibility is authenticated.
  • 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

The API contract

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.

SurveyJS bundles

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.

Tests, lint, types

./manage.py test
ruff check .
ruff format --check .
cd worker && npm test