The publishability gate caught internal tooling names and developer paths but nothing site-specific, so roughly sixty leaks reached the wiki: the site name in ten documents, real fleet hostnames in the collector and GE-Enforce examples, an internal database name through the whole import guide, imaging-share paths, and a maintainer's username as the Deciders line of every ADR and inside a generated curl example. None of it is a security matter on an air-gapped fleet. It matters because these pages are read by engineers at other plants, and a document that names one site throughout reads as that site's notes rather than a product's documentation - which is exactly what it then gets treated as. Examples now use neutral hostnames, the site is "the reference site" where the distinction carries meaning, and ADRs are decided by "ShopDB maintainers". The gate carries all of these patterns, so the next one fails a build. Two documents leave docs/ because they were never written for an outside reader. PROJECT-REVIEW.md is an internal health memo pinned to a commit from July, whose headline finding (an untracked playbook) has since been fixed - it is history, and git holds it. PILOT-DEPLOY.md is one site's own cutover runbook, complete with a "re-measure before publishing" placeholder; it moves next to the loader it belongs to, in scripts/site_imports/wjf/. ADR-015 is AMENDED rather than rewritten. Its enforcement section still said report-only and its backlog still listed hardcodes that are now cleared, which left the record contradicting itself. The amendment says what changed and why the report-only period ended; the original text stays, because what the decision looked like when it was taken is the part worth keeping. Also corrects llms.txt's response envelope, which had errors at the top level and pagination at meta.total. Both are nested one deeper, so anything written against that description read undefined on every error it tried to handle.
134 lines
7.0 KiB
Markdown
134 lines
7.0 KiB
Markdown
# GE-Enforce: the gea-shopfloor-display scope
|
|
|
|
Displays are the share-less corner of the fleet. They are Entra-joined,
|
|
credential-less kiosk PCs that pull their manifest over HTTPS on port 443 and
|
|
authenticate with a read-only service PAT scoped `geenforce.fetch`, sent as
|
|
`X-API-Key`. They have no SMB share mount. The kiosk engine and the kiosk
|
|
browser are baked into the display image, not shipped over HTTPS, so the display
|
|
manifest heals POLICY / CONFIG drift only, never EXEs. It is self-sufficient and
|
|
does not inherit the fleet-wide `common` scope (see below).
|
|
|
|
## The display fetch token MUST be resource-bound
|
|
|
|
The same read-only key ships to every display (delivered by DSC, or baked into
|
|
the image), so it must not be a skeleton key for the whole content store. Mint
|
|
the display token bound to just this scope, so a leak cannot pull any other
|
|
scope's manifest or any blob by hash:
|
|
|
|
```
|
|
POST /api/apitokens
|
|
{ "name": "display fetch", "scopes": ["geenforce.fetch"],
|
|
"resourcescopes": ["gea-shopfloor-display"] }
|
|
```
|
|
|
|
With `resourcescopes` set, `GET /manifest?pctype=<other>` returns 403 and
|
|
`GET /payload/<sha>` returns 404 for any blob the display scope does not
|
|
reference. `resourcescopes` NULL (unset) = unrestricted, for back-compat with
|
|
existing service tokens. Rotate by minting a new bound token and revoking the
|
|
old one (deactivate it server-side); DSC re-delivers, or re-image.
|
|
|
|
There are three display subtypes, selected by `C:\Enrollment\display-type.txt`:
|
|
`Dashboard`, `Lobby`, and `3DPrintRoom`.
|
|
|
|
## Authoring the scope
|
|
|
|
The scope is authored programmatically by
|
|
`plugins/geenforce/seed_display_scope.py`, which builds a manifest dict and
|
|
hands it to `service.replace_scope_draft` (the same call the `import-share` CLI
|
|
uses), then attaches the inline dispatcher payload. From a Flask app context:
|
|
|
|
```python
|
|
from plugins.geenforce.seed_display_scope import seed_display_scope
|
|
seed_display_scope(publish=True) # publish=False leaves it as a draft
|
|
```
|
|
|
|
`replace_scope_draft` is an idempotent draft rebuild. `publish=True` additionally
|
|
freezes an immutable published snapshot (that step is not idempotent: it always
|
|
creates a new version).
|
|
|
|
### What the scope contains
|
|
|
|
1. Four `Registry` drift-heal entries that re-assert the Microsoft Edge kiosk
|
|
relaunch policies set at imaging by `09-Setup-Display.ps1`. Each writes the
|
|
value and detects drift with `DetectionMethod = ValueMatches` against the
|
|
same path/name, so a display that loses a policy self-heals on the next
|
|
enforce cycle with no keyboard or mouse on site:
|
|
- `RelaunchNotification = 2` (DWord, Required auto-restart)
|
|
- `RelaunchNotificationPeriod = 3600000` (DWord, 1 hour)
|
|
- `RelaunchHeadsUpPeriod = 60000` (DWord, 1 minute)
|
|
- `RelaunchWindow` (String, JSON, 02:00 start, 120 minute duration)
|
|
2. One `PS1` dispatcher, delivered inline over HTTPS. It reads
|
|
`C:\Enrollment\display-type.txt` and launches the kiosk target for the
|
|
subtype. The subtype -> route map is a data-driven table
|
|
(`DISPLAY_TYPE_TARGETS`) at the top of both the seed module and the generated
|
|
script, so targets are easy to edit. `DetectionMethod = Always` so it
|
|
re-asserts each cycle, but the script is idempotent (it skips relaunch if a
|
|
kiosk process is already serving the target URL).
|
|
|
|
### Role resolution: server first, display-type.txt fallback
|
|
|
|
The dispatcher first asks the server: `GET
|
|
/api/dashboarddefaults/display-role?fqdn=<fqdn>` (public, unauthenticated). A row
|
|
in `dashboarddefaults` keyed by the display's FQDN (IP fallback) wins and returns
|
|
the role and frontend path directly. Only when the server is unreachable or has
|
|
no mapping does the dispatcher fall back to the local `display-type.txt` map
|
|
below. To repurpose a display, edit its `dashboarddefaults` row; the change takes
|
|
effect on the next enforce cycle.
|
|
|
|
Fallback map (local file):
|
|
|
|
| display-type.txt | kiosk route | notes |
|
|
| --- | --- | --- |
|
|
| `Dashboard` | `/shopfloor` | core ShopfloorDashboard, standalone full-screen |
|
|
| `Lobby` | `/tv` | slides plugin TV dashboard (surface `lobby`) |
|
|
| `3DPrintRoom` | `/parts-kiosk` | **PLACEHOLDER, TODO-confirm** printedparts parts kiosk route; confirm the real 3D-print-room target with the floor team before publishing to production displays |
|
|
|
|
### Dashboard-defaults FQDN keying
|
|
|
|
`dashboarddefaults` rows were historically keyed by IP. Migration
|
|
`7d31_dashboarddefault_fqdn` added an `fqdn` column; resolution is now FQDN-first
|
|
with IP as fallback (`_resolve_default` in
|
|
`shopdb/core/api/dashboarddefaults.py`). FQDNs are stored lowercase. This
|
|
survives DHCP churn on kiosk subnets. `POST /api/dashboarddefaults` accepts
|
|
`fqdn`, `ipaddress`, `displayrole` (`dashboard`|`lobby`|`partskiosk`),
|
|
`businessunitid`, and `description`; `displaypath` is not stored but derived from
|
|
the role (`DISPLAY_ROLE_PATHS`). Two public read endpoints consume it:
|
|
`/api/dashboarddefaults/display-role` (dispatcher) and
|
|
`/api/dashboarddefaults/visitor-location` (lobby business-unit lookup). The
|
|
server derives a display's FQDN from its reported BIOS serial as
|
|
`F<serial>.<domain>` (`derive_display_fqdn`, domain from the `display_fqdn_domain`
|
|
setting); the dispatcher in `plugins/geenforce/seed_display_scope.py` builds the
|
|
same FQDN client-side for its lookup.
|
|
|
|
### Legacy autostart self-heal
|
|
|
|
The dispatcher also cleans up after the old GE Aerospace Dashboard / Lobby
|
|
Display Inno installers, which planted autostarts (a Public-Desktop `.lnk`, an
|
|
all-users Startup `.lnk`, and an `HKLM ...\CurrentVersion\Run` value) that
|
|
relaunch Edge at now-dead URLs (`/shopfloor-dashboard/`, `/tv-dashboard/`) and
|
|
white-screen. The 32-bit installer's Run value was WOW64-redirected into
|
|
`Wow6432Node`, which is why it survived earlier cleanup. Every enforce cycle the
|
|
dispatcher sweeps both registry views, all loaded user hives, Run/RunOnce/policy
|
|
Run keys, and every per-user and common Startup folder, matching by legacy name
|
|
and by the old URLs, then kills any old-URL Edge. The kiosk shortcut it writes is
|
|
a direct Edge shortcut (no launcher or VBS). The fix ships by re-publishing this
|
|
code-authored scope (`seed_display_scope(publish=True)`), not an import-share.
|
|
`<imaging-share>/github/find-legacy-kiosk-autostart.ps1` is a read-only locator for
|
|
stragglers.
|
|
|
|
## Self-sufficient: displays do NOT inherit common
|
|
|
|
The `gea-shopfloor-display` scope carries everything a display enforces. It does
|
|
NOT inherit the fleet-wide `common` scope. Displays run the enforcer with
|
|
common-merge off (the client default; common-merge is opt-in via
|
|
`Invoke-ShopdbEnforce.ps1 -IncludeCommon`), so `common`'s SMB-backed fleet
|
|
entries (Adobe, Oracle, OpenText, Defect Tracker, EventSaver, printer map,
|
|
self-update, asset-reporting, ...) never reach a share-less display.
|
|
|
|
This was a deliberate decision: a display needs none of common's software, and
|
|
inheriting common would have forced repackaging every SMB `common` payload as
|
|
`http`/`inline` for a share-less box. Keeping the display scope self-sufficient
|
|
avoids all of that. If a future non-display share-less PC genuinely needs the
|
|
fleet-wide entries, that is what `-IncludeCommon` plus a per-entry SMB->http
|
|
payload conversion would be for -- but displays do not use it.
|