# Allow the non-login cast service user to manage Air system functions without
# a password. The human SSH user `air` intentionally does not get this full
# allowlist; it only retains cast.service restart during the transition from
# older images where cast.service still ran as `air`.
#
# Strict policy: the cast service user is permitted to invoke ONLY the wrapper
# scripts /usr/local/sbin/air-tailscale and /usr/local/sbin/air-zerotier,
# never the raw /usr/bin/tailscale or /usr/sbin/zerotier-cli binaries.
# The wrappers enforce a hard subcommand+argv allowlist (see those files).
#
# Why not enumerate raw-binary argv forms in this file?
# -----------------------------------------------------
# In sudoers, a command with no argv constraint permits ANY additional
# arguments. So `/usr/bin/tailscale up` would silently allow
#   sudo tailscale up --ssh --operator=root --exit-node=<evil>
# which hands any tailnet peer a root shell on the drone. For fixed systemctl
# operations, enumerate the exact argv after the binary path. Do not append
# `""` after a non-empty argv list: deployed Pis proved sudo treats that as a
# literal empty argument requirement, so `/bin/systemctl restart cast.service`
# no longer matches. Use `""` only for commands that truly take no arguments.
# Maintaining a large variable argv matrix in /etc/sudoers.d/ is brittle;
# centralising it in wrapper scripts is auditable and unit-testable.
#
# The systemctl entries below enumerate exact argv forms so that
#   sudo systemctl start tailscaled --user
# (or any other smuggled flag) is rejected by sudo itself.
#
# If you need a new VPN operation, add it to the appropriate wrapper's
# allowlist; do NOT widen this sudoers file.

Cmnd_Alias AIR_TAILSCALE = /usr/local/sbin/air-tailscale
Cmnd_Alias AIR_ZEROTIER  = /usr/local/sbin/air-zerotier
Cmnd_Alias AIR_VPN_CTL   = /bin/systemctl start zerotier-one, \
                           /bin/systemctl stop  zerotier-one, \
                           /bin/systemctl enable zerotier-one, \
                           /bin/systemctl disable zerotier-one, \
                           /bin/systemctl start tailscaled, \
                           /bin/systemctl stop  tailscaled, \
                           /bin/systemctl enable tailscaled, \
                           /bin/systemctl disable tailscaled
# Exact-argv allowlist for service restarts. Use --no-block because the
# caller is usually cast-server itself; waiting for systemd to restart the
# unit can kill the caller before systemctl returns, which looks like a
# network error even though the restart was accepted.
Cmnd_Alias AIR_OTA_CTL   = /bin/systemctl --no-block restart cast.service, \
                           /bin/systemctl restart cast.service
# The oneshot `air-ota-apply.service` runs as root, escapes the
# cast.service ProtectSystem sandbox, and swaps /usr/local/bin/
# cast-server from a marker file written by services::ota. Two exact
# argv forms: bare `start` (fire-and-forget) and the `--wait` variant
# that cast-server actually uses so it can surface apply errors.
Cmnd_Alias AIR_OTA_APPLY = /bin/systemctl start air-ota-apply.service, \
                           /bin/systemctl start --wait air-ota-apply.service

# SSH key provisioning over the web UI. The wrapper script enforces
# the list/add/remove subcommand allowlist and validates pubkeys via
# `ssh-keygen -lf -`. NO trailing "" so the script can receive the
# `add` subcommand and the `remove <fpr>` argument. The script itself
# narrows argv and validates format.
Cmnd_Alias AIR_SSH_KEYS  = /usr/local/sbin/air-ssh-keys
# Operator-driven sensor override (Camera page dropdown). The helper
# validates its single positional arg against a strict regex + the
# /boot/firmware/overlays/ directory, edits config.txt atomically,
# and schedules a reboot. NO trailing "" because we need the arg
# form `air-set-camera-overlay imx477`.
Cmnd_Alias AIR_CAMERA_OVERLAY = /usr/local/sbin/air-set-camera-overlay
# Operator-initiated power control from the System page. Exact-argv
# entries.
# `systemctl reboot` and `systemctl poweroff` are deferred to
# PID 1 via dbus, so no TTY needed. Admin-role + rate-limit +
# confirm-dialog gate the caller side.
Cmnd_Alias AIR_POWER_CTL = /bin/systemctl reboot, \
                           /bin/systemctl poweroff
# Operator-driven network-condition simulator (System → Network page).
# The wrapper validates every argument with a strict regex + range
# check; see /usr/local/sbin/air-netshape for the full argv
# specification. sudoers here only names the wrapper path — no
# trailing `""`, because the wrapper takes variable argv
# (apply <rate> <latency> ..., clear, status).
Cmnd_Alias AIR_NETSHAPE  = /usr/local/sbin/air-netshape
# WiFi management from the web UI Network page. The wrapper hard-
# codes an `nmcli` subcommand allowlist (scan / connect / disconnect /
# status) and validates SSID + PSK length before invoking nmcli. NO
# trailing `""` because the wrapper needs to accept the SSID +
# password positional args. Lets the operator connect a freshly-
# flashed Pi to a WiFi network without having to SSH in and run
# nmcli by hand — particularly important for license activation in
# direct-cable + no-router field setups.
Cmnd_Alias AIR_WIFI      = /usr/local/sbin/air-wifi

cast ALL=(root) NOPASSWD: AIR_TAILSCALE, AIR_ZEROTIER, AIR_VPN_CTL, AIR_OTA_CTL, AIR_OTA_APPLY, AIR_SSH_KEYS, AIR_CAMERA_OVERLAY, AIR_POWER_CTL, AIR_NETSHAPE, AIR_WIFI
# Transition-only: an older cast.service process may still be running as `air`
# while OTA installs this file and then calls `sudo systemctl restart
# cast.service`. Do not grant AIR_OTA_APPLY or wrapper access to `air`.
air ALL=(root) NOPASSWD: AIR_OTA_CTL
Defaults:cast !requiretty
Defaults:air !requiretty
# No env_keep entries: TS_AUTHKEY is no longer forwarded via sudo env. The
# cast service stages the tailscale auth key in /run/air/ts-authkey (0600,
# cast-owned) and air-tailscale reads + unlinks it on `up`. Keeping sudo's
# default env-strip in place means any future leak of an env var from the
# cast service user cannot ride into root-privileged commands.
