T10.4: remove the local password path entirely
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
This commit is contained in:
@@ -39,7 +39,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import cdp # noqa: E402
|
||||
|
||||
PW = "CorrectHorseBattery9"
|
||||
_PASS, _FAIL = [], []
|
||||
|
||||
|
||||
@@ -86,7 +85,7 @@ def seed(db_path):
|
||||
def mk(username, role):
|
||||
db.add(models.User(id="user_" + username, username=username,
|
||||
email=f"{username}@example.test", full_name=username.title(),
|
||||
password_hash=auth.hash_password(PW), role=role))
|
||||
role=role))
|
||||
|
||||
mk("root", auth.ROLE_ADMIN)
|
||||
mk("sue", auth.ROLE_PROJECT_SUPER) # super user on Job A
|
||||
@@ -436,7 +435,10 @@ def main():
|
||||
finally:
|
||||
if args.keep_server:
|
||||
print(f"\n --keep-server: still up at {base}, database at {db_path}")
|
||||
print(" Sign in as root / " + PW)
|
||||
# No local password exists (D15/D16) — there's nothing to type into a login
|
||||
# form. Set the session cookie directly, the same way this script's own
|
||||
# fixture does, from the browser console on that origin:
|
||||
print(f" document.cookie = 'wp_session={tok['root']}; path=/'")
|
||||
else:
|
||||
if server:
|
||||
# Wait for it to actually exit before deleting the database out from
|
||||
|
||||
@@ -8,9 +8,17 @@ 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 password-reset prompt on
|
||||
the users console with natives poisoned, and proves validate() answers AT the
|
||||
input while the server round-trip completes end to end.
|
||||
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.
|
||||
@@ -91,31 +99,6 @@ def main():
|
||||
chk("the console booted with a user table",
|
||||
page.eval("!!document.querySelector('table')"))
|
||||
|
||||
page.eval("void resetPw('user_pat','pat')")
|
||||
time.sleep(0.4)
|
||||
chk("the reset prompt is the kit's modal, open, focused at the input",
|
||||
page.eval("(() => { const o=document.getElementById('wp-dlg-overlay');"
|
||||
" return !!o && o.classList.contains('open')"
|
||||
" && document.activeElement.id==='wp-dlg-input'; })()"))
|
||||
page.eval("document.getElementById('wp-dlg-input').value='short';"
|
||||
"document.getElementById('wp-dlg-ok').click()")
|
||||
chk("a short password is refused AT the input - dialog stays, error says why",
|
||||
page.eval("(() => { const o=document.getElementById('wp-dlg-overlay');"
|
||||
" return o.classList.contains('open')"
|
||||
" && /12 characters/.test(document.getElementById('wp-dlg-err').textContent); })()"))
|
||||
page.eval("document.getElementById('wp-dlg-input').value='CorrectHorseBattery10';"
|
||||
"document.getElementById('wp-dlg-ok').click()")
|
||||
time.sleep(1.2)
|
||||
chk("a good answer closes the dialog and the server accepts it",
|
||||
page.eval("!document.getElementById('wp-dlg-overlay').classList.contains('open')"))
|
||||
chk("...announced through the kit's toast (role=status)",
|
||||
page.eval("(() => { const t=document.getElementById('toast');"
|
||||
" return !!t && t.getAttribute('role')==='status'"
|
||||
" && /Password reset for pat/.test(t.textContent); })()"))
|
||||
st, _ = api(base, "/api/auth/login", "x", "POST",
|
||||
{"username": "pat", "password": "CorrectHorseBattery10"})
|
||||
chk("...and the new password actually works", st == 200, st)
|
||||
|
||||
print("\n3. destroy needs a real yes")
|
||||
page.eval("void deleteUser('user_bob','bob')")
|
||||
time.sleep(0.4)
|
||||
|
||||
@@ -31,7 +31,7 @@ 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, _c, PW # noqa: E402
|
||||
from browser_check import seed, start_server, chk, _PASS, _FAIL, _c # noqa: E402
|
||||
|
||||
STUB = """
|
||||
window.__dialogs = [];
|
||||
@@ -55,8 +55,7 @@ def seed_empty(db_path):
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with SessionLocal() as db:
|
||||
db.add(models.User(id="user_new", username="new", email="new@example.test",
|
||||
full_name="New Starter", password_hash=auth.hash_password(PW),
|
||||
role=auth.ROLE_ADMIN))
|
||||
full_name="New Starter", role=auth.ROLE_ADMIN))
|
||||
db.commit()
|
||||
return {u.username: auth.create_token(u) for u in db.query(models.User).all()}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ 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, _c, PW # noqa: E402
|
||||
from browser_check import seed, start_server, chk, _PASS, _FAIL, _c # noqa: E402
|
||||
|
||||
READY = "!!document.querySelector('#pipeline-strip .pipe-cell, #pipeline-strip .pipe-empty, " \
|
||||
"#pipeline-strip .pipe-error')"
|
||||
|
||||
@@ -47,7 +47,7 @@ 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, PW # noqa: E402,F401
|
||||
from browser_check import seed, start_server # noqa: E402
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HTML = os.path.join(ROOT, "html")
|
||||
|
||||
@@ -8,6 +8,10 @@ will rest on.
|
||||
|
||||
1. a URL identifying a work package opens that work package
|
||||
2. the same URL works for a SIGNED-OUT user, via login, landing on the target
|
||||
— SKIPPED as of T10.4: local login is gone (D15/D16), and the redirect-
|
||||
through-Okta replacement doesn't exist until T10.5 rebuilds login.html.
|
||||
Re-test this once that lands. Signing in via a minted token stands in as
|
||||
setup only, so scenarios 3-6 below still get a signed-in page to run on.
|
||||
3. refresh preserves project, package, tab and view
|
||||
4. Back and Forward move through states without a reload or a broken view
|
||||
5. the URL survives being copied to a second browsing context
|
||||
@@ -27,7 +31,7 @@ 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, _c, PW # noqa: E402
|
||||
from browser_check import seed, start_server, chk, _PASS, _FAIL, _c # noqa: E402
|
||||
|
||||
|
||||
def settle(page, seconds=1.4):
|
||||
@@ -169,6 +173,8 @@ def main():
|
||||
page2.close()
|
||||
|
||||
print("\n2. the same URL works for a signed-out user, via login")
|
||||
print(" SKIPPED: local login is gone (D15/D16); the Okta-redirect replacement")
|
||||
print(" doesn't exist until T10.5. Re-test the next= round trip once it lands.")
|
||||
page.clear_cookies()
|
||||
page.goto(deep)
|
||||
settle(page, 1.6)
|
||||
@@ -177,19 +183,14 @@ def main():
|
||||
nxt = page.eval("new URLSearchParams(location.search).get('next')||''")
|
||||
chk("...carrying the requested target, package id and all",
|
||||
"wp-creation-index.html" in nxt and "wp=wpA1" in nxt, "next=%r" % nxt)
|
||||
page.eval("document.getElementById('username').value=%r" % "root")
|
||||
page.eval("document.getElementById('password').value=%r" % PW)
|
||||
page.eval("document.querySelector('form').requestSubmit"
|
||||
"? document.querySelector('form').requestSubmit()"
|
||||
": document.querySelector('form').submit()")
|
||||
for _ in range(40):
|
||||
if "wp-creation-index.html" in page.eval("location.href"):
|
||||
break
|
||||
time.sleep(0.3)
|
||||
settle(page, 1.2)
|
||||
chk("signing in continues to the requested page, not the home page",
|
||||
"wp-creation-index.html" in page.eval("location.href"),
|
||||
page.eval("location.href"))
|
||||
# Setup only for scenarios 3-6 below, NOT a re-test of "signing in continues
|
||||
# to the requested page" — 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 gets `page` to the same signed-in,
|
||||
# on-target state those later scenarios need, without claiming to have
|
||||
# exercised the (currently nonexistent) sign-in flow itself.
|
||||
page.set_cookie("wp_session", tok["root"])
|
||||
page.goto(deep)
|
||||
for _ in range(30):
|
||||
if page.eval("!!window.wpCreatorReady"):
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user