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:
@@ -183,11 +183,9 @@ function managerRow(u){
|
||||
: projRoleReadonly(u, can, why);
|
||||
|
||||
const actions = [];
|
||||
if(can && !me) actions.push('<button class="mini" onclick="resetPw(\''+uid+'\',\''+uname+'\')">Reset password</button>');
|
||||
if(can && !me) actions.push('<button class="mini" onclick="toggleActive(\''+uid+'\','+(!u.is_active)+')">'+
|
||||
(u.is_active?'Disable':'Enable')+'</button>');
|
||||
if(can && !me) actions.push('<button class="mini danger" onclick="deleteUser(\''+uid+'\',\''+uname+'\')">Delete</button>');
|
||||
if(me) actions.push('<button class="mini" disabled title="Use the Password link in the top bar to change your own">—</button>');
|
||||
if(!can && !me) actions.push('<span class="note" style="margin:0" title="'+uesc(why)+'">read-only</span>');
|
||||
|
||||
return '<tr'+(can||me ? '' : ' class="is-locked"')+'>'+
|
||||
@@ -254,19 +252,6 @@ function projAccessCell(u){
|
||||
// ── row actions ───────────────────────────────────────────────────────────────
|
||||
// Each one reloads on failure so a control can never sit there showing a value the
|
||||
// server refused.
|
||||
async function resetPw(id, username){
|
||||
// The min-12 rule was stated in the prompt label and enforced only by the
|
||||
// server round-trip; the kit's validate() answers AT the input instead.
|
||||
const pw = await wpPromptDialog({title:'Reset password',
|
||||
message:'Set a new password for "'+username+'". Their existing sessions are signed out.',
|
||||
label:'New password (min 12 characters)',
|
||||
validate:v => (v && v.length >= 12) ? '' : 'At least 12 characters.'});
|
||||
if(pw === null) return;
|
||||
const { status, json } = await api('POST','/api/auth/users/'+id+'/password',{new_password:pw});
|
||||
if(status === 200) toast('Password reset for '+username+'. Their existing sessions are signed out.');
|
||||
else wpAlertDialog({title:'Reset failed', message:'Could not reset the password: '+apiError(status, json)});
|
||||
}
|
||||
|
||||
async function toggleActive(id, makeActive){
|
||||
const { status, json } = await api('POST','/api/auth/users/'+id+'/active',{is_active:makeActive});
|
||||
if(status === 200) loadUsers();
|
||||
@@ -344,25 +329,27 @@ function renderCreateForm(){
|
||||
async function createUser(){
|
||||
const msg = document.getElementById('users-create-msg');
|
||||
const val = id => (document.getElementById(id)||{}).value || '';
|
||||
// The username entered here MUST match what Okta's identity claim will send for
|
||||
// this person exactly — this creates the account ahead of their first sign-in,
|
||||
// and that's how a later Okta sign-in finds this row instead of provisioning a
|
||||
// second one. See create_user()'s docstring in server/app.py.
|
||||
const username = val('nu-username').trim();
|
||||
const password = val('nu-password');
|
||||
const project_ids = [...document.querySelectorAll('#nu-project-list input[type=checkbox]:checked')]
|
||||
.map(c => c.value);
|
||||
const say = (color, text) => { msg.style.color = color; msg.textContent = text; };
|
||||
if(!username){ say('var(--red)','Username is required.'); return; }
|
||||
if(password.length < 12){ say('var(--red)','Password must be at least 12 characters.'); return; }
|
||||
if(_scope.scope !== 'all' && !project_ids.length){
|
||||
say('var(--red)','Pick at least one project — you administer users per project.'); return;
|
||||
}
|
||||
say('var(--muted)','Creating…');
|
||||
const { status, json } = await api('POST','/api/auth/users',{
|
||||
username, password, project_ids,
|
||||
username, project_ids,
|
||||
full_name: val('nu-fullname').trim(), email: val('nu-email').trim(),
|
||||
role: val('nu-role'), project_role: val('nu-project-role'),
|
||||
});
|
||||
if(status === 200){
|
||||
say('var(--green)','✓ Created '+username+'.');
|
||||
['nu-username','nu-fullname','nu-email','nu-password'].forEach(id => document.getElementById(id).value = '');
|
||||
['nu-username','nu-fullname','nu-email'].forEach(id => document.getElementById(id).value = '');
|
||||
loadUsers();
|
||||
} else {
|
||||
say('var(--red)','✕ '+apiError(status, json, 'Could not create the account'));
|
||||
|
||||
Reference in New Issue
Block a user