docs: former-api-specs (MES + DNC/Delmia) + inbound compile-error known issue

- former-api-specs/mes: Alarm-API, MoveIn-MoveOut-API, API-key authgaps (from ~/Desktop/mesapi)
- former-api-specs/dnc: Delmia-Integration-API — Delmia document service + WW recipe-download notify (from ~/Desktop/delmiaintegration)
- known-issues: inbound API compile error not client-visible; no api-method validate
This commit is contained in:
Joseph Doherty
2026-06-26 04:13:19 -04:00
parent 33da8c797c
commit 8a78e759c0
6 changed files with 1009 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
# WWSupport MES API — API key authentication: support & gaps
How the legacy **WWSupport / APIServer** ServiceStack service authenticates API-key callers, and
the API-key-specific gaps in it **as of 2026-06-25** (the state of the `~/Desktop/mesapi` copy).
Captured as reference for the ScadaBridge migration so the replacement Inbound API does not inherit
these weaknesses.
> Scope: defensive review of an internally-owned legacy service — weaknesses and remediations, no
> exploit steps. Limited to API-key auth (other auth/config concerns are out of scope here).
Source files: `APIServer/AppHost.cs`, `APIServer.ServiceInterface/MesServices.cs`,
`APIServer/App.config`.
---
## What's supported (API key)
- **`ApiKeyAuthProvider`** is registered in the host `AuthFeature` (`AppHost.cs:63-70`) with
`SessionCacheDuration = 30 min` and `RequireSecureConnection = false`.
- **Keys are DB-backed.** Users, roles, and keys live in SQL via `OrmLiteAuthRepository`
(`UseDistinctRoleTables = true`); `InitSchema()` creates the tables at startup
(`AppHost.cs:53-59`). There is no API-key config in `App.config` — keys are issued/stored by the
ServiceStack auth repository.
- **Authorization.** A valid key authenticates the request; the key's user must also hold the
`MESAPI` role to reach any operation (`MesServices.cs:6-8`):
```csharp
[Authenticate]
[RequiredRole("MESAPI")]
public class MesServices : Service { ... } // movein, moveout, alarmstatus, simplealarmstatus
```
- **Transports for the key** (ServiceStack defaults, nothing overridden):
`Authorization: Bearer <key>`, HTTP Basic with the key as the username, or — since
`AllowInHttpParams` defaults on — `?apikey=<key>` in the query string/form.
- **Per-key environment tag.** A key whose `Environment == "test"` is routed to a `TestDb`
connection instead of production (`AppHost.GetDbConnection`, `AppHost.cs:102-108`).
- **Transport.** The listener is plain HTTP — `http://*:9501/` (DEV) / `http://*:9500/` (QA/PROD)
(`App.config:57,68,80`); no HTTPS listener is configured (relevant to gap #1).
---
## Gaps & risks (worst first)
### 1. 🟠 High — the key is exposed in transit and in logs
Three settings compound:
- **No TLS** — plain-HTTP listeners (`App.config:57`) and `RequireSecureConnection = false`
(`AppHost.cs:69`), so the key crosses the network in cleartext to any on-path observer.
- **Key accepted in the URL** — `AllowInHttpParams` (default on) makes `?apikey=<key>` valid, so
keys land in proxy logs and browser history.
- **Request logging persists it** — the enabled `RequestLogsFeature` writes request data to a CSV
on disk (`AppHost.cs:79-86`), so a query-string key can be written to the log file.
**Fix:** terminate TLS and set `RequireSecureConnection = true`; set
`ApiKeyAuthProvider { AllowInHttpParams = false }` so keys must travel in the `Authorization`
header; redact auth fields from the request log and restrict the log file's ACLs.
### 2. 🟡 Medium — keys are not scoped to methods
A valid key + the `MESAPI` role grants access to **every** `[RequiredRole("MESAPI")]` endpoint
(move-in, move-out, both alarm reads). There is no per-key allow-list of methods, so one leaked
integration key exposes the entire MES surface.
**Fix:** scope keys per integration (separate roles/keys for read vs. move-in vs. move-out), the
way the ScadaBridge Inbound API does (keys carry an explicit method allow-list).
### 3. 🟢 LowMedium — the per-key `test` redirect is half-wired
`GetDbConnection` opens a `"TestDb"` connection for `Environment == "test"` keys
(`AppHost.cs:104-106`), but **no `TestDb` connection string is defined** in `App.config` — such
requests would throw. Separately, the move-out cycle-data read uses a raw
`ConnectionStrings["BatchDB"]` connection (see `MoveIn-MoveOut-API.md`), so it ignores the
redirect and reads production data even with a `test` key.
**Fix:** define `TestDb` (or remove the redirect), and route the raw cycle-data read through the
same environment-aware connection so a `test` key never touches production.
---
## Remediation checklist (priority order)
- [ ] **Enforce TLS** + `RequireSecureConnection = true`; stop accepting the key in the URL
(`AllowInHttpParams = false`); redact/secure the request logs (#1).
- [ ] **Scope API keys per method/integration** instead of one coarse `MESAPI` role (#2).
- [ ] **Fix or remove the `test` → `TestDb` redirect** and make the raw cycle-data read
environment-aware (#3).
---
## Contrast: ScadaBridge Inbound API
ScadaBridge uses an **`X-API-Key` header** on the data plane (`POST /api/{method}`), validated
server-side, with each key **scoped to an explicit method allow-list** — versus one coarse
`MESAPI` role granting all endpoints here. This file documents the legacy API-key posture so that
difference is intentional and verifiable during cutover.
### Accepted auth transports
| Transport | mesapi (ServiceStack `ApiKeyAuthProvider`) | ScadaBridge Inbound API |
|-----------|--------------------------------------------|-------------------------|
| `Authorization: Bearer <key>` | ✅ | ✅ — `Bearer sbk_<keyId>_<secret>` (the `Bearer ` prefix is optional; a bare token in `Authorization` also works) |
| `Authorization: Basic <base64("<key>:")>` (key as username) | ✅ | ❌ |
| `X-API-Key: <key>` | ❌ (not supported by stock ServiceStack) | ✅ — raw token `sbk_<keyId>_<secret>` |
| `?apikey=<key>` (query string / form param) | ✅ (`AllowInHttpParams` defaults on) | ❌ — headers only |
| Session cookie after first auth (`ss-id`/`ss-pid`) | ✅ (30-min `SessionCacheDuration`) | ❌ — stateless; every request re-presents the key |
Evidence: mesapi is stock `new ApiKeyAuthProvider(AppSettings)` with no header customization (so
ServiceStack defaults apply); ScadaBridge logic is `EndpointExtensions.cs:83-95`.
Notes:
- **The only common header is `Authorization: Bearer`** — the portable choice for a client that
must talk to both.
- **`X-API-Key` is ScadaBridge-only**; **`Basic` and `?apikey=` are mesapi-only.** A `curl -H
"X-API-Key: …"` authenticates ScadaBridge but is *rejected* by mesapi.
- **Precedence when two are sent:** ScadaBridge — `Authorization` wins over `X-API-Key`; mesapi —
ServiceStack checks `Authorization` (Bearer/Basic) before the `apikey` param.
- **mesapi is looser, ScadaBridge is tighter:** mesapi accepts the key in the URL and over a
long-lived session cookie (more leak surface — see gap #1); ScadaBridge restricts to two
headers, no URL param, no session.
- **Token shape & verification:** mesapi keys are opaque ServiceStack keys checked against the auth
repo; ScadaBridge keys are structured `sbk_<keyId>_<secret>` verified by a peppered-HMAC
constant-time compare and **scoped to specific method names** (case-sensitive).