Six findings from a four-agent review of the imported work, plus the contract bump the import owed. Nothing here changes what the imported features do. The contract version was not bumped for a change that widens what a plugin may declare. The asset-panel vocabulary gained two tab groups (serviceprocess, reports), a datetime column format, a map.body key and a tabs section that renders as a table. 0.29.0 bumped for exactly this class of change when the optional group was added, so this is 0.31.0 with a ledger row. All four are optional and resolve server-side when absent, so a plugin declaring none of them is unaffected. Site Backups now answers honestly where it has nothing behind it. The overlay is a compose profile: a separate container writes sets onto a shared volume and the api container only reads them. A Windows/IIS install runs no such container and the default root resolves to a drive-letter path nothing creates, so Back Up Now either failed on PermissionError or reported a run queued that would never happen, under a banner reading "No verified backup yet" - which describes a backup that failed, not a feature that was never installed. The three write routes refuse with the same 404 shape the configuration-policy routes use for a disabled feature, the list and status routes report whether a store exists, and the page says so instead of offering buttons. Two listAll callers still read a response that is not there. listAll resolves to a plain array through fetchAllPages, so response.data.data throws: PC Types settings rendered empty, and in Configuration Policies the throw happened inside the shared try, so PC types, applications, the action catalog and monitors were all left unassigned by one bad read. The agent package upload cannot succeed on IIS as shipped. MAX_CONTENT_LENGTH went to 256 MB to admit the ~69 MB tray binary, but IIS request filtering caps a body at 30,000,000 bytes and answers 413 before waitress sees the request, so web.config needs the matching requestLimits. The backup image could not be built by anyone outside a TLS-intercepting network. Dockerfile.backup did a plain COPY of a gitignored corporate root CA, and a COPY of a missing file fails the build. The trailing glob makes it a no-op when the cert is absent. endpointstates.domain was the one new string column written without a clamp. Under STRICT_TRANS_TABLES an over-long value raises Data too long and aborts the whole check-in, so a single malformed report would stop a machine reporting telemetry and events entirely. Not fixed here, and left with upstream: the restore path finalizes its schema upgrade only from a browser poll loop, so closing the tab mid-restore leaves the data behind the code; it restores instance files from the newest snapshot rather than the set that was asked for; and it takes no safety dump before the destructive load. All three are in the Docker-only overlay, which this site does not run, and none can be validated here. Scope: product
152 lines
7.1 KiB
XML
152 lines
7.1 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!--
|
|
IIS site config for shopdb-flask via HttpPlatformHandler.
|
|
|
|
IIS launches waitress (a Windows-friendly WSGI server; gunicorn does NOT run
|
|
on Windows) and forwards requests to it on a private loopback port that IIS
|
|
assigns via %HTTP_PLATFORM_PORT%. One process serves both /api and the built
|
|
Vue SPA (frontend/dist), so no separate static site is needed.
|
|
|
|
Prerequisites on the box:
|
|
- HttpPlatformHandler IIS module installed
|
|
(https://www.iis.net/downloads/microsoft/httpplatformhandler)
|
|
- URL Rewrite module installed (only for the optional X-Forwarded-For rule)
|
|
- Python 3.14 + a venv at APP_ROOT\venv with requirements.txt + waitress
|
|
- Secrets live in APP_ROOT\.env (wsgi.py load_dotenv() reads it). Keep them
|
|
OUT of this file. Lock .env ACLs to the app pool identity + admins.
|
|
|
|
Replace APP_ROOT (C:\shopdb-flask below) with the real deploy path. The IIS
|
|
site's physical path MUST be APP_ROOT (where wsgi.py lives).
|
|
-->
|
|
<configuration>
|
|
<system.webServer>
|
|
|
|
<handlers>
|
|
<add name="httpplatformhandler" path="*" verb="*"
|
|
modules="httpPlatformHandler" resourceType="Unspecified" />
|
|
</handlers>
|
|
|
|
<httpPlatform
|
|
processPath="C:\shopdb-flask\venv\Scripts\waitress-serve.exe"
|
|
arguments="--port=%HTTP_PLATFORM_PORT% --host=127.0.0.1 --threads=8 --trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for --trusted-proxy-count=1 wsgi:app"
|
|
stdoutLogEnabled="true"
|
|
stdoutLogFile="C:\shopdb-flask\logs\httpplatform"
|
|
startupTimeLimit="120"
|
|
startupRetryCount="3">
|
|
<environmentVariables>
|
|
<!-- FLASK_ENV MUST be production here or wsgi.py defaults to the dev
|
|
config (SQL echo, debug, wrong DB URL). Real secrets go in .env. -->
|
|
<environmentVariable name="FLASK_ENV" value="production" />
|
|
<environmentVariable name="PYTHONPATH" value="C:\shopdb-flask" />
|
|
<!-- Subpath method only: when this web.config sits in an IIS
|
|
Application (e.g. /ops) under an existing site instead of its own
|
|
site, tell the app its mount path. Must match the alias the
|
|
Application was created with AND the VITE_BASE_PATH the frontend
|
|
was built with ('/ops/'). Omit for the own-site method.
|
|
<environmentVariable name="MOUNT_PATH" value="/ops" />
|
|
-->
|
|
</environmentVariables>
|
|
</httpPlatform>
|
|
|
|
<!--
|
|
Forward the real client IP, so the audit log, the kiosk visitor-location
|
|
feature (IP -> business unit), the GE-Enforce IP allowlist and per-host
|
|
login rate limiting all see the caller rather than the loopback address
|
|
HttpPlatformHandler connects from.
|
|
|
|
IIS does not set X-Forwarded-For on its own. Without the rule below there
|
|
is no such header at all, and every client looks like 127.0.0.1 - so the
|
|
allowlist and the visitor-location lookup silently stop working.
|
|
|
|
ONLY CORRECT WHEN IIS IS DIRECTLY EXPOSED. It overwrites the header with
|
|
REMOTE_ADDR, which is what stops a client spoofing its own X-Forwarded-For.
|
|
Behind a reverse proxy (ARR, a load balancer) REMOTE_ADDR is the PROXY, so
|
|
this rule would destroy the real client IP - there, leave it disabled and
|
|
let the proxy set the header.
|
|
|
|
It ships DISABLED because it needs the URL Rewrite module; enabled without
|
|
it, IIS returns HTTP 500.19 ("configuration section not well-formed").
|
|
|
|
There is deliberately NO <allowedServerVariables> block below. Setting a
|
|
server variable requires that variable to be allowed, but the section
|
|
system.webServer/rewrite/allowedServerVariables ships with
|
|
overrideModeDefault="Deny", so declaring it in an application's own
|
|
web.config is refused outright: IIS answered 500.52 with error 0x80070021,
|
|
"this configuration section cannot be used at this path", BEFORE it ever
|
|
reached httpPlatformHandler - so python was never launched and the stdout
|
|
log stayed empty, which looks like an application fault and is not one.
|
|
|
|
The installer instead allows the single variable at server level, which
|
|
grants exactly HTTP_X_FORWARDED_FOR rather than unlocking the section and
|
|
letting every site on the machine declare arbitrary server variables.
|
|
|
|
The installer handles both: -ClientIpSource direct installs URL Rewrite
|
|
from the bundle, allows the variable, and enables this; -ClientIpSource
|
|
proxy leaves it alone. By hand: install URL Rewrite, run
|
|
appcmd set config /section:system.webServer/rewrite/allowedServerVariables ^
|
|
/+"[name='HTTP_X_FORWARDED_FOR']" /commit:apphost
|
|
then delete the two marker lines below.
|
|
-->
|
|
<!-- SHOPDB-CLIENTIP-BEGIN
|
|
<rewrite>
|
|
<rules>
|
|
<rule name="Set X-Forwarded-For" stopProcessing="false">
|
|
<match url=".*" />
|
|
<serverVariables>
|
|
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
|
|
</serverVariables>
|
|
<action type="None" />
|
|
</rule>
|
|
</rules>
|
|
</rewrite>
|
|
SHOPDB-CLIENTIP-END -->
|
|
|
|
<security>
|
|
<requestFiltering>
|
|
<!-- IIS request filtering caps a request body at 30,000,000 bytes by
|
|
default and answers 413 before waitress sees it. The agent package
|
|
upload accepts 128 MB (MAX_CONTENT_LENGTH is 256 MB), and the tray
|
|
binary alone is ~69 MB, so the IIS cap has to be raised to match or
|
|
every agent upload fails on Windows while working under Docker. -->
|
|
<requestLimits maxAllowedContentLength="268435456" />
|
|
</requestFiltering>
|
|
</security>
|
|
|
|
</system.webServer>
|
|
|
|
<!--
|
|
Installer downloads: serve /installers/* as IIS static files instead of
|
|
forwarding them to Flask. The handler above is path="*", so without this a
|
|
request for /installers/Foo.exe goes to waitress, which has no such route
|
|
(SPA fallback), and large binaries would stream through a Python thread.
|
|
|
|
This <location> clears the httpPlatformHandler for that one subpath and puts
|
|
the static file handler back, so IIS serves the bytes directly (kernel-mode,
|
|
range/resume, no Python thread held).
|
|
|
|
Requires a physical folder at APP_ROOT\installers (the site's physical path
|
|
is APP_ROOT). Drop the installer binaries there, e.g. robocopy them from the
|
|
classic wwwroot\installers. The stored installpath 'installers/Foo.exe' then
|
|
resolves to <mount>/installers/Foo.exe (e.g. /shopdb/installers/Foo.exe).
|
|
|
|
.exe/.msi are given an explicit MIME map; if the parent site has a Request
|
|
Filtering rule that denies executable extensions, also allow them there.
|
|
-->
|
|
<location path="installers">
|
|
<system.webServer>
|
|
<handlers>
|
|
<clear />
|
|
<add name="StaticFile" path="*" verb="*"
|
|
modules="StaticFileModule" resourceType="File"
|
|
requireAccess="Read" />
|
|
</handlers>
|
|
<staticContent>
|
|
<remove fileExtension=".exe" />
|
|
<mimeMap fileExtension=".exe" mimeType="application/octet-stream" />
|
|
<remove fileExtension=".msi" />
|
|
<mimeMap fileExtension=".msi" mimeType="application/octet-stream" />
|
|
</staticContent>
|
|
</system.webServer>
|
|
</location>
|
|
</configuration>
|