Ctrl K
ComunidadGuías…

src/boot.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//! Container entrypoint

use std::{
    fs,
    os::unix::{
        fs::{MetadataExt as _, PermissionsExt as _},
        process::CommandExt as _,
    },
    path::{Path, PathBuf},
    process::Command,
    time::Duration,
};

use ak_common::config;
use argh::FromArgs;
use eyre::{Result, WrapErr as _, eyre};
#[cfg(target_os = "linux")]
use nix::unistd::setgroups;
use nix::{
    fcntl::{AT_FDCWD, AtFlags},
    unistd::{Gid, Group, Uid, User, getuid, setgid, setuid},
};
use tracing::{info, warn};

/// The unprivileged user the server runs as.
const AK_USER: &str = "authentik";
/// Mounted by deployments that let the worker manage outpost containers.
const DOCKER_SOCKET: &str = "/var/run/docker.sock";
/// Where Python's prometheus client keeps the metrics of each process.
const PROMETHEUS_MULTIPROC_DIR: &str = "PROMETHEUS_MULTIPROC_DIR";

#[derive(Debug, FromArgs, PartialEq)]
/// Run a Django management command.
#[argh(subcommand, name = "manage")]
pub(crate) struct Manage {
    /// the management command and its arguments
    #[argh(positional, greedy)]
    args: Vec<String>,
}

#[derive(Debug, FromArgs, PartialEq)]
/// Run the test suite.
#[argh(subcommand, name = "test-all")]
#[expect(
    clippy::empty_structs_with_brackets,
    reason = "argh doesn't support unit structs"
)]
pub(crate) struct TestAll {}

#[derive(Debug, FromArgs, PartialEq)]
/// Print the configuration, or the values of the given keys.
#[argh(subcommand, name = "dump_config")]
pub(crate) struct DumpConfig {
    /// the keys to print
    #[argh(positional, greedy)]
    keys: Vec<String>,
}

#[derive(Debug, FromArgs, PartialEq)]
/// Run bash.
#[argh(subcommand, name = "bash")]
pub(crate) struct Bash {
    /// the arguments for bash
    #[argh(positional, greedy)]
    args: Vec<String>,
}

#[derive(Debug, FromArgs, PartialEq)]
/// Run sh.
#[argh(subcommand, name = "sh")]
pub(crate) struct Sh {
    /// the arguments for sh
    #[argh(positional, greedy)]
    args: Vec<String>,
}

#[derive(Debug, FromArgs, PartialEq)]
/// Idle, so that a shell can be opened in the container.
#[argh(subcommand, name = "debug")]
#[expect(
    clippy::empty_structs_with_brackets,
    reason = "argh doesn't support unit structs"
)]
pub(crate) struct Debug {}

