Files
sales-trainer/docs/test-evidence/2026-08-13-sprint-1-security.md

10 KiB

Sprint 1 Security Hotfix Evidence — 2026-08-13

Scope

Implemented and verified the Sprint 1 security packets against isolated temporary data only:

  • S1.1 pytest security harness
  • S1.2 identity-bound first-time setup
  • S1.3 tenant-scoped admin provisioning and updates
  • S1.4 request-level setup/user/organization guards
  • S1.5 fail-closed bootstrap and JWT configuration
  • S1.6 role-aware group/persona response redaction
  • Trainee-created persona variants are stored in an owner-private group
  • Organization admin PATCH read-modify-write updates use cross-process record locking
  • Reviewer follow-up: fail-closed persona allowlist, generic internal errors, upload limits/cleanup, protected super-admin mutations, strict JWT tenant claims, orphan-tenant login denial, and strict boolean terms consent

No production data, .env contents, credentials, deployment settings, commit, or push were touched.

Security behavior verified

Control Evidence
Setup cannot target another account and the one-time transition is atomic backend/app/api/auth_routes.py:56-91, backend/app/auth/users.py:181-229, backend/app/storage/store.py:112-173, backend/tests/test_auth_security.py
Setup requires must_setup and accepted terms backend/app/api/auth_routes.py:66-81, backend/tests/test_auth_security.py
Cross-tenant provisioning/update denied backend/app/api/admin_routes.py:97-163, backend/tests/test_admin_tenant_isolation.py
Non-super-admin cannot change role/active state backend/app/api/admin_routes.py:165-179, backend/tests/test_admin_tenant_isolation.py
Super-admin group index spans tenants while tenant admins remain scoped backend/app/api/group_routes.py:182-207, backend/tests/test_admin_tenant_isolation.py
Existing tokens re-check user/org state backend/app/api/helpers.py:44-72, backend/tests/test_request_auth_guards.py
Missing/inactive org and token org mismatch denied backend/app/api/helpers.py:55-67, backend/tests/test_request_auth_guards.py
Production bootstrap has no known password fallback backend/app/config.py:49-81, backend/app/factory.py:13-32, backend/tests/test_bootstrap_config.py
Analyze/get/list/persona/variant responses use one policy backend/app/api/group_routes.py:25-63,239-385, backend/tests/test_group_redaction.py
Trainee group view hides uploaded filenames and parsed source text backend/app/api/group_routes.py:51-70, backend/tests/test_group_redaction.py
Trainee variant cannot mutate the shared group or appear to another trainee backend/app/api/group_routes.py:373-402, backend/app/services/groups.py:74-102, backend/tests/test_group_redaction.py

Verification results

Executed from repository root unless noted:

backend/.venv/bin/python -m pytest backend/tests -q
77 passed

backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py -q
17 passed

backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py::test_complete_setup_reserves_email_across_concurrent_users -q
1 passed

backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py::test_complete_setup_is_atomic_under_concurrency -q
1 passed

backend/.venv/bin/python -m pytest backend/tests/test_auth_security.py::test_complete_setup_is_atomic_across_worker_processes -q
1 passed

backend/.venv/bin/python -m pytest backend/tests/test_group_redaction.py -q
9 passed

backend/.venv/bin/python -m pytest backend/tests/test_sprint1_review_findings.py -q
24 passed

backend/.venv/bin/python -m pytest backend/tests/test_request_auth_guards.py -q
12 passed

backend/.venv/bin/python -m pytest backend/tests/test_bootstrap_config.py -q
8 passed

backend/.venv/bin/python -m compileall -q backend/app backend/tests backend/scripts
exit 0

backend/scripts/test_*.py
12/12 passed:
  test_e2e.py
  test_ip_protection.py
  test_m0.py
  test_m1.py
  test_resume_decision.py
  test_routes.py
  test_saas_tenant.py
  test_scenario.py
  test_security.py
  test_setup.py
  test_user_journey.py
  test_variant.py

cd frontend && npm run build
vite transformed 1,772 modules; build passed

git diff --check
passed

