test: authorize headless portal adapters safely

This commit is contained in:
2026-09-17 14:49:36 +10:00
parent babc58637f
commit d1ad84db5b
6 changed files with 233 additions and 14 deletions
+125 -14
View File
@@ -13,6 +13,107 @@ wm_require() {
}
}
wm_nmcli_permission() {
local permission="$1"
awk -F: -v permission="$permission" '$1 == permission { print $2; exit }' \
<<<"${WM_NMCLI_PERMISSIONS:-}"
}
wm_prepare_networkmanager_authorization() {
# A GUI Polkit agent can authorize direct nmcli actions. An SSH/headless
# shell has no such agent on many Linux hosts, even for a sudo-capable
# developer. Resolve that host-side boundary before an erase/upload, then
# use one fixed command path rather than suppressing an unauthorized scan
# and later misreporting an SSID timeout.
local permission value direct=yes
case "${WM_NMCLI_AUTH:-auto}" in
auto|direct|sudo) ;;
*)
echo 'WM_NMCLI_AUTH must be auto, direct, or sudo.' >&2
return 2
;;
esac
[[ "${WM_NMCLI_AUTH_READY:-no}" == yes ]] && return 0
WM_NMCLI_PERMISSIONS="$(nmcli -t -f PERMISSION,VALUE general permissions 2>/dev/null || true)"
for permission in \
org.freedesktop.NetworkManager.wifi.scan \
org.freedesktop.NetworkManager.network-control \
org.freedesktop.NetworkManager.settings.modify.system; do
value="$(wm_nmcli_permission "$permission")"
[[ "$value" == yes ]] || direct=no
done
case "${WM_NMCLI_AUTH:-auto}" in
direct)
WM_NMCLI_MODE=direct
;;
sudo)
WM_NMCLI_MODE=sudo
;;
auto)
if [[ "$direct" == yes ]]; then
WM_NMCLI_MODE=direct
elif [[ -z "${DISPLAY:-}" && -z "${WAYLAND_DISPLAY:-}" && -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
WM_NMCLI_MODE=sudo
else
# Give a graphical Polkit agent the chance to authorize the
# action. A failure is reported verbatim by wm_nmcli.
WM_NMCLI_MODE=direct
fi
;;
esac
if [[ "$WM_NMCLI_MODE" == sudo ]]; then
command -v sudo >/dev/null 2>&1 || {
echo 'NetworkManager requires authorization, but sudo is unavailable. Use a graphical Polkit session or install/configure sudo.' >&2
return 1
}
echo 'NetworkManager requires scoped authorization for the named portal adapter; validating sudo before the board is flashed.' >&2
sudo -v || {
echo 'Could not validate sudo for the scoped NetworkManager portal actions.' >&2
return 1
}
fi
WM_NMCLI_AUTH_READY=yes
export WM_NMCLI_MODE WM_NMCLI_AUTH_READY WM_NMCLI_PERMISSIONS
}
wm_report_networkmanager_authorization() {
local permission value direct=yes
WM_NMCLI_PERMISSIONS="$(nmcli -t -f PERMISSION,VALUE general permissions 2>/dev/null || true)"
for permission in \
org.freedesktop.NetworkManager.wifi.scan \
org.freedesktop.NetworkManager.network-control \
org.freedesktop.NetworkManager.settings.modify.system; do
value="$(wm_nmcli_permission "$permission")"
[[ "$value" == yes ]] || direct=no
done
if [[ "$direct" == yes ]]; then
echo 'NetworkManager portal authorization: direct.'
elif [[ -z "${DISPLAY:-}" && -z "${WAYLAND_DISPLAY:-}" && -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
echo 'NetworkManager portal authorization: scoped sudo will be requested before a portal command flashes the board.'
else
echo 'NetworkManager portal authorization: graphical Polkit may authorize actions; set WM_NMCLI_AUTH=sudo to use scoped sudo instead.'
fi
}
wm_nmcli() {
# Only this small set of nmcli calls is elevated when the preflight selects
# sudo. The runner, artifacts, and session record remain owned by the
# invoking developer; every mutating call still names the guarded adapter
# or a generated temporary connection.
if [[ "${WM_NMCLI_MODE:-direct}" == sudo ]]; then
sudo -n true || {
echo 'The sudo authorization for scoped NetworkManager actions expired; run sudo -v and retry the portal command.' >&2
return 1
}
sudo -n -- nmcli "$@"
else
nmcli "$@"
fi
}
wm_default_route_interface() {
ip route show default 2>/dev/null | awk '/^default/{print $5; exit}'
}
@@ -47,7 +148,7 @@ wm_require_client_adapter() {
echo "Refusing to use the host default-route interface: $interface" >&2
return 1
}
active_connection="$(nmcli -g GENERAL.CONNECTION device show "$interface" 2>/dev/null || true)"
active_connection="$(wm_nmcli -g GENERAL.CONNECTION device show "$interface" 2>/dev/null || true)"
if [[ -n "$active_connection" && "$active_connection" != "--" && "$allow_takeover" != "yes" ]]; then
echo "Client adapter $interface already has connection '$active_connection'." >&2
echo "Pass --take-over-client-adapter to replace only that adapter's connection." >&2
@@ -64,14 +165,24 @@ wm_portal_ssid() {
}
wm_wait_for_portal_ssid() {
local interface="$1" ssid="$2" attempt
nmcli device wifi rescan ifname "$interface" >/dev/null 2>&1 || true
local interface="$1" ssid="$2" attempt advertised
if ! wm_nmcli device wifi rescan ifname "$interface"; then
echo "NetworkManager could not scan the selected portal adapter: $interface" >&2
return 1
fi
for attempt in $(seq 1 45); do
if nmcli -t -f SSID device wifi list ifname "$interface" | grep -Fxq "$ssid"; then
if ! advertised="$(wm_nmcli -t -f SSID device wifi list ifname "$interface")"; then
echo "NetworkManager could not read Wi-Fi scan results from $interface." >&2
return 1
fi
if grep -Fxq "$ssid" <<<"$advertised"; then
return 0
fi
sleep 1
nmcli device wifi rescan ifname "$interface" >/dev/null 2>&1 || true
if ! wm_nmcli device wifi rescan ifname "$interface"; then
echo "NetworkManager could not refresh Wi-Fi scan results from $interface." >&2
return 1
fi
done
echo "Portal SSID not detected on $interface: $ssid" >&2
return 1
@@ -80,8 +191,8 @@ wm_wait_for_portal_ssid() {
wm_remove_connection_by_name() {
local name="$1"
[[ -n "$name" ]] || return 0
nmcli connection down "$name" >/dev/null 2>&1 || true
nmcli connection delete "$name" >/dev/null 2>&1 || true
wm_nmcli connection down "$name" >/dev/null 2>&1 || true
wm_nmcli connection delete "$name" >/dev/null 2>&1 || true
}
wm_create_portal_connection() {
@@ -102,12 +213,12 @@ wm_create_portal_connection() {
unset WM_PORTAL_CONNECTION_UUID WM_PORTAL_CONNECTION_NAME
return 1
fi
nmcli device disconnect "$interface" >/dev/null 2>&1 || true
wm_nmcli device disconnect "$interface" >/dev/null 2>&1 || true
if ! wm_wait_for_portal_ssid "$interface" "$ssid"; then
wm_cleanup_created_connection
return 1
fi
if ! nmcli connection add type wifi ifname "$interface" con-name "$name" ssid "$ssid" \
if ! wm_nmcli connection add type wifi ifname "$interface" con-name "$name" ssid "$ssid" \
ipv4.method auto ipv4.never-default yes ipv6.method ignore connection.autoconnect no >/dev/null; then
wm_cleanup_created_connection
return 1
@@ -115,7 +226,7 @@ wm_create_portal_connection() {
# NetworkManager creates the UUID at `connection add`, before any later
# configuration or association step. Capture it immediately so cleanup
# has an exact identifier throughout the remaining critical section.
uuid="$(nmcli -g connection.uuid connection show "$name")"
uuid="$(wm_nmcli -g connection.uuid connection show "$name")"
if [[ -z "$uuid" || "$uuid" == "--" ]]; then
wm_cleanup_created_connection
echo "NetworkManager did not return a UUID for the portal connection." >&2
@@ -130,11 +241,11 @@ wm_create_portal_connection() {
wm_cleanup_created_connection
return 1
fi
if ! nmcli connection modify "$name" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$password"; then
if ! wm_nmcli connection modify "$name" wifi-sec.key-mgmt wpa-psk wifi-sec.psk "$password"; then
wm_cleanup_created_connection
return 1
fi
if ! nmcli connection up "$name" ifname "$interface"; then
if ! wm_nmcli connection up "$name" ifname "$interface"; then
wm_cleanup_created_connection
return 1
fi
@@ -152,8 +263,8 @@ wm_verify_portal_route() {
wm_remove_connection() {
local uuid="${1:-}" name="${2:-}"
if [[ -n "$uuid" ]]; then
nmcli connection down uuid "$uuid" >/dev/null 2>&1 || true
nmcli connection delete uuid "$uuid" >/dev/null 2>&1 || true
wm_nmcli connection down uuid "$uuid" >/dev/null 2>&1 || true
wm_nmcli connection delete uuid "$uuid" >/dev/null 2>&1 || true
elif [[ -n "$name" ]]; then
wm_remove_connection_by_name "$name"
fi
+4
View File
@@ -242,6 +242,7 @@ start_portal_session() {
wm_acquire_hardware_lock
wm_require_no_active_session
wm_require_client_adapter "$client_interface" "$takeover"
wm_prepare_networkmanager_authorization
prepare_output_dir
ssid="$(wm_portal_ssid "$platform")"
@@ -490,6 +491,7 @@ run_ota_contract() {
finish_portal_session() {
wm_load_state
wm_prepare_networkmanager_authorization
wm_remove_connection "$WM_PORTAL_CONNECTION_UUID" "$WM_PORTAL_CONNECTION_NAME"
wm_clear_state
}
@@ -517,6 +519,7 @@ case "$command_name" in
require_common
wm_acquire_hardware_lock
wm_require_client_adapter "$client_interface" "$takeover"
wm_report_networkmanager_authorization
printf 'Portal hardware prerequisites are ready. Main route is untouched; client adapter: %s\n' "$client_interface"
;;
up)
@@ -575,6 +578,7 @@ case "$command_name" in
wm_acquire_hardware_lock
wm_require_no_active_session
wm_require_client_adapter "$client_interface" "$takeover"
wm_prepare_networkmanager_authorization
prepare_output_dir
prepare_ota_firmware
prepare_ota_contract_image
+69
View File
@@ -11,6 +11,9 @@ mkdir -p "$stub_bin"
export CALL_LOG="$tmp/calls.log"
export WM_HARDWARE_LOCK_FILE="$tmp/hardware.lock"
export XDG_STATE_HOME="$tmp/state"
# Most mock cases exercise the direct desktop-Polkit path. Individual cases
# below explicitly select the headless scoped-sudo path.
export WM_NMCLI_AUTH=direct
printf '%s\n' '#!/usr/bin/env bash' \
'if [[ "$1" == "link" && "$2" == "show" ]]; then exit 0; fi' \
@@ -18,12 +21,22 @@ printf '%s\n' '#!/usr/bin/env bash' \
'echo "192.168.4.1 dev wlan-client src 192.168.4.2"' >"$stub_bin/ip"
printf '%s\n' '#!/usr/bin/env bash' \
'printf "%s\\n" "$*" >>"$CALL_LOG"' \
'if [[ "${NMCLI_REQUIRE_SUDO:-}" == "yes" && "${RUN_AS_SUDO:-}" != "yes" ]]; then echo "Error: Insufficient privileges" >&2; exit 7; fi' \
'if [[ "${NMCLI_FAIL_SCAN:-}" == "yes" && "$1" == "device" && "$2" == "wifi" && "$3" == "rescan" ]]; then echo "fixture scan failure" >&2; exit 7; fi' \
'if [[ "${NMCLI_FAIL_ADD:-}" == "yes" && "$1" == "connection" && "$2" == "add" ]]; then exit 7; fi' \
'if [[ "${NMCLI_SIGNAL_PARENT:-}" == "yes" && "$1" == "connection" && "$2" == "add" ]]; then kill -TERM "$PPID"; exit 0; fi' \
'if [[ "${NMCLI_FAIL_UP:-}" == "yes" && "$1" == "connection" && "$2" == "up" ]]; then exit 7; fi' \
'if [[ "$1" == "-t" && "$2" == "-f" && "$3" == "SSID" ]]; then echo "WM Contract ESP8266"; exit 0; fi' \
'if [[ "$1" == "-g" && "$2" == "connection.uuid" ]]; then echo "stub-uuid"; exit 0; fi' \
'if [[ "$1" == "-g" ]]; then echo "--"; fi' >"$stub_bin/nmcli"
printf '%s\n' '#!/usr/bin/env bash' \
'printf "sudo %s\\n" "$*" >>"$CALL_LOG"' \
'if [[ "${SUDO_FAIL:-}" == "yes" ]]; then exit 1; fi' \
'if [[ "$1" == "-v" ]]; then exit 0; fi' \
'if [[ "$1" == "-n" ]]; then shift; fi' \
'if [[ "$1" == "true" ]]; then exit 0; fi' \
'[[ "$1" == "--" ]] && shift' \
'RUN_AS_SUDO=yes exec "$@"' >"$stub_bin/sudo"
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$stub_bin/pio"
printf '%s\n' '#!/usr/bin/env bash' \
'printf "docker %s\n" "$*" >>"$CALL_LOG"' \
@@ -80,6 +93,23 @@ fi
# A failed association must delete the only connection it just created.
source "$root/tools/lib/portal-hardware-session.sh"
# A NetworkManager failure must remain distinct from an absent portal SSID.
: >"$CALL_LOG"
export NMCLI_FAIL_SCAN=yes
if scan_output="$(wm_wait_for_portal_ssid wlan-client 'fixture portal' 2>&1)"; then
echo 'failed Wi-Fi scan was reported as an SSID result' >&2
exit 1
fi
unset NMCLI_FAIL_SCAN
[[ "$scan_output" == *'NetworkManager could not scan the selected portal adapter'* ]] || {
echo 'failed Wi-Fi scan did not report the NetworkManager error path' >&2
exit 1
}
if grep -Fq 'connection add' "$CALL_LOG"; then
echo 'failed Wi-Fi scan continued into connection creation' >&2
exit 1
fi
wm_wait_for_portal_ssid() { return 0; }
export NMCLI_FAIL_UP=yes
if wm_create_portal_connection wlan-client 'fixture portal' placeholder esp8266; then
@@ -102,11 +132,50 @@ if wm_create_portal_connection wlan-client 'fixture portal' placeholder esp8266;
fi
unset NMCLI_FAIL_ADD
grep -Eq 'connection delete wifimanager-portal-' "$CALL_LOG"
if grep -Fq 'sudo ' "$CALL_LOG"; then
echo 'generic NetworkManager failure unexpectedly retried with sudo' >&2
exit 1
fi
[[ ! -e "$(wm_state_file)" ]] || {
echo 'failed connection creation left pending recovery state behind' >&2
exit 1
}
# A non-graphical SSH shell often has no Polkit agent. Preflight the scoped
# sudo path once, then use it for only the named portal adapter actions; never
# require the developer to run the entire runner as root.
: >"$CALL_LOG"
export NMCLI_REQUIRE_SUDO=yes
export WM_NMCLI_AUTH=sudo
unset WM_NMCLI_AUTH_READY WM_NMCLI_MODE WM_NMCLI_PERMISSIONS
wm_prepare_networkmanager_authorization
wm_wait_for_portal_ssid() { return 0; }
if ! wm_create_portal_connection wlan-client 'fixture portal' placeholder esp8266; then
echo 'authorization fallback did not create the portal connection' >&2
exit 1
fi
wm_cleanup_created_connection
unset NMCLI_REQUIRE_SUDO
export WM_NMCLI_AUTH=direct
unset WM_NMCLI_AUTH_READY WM_NMCLI_MODE WM_NMCLI_PERMISSIONS
grep -Fq 'sudo -n -- nmcli connection add' "$CALL_LOG"
[[ ! -e "$(wm_state_file)" ]] || {
echo 'authorization fallback left portal recovery state behind' >&2
exit 1
}
# A failed sudo validation must stop before the runner can erase or flash a
# board; it is not an SSID discovery failure.
export WM_NMCLI_AUTH=sudo SUDO_FAIL=yes
unset WM_NMCLI_AUTH_READY WM_NMCLI_MODE WM_NMCLI_PERMISSIONS
if wm_prepare_networkmanager_authorization >/dev/null 2>&1; then
echo 'failed scoped sudo validation was accepted' >&2
exit 1
fi
unset SUDO_FAIL
export WM_NMCLI_AUTH=direct
unset WM_NMCLI_AUTH_READY WM_NMCLI_MODE WM_NMCLI_PERMISSIONS
# A portal scan failure must stop before `connection add` and clear the
# pre-add pending record, even though this helper is invoked in an `if`.
wm_wait_for_portal_ssid() { return 1; }