Merge pull request #6334 from KazeTachinuu/enterprise-8021x-wifi

Connect to enterprise (802.1X) Wi-Fi networks from the network panel
This commit is contained in:
David Heinemeier Hansson
2026-07-24 10:51:04 -07:00
committed by GitHub
2 changed files with 87 additions and 11 deletions
+14
View File
@@ -232,6 +232,19 @@ function isProtected(security, openSecurity) {
return security !== openSecurity
}
// The password arrives on stdin and reaches nmcli through the scriptable
// `connection edit` editor -- argv is world-readable in /proc, so the secret
// must never be an argument (printf is a bash builtin, so no process spawns
// with it either).
var enterpriseConnectScript =
"u=$(uuidgen); IFS= read -r pw;" +
" nmcli connection add type wifi con-name \"$1\" ssid \"$1\" connection.uuid \"$u\"" +
" wifi-sec.key-mgmt wpa-eap 802-1x.eap peap 802-1x.phase2-auth mschapv2" +
" 802-1x.identity \"$2\" 802-1x.auth-timeout 8 >/dev/null" +
" && printf 'set 802-1x.password %s\\nsave\\nquit\\n' \"$pw\" | nmcli connection edit uuid \"$u\" >/dev/null" +
" && nmcli connection up uuid \"$u\"" +
" || { nmcli connection delete uuid \"$u\" >/dev/null 2>&1; false; }"
function networkFailureReason(reason, reasons) {
var r = reasons || {}
if (reason === r.NoSecrets) return "Passphrase required"
@@ -263,6 +276,7 @@ if (typeof module !== "undefined") {
sortWifiRows: sortWifiRows,
wifiSectionTitle: wifiSectionTitle,
isProtected: isProtected,
enterpriseConnectScript: enterpriseConnectScript,
networkFailureReason: networkFailureReason
}
}
+73 -11
View File
@@ -16,7 +16,13 @@ Panel {
// Centralized close so callers can't forget to drop the passphrase prompt.
function close() {
root.controller.hide()
cancelPasswordPrompt()
}
function cancelPasswordPrompt() {
passwordSsid = ""
passwordText = ""
identityText = ""
}
// Live connection details from `ip` / /sys / iw.
@@ -84,6 +90,7 @@ Panel {
property string failureReason: ""
property string passwordSsid: ""
property string passwordText: ""
property string identityText: ""
// True while any wifi action is mid-flight. Rows
// disable themselves on this so clicks on the other rows don't silently
@@ -489,7 +496,10 @@ Panel {
}
function openPasswordPrompt(ssid) {
if (passwordSsid !== ssid) passwordText = ""
if (passwordSsid !== ssid) {
passwordText = ""
identityText = ""
}
passwordSsid = ssid
}
@@ -567,6 +577,26 @@ Panel {
runNetworkAction("connect", networkForSsid(ssid), function(network) { network.connectWithPsk(passphrase) })
}
function connectEnterprise(ssid, identity, passphrase) {
runNetworkAction("connect", networkForSsid(ssid), function(network) {
enterpriseConnect.secret = passphrase
enterpriseConnect.command = ["bash", "-c", Model.enterpriseConnectScript, "nmcli-eap", ssid, identity]
enterpriseConnect.running = true
})
}
// Creates and activates the 802.1X profile (see Model.enterpriseConnectScript).
// The password goes over stdin, never argv.
Process {
id: enterpriseConnect
property string secret: ""
stdinEnabled: true
onStarted: {
write(secret + "\n")
secret = ""
}
}
function disconnect(network) {
runNetworkAction("disconnect", network || connectedWifiNetwork, function(net) { net.disconnect() })
}
@@ -1235,6 +1265,9 @@ 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 isEnterprise: net
? (net.security === WifiSecurityType.Wpa2Eap || net.security === WifiSecurityType.WpaEap)
: false
readonly property bool canForgetFromLock: isKnown && isProtected && !isConnected
readonly property bool isSelected: root.focusSection === "wifi" && root.selectedIndex === index
readonly property bool forgetFocused: isSelected && root.wifiActionFocused && canForgetFromLock
@@ -1251,6 +1284,12 @@ Panel {
readonly property bool isFailed: root.failureReason !== "" && root.failureSsid === (net ? net.ssid : "")
readonly property bool isPasswordOpen: root.passwordSsid !== "" && root.passwordSsid === (net ? net.ssid : "")
function submitCredentials() {
if (!net || root.busy || root.passwordText.length === 0) return
if (!isEnterprise) return root.connectWithPassphrase(net.ssid, root.passwordText)
if (root.identityText.length > 0) root.connectEnterprise(net.ssid, root.identityText, root.passwordText)
}
Connections {
target: row.net ? row.net.network : null
function onConnectionFailed(reason) {
@@ -1450,15 +1489,40 @@ Panel {
anchors.leftMargin: Style.space(10)
anchors.rightMargin: Style.space(10)
anchors.topMargin: Style.space(4)
implicitHeight: pwField.implicitHeight + Style.spacing.rowGap
implicitHeight: (idField.visible ? idField.implicitHeight + Style.space(4) : 0) + pwField.implicitHeight + Style.spacing.rowGap
height: implicitHeight
TextField {
id: idField
visible: row.isEnterprise && !row.isBusy && !row.isFailed
anchors.left: parent.left
anchors.right: connectPwBtn.left
anchors.top: parent.top
anchors.rightMargin: Style.space(6)
placeholderText: "Identity (user@domain)"
font.family: Style.font.family
font.pixelSize: Style.font.body
foreground: root.bar.foreground
horizontalPadding: Style.spacing.controlGap
verticalPadding: Style.spacing.controlPaddingY
enabled: !row.isBusy
text: row.isPasswordOpen ? root.identityText : ""
onAccepted: pwField.forceActiveFocus()
onTextChanged: if (row.isPasswordOpen && text !== root.identityText) root.identityText = text
Keys.onEscapePressed: root.cancelPasswordPrompt()
onVisibleChanged: if (visible) Qt.callLater(forceActiveFocus)
Component.onCompleted: if (visible) Qt.callLater(forceActiveFocus)
}
TextField {
id: pwField
visible: !row.isBusy && !row.isFailed
anchors.left: parent.left
anchors.right: connectPwBtn.left
anchors.verticalCenter: parent.verticalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.spacing.rowGap / 2
anchors.rightMargin: Style.space(6)
password: true
placeholderText: "Passphrase"
@@ -1470,14 +1534,12 @@ Panel {
enabled: !row.isBusy
text: row.isPasswordOpen ? root.passwordText : ""
onAccepted: {
if (!root.busy && row.net && text.length > 0) root.connectWithPassphrase(row.net.ssid, text)
}
onAccepted: row.submitCredentials()
onTextChanged: if (row.isPasswordOpen && text !== root.passwordText) root.passwordText = text
Keys.onEscapePressed: { root.passwordSsid = ""; root.passwordText = "" }
Keys.onEscapePressed: root.cancelPasswordPrompt()
onVisibleChanged: if (visible) Qt.callLater(forceActiveFocus)
Component.onCompleted: if (visible) Qt.callLater(forceActiveFocus)
onVisibleChanged: if (visible && !row.isEnterprise) Qt.callLater(forceActiveFocus)
Component.onCompleted: if (visible && !row.isEnterprise) Qt.callLater(forceActiveFocus)
}
BorderSurface {
@@ -1510,12 +1572,12 @@ Panel {
visible: !row.isBusy && !row.isFailed
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
enabled: row.net && pwField.text.length > 0
enabled: row.net && pwField.text.length > 0 && (!row.isEnterprise || idField.text.length > 0)
iconText: "󰄬"
tooltipText: "Connect"
foreground: root.bar.foreground
fontFamily: root.bar.fontFamily
onClicked: if (row.net) root.connectWithPassphrase(row.net.ssid, root.passwordText)
onClicked: row.submitCredentials()
}
}
}