Security closeout: settings public allowlist, audit.view gating, test-user guard

Settings exposure (review medium): GET /api/settings and /api/settings/<key>
now return the full table only to an authenticated principal. Unauthenticated
callers (kiosk dashboards, print pages, login screen, setup router) get just a
public allowlist - categories branding + map plus a named set (site_base_url,
facility_name, printer_hostname_template, contact_email_domain,
servicenow_enabled, setup_complete). A non-public single-key GET returns 404 so
existence is not confirmed. Secrets stay masked in both cases. Closes the
unauthenticated enumeration of smtp_host / employee_db_host / zabbix_url /
servicenow URLs. Allowlist mirrors the keys siteSettings.js + mapConfig.js +
setupState.js read before login.

audit.view (review low): the three audit-read routes (list, entity-history,
stats) were jwt_required only despite a defined-but-unwired audit.view
permission; now gated by it (seeded to admin), so a role-less member or unscoped
PAT can no longer read the cross-user audit trail.

flask seed test-user (review low): refuses outside DEBUG/TESTING - it creates
the well-known admin/admin123; production sites use `flask seed admin`.

Tests: unauthenticated allowlist + authed-full-masked + private-key-404, and
member-403 / admin-200 on audit routes. 336 authz tests pass; naming green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-13 08:36:19 -04:00
parent cd353b6432
commit d49baeb5fa
5 changed files with 104 additions and 9 deletions

View File

@@ -280,11 +280,19 @@ def seed_reference_data():
@seed_cli.command('test-user')
@with_appcontext
def seed_test_user():
"""Create a test admin user."""
"""Create a test admin user (admin / admin123). DEV ONLY."""
from flask import current_app
from shopdb.extensions import db
from shopdb.core.models import User, Role
from werkzeug.security import generate_password_hash
# Refuse in production: this seeds a well-known credential. Sites bootstrap
# a real admin with `flask seed admin` (generated password) or the wizard.
if not (current_app.config.get('DEBUG') or current_app.config.get('TESTING')):
raise click.ClickException(
'seed test-user is dev-only (creates admin/admin123). '
'Use `flask seed admin` to create a production admin.')
# Create admin role if not exists
admin_role = Role.query.filter_by(rolename='admin').first()
if not admin_role: