diff --git a/shell/plugins/panels/network/Model.js b/shell/plugins/panels/network/Model.js index e9b0594a..ff10f489 100644 --- a/shell/plugins/panels/network/Model.js +++ b/shell/plugins/panels/network/Model.js @@ -322,6 +322,17 @@ function networkFailureReason(reason, reasons) { return "Failed to connect" } +// Whether a failed connect should reopen the passphrase prompt. NoSecrets +// always means credentials are missing. An auth timeout on a protected +// network means the saved passphrase is wrong (the same profile a first +// failed attempt leaves behind as "known"), so the user needs a chance to +// re-enter it -- connectWithPsk overwrites the stored PSK on submit. +function shouldRepromptPassphrase(reason, isProtected, reasons) { + var r = reasons || {} + if (reason === r.NoSecrets) return true + return !!isProtected && reason === r.WifiAuthTimeout +} + if (typeof module !== "undefined") { module.exports = { parseNetworkStatus: parseNetworkStatus, @@ -348,6 +359,7 @@ if (typeof module !== "undefined") { wifiSectionTitle: wifiSectionTitle, isProtected: isProtected, enterpriseConnectScript: enterpriseConnectScript, - networkFailureReason: networkFailureReason + networkFailureReason: networkFailureReason, + shouldRepromptPassphrase: shouldRepromptPassphrase } } diff --git a/shell/plugins/panels/network/Panel.qml b/shell/plugins/panels/network/Panel.qml index af9f144a..bb160f07 100644 --- a/shell/plugins/panels/network/Panel.qml +++ b/shell/plugins/panels/network/Panel.qml @@ -98,6 +98,16 @@ Panel { property string passwordText: "" property string identityText: "" + // ConnectionFailReason values as a plain object, so Model.js helpers stay + // pure JS and Node-testable. + readonly property var connectionFailReasons: ({ + NoSecrets: ConnectionFailReason.NoSecrets, + WifiAuthTimeout: ConnectionFailReason.WifiAuthTimeout, + WifiNetworkLost: ConnectionFailReason.WifiNetworkLost, + WifiClientDisconnected: ConnectionFailReason.WifiClientDisconnected, + WifiClientFailed: ConnectionFailReason.WifiClientFailed + }) + // True while any wifi action is mid-flight. Rows // disable themselves on this so clicks on the other rows don't silently // no-op against runNetworkAction's serialized guard. @@ -700,13 +710,11 @@ Panel { } function networkFailureReason(reason) { - return Model.networkFailureReason(reason, { - NoSecrets: ConnectionFailReason.NoSecrets, - WifiAuthTimeout: ConnectionFailReason.WifiAuthTimeout, - WifiNetworkLost: ConnectionFailReason.WifiNetworkLost, - WifiClientDisconnected: ConnectionFailReason.WifiClientDisconnected, - WifiClientFailed: ConnectionFailReason.WifiClientFailed - }) + return Model.networkFailureReason(reason, connectionFailReasons) + } + + function shouldRepromptPassphrase(reason, isProtected) { + return Model.shouldRepromptPassphrase(reason, isProtected, connectionFailReasons) } function checkActionCompletion(network) { @@ -882,7 +890,11 @@ Panel { Timer { id: actionTimeout - interval: 15000 + // Must outlast NetworkManager's 25s supplicant timeout: a wrong saved + // PSK fails with WifiAuthTimeout at ~25s, and that failure has to land + // while the action is still tracked to show "Wrong password" and reopen + // the passphrase prompt. + interval: 30000 repeat: false onTriggered: { if (!root.actionKind) return @@ -1567,8 +1579,12 @@ Panel { Connections { target: row.net ? row.net.network : null function onConnectionFailed(reason) { + // Background auto-connect retries fire this too; only reprompt for + // the connect started from this panel. Checked before + // failNetworkAction, which clears the action state. + var ours = root.actionKind === "connect" && root.actionSsid === (row.net.ssid || "") root.failNetworkAction(row.net.network, reason) - if (reason === ConnectionFailReason.NoSecrets) root.openPasswordPrompt(row.net.ssid) + if (ours && root.shouldRepromptPassphrase(reason, row.isProtected)) root.openPasswordPrompt(row.net.ssid) } function onConnectedChanged() { if (row.net) root.checkActionCompletion(row.net.network) @@ -1750,9 +1766,10 @@ Panel { } } - // Inline passphrase prompt — only shown when we hit a protected network - // we don't have saved credentials for. Submitting (Enter or the check - // button) fires connect; Esc cancels back to the row. + // Inline passphrase prompt — shown when we hit a protected network we + // don't have saved credentials for, or when a connect fails because the + // saved passphrase is wrong. Submitting (Enter or the check button) fires + // connect; Esc cancels back to the row. Item { id: passwordPanel visible: row.isPasswordOpen diff --git a/test/shell.d/network-test.sh b/test/shell.d/network-test.sh index 2d17d256..18007aea 100644 --- a/test/shell.d/network-test.sh +++ b/test/shell.d/network-test.sh @@ -110,6 +110,11 @@ assertEqual(network.networkFailureReason(1, reasons), 'Passphrase required', 'ne assertEqual(network.networkFailureReason(2, reasons), 'Wrong password', 'network maps auth timeout failures') assertEqual(network.networkFailureReason(99, reasons), 'Failed to connect', 'network maps unknown failures') +assertEqual(network.shouldRepromptPassphrase(reasons.NoSecrets, false, reasons), true, 'network reprompts when secrets are missing') +assertEqual(network.shouldRepromptPassphrase(reasons.WifiAuthTimeout, true, reasons), true, 'network reprompts a protected network after a wrong password') +assertEqual(network.shouldRepromptPassphrase(reasons.WifiAuthTimeout, false, reasons), false, 'network does not reprompt an open network on auth timeout') +assertEqual(network.shouldRepromptPassphrase(reasons.WifiClientFailed, true, reasons), false, 'network does not reprompt on generic connection failures') + assertEqual(network.bandLabel('2.4'), '2.4ghz', 'network labels the 2.4GHz band') assertEqual(network.bandLabel('6'), '6ghz', 'network labels the 6GHz band')