liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Execute the bytes checked at start, even if the approved path is replaced.

use super::{TemplatePolicy, TemplateRecord};
use crate::services::lifecycle::Lifecycle;
use crate::services::lifecycle::site::SiteError;
use sha2::{Digest, Sha256};
use std::io::{Read, Write};
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};

/// The child executes a private copy, never a second lookup of the mutable
/// approved path. It stays present for interpreters until the child exits.
pub(in super::super) struct StagedCommand {
    directory: PathBuf,
    command: PathBuf,
}
impl StagedCommand {
    pub(in super::super) fn command(&self) -> &Path {
        &self.command
    }
    fn create(parent: &Path) -> Result<Self, SiteError> {
        let mut nonce = [0_u8; 32];
        std::fs::File::open("/dev/urandom")
            .and_then(|mut random| random.read_exact(&mut nonce))
            .map_err(|_| SiteError::Invalid("template staging random source"))?;
        let directory = parent.join(format!("approved-{}", hex::encode(nonce)));
        std::fs::DirBuilder::new()
            .mode(0o700)
            .create(&directory)
            .map_err(|_| SiteError::Invalid("template staging directory"))?;
        Ok(Self {
            command: directory.join("program"),
            directory,
        })
    }
}
impl Drop for StagedCommand {
    fn drop(&mut self) {
        match std::fs::remove_file(&self.command) {
            Ok(()) => {}
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
            Err(_) => tracing::error!("template_staging_file_cleanup_failed"),
        }
        if std::fs::remove_dir(&self.directory).is_err() {
            tracing::error!("template_staging_directory_cleanup_failed");
        }
    }
}

impl TemplatePolicy {
    fn stage(&self, template: &TemplateRecord, parent: &Path) -> Result<StagedCommand, SiteError> {
        let (_, source) = self.executable(&template.proposal.name, &template.proposal.command)?;
        let staged = StagedCommand::create(parent)?;
        let mut destination = std::fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .mode(0o600)
            .open(&staged.command)
            .map_err(|_| SiteError::Invalid("template staging file"))?;
        let mut source = source.take(self.command_bytes.get().saturating_add(1));
        let mut hash = Sha256::new();
        let mut buffer = [0_u8; 8192];
        let mut total = 0_u64;
        loop {
            let n = source
                .read(&mut buffer)
                .map_err(|_| SiteError::Invalid("template command read"))?;
            if n == 0 {
                break;
            }
            total = total
                .saturating_add(u64::try_from(n).map_err(|_| SiteError::Bound("command_bytes"))?);
            if total > self.command_bytes.get() {
                return Err(SiteError::Bound("command_bytes"));
            }
            let bytes = buffer.get(..n).ok_or(SiteError::Store)?;
            hash.update(bytes);
            destination
                .write_all(bytes)
                .map_err(|_| SiteError::Invalid("template staging write"))?;
        }
        let actual = hex::encode(hash.finalize());
        if actual != template.proposal.digest {
            return Err(digest_changed(template, &actual));
        }
        destination
            .set_permissions(std::fs::Permissions::from_mode(0o500))
            .map_err(|_| SiteError::Invalid("template staging permissions"))?;
        destination
            .sync_all()
            .map_err(|_| SiteError::Invalid("template staging flush"))?;
        drop(destination);
        Ok(staged)
    }
}

pub(super) fn digest_changed(template: &TemplateRecord, actual: &str) -> SiteError {
    SiteError::Refused(format!(
        "template {}: {} is not the file approved on {} (digest {} != {})",
        template.proposal.name,
        template.proposal.command.display(),
        template.approved_at_ms,
        template.proposal.digest,
        actual
    ))
}

impl Lifecycle {
    /// Keeps the checked executable private until the process watcher releases
    /// it. The persistence directory must be writable only by the exchange's
    /// trusted operator; builder homes remain contained by the runner.
    pub(in super::super) fn stage_template(
        &self,
        template: &TemplateRecord,
    ) -> Result<StagedCommand, SiteError> {
        if template.retired {
            return Err(SiteError::Refused(format!(
                "template {} is retired",
                template.proposal.name
            )));
        }
        let policy = self
            .site_bindings()?
            .templates
            .as_ref()
            .ok_or(SiteError::Invalid("[auth.lys.templates] roots"))?;
        policy.stage(template, &self.dir)
    }
}