Fix passwordless OWE Wi-Fi handling (#7238)
This commit is contained in:
@@ -299,8 +299,17 @@ function wifiSectionTitle(wifiNetworks, index) {
|
||||
return ""
|
||||
}
|
||||
|
||||
function isProtected(security, openSecurity) {
|
||||
return security !== openSecurity
|
||||
// OWE (Enhanced Open) encrypts traffic without authenticating the user, so it
|
||||
// has no credentials to collect. The panel's lock is a credentials-required
|
||||
// affordance, so OWE should neither show it nor open its attached prompt.
|
||||
function requiresCredentials(security, openSecurity, oweSecurity) {
|
||||
// Only explicit passwordless types bypass the prompt. Unknown security
|
||||
// stays credentialed as the conservative fallback.
|
||||
return security !== openSecurity && security !== oweSecurity
|
||||
}
|
||||
|
||||
function canForgetNetwork(network) {
|
||||
return !!(network && network.known && !network.connected)
|
||||
}
|
||||
|
||||
// The password arrives on stdin and reaches nmcli through the scriptable
|
||||
@@ -316,10 +325,10 @@ var enterpriseConnectScript =
|
||||
" && nmcli connection up uuid \"$u\"" +
|
||||
" || { nmcli connection delete uuid \"$u\" >/dev/null 2>&1; false; }"
|
||||
|
||||
function networkFailureReason(reason, reasons) {
|
||||
function networkFailureReason(reason, needsCredentials, reasons) {
|
||||
var r = reasons || {}
|
||||
if (reason === r.NoSecrets) return "Passphrase required"
|
||||
if (reason === r.WifiAuthTimeout) return "Wrong password"
|
||||
if (needsCredentials && reason === r.NoSecrets) return "Passphrase required"
|
||||
if (needsCredentials && reason === r.WifiAuthTimeout) return "Wrong password"
|
||||
if (reason === r.WifiNetworkLost) return "Network lost"
|
||||
if (reason === r.WifiClientDisconnected) return "Disconnected"
|
||||
if (reason === r.WifiClientFailed) return "Connection failed"
|
||||
@@ -327,14 +336,15 @@ function networkFailureReason(reason, reasons) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// means credentials are missing only for a network that actually uses them.
|
||||
// An auth timeout on such a 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, needsCredentials, reasons) {
|
||||
var r = reasons || {}
|
||||
if (reason === r.NoSecrets) return true
|
||||
return !!isProtected && reason === r.WifiAuthTimeout
|
||||
if (!needsCredentials) return false
|
||||
return reason === r.NoSecrets || reason === r.WifiAuthTimeout
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
@@ -361,7 +371,8 @@ if (typeof module !== "undefined") {
|
||||
wifiRow: wifiRow,
|
||||
sortWifiRows: sortWifiRows,
|
||||
wifiSectionTitle: wifiSectionTitle,
|
||||
isProtected: isProtected,
|
||||
requiresCredentials: requiresCredentials,
|
||||
canForgetNetwork: canForgetNetwork,
|
||||
enterpriseConnectScript: enterpriseConnectScript,
|
||||
networkFailureReason: networkFailureReason,
|
||||
shouldRepromptPassphrase: shouldRepromptPassphrase
|
||||
|
||||
@@ -398,7 +398,7 @@ Panel {
|
||||
}
|
||||
|
||||
function canForgetNetwork(net) {
|
||||
return !!(net && net.known && isProtected(net.security) && !net.connected)
|
||||
return Model.canForgetNetwork(net)
|
||||
}
|
||||
|
||||
function canShareNetwork(net) {
|
||||
@@ -417,8 +417,8 @@ Panel {
|
||||
}
|
||||
|
||||
// Enter/Space on the highlighted row. Mirrors row-click semantics:
|
||||
// connected → disconnect, protected-unknown → password prompt,
|
||||
// open/known → connect.
|
||||
// connected → disconnect, credentials-required/unknown → prompt,
|
||||
// passwordless/known → connect.
|
||||
function activateSelected() {
|
||||
if (busy || selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
|
||||
var net = wifiNetworks[selectedIndex]
|
||||
@@ -428,8 +428,8 @@ Panel {
|
||||
// connectedWifiNetwork when handed null, so a row left stale by scan churn
|
||||
// would otherwise tear down whatever is connected now instead.
|
||||
if (net.connected) { disconnectRow(net.ssid); return }
|
||||
if (isProtected(net.security) && !net.known) { openPasswordPrompt(net.ssid); return }
|
||||
connectKnown(net.ssid)
|
||||
if (requiresCredentials(net.security) && !net.known) { openPasswordPrompt(net.ssid); return }
|
||||
connectDirectly(net.ssid)
|
||||
}
|
||||
|
||||
// Bar pill state, derived from the native NetworkManager service so the
|
||||
@@ -680,8 +680,8 @@ Panel {
|
||||
root.close()
|
||||
}
|
||||
|
||||
function isProtected(security) {
|
||||
return Model.isProtected(security, WifiSecurityType.Open)
|
||||
function requiresCredentials(security) {
|
||||
return Model.requiresCredentials(security, WifiSecurityType.Open, WifiSecurityType.Owe)
|
||||
}
|
||||
|
||||
function openPasswordPrompt(ssid) {
|
||||
@@ -735,18 +735,18 @@ Panel {
|
||||
if (!network || actionKind === "" || actionSsid !== (network.name || "")) return
|
||||
actionTimeout.stop()
|
||||
failureSsid = actionSsid
|
||||
failureReason = networkFailureReason(reason)
|
||||
failureReason = networkFailureReason(reason, requiresCredentials(network.security))
|
||||
actionSsid = ""
|
||||
actionKind = ""
|
||||
refresh()
|
||||
}
|
||||
|
||||
function networkFailureReason(reason) {
|
||||
return Model.networkFailureReason(reason, connectionFailReasons)
|
||||
function networkFailureReason(reason, needsCredentials) {
|
||||
return Model.networkFailureReason(reason, needsCredentials, connectionFailReasons)
|
||||
}
|
||||
|
||||
function shouldRepromptPassphrase(reason, isProtected) {
|
||||
return Model.shouldRepromptPassphrase(reason, isProtected, connectionFailReasons)
|
||||
function shouldRepromptPassphrase(reason, needsCredentials) {
|
||||
return Model.shouldRepromptPassphrase(reason, needsCredentials, connectionFailReasons)
|
||||
}
|
||||
|
||||
function checkActionCompletion(network) {
|
||||
@@ -756,7 +756,7 @@ Panel {
|
||||
else if (actionKind === "forget" && !network.known && !network.stateChanging) clearNetworkAction()
|
||||
}
|
||||
|
||||
function connectKnown(ssid) {
|
||||
function connectDirectly(ssid) {
|
||||
runNetworkAction("connect", networkForSsid(ssid), function(network) { network.connect() })
|
||||
}
|
||||
|
||||
@@ -1587,8 +1587,8 @@ Panel {
|
||||
}
|
||||
|
||||
// A single Wi-Fi network entry. Collapses to a one-line pill normally;
|
||||
// expands inline to a passphrase prompt when the user picks a protected
|
||||
// network we don't have credentials for. Clicking a connected row
|
||||
// expands inline to a passphrase prompt when the user picks a network that
|
||||
// requires credentials we do not have. Clicking a connected row
|
||||
// disconnects.
|
||||
component NetworkRow: CursorSurface {
|
||||
id: row
|
||||
@@ -1597,14 +1597,14 @@ Panel {
|
||||
|
||||
readonly property bool isConnected: net && net.connected
|
||||
readonly property bool isKnown: !!(net && net.known)
|
||||
readonly property bool isProtected: net ? root.isProtected(net.security) : false
|
||||
readonly property bool requiresCredentials: net ? root.requiresCredentials(net.security) : false
|
||||
readonly property bool isEnterprise: net
|
||||
? (net.security === WifiSecurityType.Wpa2Eap || net.security === WifiSecurityType.WpaEap)
|
||||
: false
|
||||
readonly property bool canForgetFromLock: isKnown && isProtected && !isConnected
|
||||
readonly property bool canForget: root.canForgetNetwork(net)
|
||||
readonly property bool isSelected: root.focusSection === "wifi" && root.selectedIndex === index
|
||||
readonly property bool forgetFocused: isSelected && root.wifiActionFocused && canForgetFromLock
|
||||
readonly property bool forgetVisible: canForgetFromLock && (forgetFocused || rightMouse.containsMouse)
|
||||
readonly property bool forgetFocused: isSelected && root.wifiActionFocused && canForget
|
||||
readonly property bool forgetVisible: canForget && (!requiresCredentials || forgetFocused || rightMouse.containsMouse)
|
||||
|
||||
hasCursor: root.cursorActive && isSelected && !root.wifiActionFocused
|
||||
current: isConnected
|
||||
@@ -1631,7 +1631,7 @@ Panel {
|
||||
// failNetworkAction, which clears the action state.
|
||||
var ours = root.actionKind === "connect" && root.actionSsid === (row.net.ssid || "")
|
||||
root.failNetworkAction(root.networkForSsid(row.net.ssid), reason)
|
||||
if (ours && root.shouldRepromptPassphrase(reason, row.isProtected)) root.openPasswordPrompt(row.net.ssid)
|
||||
if (ours && root.shouldRepromptPassphrase(reason, row.requiresCredentials)) root.openPasswordPrompt(row.net.ssid)
|
||||
}
|
||||
function onConnectedChanged() {
|
||||
if (row.net) root.checkActionCompletion(root.networkForSsid(row.net.ssid))
|
||||
@@ -1692,11 +1692,11 @@ Panel {
|
||||
root.disconnectRow(row.net.ssid)
|
||||
return
|
||||
}
|
||||
if (row.isProtected && !row.isKnown) {
|
||||
if (row.requiresCredentials && !row.isKnown) {
|
||||
root.openPasswordPrompt(row.net.ssid)
|
||||
return
|
||||
}
|
||||
root.connectKnown(row.net.ssid)
|
||||
root.connectDirectly(row.net.ssid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1719,11 +1719,12 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Shows a lock glyph for protected networks. Known disconnected
|
||||
// networks reveal the forget action when hovering that right edge.
|
||||
// The right edge shows a lock for networks that require credentials and
|
||||
// reveals Forget on hover. Known passwordless networks show Forget
|
||||
// directly rather than reserving an invisible or misleading target.
|
||||
Item {
|
||||
id: rightAction
|
||||
visible: row.isProtected
|
||||
visible: row.requiresCredentials || row.canForget
|
||||
width: Style.space(22)
|
||||
implicitHeight: lockIndicator.implicitHeight
|
||||
anchors.right: parent.right
|
||||
@@ -1731,6 +1732,7 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: lockIndicator
|
||||
visible: row.requiresCredentials || row.forgetVisible
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
@@ -1754,7 +1756,7 @@ Panel {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
enabled: row.canForgetFromLock && !root.busy
|
||||
enabled: row.canForget && !root.busy
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index; root.wifiActionFocused = true }
|
||||
onClicked: if (row.net) root.forget(row.net)
|
||||
|
||||
@@ -189,13 +189,82 @@ assertDeepEqual(
|
||||
'network wifi rows project exactly the primitive fields, so each delegate stores no live QObject'
|
||||
)
|
||||
|
||||
const reasons = { NoSecrets: 1, WifiAuthTimeout: 2, WifiNetworkLost: 3, WifiClientDisconnected: 4, WifiClientFailed: 5 }
|
||||
assertEqual(network.networkFailureReason(1, reasons), 'Passphrase required', 'network maps missing passphrase failures')
|
||||
assertEqual(network.networkFailureReason(2, reasons), 'Wrong password', 'network maps auth timeout failures')
|
||||
assertEqual(network.networkFailureReason(99, reasons), 'Failed to connect', 'network maps unknown failures')
|
||||
const security = {
|
||||
Wpa3SuiteB192: 0,
|
||||
Sae: 1,
|
||||
Wpa2Eap: 2,
|
||||
Wpa2Psk: 3,
|
||||
WpaEap: 4,
|
||||
WpaPsk: 5,
|
||||
StaticWep: 6,
|
||||
DynamicWep: 7,
|
||||
Leap: 8,
|
||||
Owe: 9,
|
||||
Open: 10,
|
||||
Unknown: 11
|
||||
}
|
||||
for (const name of ['Wpa3SuiteB192', 'Sae', 'Wpa2Eap', 'Wpa2Psk', 'WpaEap', 'WpaPsk', 'StaticWep', 'DynamicWep', 'Leap', 'Unknown']) {
|
||||
assertEqual(network.requiresCredentials(security[name], security.Open, security.Owe), true, 'network asks for ' + name + ' credentials')
|
||||
}
|
||||
assertEqual(network.requiresCredentials(security.Owe, security.Open, security.Owe), false, 'network does not ask for OWE credentials')
|
||||
assertEqual(network.requiresCredentials(security.Open, security.Open, security.Owe), false, 'network does not ask for open-network credentials')
|
||||
|
||||
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')
|
||||
assert(
|
||||
/Model\.requiresCredentials\(security, WifiSecurityType\.Open, WifiSecurityType\.Owe\)/.test(panelSource),
|
||||
'network wires the Quickshell OWE enum into credential detection'
|
||||
)
|
||||
assert(
|
||||
/if \(requiresCredentials\(net\.security\) && !net\.known\)/.test(panelSource),
|
||||
'network keyboard activation gates unknown-network prompts on credential requirements'
|
||||
)
|
||||
assert(
|
||||
/if \(row\.requiresCredentials && !row\.isKnown\)/.test(panelSource),
|
||||
'network row clicks gate unknown-network prompts on credential requirements'
|
||||
)
|
||||
assert(
|
||||
/shouldRepromptPassphrase\(reason, row\.requiresCredentials\)/.test(panelSource),
|
||||
'network failure reprompts use the row credential requirement'
|
||||
)
|
||||
assert(
|
||||
/networkFailureReason\(reason, requiresCredentials\(network\.security\)\)/.test(panelSource),
|
||||
'network failure copy uses the live network credential requirement'
|
||||
)
|
||||
assert(
|
||||
/readonly property bool canForget: root\.canForgetNetwork\(net\)/.test(panelSource),
|
||||
'network rows derive forget eligibility from the tested model helper'
|
||||
)
|
||||
const rightAction = panelSource.match(/Item \{\s*id: rightAction\b[\s\S]*?\n {6}\}/)
|
||||
assert(rightAction, 'network has a right-edge action target')
|
||||
assert(
|
||||
/visible: row\.requiresCredentials \|\| row\.canForget/.test(rightAction[0]),
|
||||
'network keeps a forget target for known passwordless networks'
|
||||
)
|
||||
const lockIndicator = panelSource.match(/Text \{\s*id: lockIndicator\b[\s\S]*?\n {8}\}/)
|
||||
assert(lockIndicator, 'network has a lock/forget indicator')
|
||||
assert(
|
||||
/visible: row\.requiresCredentials \|\| row\.forgetVisible/.test(lockIndicator[0]),
|
||||
'network hides the lock on passwordless networks until showing their forget action'
|
||||
)
|
||||
assert(
|
||||
/forgetVisible: canForget && \(!requiresCredentials \|\| forgetFocused \|\| rightMouse\.containsMouse\)/.test(panelSource),
|
||||
'network shows the forget action directly for known passwordless networks'
|
||||
)
|
||||
|
||||
const reasons = { NoSecrets: 1, WifiAuthTimeout: 2, WifiNetworkLost: 3, WifiClientDisconnected: 4, WifiClientFailed: 5 }
|
||||
assertEqual(network.networkFailureReason(reasons.NoSecrets, true, reasons), 'Passphrase required', 'network maps missing credential failures')
|
||||
assertEqual(network.networkFailureReason(reasons.WifiAuthTimeout, true, reasons), 'Wrong password', 'network maps credentialed auth timeouts')
|
||||
assertEqual(network.networkFailureReason(reasons.NoSecrets, false, reasons), 'Failed to connect', 'network gives passwordless missing-secret failures generic copy')
|
||||
assertEqual(network.networkFailureReason(reasons.WifiAuthTimeout, false, reasons), 'Failed to connect', 'network gives passwordless auth timeouts generic copy')
|
||||
assertEqual(network.networkFailureReason(99, true, reasons), 'Failed to connect', 'network maps unknown failures')
|
||||
|
||||
assertEqual(network.canForgetNetwork({ known: true, connected: false, security: security.Owe }), true, 'network can forget known disconnected OWE networks')
|
||||
assertEqual(network.canForgetNetwork({ known: true, connected: false, security: security.Open }), true, 'network can forget known disconnected open networks')
|
||||
assertEqual(network.canForgetNetwork({ known: false, connected: false, security: security.Owe }), false, 'network cannot forget unknown networks')
|
||||
assertEqual(network.canForgetNetwork({ known: true, connected: true, security: security.Owe }), false, 'network cannot forget the connected network')
|
||||
|
||||
assertEqual(network.shouldRepromptPassphrase(reasons.NoSecrets, true, reasons), true, 'network reprompts when required credentials are missing')
|
||||
assertEqual(network.shouldRepromptPassphrase(reasons.NoSecrets, false, reasons), false, 'network does not ask a passwordless network for missing secrets')
|
||||
assertEqual(network.shouldRepromptPassphrase(reasons.WifiAuthTimeout, true, reasons), true, 'network reprompts a credentialed 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')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user