Settings: temporary UAT secret-unmask gate (SLAB_UAT_UNMASK_SECRETS)
Owner-directed, LAN/HTTP dev-test only. When the env flag is set,
GET /api/v1/settings returns decrypted secret values (string) instead of
{configured: bool}; the UI prefills and shows them in clear text. Default
off -> SETTINGS-75 masking and the existing tests stay intact. Revert
before any non-UAT deployment.
This commit is contained in:
@@ -59,10 +59,24 @@ def get_settings() -> dict[str, Any]:
|
|||||||
result: dict[str, Any] = {}
|
result: dict[str, Any] = {}
|
||||||
result["woo_base_url"] = os.environ.get("WOO_BASE_URL", "")
|
result["woo_base_url"] = os.environ.get("WOO_BASE_URL", "")
|
||||||
|
|
||||||
|
# TEMPORARY UAT override (owner-directed, LAN/HTTP only): when set, secrets are
|
||||||
|
# returned as their decrypted plaintext so they can be inspected on the dev-test
|
||||||
|
# box. Default OFF -> SETTINGS-75 masking intact. Remove before prod.
|
||||||
|
unmask = os.environ.get("SLAB_UAT_UNMASK_SECRETS") == "1"
|
||||||
|
|
||||||
for k in sorted(ALL_KEYS):
|
for k in sorted(ALL_KEYS):
|
||||||
row = rows.get(k, {})
|
row = rows.get(k, {})
|
||||||
if k in SECRET_KEYS:
|
if k in SECRET_KEYS:
|
||||||
enc = row.get("value_enc")
|
enc = row.get("value_enc")
|
||||||
|
if unmask:
|
||||||
|
if enc:
|
||||||
|
try:
|
||||||
|
result[k] = decrypt(enc)
|
||||||
|
except Exception:
|
||||||
|
result[k] = ""
|
||||||
|
else:
|
||||||
|
result[k] = ""
|
||||||
|
else:
|
||||||
if enc:
|
if enc:
|
||||||
try:
|
try:
|
||||||
decrypt(enc) # verify roundtrip possible
|
decrypt(enc) # verify roundtrip possible
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
interface SecretState { configured: boolean }
|
interface SecretState { configured: boolean }
|
||||||
|
// Masked mode: object {configured}. Unmasked UAT mode (SLAB_UAT_UNMASK_SECRETS): plain string.
|
||||||
|
type Secret = SecretState | string;
|
||||||
interface Settings {
|
interface Settings {
|
||||||
woo_base_url: string;
|
woo_base_url: string;
|
||||||
woo_wp_username: string | null;
|
woo_wp_username: string | null;
|
||||||
woo_app_password: SecretState;
|
woo_app_password: Secret;
|
||||||
inference_base_url: SecretState;
|
inference_base_url: Secret;
|
||||||
inference_api_key: SecretState;
|
inference_api_key: Secret;
|
||||||
inference_model: string | null;
|
inference_model: string | null;
|
||||||
inference_enabled: boolean;
|
inference_enabled: boolean;
|
||||||
content_llm_enabled: boolean;
|
content_llm_enabled: boolean;
|
||||||
@@ -24,6 +26,11 @@
|
|||||||
// local edit buffers (write-only for secrets)
|
// local edit buffers (write-only for secrets)
|
||||||
let form: any = {};
|
let form: any = {};
|
||||||
|
|
||||||
|
// Unmasked UAT mode returns the secret as a plain string; masked mode returns {configured}.
|
||||||
|
const isUnmasked = (s: Secret) => typeof s === 'string';
|
||||||
|
const secretValue = (s: Secret): string => (isUnmasked(s) ? (s as string) : '');
|
||||||
|
const secretConfigured = (s: Secret): boolean => (isUnmasked(s) ? (s as string).length > 0 : s.configured);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading = true;
|
loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -39,10 +46,10 @@
|
|||||||
brand_voice: settings.brand_voice || '',
|
brand_voice: settings.brand_voice || '',
|
||||||
geo_context: settings.geo_context || '',
|
geo_context: settings.geo_context || '',
|
||||||
woo_create_status: settings.woo_create_status || 'draft',
|
woo_create_status: settings.woo_create_status || 'draft',
|
||||||
// secrets: only send if user types
|
// secrets: prefill when unmasked (UAT), else blank (masked, write-only)
|
||||||
woo_app_password: '',
|
woo_app_password: secretValue(settings.woo_app_password),
|
||||||
inference_base_url: '',
|
inference_base_url: secretValue(settings.inference_base_url),
|
||||||
inference_api_key: '',
|
inference_api_key: secretValue(settings.inference_api_key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -77,10 +84,10 @@
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
settings = await res.json();
|
settings = await res.json();
|
||||||
message = 'Saved';
|
message = 'Saved';
|
||||||
// clear secret inputs after save
|
// refresh secret buffers from the (possibly unmasked) server state
|
||||||
form.woo_app_password = '';
|
form.woo_app_password = secretValue(settings.woo_app_password);
|
||||||
form.inference_base_url = '';
|
form.inference_base_url = secretValue(settings.inference_base_url);
|
||||||
form.inference_api_key = '';
|
form.inference_api_key = secretValue(settings.inference_api_key);
|
||||||
} else {
|
} else {
|
||||||
const err = await res.json();
|
const err = await res.json();
|
||||||
message = err.error?.message || 'Save failed';
|
message = err.error?.message || 'Save failed';
|
||||||
@@ -124,18 +131,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Woo App Password {settings.woo_app_password.configured ? '(configured)' : '(not set)'}</label>
|
<label>Woo App Password {secretConfigured(settings.woo_app_password) ? '(configured)' : '(not set)'}</label>
|
||||||
<input type="password" bind:value={form.woo_app_password} placeholder={settings.woo_app_password.configured ? '••••••••' : ''} />
|
<input type={isUnmasked(settings.woo_app_password) ? 'text' : 'password'} bind:value={form.woo_app_password} placeholder={isUnmasked(settings.woo_app_password) ? '' : '••••••••'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Inference Base URL {settings.inference_base_url.configured ? '(configured)' : '(not set)'}</label>
|
<label>Inference Base URL {secretConfigured(settings.inference_base_url) ? '(configured)' : '(not set)'}</label>
|
||||||
<input bind:value={form.inference_base_url} placeholder={settings.inference_base_url.configured ? '••••••••' : ''} />
|
<input bind:value={form.inference_base_url} placeholder={isUnmasked(settings.inference_base_url) ? '' : '••••••••'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Inference API Key {settings.inference_api_key.configured ? '(configured)' : '(not set)'}</label>
|
<label>Inference API Key {secretConfigured(settings.inference_api_key) ? '(configured)' : '(not set)'}</label>
|
||||||
<input type="password" bind:value={form.inference_api_key} placeholder={settings.inference_api_key.configured ? '••••••••' : ''} />
|
<input type={isUnmasked(settings.inference_api_key) ? 'text' : 'password'} bind:value={form.inference_api_key} placeholder={isUnmasked(settings.inference_api_key) ? '' : '••••••••'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -182,17 +189,8 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.settings { max-width: 600px; color: #eee; }
|
.settings { max-width: 600px; color: #eee; }
|
||||||
.settings label { display: block; margin: 8px 0 4px; }
|
label { display: block; margin: 8px 0 4px; }
|
||||||
.settings input,
|
input, select, option { color: #eee; background: #1c1c1c; width: 100%; padding: 6px; }
|
||||||
.settings select {
|
option { color: #eee; background: #1c1c1c; }
|
||||||
color: #eee;
|
button { margin-top: 16px; }
|
||||||
background: #1c1c1c;
|
|
||||||
width: 100%;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
.settings option {
|
|
||||||
color: #eee;
|
|
||||||
background: #1c1c1c;
|
|
||||||
}
|
|
||||||
.settings button { margin-top: 16px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user