Real deletion (D15's 'full replacement'), not a toggle. Okta is now the only credential this app accepts anywhere. Backend: - server/models.py: drop User.password_hash. - server/alembic/versions/1d60a608bb51_...: matching migration (op.drop_column, same plain-drop precedent as project_role/locked_until/etc.; downgrade re-adds it with server_default=''). - server/auth.py: remove hash_password/verify_password/password_problem/ MIN_PASSWORD_LEN/_COMMON_PASSWORDS, create_reset_token/decode_reset_token/ RESET_MINUTES, the bcrypt import. Roles/tokens/cookies/get_current_user untouched. - server/app.py: remove login(), the whole self-service reset-password block (forgot-password/reset-available/reset-password), and change_password() (POST /api/auth/password). Rework create_user() to drop the password field (with a docstring note: the username must exactly match the eventual Okta identity claim, or a later sign-in provisions a second account instead of matching this one). Remove admin_reset_password() outright - nothing left to reset. Fixes a bug this task's own predecessor left behind: okta_callback()'s JIT provisioning (T10.3) was still setting password_hash="", which would have raised TypeError the moment the column was actually dropped. Admin bootstrap (D16): server/manage_users.py moves from creating accounts (create/create-admin/reset-password, all password-based) to a single 'promote <username> --role <role>' command that changes the role on a row Okta's JIT provisioning already created - the documented path for naming the first admin. list/disable/enable unchanged. Frontend: html/users.js drops the password field and validation from createUser(), removes resetPw() and its button (nothing left to reset). html/users.html drops the #nu-password input, adds a tooltip on username explaining the exact-match-to-Okta requirement. html/auth-guard.js removes the wpChangePassword dialog; html/wp-sidenav.js removes the 'Password' menu item that opened it. Tests: tests/browser_check.py and tests/launcher_check.py stop hashing a password to seed fixture rows (and the --keep-server hint now prints a ready-to-use cookie-setting snippet instead of a dead username/password). tests/pipeline_check.py and tests/token_check.py drop an unused PW import. tests/console_dialogs_check.py: the admin password-reset dialog it drove no longer exists, so that scenario is removed - the prompt-with-validate() UI pattern it exercised is still covered via creator_dialogs_check.py's wp-creation-app.js call sites, noted in this file's docstring so the coverage move isn't silent. tests/url_state_check.py: the "next= survives a real sign-in via login" scenario is explicitly marked SKIPPED (not deleted, not faked) - that promise is specific to the login FORM this task removed and can't be honestly re-proven until T10.5 rebuilds it as an Okta redirect; a minted-token cookie now stands in as setup only, so scenarios 3-6 in that file still get a signed-in page to run against. server/smoketest.py and server/seed_demo.py: switched from POST /api/auth/login to minting a session the same way okta_callback() does (auth.create_token(), seeded into the cookie jar) rather than waiting on T10.7. This is a real operational change, documented in both files' own AUTHENTICATION sections: they now need to run where AUTH_SECRET_KEY and the database match the target server's (inside the api container, or local dev) - they can no longer sign in to an arbitrary remote URL from an unrelated workstation, because Okta requires a real browser and these are stdlib scripts. The account must already exist; neither script creates or promotes one. server/requirements.txt: bcrypt dropped, nothing imports it anymore. Verified: full Alembic chain (baseline through this migration) upgrades and downgrades cleanly against a throwaway SQLite DB. okta_callback() JIT provisioning re-tested against the post-migration schema (would have thrown before the password_hash="" fix above). create_user() verified via a live HTTP call with no password field. manage_users.py promote verified end to end (seed a JIT-shaped row at project_user, promote to admin, list). smoketest.py and seed_demo.py both run to completion against a live uvicorn instance using the new minted-session path - 25/25 checks, including logout actually invalidating the session (proving the cookie-jar seeding didn't just fake the sign-in, it preserved the real expiry mechanics). wave-10.md T10.4 / D15 / D16
133 lines
5.5 KiB
Python
133 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Are the consoles' and launcher's 21 native dialogs gone? — BL-024, 2026-08-20.
|
|
|
|
S1 counted 79 native dialogs app-wide; its two tasks (T5.8 wizard, T7.9
|
|
creator) removed 58 and the audit found the remaining 21 on surfaces no S1
|
|
task named: admin.js (6), users.js (10), the launcher's inline script (5).
|
|
They now go through `wp-dialog.js` — the T7.9 kit extracted as a shared,
|
|
self-injecting component (guarded so the creator's inline copy still wins on
|
|
its own page).
|
|
|
|
Static half greps the counts; browser half drives the users console's dialogs
|
|
with natives poisoned and proves they still work end to end.
|
|
|
|
Used to drive this via the password-reset prompt specifically, because it was
|
|
the one place on this page exercising wp-dialog.js's PROMPT variant (text input
|
|
+ client-side validate()) rather than its confirm variant. T10.4 (D15/D16)
|
|
removed admin password reset entirely — there is no password to reset anymore
|
|
— so that coverage moved with it. The prompt-with-validate() pattern itself is
|
|
still exercised, just not on this page: see creator_dialogs_check.py for
|
|
wp-creation-app.js's own wpPromptDialog() call sites. If users.html ever grows
|
|
a new prompt-style dialog, it belongs back in this file.
|
|
|
|
Boots its own throwaway SQLite + uvicorn + headless browser; run it alone.
|
|
Exit 0 all passed, 1 a failure, 2 could not run.
|
|
"""
|
|
import io
|
|
import os
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
import cdp # noqa: E402
|
|
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
|
|
from qa_gate_check import api # noqa: E402
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
HTML = os.path.join(ROOT, "html")
|
|
|
|
|
|
def ascii_(v, n=240):
|
|
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
|
|
|
|
|
|
def strip_js(src):
|
|
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
|
return "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
|
|
|
|
|
|
def natives(name):
|
|
code = strip_js(io.open(os.path.join(HTML, name), encoding="utf-8").read())
|
|
return len(re.findall(r"(?<![\w.$])(alert|confirm|prompt)\(", code))
|
|
|
|
|
|
def main():
|
|
exe = cdp.find_browser()
|
|
if not exe:
|
|
print("no headless-capable browser found; set WP_BROWSER.")
|
|
return 2
|
|
|
|
print("\n1. the counts (baseline 6 + 10 + 5 = 21)")
|
|
for name in ("admin.js", "users.js", "index.html"):
|
|
chk("%s: 0 native dialogs" % name, natives(name) == 0, natives(name))
|
|
chk("wp-dialog.js exists, has the guard, and no natives of its own",
|
|
natives("wp-dialog.js") == 0
|
|
and "typeof global.wpConfirmDialog === 'function'" in
|
|
io.open(os.path.join(HTML, "wp-dialog.js"), encoding="utf-8").read())
|
|
for page in ("index.html", "admin.html", "users.html"):
|
|
chk("%s loads the kit" % page,
|
|
'src="wp-dialog.js"' in io.open(os.path.join(HTML, page), encoding="utf-8").read())
|
|
chk("the creator keeps its own copy (it owns the same-id markup in its HTML)",
|
|
"function wpConfirmDialog" in
|
|
io.open(os.path.join(HTML, "wp-creation-app.js"), encoding="utf-8").read())
|
|
|
|
tmpdir = tempfile.mkdtemp(prefix="wpsuite-condlg-")
|
|
db_path = os.path.join(tmpdir, "check.db")
|
|
server = None
|
|
browser = None
|
|
try:
|
|
tok = seed(db_path)
|
|
port = cdp.free_port()
|
|
base = "http://127.0.0.1:%d" % port
|
|
server = start_server(port, db_path)
|
|
|
|
print("\n2. the users console, natives poisoned")
|
|
browser = cdp.Browser(exe)
|
|
page = browser.page()
|
|
page.clear_cookies()
|
|
page.set_cookie("wp_session", tok["root"])
|
|
page.viewport(1440, 900)
|
|
page.goto(base + "/users.html")
|
|
time.sleep(2.0)
|
|
page.eval("window.alert=()=>{throw new Error('native alert reached')};"
|
|
"window.confirm=()=>{throw new Error('native confirm reached')};"
|
|
"window.prompt=()=>{throw new Error('native prompt reached')};")
|
|
chk("the console booted with a user table",
|
|
page.eval("!!document.querySelector('table')"))
|
|
|
|
print("\n3. destroy needs a real yes")
|
|
page.eval("void deleteUser('user_bob','bob')")
|
|
time.sleep(0.4)
|
|
chk("the delete asks through the kit, spelling out what goes with it",
|
|
page.eval("(() => { const o=document.getElementById('wp-dlg-overlay');"
|
|
" return o.classList.contains('open')"
|
|
" && /cannot be undone/.test(document.getElementById('wp-dlg-msg').textContent); })()"))
|
|
page.eval("document.getElementById('wp-dlg-cancel').click()")
|
|
time.sleep(0.6)
|
|
st, users = api(base, "/api/auth/users", tok["root"])
|
|
chk("cancel means no: bob is still an account",
|
|
st == 200 and any(u.get("username") == "bob" for u in (users or [])),
|
|
ascii_([u.get("username") for u in (users or [])]))
|
|
errs = [e for e in page.js_errors() if "beforeunload" not in e]
|
|
chk("no JavaScript errors, and no path reached a native dialog (they throw here)",
|
|
not errs, ascii_(errs[:3]))
|
|
finally:
|
|
if browser:
|
|
browser.close()
|
|
if server:
|
|
server.terminate()
|
|
|
|
print("\n" + "-" * 54)
|
|
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
|
|
for f in _FAIL:
|
|
print(" - " + f)
|
|
return 1 if _FAIL else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|