Files
Project-SDE-WP-Suite/server/.env.example
Matt Mabrey f023192b74 T10.6 - deployment docs and env var reference describe Okta, not the never-shipped LDAP config
D13 never shipped, so DEPLOYMENT.md, server/.env.example and server/README.md
still described the original local-password system as of this task starting -
POST /api/auth/login, bcrypt password_hash, create-admin with a prompted
password, self-service reset-password email flow, AUTH_RESET_MINUTES /
AUTH_RESET_COOLDOWN_SECONDS. All of that is gone as of T10.4; these three files
now describe what actually runs.

server/.env.example and DEPLOYMENT.md's env block both gain the five OKTA_*
variables (ISSUER, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, IDENTITY_CLAIM),
explained the same way AUTH_SECRET_KEY already was - what it does, where to
get it, what happens if it's missing.

Also updated, not originally named in T10.6's bullet but required for the
documented vars to actually reach a running container: docker-compose.yml's
api service sets environment: as an explicit allowlist, not env_file, so the
four new OKTA_* entries had to be added there too or .env would document
something that silently does nothing. OKTA_IDENTITY_CLAIM specifically is NOT
${OKTA_IDENTITY_CLAIM:-} - compose setting an env var to an empty string is
not the same as leaving it unset, and server/okta_auth.py's own default
(preferred_username) only kicks in when the var is truly unset. Mirrored the
same default in the compose file instead, or every deployment that leaves the
optional line commented out in .env would 503 on every sign-in looking for a
claim literally named "".

server/README.md: replaced the login-portal section with the Okta flow
(access gating is Okta's job, not this app's - roles/authorization stay
local), replaced "create the first admin" with the promote-not-create
bootstrap path (D16) and its no-break-glass posture, replaced the curl-based
login example in Quick Test with a pointer to smoketest.py's own
session-minting technique (there is nothing left to curl - Okta requires a
real browser).