Additional reviewer-remediation checks:

  • Admin persona responses use an explicit allowlist; unknown future persona fields are omitted.
  • Group envelopes and tenant-admin input views use explicit allowlists; unknown top-level fields, uploaded filenames, and parsed source payloads are omitted from non-super-admin responses.
  • Legacy raw group.error values are normalized at the API boundary; new failures persist only sanitized error codes and return generic client messages.
  • Flask MAX_CONTENT_LENGTH and per-file byte checks enforce upload bounds; partial uploads are removed on save, parse, parser-import, request-data, missing-input, multi-file, and group-persistence failures.
  • Auth, setup, session-start, persona-update, and profile error paths return fixed public messages; exception logs contain only error types. Active user/org state is literal-boolean fail-closed, and invited-user email uniqueness is enforced by the same locked create path.
  • Login responses also expose must_setup only when the stored value is literal True; no truthiness coercion remains on the reviewed auth path.
  • Super-admin creation/promotion and role/active mutation paths are blocked through the admin API.
  • JWT requests require an exact org_id; orphaned/inactive organizations and malformed active values cannot log in or pass request guards; placeholder/default bootstrap values are rejected; terms acceptance requires JSON boolean true.
  • Trainee private-persona endpoints use the same revealable serializer as shared-group views.
  • First-time setup uses a collection lock plus a locked conditional update; threaded and forked-worker concurrent attempts produce exactly one winner, concurrent setup of different accounts cannot reserve the same email address, and invite creation cannot race setup for the same email in either threads or separate worker processes. Service-level setup requires literal accepted_terms is True, user creation rejects non-boolean must_setup, and the set_email/setup race has a dedicated regression test. Tenant guards reject missing object tenant IDs instead of defaulting to org-default.

Explicit limitations

  • Docker verification was not run because Docker is not installed in the local environment.
  • Real-provider LLM QA was not run; no live provider credential was used.
  • Live bootstrap credential/JWT rotation was not performed automatically. Before restoring untrusted/public access, an operator must deploy behind restricted access, set/rotate credentials and JWT_SECRET, inspect audit data, and run a fresh authenticated smoke test.
  • Sprint 2 product-correctness defects remain out of scope: session identity/concurrency, persona reply parsing, final-judge debrief, and admin Preview Mode. The private-group lookup still uses the current JSON store's process-local per-file locks; collection-level scan/create atomicity remains an explicit Sprint 2 task.

Current verification — 2026-08-14

The exact current uncommitted tree was re-run after the latest response-boundary, tenant-fallback, session/debrief serializer, and organization-update race fixes:

backend/.venv/bin/python -m pytest backend/tests -q
97 passed in 23.50s

This is the post-remediation exact current-tree result. Earlier background run
(completed before the latest current-tree rerun):
proc_ba80f06ca949 — 91 passed in 928.85s (0:15:28)

Another earlier background run:
proc_efcee6c93382 — 93 passed in 23.11s

The 91- and 93-test results are retained as supporting historical evidence.
The 97-test run above is the authoritative post-remediation exact current-tree
result.

backend/scripts/test_*.py
12/12 passed

cd frontend && npm run build
passed; 1,772 modules transformed

backend/.venv/bin/python -m compileall -q backend/app backend/tests backend/scripts
AST parse + git diff --check
passed

added-line security scan
hardcoded_secret=0
shell_injection=0
eval_exec=0
pickle=0
sql_format=0

Additional current-tree regression coverage includes:

  • user/admin listings use a closed user allowlist; password_hash and unknown internal fields are omitted;
  • provider-controlled final judge output is constrained to a closed debrief allowlist;
  • trainee session/list/resume/get responses omit user_id, internal judge state, provider metadata, and unknown message fields;
  • authenticated group/private-persona mutations reject missing tenant identity instead of falling back to org-default;
  • malformed group input/persona shapes serialize safely and admin organization plan/seats fields reject coercion;
  • test_org_updates_preserve_concurrent_fields_across_processes verifies that concurrent organization active and seats updates do not lose fields across worker processes; JsonStore.update() holds the per-record process lock during read-modify-write.

Independent review

The earlier schema-valid packet/scoped approvals and reviewer timeouts are not approvals of this exact current tree. The exact-tree read-only reviewer (deleg_6e386e71) timed out after 600.19 seconds without schema-valid JSON. The later two-workstream batch (deleg_da73aca4) also timed out in both tasks. The bounded exact-tree reviewer (deleg_fccee30d) returned schema-valid passed=false with one concrete finding: organization admin updates lacked cross-process locking in JsonStore.update(). That finding was fixed and the 97-test run above was executed afterward. The fresh post-remediation exact-tree reviewer (deleg_7bcf0dfd) returned the required schema-valid verdict passed=true, with security_concerns=[] and logic_errors=[]. The Sprint 1 code/reviewer gate is therefore closed. Timeout, partial transcript, delegation status, or local test success must not be treated as approval.

Deployment decision

Do not restore public/untrusted access. The exact-tree code/reviewer gate is closed, but the separate live-operation gate remains pending. Docker verification, real-provider LLM QA, browser/mobile QA, restricted deployment, bootstrap credential change, JWT_SECRET rotation, audit inspection, and authenticated smoke have not been completed. The private-group filesystem scan/create collection race remains explicitly deferred to Sprint 2/S2.1.