proto(woo): blocking setup gate; require X-Woo-* on Woo HTTP routes

Full-screen WooSetupGate on App mount when this browser lacks device WP
user + app password. Save + Test Woo dismisses only after a successful
test. Publish/test-woo auth failures reopen the gate.

Server: client_for_request requires X-Woo-* on publish, taxonomy sync,
and test-woo (401 woo_device_creds_required); health probe keeps
Settings. Optional SLAB_WOO_ALLOW_SETTINGS_FALLBACK for legacy tests.

See PROTOTYPE.md.
This commit is contained in:
grokbot_ty
2026-09-26 19:20:16 -07:00
parent 2f6335ec26
commit 2f9b8ebd02
16 changed files with 600 additions and 58 deletions
+38 -12
View File
@@ -1,12 +1,11 @@
"""WooCommerce REST v3 client (stdlib urllib). Basic-auth, HTTPS-only, paginated.
Default creds come from Settings (wp_username plain, app_password AES-GCM).
Missing creds raise WooCredentialsMissing (fail-closed), distinct from
WooAuthError (401 from the store).
Shop-default creds live in Settings (wp_username plain, app_password AES-GCM) and
are used by the health probe only. HTTP Woo routes (publish / taxonomy sync /
test-woo) require X-Woo-Username + X-Woo-App-Password on the request — no
Settings fallback. Missing headers raise WooDeviceCredsRequired. See PROTOTYPE.md.
Prototype per-user override: callers may pass X-Woo-Username + X-Woo-App-Password
request headers into client_for_request(); those build a per-request WooClient
and are never written into Settings. See PROTOTYPE.md.
Optional escape hatch for legacy tests: SLAB_WOO_ALLOW_SETTINGS_FALLBACK=1.
"""
import base64
@@ -30,6 +29,10 @@ class WooCredentialsMissing(WooError):
"""Woo base URL or credentials are not configured (fail-closed)."""
class WooDeviceCredsRequired(WooCredentialsMissing):
"""X-Woo-Username + X-Woo-App-Password required; Settings fallback disabled."""
class WooAuthError(WooError):
"""The store rejected the credentials (401)."""
@@ -188,9 +191,9 @@ class WooClient:
# Woo-touching API calls via:
# X-Woo-Username
# X-Woo-App-Password
# Server uses those for THIS request's WooClient only; never writes them into
# the Settings row. If either header is missing/blank, fall back to the
# shop-default singleton (woo_wp_username + encrypted woo_app_password).
# HTTP Woo routes REQUIRE both headers (no Settings fallback). Health probe
# still uses shop-default Settings via WooClient.from_settings / settings_woo_creds.
# Never write override headers into the Settings row.
HEADER_WOO_USERNAME = "X-Woo-Username"
HEADER_WOO_APP_PASSWORD = "X-Woo-App-Password"
@@ -218,10 +221,29 @@ def request_woo_override(headers) -> tuple[str, str] | None:
return None
def client_for_request(conn, headers=None, request: Callable[..., tuple[int, dict, bytes]] | None = None) -> WooClient:
"""Build a WooClient for one HTTP request: override headers → Settings fallback.
def _allow_settings_fallback() -> bool:
"""Legacy-test escape hatch. Production HTTP Woo routes leave this unset."""
import os
Never mutates Settings. Inference stays on shared Settings (caller responsibility).
return os.environ.get("SLAB_WOO_ALLOW_SETTINGS_FALLBACK", "").strip().lower() in (
"1",
"true",
"yes",
)
def client_for_request(
conn,
headers=None,
request: Callable[..., tuple[int, dict, bytes]] | None = None,
*,
require_override: bool = True,
) -> WooClient:
"""Build a WooClient for one HTTP Woo request.
Requires X-Woo-* headers by default (no Settings fallback). Set
require_override=False or SLAB_WOO_ALLOW_SETTINGS_FALLBACK=1 only for
health-adjacent / legacy-test paths. Never mutates Settings.
"""
base, settings_user, settings_enc = settings_woo_creds(conn)
override = request_woo_override(headers)
@@ -230,4 +252,8 @@ def client_for_request(conn, headers=None, request: Callable[..., tuple[int, dic
if request is None:
request = _default_request
return WooClient(user, password, base, request)
if require_override and not _allow_settings_fallback():
raise WooDeviceCredsRequired(
"X-Woo-Username and X-Woo-App-Password are required on this device"
)
return WooClient.from_settings(base, settings_user, settings_enc, request=request)