/// Where Python's prometheus client keeps the metrics of each process.
fn prometheus_dir() -> Result<PathBuf> {
    let dir = std::env::var_os(PROMETHEUS_MULTIPROC_DIR)
        .filter(|dir| !dir.is_empty())
        .map_or_else(
            || std::env::temp_dir().join("authentik_prometheus_tmp"),
            PathBuf::from,
        );
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// Replace this process with `cmd`. Only returns when that fails.
fn exec(mut cmd: Command) -> Result<()> {
    let err = cmd.exec();
    Err(err).wrap_err_with(|| format!("failed to run {}", cmd.get_program().display()))
}

/// Run `cmd` and wait for it, failing when it fails.
fn run(cmd: &mut Command) -> Result<()> {
    let status = cmd
        .status()
        .wrap_err_with(|| format!("failed to run {}", cmd.get_program().display()))?;
    if !status.success() {
        return Err(eyre!(
            "{} exited with {status}",
            cmd.get_program().display()
        ));
    }
    Ok(())
}

/// Install what `test-all` and `AUTHENTIK_DEBUGGER` need.
fn prepare_debug() -> Result<()> {
    // Only in the container, a development checkout has all of it already
    if !Path::new("/ak-root").is_dir() {
        return Ok(());
    }
    let venv = std::env::var_os("VENV_PATH").ok_or_else(|| eyre!("VENV_PATH is not set"))?;
    info!("installing the development dependencies");
    run(Command::new("apt-get").arg("update"))?;
    run(Command::new("apt-get")
        .args(["install", "-y", "--no-install-recommends"])
        .args([
            "krb5-kdc",
            "krb5-user",
            "krb5-admin-server",
            "libkrb5-dev",
            "gcc",
        ])
        .env("DEBIAN_FRONTEND", "noninteractive"))?;
    run(Command::new("uv")
        .args(["sync", "--active", "--locked"])
        .env("VIRTUAL_ENV", venv))?;
    // The test suite writes its report there after dropping to `authentik`, and can't create it
    let user = User::from_name(AK_USER)?.ok_or_else(|| eyre!("user {AK_USER} does not exist"))?;
    fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open("/unittest.xml")?;
    nix::unistd::chown("/unittest.xml", Some(user.uid), Some(user.gid))?;
    Ok(())
}

/// `chown -R`, skipping entries that already have the right owner.
fn chown_tree(path: &Path, uid: Uid, gid: Gid) -> Result<()> {
    if !path.exists() {
        return Ok(());
    }
    nix::unistd::chown(path, Some(uid), Some(gid))?;
    let mut stack = vec![path.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let Ok(entries) = fs::read_dir(&dir) else {
            continue;
        };
        for entry in entries.flatten() {
            let child = entry.path();
            if entry.file_type().is_ok_and(|t| t.is_dir()) {
                stack.push(child.clone());
            }
            let Ok(meta) = fs::symlink_metadata(&child) else {
                continue;
            };
            if meta.uid() == uid.as_raw() && meta.gid() == gid.as_raw() {
                continue;
            }
            if let Err(err) = nix::unistd::fchownat(
                AT_FDCWD,
                &child,
                Some(uid),
                Some(gid),
                AtFlags::AT_SYMLINK_NOFOLLOW,
            ) {
                warn!(path = %child.display(), %err, "could not change owner");
            }
        }
    }
    Ok(())
}

fn add_mode(path: &Path, bits: u32) -> Result<()> {
    if !path.is_dir() {
        return Ok(());
    }
    let mut perms = fs::metadata(path)?.permissions();
    perms.set_mode(perms.mode() | bits);
    fs::set_permissions(path, perms)?;
    Ok(())
}

/// The gid that owns a mounted docker socket, if there is one.
fn docker_socket_gid() -> Option<Gid> {
    fs::metadata(DOCKER_SOCKET)
        .ok()
        .map(|meta| Gid::from_raw(meta.gid()))
}

/// Fix up ownership while still root, then switch to `authentik`. Returns its home directory.
fn become_authentik(prometheus_dir: &Path) -> Result<PathBuf> {
    let user = User::from_name(AK_USER)?.ok_or_else(|| eyre!("user {AK_USER} does not exist"))?;
    let mut groups = vec![user.gid];

    if let Some(gid) = docker_socket_gid()
        && !groups.contains(&gid)
    {
        // setgroups takes the numeric gid directly
        let name = Group::from_gid(gid)
            .ok()
            .flatten()
            .map_or_else(|| "unnamed".to_owned(), |g| g.name);
        info!(
            gid = gid.as_raw(),
            group = name,
            "granting access to the docker socket"
        );
        groups.push(gid);
    }

    for path in [Path::new("/data"), Path::new("/certs"), prometheus_dir] {
        chown_tree(path, user.uid, user.gid)
            .wrap_err_with(|| format!("failed to change owner of {}", path.display()))?;
    }
    // Mirrors the old `chmod ug+rwx /data` and `chmod ug+rx /certs`
    add_mode(Path::new("/data"), 0o770)?;
    // 'certs' deliberately gets no owner write bit
    add_mode(Path::new("/certs"), 0o550)?;

    #[cfg(target_os = "linux")]
    setgroups(&groups)?;
    setgid(user.gid)?;
    setuid(user.uid)?;
    // Still being root would make `boot` loop
    if getuid() != user.uid || getuid().is_root() {
        return Err(eyre!("failed to drop privileges to {AK_USER}"));
    }
    Ok(user.dir)
}

fn wait_for_db() -> Result<()> {
    run(Command::new("python").args(["-m", "lifecycle.wait_for_db"]))?;
    info!("bootstrap completed");
    Ok(())
}

fn run_migrations() -> Result<()> {
    run(Command::new("python").args(["-m", "lifecycle.migrate"]))?;
    info!("migrations completed");
    Ok(())
}

/// Get the process ready to run the server or the worker.
pub(crate) fn bootstrap() -> Result<()> {
    let prometheus_dir = prometheus_dir()?;
    let mut env = Vec::new();
    if std::env::var_os(PROMETHEUS_MULTIPROC_DIR).as_deref() != Some(prometheus_dir.as_os_str()) {
        env.push((PROMETHEUS_MULTIPROC_DIR, prometheus_dir.clone()));
    }
    if getuid().is_root() {
        config::init()?;
        if config::get().debugger {
            prepare_debug()?;
        }
        env.push(("HOME", become_authentik(&prometheus_dir)?));
    }
    // Changing our own environment needs unsafe code, so exec ourselves with the new one
    if !env.is_empty() {
        let mut cmd = Command::new(std::env::current_exe()?);
        cmd.args(std::env::args_os().skip(1)).envs(env);
        return exec(cmd);
    }
    wait_for_db()?;
    run_migrations()
}

pub(crate) fn manage(args: &Manage) -> Result<()> {
    // manage.py waits for the database itself
    let mut cmd = Command::new("python");
    cmd.args(["-m", "manage"])
        .args(&args.args)
        .env(PROMETHEUS_MULTIPROC_DIR, prometheus_dir()?);
    exec(cmd)
}

pub(crate) fn test_all() -> Result<()> {
    let prometheus_dir = prometheus_dir()?;
    // manage.py waits for the database itself
    let mut cmd = Command::new("python");
    cmd.args(["-m", "manage", "test", "authentik"])
        .env(PROMETHEUS_MULTIPROC_DIR, &prometheus_dir);
    if getuid().is_root() {
        prepare_debug()?;
        cmd.env("HOME", become_authentik(&prometheus_dir)?);
    }
    exec(cmd)
}

pub(crate) fn dump_config(args: &DumpConfig) -> Result<()> {
    let mut cmd = Command::new("python");
    cmd.args(["-m", "authentik.lib.config"])
        .args(&args.keys)
        .env(PROMETHEUS_MULTIPROC_DIR, prometheus_dir()?);
    exec(cmd)
}

fn shell(program: &str, args: &[String]) -> Result<()> {
    let mut cmd = Command::new(program);
    cmd.args(args)
        .env(PROMETHEUS_MULTIPROC_DIR, prometheus_dir()?);
    exec(cmd)
}

pub(crate) fn bash(args: &Bash) -> Result<()> {
    shell("bash", &args.args)
}

pub(crate) fn sh(args: &Sh) -> Result<()> {
    shell("sh", &args.args)
}

pub(crate) fn idle() -> ! {
    loop {
        std::thread::sleep(Duration::from_hours(1));
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // `lifecycle/ak.py` puts a `--` in front of these, so that argh keeps what follows as it is
    #[test]
    fn arguments_after_the_separator_are_kept() {
        for args in [
            &["--help"][..],
            &["help"],
            &["shell", "-c", "print(1)"],
            &["migrate", "--", "--fake"],
        ] {
            let argv: Vec<&str> = std::iter::once("--").chain(args.iter().copied()).collect();
            let manage = Manage::from_args(&["manage"], &argv).expect("failed to parse");
            assert_eq!(manage.args, args);
        }
    }

    #[test]
    fn shell_flags_are_kept() {
        let bash = Bash::from_args(&["bash"], &["--", "-c", "echo hi"]).expect("failed to parse");
        assert_eq!(bash.args, ["-c", "echo hi"]);
    }
}