Blob Blame History Raw
#!/usr/bin/python3
"""
Add or modify user accounts

Add or modify user accounts inside the tree.

WARNING: This stage uses chroot() to run the `useradd` or `usermod` binary
from inside the tree. This will fail for cross-arch builds and may fail or
misbehave if the `usermod`/`useradd` binary inside the tree makes incorrect
assumptions about its host system.
"""


import json
import subprocess
import sys
import os

SCHEMA = """
"additionalProperties": false,
"properties": {
  "users": {
    "type": "object",
    "description": "Keys are usernames, values are objects giving user info.",
    "propertyNames": {
      "pattern": "^[A-Za-z0-9_][A-Za-z0-9_-]{0,31}$"
    },
    "additionalProperties": {
      "type": "object",
      "properties": {
        "uid": {
          "description": "User UID",
          "type": "number"
        },
        "gid": {
          "description": "User GID",
          "type": "number"
        },
        "groups": {
          "description": "Array of group names for this user",
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "description": {
          "description": "User account description (or full name)",
          "type": "string"
        },
        "home": {
          "description": "Path to user's home directory",
          "type": "string"
        },
        "shell": {
          "description": "User's login shell",
          "type": "string"
        },
        "password": {
          "description": "User's encrypted password, as returned by crypt(3)",
          "type": "string"
        },
        "key": {
          "description": "SSH Public Key to add to ~/.ssh/authorized_keys",
          "type": "string"
        }
      }
    }
  }
}
"""


def getpwnam(root, name):
    """Similar to pwd.getpwnam, but takes a @root parameter"""
    with open(f"{root}/etc/passwd") as f:
        for line in f:
            passwd = line.split(":")
            if passwd[0] == name:
                return passwd
        return None


def useradd(root, name, uid=None, gid=None, groups=None, description=None, home=None, shell=None, password=None):
    arguments = []
    if uid is not None:
        arguments += ["--uid", str(uid), "-o"]
    if gid is not None:
        arguments += ["--gid", str(gid)]
    if groups:
        arguments += ["--groups", ",".join(groups)]
    if description is not None:
        arguments += ["--comment", description]
    if home:
        arguments += ["--home-dir", home]
    if shell:
        arguments += ["--shell", shell]
    if password is not None:
        arguments += ["--password", password]

    subprocess.run(["chroot", root, "useradd", *arguments, name], check=True)


def usermod(root, name, gid=None, groups=None, description=None, home=None, shell=None, password=None):
    arguments = []
    if gid is not None:
        arguments += ["--gid", gid]
    if groups:
        arguments += ["--groups", ",".join(groups)]
    if description is not None:
        arguments += ["--comment", description]
    if home:
        arguments += ["--home", home]
    if shell:
        arguments += ["--shell", shell]
    if password is not None:
        arguments += ["--password", password]

    if arguments:
        subprocess.run(["chroot", root, "usermod", *arguments, name], check=True)


def add_ssh_key(root, user, key):
    _, _, uid, gid, _, home, _ = getpwnam(root, user)
    ssh_dir = f"{root}/{home}/.ssh"
    authorized_keys = f"{ssh_dir}/authorized_keys"

    if not os.path.exists(ssh_dir):
        os.mkdir(ssh_dir, 0o700)
        os.chown(ssh_dir, int(uid), int(gid))

    with open(authorized_keys, "a") as f:
        f.write(f"{key}\n")

    os.chown(authorized_keys, int(uid), int(gid))
    os.chmod(authorized_keys, 0o600)


def main(tree, options):
    users = options["users"]

    for name, user_options in users.items():
        uid = user_options.get("uid")
        gid = user_options.get("gid")
        groups = user_options.get("groups")
        description = user_options.get("description")
        home = user_options.get("home")
        shell = user_options.get("shell")
        password = user_options.get("password")

        passwd = getpwnam(tree, name)
        if passwd is not None:
            if uid is not None:
                print(f"Error: can't set uid of existing user '{name}'")
                return 1
            usermod(tree, name, gid, groups, description, home, shell, password)
        else:
            useradd(tree, name, uid, gid, groups, description, home, shell, password)

        key = user_options.get("key")  # Public SSH key
        if key:
            add_ssh_key(tree, name, key)

    return 0


if __name__ == '__main__':
    args = json.load(sys.stdin)
    r = main(args["tree"], args["options"])
    sys.exit(r)