DEPLOYMENT.md: same treatment for its own copies of the env block, the
Portainer var list, the users table's password_hash column, the auth
endpoints summary, the smoke-test walkthrough (WP_SMOKE_USER only, must run
inside the api container or local dev sharing AUTH_SECRET_KEY/DATABASE_URL -
no longer targetable from an arbitrary remote workstation), the entire
"Self-service password reset" section (replaced with "Sign-in and admin
bootstrap (Okta)"), and the project_super_user role description / exclusive-
scope bullet, both of which named "reset passwords" as something that no
longer exists.

Left alone, logged rather than fixed here per CLAUDE.md scope discipline:
- users.failed_attempts / locked_until columns are still in the schema and
  still reset to 0/None on every Okta sign-in, but nothing increments them
  anymore since local login() is gone - vestigial, not documented as active
  lockout behavior in either doc now, but not migrated away either.
- server/README.md's "Production - Docker Compose" section (### 1-5) is a
  self-contained alternate quickstart that already duplicated and diverged
  from the real root docker-compose.yml before this task; it uses env_file
  rather than an explicit allowlist so it isn't broken by this change, but
  it's still a second source of truth nobody asked this task to reconcile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 13:47:48 -07:00

89 lines
5.1 KiB
Plaintext

# Copy to .env (dev) or set these in the systemd unit (prod).
# PostgreSQL connection (production). Format:
# postgresql+psycopg://USER:PASSWORD@HOST:5432/DBNAME
DATABASE_URL=postgresql+psycopg://wpsuite:CHANGE_ME@localhost:5432/wpsuite
# If DATABASE_URL is omitted entirely, the API falls back to a local SQLite
# file (sqlite:///./wpsuite.db) — handy for trying it out without Postgres.
# Only needed for CROSS-ORIGIN local development (comma-separated). In
# production the site is same-origin via NGINX, so leave this unset.
# CORS_ORIGINS=http://localhost:5500
# ── Authentication ────────────────────────────────────────────────────────────
# There is no local password (D15/D16) — Okta OIDC is the only way in. Sign-in
# still ends the same way it always did: a signed JWT in an HttpOnly session
# cookie, which is what the four vars right below this line are for. The five
# OKTA_* vars after that are what makes the actual sign-in possible; without
# them the API starts (this is not a hard failure like AUTH_SECRET_KEY), but
# describe()'s startup log line says so and nobody can sign in.
# Secret used to sign session cookies (JWTs), AFTER Okta has confirmed who
# someone is — this app still decides roles/authorization locally, unchanged
# by Okta (see server/okta_auth.py). REQUIRED in production: if unset, the API
# falls back to a random per-process key, so logins reset on every restart and
# break across multiple gunicorn workers. Generate a strong one:
# python -c "import secrets; print(secrets.token_urlsafe(48))"
AUTH_SECRET_KEY=CHANGE_ME_run_the_command_above
# How long a login lasts before re-authentication (hours). Default 12.
# AUTH_SESSION_HOURS=12
# ── Okta OIDC (required — this is the only sign-in path) ───────────────────────
# The Okta *authorization server* issuer, e.g. https://yourorg.okta.com/oauth2/default
# or a custom authorization server URL. The API discovers the authorize/token/
# jwks endpoints from <OKTA_ISSUER>/.well-known/openid-configuration — nothing
# else about Okta's endpoints is hand-entered.
OKTA_ISSUER=https://your-org.okta.com/oauth2/default
# Client ID and secret from the Okta app integration (Sign-in method: OIDC -
# Authorization Code, Application type: Web Application). The secret is exactly
# that — treat it like AUTH_SECRET_KEY, never commit it.
OKTA_CLIENT_ID=CHANGE_ME
OKTA_CLIENT_SECRET=CHANGE_ME
# Must exactly match a "Sign-in redirect URI" registered on the Okta app
# integration, scheme and path included, e.g.:
# https://wp-suite.company.local/api/auth/okta/callback
OKTA_REDIRECT_URI=CHANGE_ME
# Which ID token claim carries this person's directory identity, matched
# against the local users.username column (server/app.py's okta_callback()).
# preferred_username is Okta's usual default for an AD-imported user; override
# it if your security team's Okta configuration uses a different claim (upn,
# a custom claim, …) — no code change needed, just this value.
# OKTA_IDENTITY_CLAIM=preferred_username
# ── Email notifications (optional) ─────────────────────────────────────────────
# WP-assignment emails are OFF by default and are turned on from the Admin
# console (Notifications & email card), where the SMTP host/port/from-address
# live. The one secret that must NOT be stored in the database — the SMTP
# password — is read from this environment variable instead. Leave it unset
# until you have the SMTP details; the toggle stays effectively off (queued
# notifications are marked "skipped", nothing is sent) until both the toggle is
# on and SMTP is configured.
# SMTP_PASSWORD=your-smtp-app-password
# ── Micron asset catalog (optional) ───────────────────────────────────────────
# Backs the searchable asset picker in the work package creator. READ-ONLY: the
# app only ever runs the single SELECT in server/assets_db.py, so give it a
# db_datareader login and nothing more.
#
# Leave this unset and the suite works normally — the picker reports that no
# catalog is configured and people type asset tags in by hand.
#
# URL-encode special characters in the password (@ = %40, # = %23, / = %2F …).
# MICRON_DB_URL=mssql+pymssql://readonly_user:PASSWORD@sqlhost.example.com:1433/MicronDB
#
# To use pyodbc instead of pymssql you must also add pyodbc to requirements.txt
# and install the Microsoft ODBC driver in the image:
# MICRON_DB_URL=mssql+pyodbc://readonly_user:PASSWORD@sqlhost.example.com/MicronDB?driver=ODBC+Driver+18+for+SQL+Server
#
# Two things to check when the picker says the catalog is unreachable:
# 1. The table/column names in ASSET_QUERY (server/assets_db.py) match the real
# Micron schema — that one constant is the whole schema contract.
# 2. The api container is on the `outbound` network in docker-compose.yml. The
# `internal` network has no default gateway, which blocks the VPN as well as
# the internet.