Files
shopdb-flask/docs/CONFIG.md
cproudlock 848a8fb34f
All checks were successful
CI / backend (push) Successful in 1m19s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
Add optional permission scopes to API tokens
A token may carry a scopes list: it then grants only those permissions,
intersected with what the owner holds at use time, with the admin role
bypass suspended and role-gated routes denied - a scoped token from an
admin account is genuinely limited. Scope ceiling enforced at
create/update too (only permissions the owner holds; 400 lists
violations) and the picker only offers what you hold. Token management
itself now requires the new apitokens.create permission (admin by
default, grantable via roles). Unscoped tokens keep the exact prior
act-as-owner behavior; imports need an unscoped admin token.
Migration 7d22.

756 tests pass; live-verified scoped 201/403 matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 08:58:31 -04:00

266 lines
14 KiB
Markdown

# Configuration Reference
shopdb-flask reads configuration from two places, and the split is deliberate:
- **Environment variables** (`.env` / container env) hold **secrets and
deploy-time wiring**: database credentials, signing keys, CORS origins, ports,
API keys. These are read once at boot by `shopdb/config.py`. Never put a
secret in the Settings table.
- **The Settings table** (seeded by `flask seed settings`, edited in the UI
under Settings or the setup wizard) holds **site preferences**: branding,
ServiceNow links, floor-map images, search toggles, facility identity. These
can change at runtime without a restart and are per-instance.
Rule of thumb: if leaking it would be a security incident, it is an environment
variable. If it is a site preference an admin should be able to change in the
UI, it is a Setting.
---
## Part 1: Environment variables
Defined in `shopdb/config.py`. Copy `.env.example` to `.env` and fill in
values. In `production` (`FLASK_ENV=production`), `ProductionConfig.validate()`
refuses to boot if `SECRET_KEY`, `JWT_SECRET_KEY`, `DATABASE_URL`, or
`CORS_ORIGINS` are missing or set to the dev defaults.
### Flask core
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `FLASK_APP` | No | `wsgi.py` | Entry point for the `flask` CLI. |
| `FLASK_ENV` | Yes | `development` | `production` for live sites (triggers `validate()`). Other values: `development`, `testing`. |
| `SECRET_KEY` | Yes (prod) | dev default | Flask session/signing key. Generate: `python -c "import secrets; print(secrets.token_urlsafe(64))"`. |
| `JWT_SECRET_KEY` | Yes (prod) | dev default | JWT signing key. Different value from `SECRET_KEY`. |
| `JWT_ACCESS_TOKEN_EXPIRES` | No | `3600` | Access-token TTL in seconds. |
| `JWT_REFRESH_TOKEN_EXPIRES` | No | `2592000` | Refresh-token TTL in seconds (30 days). |
| `CORS_ORIGINS` | Yes (prod) | `http://localhost:5173` | Comma-separated explicit origins. Wildcard `*` is rejected in production. |
| `LOG_LEVEL` | No | `INFO` | Logging verbosity. |
### Database
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `DATABASE_URL` | Yes (prod) | dev localhost URL | `mysql+pymysql://<user>:<pass>@<host>:<port>/<db>?charset=utf8mb4`. Keep `?charset=utf8mb4`. |
### Authentication rate limiting
IP-based fixed-window limit on the login endpoint, defense-in-depth atop the
per-account lockout. Uses the existing cache extension (per-process, so the
limit is approximate across multiple gunicorn workers).
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `AUTH_RATELIMIT_ENABLED` | No | `True` | Set `False` to disable (TestingConfig disables it). |
| `AUTH_RATELIMIT_MAX` | No | `30` | Max login attempts per source IP per window before 429. |
| `AUTH_RATELIMIT_WINDOW_SECONDS` | No | `300` | Window length in seconds. |
### Collector ingest (ADR-006)
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `COLLECTOR_API_KEY` | No | (empty) | Shared key for `/api/collector/*`. Endpoint fails closed (denies) when unset. Sent as the `X-API-Key` header. |
| `COLLECTOR_API_KEY_<PLUGIN>` | No | (empty) | Per-plugin override, e.g. `COLLECTOR_API_KEY_COMPUTERS`. Checked before the shared key. |
### Zabbix (printer supply monitoring)
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `ZABBIX_ENABLED` | No | `false` | Enable the Zabbix integration. |
| `ZABBIX_URL` | No | (empty) | Zabbix API URL. |
| `ZABBIX_TOKEN` | No | (empty) | Zabbix API bearer token. |
Note: Zabbix can also be configured via the Settings table (`zabbix_enabled`,
`zabbix_url`, `zabbix_token`). The environment values are the boot-time wiring;
prefer the Settings entries for runtime changes.
### Employee directory database (optional, read-only)
Separate HR/employee lookup DB consumed by the notifications plugin and the
public kiosks. There is no safe default for the password; an unset password
fails loud rather than trying a guessed credential.
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `EMPLOYEE_DB_HOST` | No | `localhost` | HR DB host. |
| `EMPLOYEE_DB_USER` | No | (empty) | HR DB user. |
| `EMPLOYEE_DB_PASSWORD` | No | (empty) | HR DB password. No safe default. |
| `EMPLOYEE_DB_NAME` | No | `wjf_employees` | HR DB name. |
Only used when `employee_directory_mode` (Setting) is `external`.
### CMMC USB database (optional, read-write)
Separate MySQL DB used by the USB plugin for check-in/out, lockers, and the log.
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `CMMC_USB_DB_HOST` | No | `localhost` | USB DB host. |
| `CMMC_USB_DB_USER` | No | (empty) | USB DB user. |
| `CMMC_USB_DB_PASSWORD` | No | (empty) | USB DB password. No safe default. |
| `CMMC_USB_DB_NAME` | No | `cmmc_usb` | USB DB name. |
Only used when `usb_directory_mode` (Setting) is `external`.
### docker-compose only
Read by `docker-compose.yml`, not by the Flask app directly.
| Variable | Required | Default | Notes |
|----------|----------|---------|-------|
| `MYSQL_ROOT_PASSWORD` | Yes | (none) | Root password for the bundled MySQL container. |
| `MYSQL_PASSWORD` | Yes | (none) | App-user password; must match the `DATABASE_URL` password. |
| `MYSQL_PORT` | No | `3306` | Host port for MySQL. Bound to `127.0.0.1` only. |
| `API_PORT` | No | `5001` | Host port for the API container. |
---
## Part 2: Settings table keys
Seeded by `flask seed settings` (idempotent; re-running adds anything missing).
Edited in the UI under Settings, or captured in the first-run setup wizard.
Values are stored as strings and typed by `valuetype`. Secrets in this table
(anything whose key contains `password`, `token`, or `secret`) are masked when
read back through the API.
### site
| Key | Default | Notes |
|-----|---------|-------|
| `setup_complete` | `false` | Set true once the first-run wizard finishes; gates the `/setup` route. |
| `employee_directory_mode` | `selfhosted` | `selfhosted` (tables in this app) or `external` (a separate HR database, see `EMPLOYEE_DB_*`). |
| `usb_directory_mode` | `selfhosted` | `selfhosted` or `external` (a separate `cmmc_usb` database, see `CMMC_USB_DB_*`). |
| `site_base_url` | (empty) | Public base URL (scheme + host) for QR codes and absolute links. Blank = use the browsing origin. |
| `facility_name` | (empty) | Facility name in the dashboard header. Blank = frontend falls back to `ShopDB`. |
| `pc_access_domain` | `device.geaerospace.net` | Domain appended to a PC hostname for remote-access links. Blank = hostname as-is. |
| `employeeid_pattern` | `^\d{9}$` | Regex a search term must match to be treated as an employee id. Invalid regex falls back to the default and never 500s. |
| `printer_hostname_template` | `Printer-{ip}.printer.geaerospace.net` | Printer hostname template. `{ip}` is the dash-separated IP address. |
| `dualpath_single_machine` | `true` | Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in the machines list, dashboard/report counts, and the floor map (the secondary bay is hidden). The data model always keeps both bay records; detail pages stay per-bay with a sibling banner. `false` lists and counts both bays separately. |
### branding
Blank values fall back to the shipped GE default asset so an un-reconfigured
install still renders. Upload replacements at Settings > Branding, which saves
them under `instance/branding/`.
| Key | Default | Notes |
|-----|---------|-------|
| `site_logo` | `/ge-aerospace-logo.svg` | Header and login-page logo. |
| `qr_logo` | `/ge-monogram.svg` | Logo composited in printer QR labels. Blank = no overlay. |
| `badge_logo` | `/ge-aerospace-logo.svg` | Logo on the machine badge print page. |
| `site_favicon` | (empty) | Browser tab favicon. Blank = shipped `/favicon.svg`. |
| `brand_primary_color` | (empty) | Primary brand color as a CSS color value (maps to `--primary`). Blank = built-in theme color. |
| `brand_primary_dark_color` | (empty) | Primary hover/active color (maps to `--primary-dark`). Blank = auto-derived by darkening the primary color ~15%. |
| `brand_accent_color` | (empty) | Accent color for secondary buttons and badges (maps to `--secondary`). Blank = built-in theme color. |
| `brand_sidebar_color` | (empty) | Sidebar background color (maps to `--sidebar-bg`). Blank = built-in theme color. |
### printing
| Key | Default | Notes |
|-----|---------|-------|
| `qr_target_printer` | (empty) | Custom URL template for printer QR labels. Blank = link to the printer page on this instance. Placeholders: `{printerid}`, `{assetid}`, `{assetnumber}`, `{serialnumber}`, `{ip}`, `{hostname}`. |
| `qr_target_usb` | (empty) | Custom URL template for USB label QR codes. Blank = link to the USB device page. Placeholders: `{id}`, `{serialnumber}`, `{alias}`. |
| `usb_label_style` | `barcode` | USB mini-label code style: `barcode` (CODE128 of the serial) or `qr` (QR code linking to the USB QR target). |
### map
| Key | Default | Notes |
|-----|---------|-------|
| `map_blueprint_light` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (light theme). Re-upload your own in Settings > Floor Map. |
| `map_blueprint_dark` | `/static/images/floorplan-placeholder.svg` | Floor-map blueprint (dark theme). |
| `map_width` | `3300` | Blueprint native width in pixels. |
| `map_height` | `2550` | Blueprint native height in pixels. |
### integrations
| Key | Default | Notes |
|-----|---------|-------|
| `servicenow_enabled` | `true` | Enable ServiceNow ticket recognition and links. Disabled = tickets render as plain text. |
| `servicenow_search_url` | geaerospaceqa.service-now.com global-search template | `{ticket}` is substituted. |
| `servicenow_ticket_prefixes` | `GEINC,GECHG,GERIT,GESCT` | Comma-separated prefixes recognized as ServiceNow tickets. |
| `servicenow_incident_url` | geaerospaceqa.service-now.com global-search template | `{ticket}` is substituted. Replace with a direct incident URL if your instance has one. |
| `servicenow_change_url` | geaerospaceqa.service-now.com global-search template | `{ticket}` is substituted. Replace with a direct change URL if your instance has one. |
| `zabbix_enabled` | `false` | Enable Zabbix for printer supply monitoring. |
| `zabbix_url` | (empty) | Zabbix API URL. |
| `zabbix_token` | (empty) | Zabbix API token (masked). |
| `warranty_dell_enabled` | `false` | Enable Dell warranty (service-tag) lookups. |
| `warranty_dell_clientid` | (empty) | Dell TechDirect API client id. |
| `warranty_dell_clientsecret` | (empty) | Dell TechDirect API client secret (masked). |
| `warranty_dell_tokenurl` | (empty) | Dell OAuth token URL. Blank = Dell default. |
| `warranty_dell_apiurl` | (empty) | Dell warranty API URL. Blank = Dell default. |
### email
| Key | Default | Notes |
|-----|---------|-------|
| `smtp_enabled` | `false` | Enable email notifications and alerts. |
| `smtp_host` | (empty) | SMTP server hostname. |
| `smtp_port` | `587` | SMTP port (587 TLS, 465 SSL, 25 plain). |
| `smtp_username` | (empty) | SMTP auth username. |
| `smtp_password` | (empty) | SMTP auth password (masked). |
| `smtp_use_tls` | `true` | Use TLS for the SMTP connection. |
| `smtp_from_address` | (empty) | From address for outgoing email. |
| `smtp_from_name` | `ShopDB` | From name for outgoing email. |
| `alert_recipients` | (empty) | Default alert recipients (comma-separated). |
### audit
| Key | Default | Notes |
|-----|---------|-------|
| `audit_retention_days` | `90` | Days to retain audit logs (0 = keep forever). |
### auth
| Key | Default | Notes |
|-----|---------|-------|
| `saml_enabled` | `false` | Enable SAML SSO. |
| `saml_idp_metadata_url` | (empty) | SAML IdP metadata URL. |
| `saml_entity_id` | (empty) | SAML SP entity id. |
| `saml_acs_url` | (empty) | SAML Assertion Consumer Service URL. |
| `saml_allow_local_login` | `true` | Allow local username/password login when SAML is on. |
| `saml_auto_create_users` | `true` | Auto-create users on first SAML login. |
| `saml_admin_group` | (empty) | SAML group name that grants the admin role. |
**Personal API tokens.** Besides login JWTs and SAML, a user may create
personal API tokens (PATs) for scripts and integrations, from Settings > API
Tokens (or `POST /api/apitokens`). A PAT is sent like a JWT
(`Authorization: Bearer shopdb_pat_...`), authenticates as its owning user
across the whole API, and does not carry the hourly `JWT_ACCESS_TOKEN_EXPIRES`
limit (it never expires unless an explicit expiry is set). Only the sha256 hash
is stored; the secret is shown once at creation. This is the recommended
credential for long-running imports (see `docs/IMPORT-API.md`). There is no env
var to configure; PATs are managed entirely through the API/UI.
Creating or managing a PAT requires the `apitokens.create` permission (admins
hold it by default; grant it to other roles from Settings > Users & Roles). By
default a PAT is unscoped and acts with the full authority of its owner. A PAT
may optionally carry a scopes list (a subset of the owner's permissions, capped
at what the owner actually holds): a scoped token grants ONLY those permissions,
intersected with the owner's live permissions at use time, and suspends the
admin bypass, so it is denied on role-gated (admin-only) endpoints and on import
mode. Use an unscoped token for admin-only work and imports.
### identifiers (dynamic)
One boolean key per asset identifier per asset type, keyed
`identifier_<name>_<assettype>_enabled` (default `true`). Admins choose which
optional identifiers show on which asset types. See ADR-001. The exact set is
generated from `IDENTIFIER_LABELS` x `IDENTIFIER_ASSETTYPES` in
`shopdb/core/api/settings.py`.
### search (dynamic)
One boolean key per search domain, keyed `search_<type>_enabled` (default
`true`). Toggles whether a domain appears in global search results. The set is
generated from `SEARCH_DOMAINS` in `shopdb/core/api/settings.py`.
---
## See also
- [DEPLOY.md](DEPLOY.md) - per-site deployment runbook
- [UPGRADE.md](UPGRADE.md) - upgrading an existing site
- [BACKUP-RESTORE.md](BACKUP-RESTORE.md) - backup and restore
- `shopdb/config.py` - authoritative env-var definitions
- `shopdb/core/api/settings.py` (`build_default_settings`) - authoritative Setting defaults