#!/usr/bin/env python3

"""Put the church's Wi-Fi on this machine, so the room can use it and
nobody in the room can read it.

Run by an administrator, as root, over SSH or from the maintenance
terminal:

    classroom-wifi "Church Wi-Fi"           # prompts for the password
    classroom-wifi --list
    classroom-wifi --forget "Old Network"

WHY THIS EXISTS RATHER THAN LETTING A TEACHER ADD ONE.

A wireless network added from the desktop is created as a USER connection:
NetworkManager writes connection.permissions naming whoever added it.
Debian then allows that user to read its secrets with no authentication at
all — org.freedesktop.NetworkManager.settings.modify.own is "yes" — so a
password typed in by one teacher is readable by the next person to sit
down, through the applet's own "Show Network's QR Code".

This creates a SYSTEM connection instead: no connection.permissions, so it
belongs to the machine rather than to a person, its secrets are stored in
a root-only file, and reading them needs an administrator. Every account
on the machine can still USE it, and NetworkManager reconnects to it at
boot on its own without anybody logging in.

That is the whole reason this is an administrator's command. The classroom
account is refused the NetworkManager actions that return secrets — see
org.ekklesia.classroom.network.rules — and that refusal would be pointless
if the room could create a connection it was allowed to read.
"""

import argparse
import getpass
import os
import subprocess
import sys

CONNECTIONS = "/etc/NetworkManager/system-connections"


def say(message):
    print(f"classroom-wifi: {message}")


def nmcli(*args, check=True):
    result = subprocess.run(
        ["nmcli"] + list(args), capture_output=True, text=True, timeout=60,
        check=False,
    )

    if check and result.returncode != 0:
        say(result.stderr.strip() or "nmcli failed")
        raise SystemExit(1)

    return result


def listing():
    result = nmcli("-t", "-f", "NAME,TYPE,UUID", "connection", "show")

    for line in result.stdout.splitlines():
        parts = line.split(":")

        if len(parts) >= 2 and "wireless" in parts[1]:
            print(f"  {parts[0]}")

    return 0


def forget(name):
    nmcli("connection", "delete", name)
    say(f"removed {name}")

    return 0


def add(ssid, password, name=None):
    name = name or ssid

    # Deliberately no connection.permissions. That single omission is the
    # difference between a connection the machine owns and one a teacher
    # owns and may therefore read.
    arguments = [
        "connection", "add",
        "type", "wifi",
        "con-name", name,
        "ifname", "*",
        "ssid", ssid,
        "connection.autoconnect", "yes",
        # Stored in the connection file, which is root-only, rather than
        # in a login keyring that belongs to whoever is signed in.
        "802-11-wireless-security.psk-flags", "0",
    ]

    if password:
        arguments += [
            "wifi-sec.key-mgmt", "wpa-psk",
            "wifi-sec.psk", password,
        ]

    nmcli(*arguments)

    path = os.path.join(CONNECTIONS, f"{name}.nmconnection")

    if os.path.exists(path):
        # NetworkManager already writes 0600 root:root. Said out loud and
        # enforced anyway, because this is the file the whole arrangement
        # rests on.
        os.chmod(path, 0o600)
        os.chown(path, 0, 0)
        mode = oct(os.stat(path).st_mode & 0o777)
        say(f"{path} is {mode} root:root")

    say(f"added {name} as a system connection")
    say("the room can use it; the classroom account cannot read it")

    return 0


def main():
    parser = argparse.ArgumentParser(
        description="Provision the church Wi-Fi as a system connection.")
    parser.add_argument("ssid", nargs="?", help="the network name")
    parser.add_argument("--name", help="connection name, if not the SSID")
    parser.add_argument("--list", action="store_true",
                        help="list the wireless connections on this machine")
    parser.add_argument("--forget", metavar="NAME",
                        help="remove a saved connection")
    parser.add_argument("--open", action="store_true",
                        help="the network has no password")
    arguments = parser.parse_args()

    if arguments.list:
        return listing()

    if os.geteuid() != 0:
        say("run this as root; it creates a connection the machine owns")
        return 1

    if arguments.forget:
        return forget(arguments.forget)

    if not arguments.ssid:
        parser.print_help()
        return 2

    password = ""

    if not arguments.open:
        # Never echoed, and never read from the command line, where it
        # would sit in a shell history and in ps.
        password = getpass.getpass("Wi-Fi password (not shown): ")

        if not password:
            say("no password given")
            return 1

    return add(arguments.ssid, password, arguments.name)


if __name__ == "__main__":
    sys.exit(main())
