geenforce: display-readiness batch (server hardening, PS client wiring, display scope)
Get GE-Enforce closer to running on credential-less Intune/Entra display PCs that pull manifest + payloads over HTTPS instead of SMB. Server (plugins/geenforce/api/routes.py): - Rate-limit + 512MB served-size ceiling on GET /payload/<sha256> (reuses the login limiter's cache pattern, config-overridable via GEENFORCE_PAYLOAD_*). - New tests: payload hardening, manifestblobs model-vs-migration parity, and a report-contract test locking the lowercase per-entry report keys. PS client (plugins/geenforce/client/): - Fix New-ShopdbReport per-entry key casing to lowercase (name/action/selfhealed/ exitcode/message) to match what the server reads; the engine emits PascalCase. - Enforce TLS 1.2 in the network functions. - Fetch + merge the fleet-wide common scope alongside the pctype scope (pctype wins on conflict; -NoCommon opt-out). - Normalize whatever the engine returns into a well-formed summary. - Make the empty-cache fail-safe observable: event-log entry + report ping instead of a silent exit 0. Manifest (plugins/geenforce/seed_display_scope.py + docs/GE-ENFORCE-DISPLAY.md): - Seed a gea-shopfloor-display scope: 4 Edge kiosk drift-heal registry entries + 1 data-driven dispatcher (Dashboard/Lobby/3DPrintRoom via display-type.txt). Kiosk EXEs stay image-baked; the manifest heals policy/config drift only. - Documents the common SMB-payload audit (entries needing http/inline before a share-less display can inherit common). Migration registry (shopdb/plugins/alembic_template.py + test): - Register the pre-existing manifestblobs and the new printersupplyalerts tables in PLUGIN_TABLE_OWNERS; update EXPECTED_HEAD_REVISION for geenforce (0002blobs), printers (0002supplyalerts), and printedparts (0004txnrev) which had drifted.
This commit is contained in:
267
plugins/geenforce/seed_display_scope.py
Normal file
267
plugins/geenforce/seed_display_scope.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""Author the gea-shopfloor-display runtime scope programmatically.
|
||||
|
||||
Displays are Intune/Entra-joined, credential-less kiosk PCs that pull their
|
||||
manifest over HTTPS (no SMB share). The kiosk engine and the kiosk browser are
|
||||
BAKED INTO THE DISPLAY IMAGE, so this scope does not ship any EXE payloads; it
|
||||
heals POLICY / CONFIG drift only, plus one dispatcher that points the kiosk at
|
||||
the right target for the display subtype.
|
||||
|
||||
What this scope contains:
|
||||
- Four Registry drift-heal entries that re-assert the Microsoft Edge kiosk
|
||||
relaunch policies from the imaging script 09-Setup-Display.ps1 (so a display
|
||||
that loses those policies self-heals on the next enforce cycle without a
|
||||
keyboard or mouse on site).
|
||||
- One inline PS1 dispatcher that reads C:\\Enrollment\\display-type.txt and
|
||||
launches the kiosk target for the subtype. The display-type -> target map is
|
||||
a data-driven table (DISPLAY_TYPE_TARGETS) so the targets are easy to edit.
|
||||
|
||||
Inheritance: the manifest model has no inheritance column. The display scope is
|
||||
a plain (non-common) runtime scope; the CLIENT merges the fleet-wide 'common'
|
||||
scope with the display scope at fetch time. So this scope carries display-only
|
||||
entries and relies on the client to layer 'common' underneath. See the common
|
||||
SMB-payload audit in docs/GE-ENFORCE-DISPLAY.md before letting a share-less
|
||||
display inherit common.
|
||||
|
||||
Authoring path mirrors how every other scope is created: build a manifest dict
|
||||
and hand it to service.replace_scope_draft (the same call the import-share CLI
|
||||
uses), then attach the inline dispatcher payload and optionally publish. Re-
|
||||
running replace_scope_draft is an idempotent draft rebuild.
|
||||
"""
|
||||
|
||||
from shopdb.api import db
|
||||
|
||||
from . import service
|
||||
|
||||
|
||||
SCOPE_NAME = 'gea-shopfloor-display'
|
||||
SCOPE_PHASE = 'runtime'
|
||||
# Match the fleet-wide 'common' manifest version so a merged display + common
|
||||
# document stays internally consistent.
|
||||
SCOPE_VERSION = '2.0'
|
||||
|
||||
# Edge kiosk relaunch policy key. Values below mirror 09-Setup-Display.ps1
|
||||
# exactly so the imaging-time state and the enforced state never disagree.
|
||||
EDGE_POLICY_PATH = 'HKLM:\\SOFTWARE\\Policies\\Microsoft\\Edge'
|
||||
RELAUNCH_WINDOW_JSON = (
|
||||
'{"entries":[{"start":{"hour":2,"minute":0},"duration_mins":120}]}')
|
||||
|
||||
# Data-driven display-type -> kiosk target map. The value of
|
||||
# C:\Enrollment\display-type.txt selects the row; the target is a route the
|
||||
# kiosk browser opens against the local kiosk base URL. Edit here to retarget a
|
||||
# subtype. Keys are matched case-insensitively by the dispatcher.
|
||||
#
|
||||
# TODO-confirm: 3DPrintRoom points at the printedparts /parts-kiosk route as a
|
||||
# PLACEHOLDER. Confirm the real 3D-print-room kiosk target with the floor team
|
||||
# before this scope is published to production displays.
|
||||
DISPLAY_TYPE_TARGETS = {
|
||||
'Dashboard': '/shopfloor',
|
||||
'Lobby': '/tv',
|
||||
'3DPrintRoom': '/parts-kiosk',
|
||||
}
|
||||
|
||||
DISPATCHER_FILENAME = 'Invoke-DisplayKioskDispatch.ps1'
|
||||
|
||||
|
||||
def _relaunch_window_targets_comment():
|
||||
"""Human note that lists the data-driven targets, for the manifest comment."""
|
||||
pairs = ', '.join(f'{name}={route}'
|
||||
for name, route in DISPLAY_TYPE_TARGETS.items())
|
||||
return pairs
|
||||
|
||||
|
||||
def build_dispatcher_script():
|
||||
"""Return the inline dispatcher PowerShell as text.
|
||||
|
||||
The display-type -> target map is emitted as a hashtable at the top of the
|
||||
script (generated from DISPLAY_TYPE_TARGETS) so the on-PC script and the
|
||||
manifest metadata agree and both stay easy to edit.
|
||||
"""
|
||||
table_lines = []
|
||||
for display_type, route in DISPLAY_TYPE_TARGETS.items():
|
||||
table_lines.append(f" '{display_type}' = '{route}'")
|
||||
table_body = ';\n'.join(table_lines)
|
||||
|
||||
return f"""# Invoke-DisplayKioskDispatch.ps1 -- gea-shopfloor-display dispatcher.
|
||||
#
|
||||
# Reads C:\\Enrollment\\display-type.txt and launches the kiosk browser at the
|
||||
# route mapped for that display subtype. The kiosk browser is baked into the
|
||||
# display image; this script only points it at the right target.
|
||||
#
|
||||
# The DisplayTypeTargets table below is the single source of truth for the
|
||||
# subtype -> route map. Edit a row to retarget a subtype.
|
||||
#
|
||||
# TODO-confirm: 3DPrintRoom uses the printedparts /parts-kiosk route as a
|
||||
# PLACEHOLDER. Confirm the real 3D-print-room target before production use.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# --- Data-driven subtype -> kiosk route map ---
|
||||
$DisplayTypeTargets = @{{
|
||||
{table_body}
|
||||
}}
|
||||
|
||||
# Base URL the kiosk browser opens; the route from the table is appended. Edit
|
||||
# to point at this site's shopdb host. Kept here so the map above stays pure.
|
||||
$KioskBaseUrl = 'https://localhost'
|
||||
|
||||
$displayTypeFile = 'C:\\Enrollment\\display-type.txt'
|
||||
if (-not (Test-Path -LiteralPath $displayTypeFile)) {{
|
||||
Write-Host "display-type.txt not found at $displayTypeFile; nothing to launch."
|
||||
return
|
||||
}}
|
||||
|
||||
$displayType = (Get-Content -LiteralPath $displayTypeFile -Raw).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($displayType)) {{
|
||||
Write-Host 'display-type.txt is empty; nothing to launch.'
|
||||
return
|
||||
}}
|
||||
|
||||
# Case-insensitive lookup so 'lobby' and 'Lobby' both resolve.
|
||||
$matchedKey = $DisplayTypeTargets.Keys |
|
||||
Where-Object {{ $_ -ieq $displayType }} |
|
||||
Select-Object -First 1
|
||||
if (-not $matchedKey) {{
|
||||
Write-Host "Unknown display-type '$displayType'; known types: $($DisplayTypeTargets.Keys -join ', ')."
|
||||
return
|
||||
}}
|
||||
|
||||
$targetRoute = $DisplayTypeTargets[$matchedKey]
|
||||
$kioskUrl = "$KioskBaseUrl$targetRoute"
|
||||
Write-Host "display-type '$displayType' -> kiosk target $kioskUrl"
|
||||
|
||||
# Idempotent: if an Edge kiosk process is already serving this URL, leave it be
|
||||
# so an enforce cycle does not relaunch the kiosk every run.
|
||||
$alreadyRunning = Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object {{ $_.CommandLine -and $_.CommandLine.Contains($kioskUrl) }}
|
||||
if ($alreadyRunning) {{
|
||||
Write-Host 'Kiosk already running for this target; leaving it in place.'
|
||||
return
|
||||
}}
|
||||
|
||||
$edgeArguments = @("--kiosk", $kioskUrl, "--edge-kiosk-type=fullscreen", "--no-first-run")
|
||||
Start-Process -FilePath 'msedge.exe' -ArgumentList $edgeArguments
|
||||
Write-Host 'Launched kiosk browser.'
|
||||
"""
|
||||
|
||||
|
||||
def _registry_drift_heal_entry(name, regname, regvalue, regtype, comment):
|
||||
"""One Type=Registry entry that writes a value and detects drift via
|
||||
ValueMatches against that same path/name.
|
||||
|
||||
DetectionValue is stored as a string; the engine string-coerces for
|
||||
ValueMatches, so a DWord value of 2 detects against '2'.
|
||||
"""
|
||||
return {
|
||||
'_comment': comment,
|
||||
'Name': name,
|
||||
'Type': 'Registry',
|
||||
'RegPath': EDGE_POLICY_PATH,
|
||||
'RegName': regname,
|
||||
'RegValue': regvalue,
|
||||
'RegType': regtype,
|
||||
'DetectionMethod': 'ValueMatches',
|
||||
'DetectionPath': EDGE_POLICY_PATH,
|
||||
'DetectionName': regname,
|
||||
'DetectionValue': str(regvalue),
|
||||
}
|
||||
|
||||
|
||||
def build_display_manifest():
|
||||
"""Return the gea-shopfloor-display manifest dict (Applications in order).
|
||||
|
||||
Four Edge kiosk relaunch-policy drift-heal entries, then the one dispatcher
|
||||
entry. The dispatcher is declared PayloadSource=inline with no hash yet; the
|
||||
seed attaches the real payload bytes (and its sha256) after the draft rows
|
||||
exist. Kept payload-free otherwise: the kiosk engine and browser are baked
|
||||
into the display image, not shipped over HTTPS.
|
||||
"""
|
||||
applications = [
|
||||
_registry_drift_heal_entry(
|
||||
'Edge kiosk RelaunchNotification (Required auto-restart)',
|
||||
'RelaunchNotification', 2, 'DWord',
|
||||
'RelaunchNotification=2 (Required): Edge auto-restarts after the '
|
||||
'notification period. Displays have no operator to dismiss the '
|
||||
'update dialog, so this is the only mode that recovers unattended. '
|
||||
'Heals drift of the policy set at imaging by 09-Setup-Display.ps1.'),
|
||||
_registry_drift_heal_entry(
|
||||
'Edge kiosk RelaunchNotificationPeriod (1 hour)',
|
||||
'RelaunchNotificationPeriod', 3600000, 'DWord',
|
||||
'Milliseconds before the forced auto-restart. 3600000 ms = 1 hour.'),
|
||||
_registry_drift_heal_entry(
|
||||
'Edge kiosk RelaunchHeadsUpPeriod (1 minute)',
|
||||
'RelaunchHeadsUpPeriod', 60000, 'DWord',
|
||||
'Milliseconds of final warning before auto-restart. 60000 ms = 1 '
|
||||
'minute.'),
|
||||
_registry_drift_heal_entry(
|
||||
'Edge kiosk RelaunchWindow (02:00-04:00)',
|
||||
'RelaunchWindow', RELAUNCH_WINDOW_JSON, 'String',
|
||||
'Overnight forced-restart window (02:00 start, 120 minute '
|
||||
'duration) so business-hour updates wait until off-hours and the '
|
||||
'dialog stays invisible during the day.'),
|
||||
{
|
||||
'_comment': (
|
||||
'Kiosk dispatcher. Reads C:\\Enrollment\\display-type.txt and '
|
||||
'launches the kiosk target for the subtype. Data-driven map: '
|
||||
+ _relaunch_window_targets_comment()
|
||||
+ '. 3DPrintRoom target is a PLACEHOLDER (/parts-kiosk); '
|
||||
'TODO-confirm the real target. Delivered inline over HTTPS '
|
||||
'(share-less displays); DetectionMethod Always so it re-asserts '
|
||||
'each cycle, but the script is idempotent (skips if the kiosk is '
|
||||
'already serving the target URL).'),
|
||||
'Name': 'Display kiosk dispatcher (display-type.txt)',
|
||||
'Type': 'PS1',
|
||||
'Script': DISPATCHER_FILENAME,
|
||||
'PayloadSource': 'inline',
|
||||
'PayloadRef': DISPATCHER_FILENAME,
|
||||
'DetectionMethod': 'Always',
|
||||
},
|
||||
]
|
||||
return {
|
||||
'Version': SCOPE_VERSION,
|
||||
'_comment': (
|
||||
'gea-shopfloor-display runtime scope. Heals Edge kiosk relaunch '
|
||||
'policy drift and dispatches the kiosk to the subtype target. No '
|
||||
'EXE payloads: kiosk engine and browser are baked into the display '
|
||||
'image. The client merges the fleet-wide common scope underneath '
|
||||
'this one at fetch time.'),
|
||||
'Applications': applications,
|
||||
}
|
||||
|
||||
|
||||
def seed_display_scope(publish=False, notes='seed gea-shopfloor-display'):
|
||||
"""Create/refresh the gea-shopfloor-display draft scope and its entries.
|
||||
|
||||
Idempotent for the draft: replace_scope_draft rebuilds the draft rows, and
|
||||
the inline dispatcher payload is content-addressed (a re-run stores the same
|
||||
bytes to the same sha256). Set publish=True to also freeze a published
|
||||
snapshot (that step is NOT idempotent: it always creates a new version).
|
||||
|
||||
Commits the session. Returns a summary dict:
|
||||
{scopeid, entrycount, entrytypes, dispatchersha256, publishedversion}.
|
||||
"""
|
||||
manifest = build_display_manifest()
|
||||
scope = service.replace_scope_draft(SCOPE_NAME, SCOPE_PHASE, manifest)
|
||||
# Flush so the new entries get entryids before the inline payload attaches.
|
||||
db.session.flush()
|
||||
|
||||
dispatcher = next(entry for entry in scope.entries
|
||||
if entry.name == 'Display kiosk dispatcher (display-type.txt)')
|
||||
scriptbytes = build_dispatcher_script().encode('utf-8')
|
||||
payload = service.store_inline_payload(
|
||||
dispatcher, DISPATCHER_FILENAME,
|
||||
'text/plain; charset=utf-8', scriptbytes)
|
||||
|
||||
publishedversion = None
|
||||
if publish:
|
||||
publishedversion = service.publish_scope(
|
||||
SCOPE_NAME, SCOPE_PHASE, notes=notes)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
'scopeid': scope.scopeid,
|
||||
'entrycount': len(scope.entries),
|
||||
'entrytypes': [entry.entrytype for entry in scope.entries],
|
||||
'dispatchersha256': payload.payloadsha256,
|
||||
'publishedversion': publishedversion,
|
||||
}
|
||||
Reference in New Issue
Block a user