One key for audit retention, and stop the SAML page claiming a feature exists

Wave 6: two settings pages that told an administrator the site was configured
when it was not. That is worse than a missing feature, because nobody goes
looking for it.

Audit retention had TWO keys. shopdb/core/services/auditretention.py reads
`audit_retentiondays` (category site, default 0). A second `audit_retention_days`
was seeded under category audit with a default of 90, and the Audit & Logging
page wrote to that one - so an administrator set a period, reloaded, saw it
persisted, and nothing ever consulted it. Retention stayed off. The page now
writes the key that is read, the duplicate is out of the seed list, and
migration 7d39 carries a value an operator actually set on the dead key over to
the live one before deleting it: their intent was "keep 30 days", and the fix
should not silently discard it. The page's copy also claimed logs "will be
automatically purged", which nothing does - purging is manual, from the site
settings page or `flask audit purge --apply`.

The Authentication page offered a full SAML configuration - metadata URL, entity
id, ACS URL, auto-create, admin group - and reported "Configured - SAML login
available" once two fields held text. There is no SAML anywhere in the codebase:
no login route, no ACS, no assertion handling. SAML IS planned, so the settings
stay and the page keeps them; what changes is that it now says plainly that
nothing consumes them yet, and the status line never claims availability.

The dangerous control there was "Allow Local Login". Turning it off did nothing
at all, so an administrator could believe they had closed local password login
while it stayed wide open. It is locked on until there is another way in, and
says why. CONFIG.md marks the whole block reserved rather than describing it as
working configuration.

Scope: product
This commit is contained in:
cproudlock
2026-09-03 14:53:57 -04:00
parent 45acaf0b8f
commit 1d2bbb3630
7 changed files with 230 additions and 37 deletions

View File

@@ -1153,6 +1153,18 @@ td.actions .btn + .btn {
margin: 0 0 1rem 0;
line-height: 1.5;
}
/* A settings block whose feature is not built yet. Reads as a warning rather
than body text, because the failure it prevents is an administrator
believing a control does something. */
.setting-notice {
border-left: 3px solid var(--warning);
background: var(--bg);
color: var(--text);
font-size: 0.9rem;
line-height: 1.5;
margin: 0 0 1rem 0;
padding: 0.6rem 0.8rem;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));

View File

@@ -8,8 +8,10 @@
<div class="setting-group">
<h3>Audit Log Retention</h3>
<p class="setting-description">
Configure how long audit logs are retained. Older logs will be automatically purged.
Set to 0 to keep logs indefinitely.
How long audit entries are kept. 0 keeps everything forever, which is
the default. Saving a period does NOT delete anything by itself:
purge from Settings &gt; Site &amp; Facility, or run
<code>flask audit purge --apply</code> on the server.
</p>
<div class="setting-row">
@@ -17,13 +19,13 @@
<span>Retention Period (days)</span>
<input
type="number"
v-model="settings.audit_retention_days"
v-model="settings.audit_retentiondays"
min="0"
placeholder="90"
@blur="saveSetting('audit_retention_days', settings.audit_retention_days)"
placeholder="0"
@blur="saveSetting('audit_retentiondays', settings.audit_retentiondays)"
:disabled="saving"
>
<small class="input-hint">0 = keep forever, recommended: 90 days</small>
<small class="input-hint">0 = keep forever</small>
</label>
</div>

View File

@@ -7,9 +7,16 @@
<div class="section-card">
<div class="setting-group">
<h3>SAML Single Sign-On</h3>
<p class="setting-notice">
<strong>Not implemented yet.</strong> These settings are stored so a
site can be configured ahead of the feature landing, but nothing
consumes them today: there is no SAML login route, and no assertion
is accepted. Sign-in is local accounts only, whatever is set here.
</p>
<p class="setting-description">
Enable SAML SSO for enterprise authentication. Users can sign in using your organization's
identity provider (Azure AD, Okta, etc.). Local login can remain enabled as a fallback.
When it ships, users will sign in through your organization's identity
provider (Azure AD, Okta, etc.), with local login available as a
fallback.
</p>
<div class="setting-row">
@@ -82,19 +89,26 @@
</label>
</div>
<!-- Locked on until SAML exists. A toggle that reads "Allow Local
Login: off" while password login still works is the dangerous
one on this page: an administrator can believe they have closed
the local door, and nothing tells them otherwise. It is not a
setting anyone can honour yet, so it does not accept a click. -->
<div class="setting-row">
<label class="toggle-label">
<span>Allow Local Login</span>
<button
class="toggle-btn"
:class="{ active: settings.saml_allow_local_login }"
@click="toggleSetting('saml_allow_local_login')"
:disabled="saving"
class="toggle-btn active"
disabled
title="Local login cannot be turned off until SAML sign-in exists"
>
<span class="toggle-slider"></span>
</button>
</label>
<small class="input-hint toggle-hint">Keep enabled for admin fallback access</small>
<small class="input-hint toggle-hint">
Always on for now. Local accounts are the only way in until SAML
sign-in ships, so this cannot be turned off yet.
</small>
</div>
<div class="setting-row">
@@ -148,15 +162,20 @@ const {
// Per-page connection status (stays with the page, not the composable).
const samlStatus = computed(() => {
if (!settings.saml_enabled) return 'inactive'
if (!settings.saml_idp_metadata_url || !settings.saml_entity_id) return 'warning'
// 'pending' rather than a healthy state even when every field is filled in:
// nothing is connected until the feature exists.
return 'pending'
})
// Never says "available". It used to report "Configured - SAML login
// available" as soon as two fields held text, which is a claim about a feature
// that does not exist: an administrator could reasonably read that as SSO
// being live and stop issuing local accounts.
const samlMessage = computed(() => {
if (!settings.saml_enabled) return 'Disabled - using local authentication only'
if (!settings.saml_idp_metadata_url) return 'IdP metadata URL not configured'
if (!settings.saml_entity_id) return 'Entity ID not configured'
return 'Configured - SAML login available'
if (!settings.saml_enabled) return 'Off - local accounts only'
if (!settings.saml_idp_metadata_url) return 'IdP metadata URL not set'
if (!settings.saml_entity_id) return 'Entity ID not set'
return 'Settings saved - waiting on SAML support, local accounts still in use'
})
onMounted(loadSettings)