liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Applies an authenticated immutable draft to a candidate snapshot.

use super::super::{Lifecycle, ServiceRecord, ServiceState};
use super::storage::Snapshot;
use super::{SiteAct, SiteChange, SiteError, validate_contracts};
use crate::auth_lys::live::drafts::ApprovalTransition;

impl Lifecycle {
    pub(super) fn apply_site_act(
        &self,
        snapshot: &mut Snapshot,
        act: &SiteAct,
        transition: &ApprovalTransition,
        prepared_at_ms: u64,
        trials: &std::collections::BTreeMap<
            String,
            std::collections::BTreeMap<u64, super::super::trial::Registration>,
        >,
    ) -> Result<(), SiteError> {
        let bindings = self.site_bindings()?;
        let limits = &bindings.limits;
        if let SiteChange::CreateSite(spec) = &act.change {
            return self.create_site(snapshot, act, spec, transition);
        }
        if matches!(
            act.change,
            SiteChange::CreateTemplate(_)
                | SiteChange::UpdateTemplate(_)
                | SiteChange::RetireTemplate
        ) {
            return self.apply_template(snapshot, act, transition);
        }
        let record = snapshot
            .records
            .get_mut(&act.target)
            .ok_or_else(|| SiteError::Refused(format!("unknown_service: {}", act.target)))?;
        if record.state != ServiceState::Building {
            return Err(SiteError::Refused(format!(
                "service {} is not building",
                record.name
            )));
        }
        if record.transitions.len() >= limits.transitions.get() {
            return Err(SiteError::Bound("transitions"));
        }
        match &act.change {
            SiteChange::BindBuilder { agent } => {
                self.bind_site_builder(record, agent, transition, prepared_at_ms, trials)?;
            }
            SiteChange::UnbindBuilder => {
                if !record.builder_bound {
                    return Err(SiteError::Invalid("site has no bound builder"));
                }
                let grants = record.builder_grants.as_ref().ok_or(SiteError::Store)?;
                bindings
                    .effects
                    .revoke(&transition.draft, "unbind-plan", &grants.plan)?;
                bindings
                    .effects
                    .revoke(&transition.draft, "unbind-serve", &grants.serve)?;
                record.builder_bound = false;
                record.builder = None;
            }
            SiteChange::Complete => {
                self.complete_site(record, &snapshot.construction, transition, trials)?;
            }
            _ => return Err(SiteError::Invalid("construction act")),
        }
        record.approvals.push(transition.clone().into());
        let before = if matches!(act.change, SiteChange::Complete) {
            ServiceState::Building
        } else {
            record.state
        };
        record.transitions.push(super::super::Transition {
            from: before,
            to: record.state,
            holder: act.holder.clone(),
            at_ms: transition.executed_at_ms,
            note: Some(act.change.action().into()),
        });
        Ok(())
    }

    fn complete_site(
        &self,
        record: &mut ServiceRecord,
        construction: &super::Construction,
        transition: &ApprovalTransition,
        trials: &std::collections::BTreeMap<
            String,
            std::collections::BTreeMap<u64, super::super::trial::Registration>,
        >,
    ) -> Result<(), SiteError> {
        let bindings = self.site_bindings()?;
        if let Some(connection) = trials
            .get(&record.id)
            .and_then(|trials| trials.keys().next())
        {
            return Err(SiteError::Refused(format!(
                "trial still registered on connection {connection}"
            )));
        }
        super::super::plan::refuse_unfinished(&record.plan)?;
        let template = record
            .template
            .as_ref()
            .and_then(|name| construction.templates.get(name))
            .filter(|template| !template.retired)
            .ok_or(SiteError::Invalid("site has no approved template"))?;
        if template.proposal.subjects != record.subjects {
            return Err(SiteError::Invalid("template subjects differ from site"));
        }
        self.verify_template(template)?;
        let grants = record
            .builder_grants
            .as_ref()
            .ok_or(SiteError::Invalid("site has no bound builder"))?;
        bindings
            .effects
            .revoke(&transition.draft, "complete-serve", &grants.serve)?;
        record.state = ServiceState::Asleep;
        record.builder_bound = false;
        Ok(())
    }

    fn bind_site_builder(
        &self,
        record: &mut ServiceRecord,
        agent: &str,
        transition: &ApprovalTransition,
        prepared_at_ms: u64,
        trials: &std::collections::BTreeMap<
            String,
            std::collections::BTreeMap<u64, super::super::trial::Registration>,
        >,
    ) -> Result<(), SiteError> {
        let bindings = self.site_bindings()?;
        let limits = &bindings.limits;
        if !crate::config::templates::is_segment(agent) || agent.len() > limits.text_bytes.get() {
            return Err(SiteError::Invalid("builder"));
        }
        if record.builder_bound {
            return Err(SiteError::Refused(format!(
                "site {} has builder {}",
                record.name,
                record.builder.as_deref().ok_or(SiteError::Store)?
            )));
        }
        if let Some(connection) = trials
            .get(&record.id)
            .and_then(|trials| trials.keys().next())
        {
            return Err(SiteError::Refused(format!(
                "trial still registered on connection {connection}"
            )));
        }
        let grants = bindings
            .effects
            .bind(&transition.draft, &record.id, agent, prepared_at_ms)?;
        record.builder = Some(agent.into());
        record.builder_grants = Some(grants);
        record.builder_bound = true;
        Ok(())
    }

    fn create_site(
        &self,
        snapshot: &mut Snapshot,
        act: &SiteAct,
        spec: &super::SiteSpec,
        transition: &ApprovalTransition,
    ) -> Result<(), SiteError> {
        let limits = &self.site_bindings()?.limits;
        validate_contracts(spec, limits)?;
        if act.target != spec.name {
            return Err(SiteError::Invalid("site target"));
        }
        if snapshot.records.contains_key(&act.target)
            || snapshot
                .records
                .values()
                .any(|record| record.name == spec.name)
        {
            return Err(SiteError::Refused(format!("name_taken: {}", spec.name)));
        }
        if snapshot
            .records
            .values()
            .filter(|record| record.purpose.is_some())
            .count()
            >= limits.sites.get()
        {
            return Err(SiteError::Bound("sites"));
        }
        for subject in &spec.subjects {
            if let Some(owner) = snapshot
                .records
                .values()
                .find(|record| record.subjects.contains(subject))
            {
                return Err(SiteError::Refused(format!(
                    "subject {subject} belongs to service {}",
                    owner.name
                )));
            }
        }
        let placer = self
            .placer
            .as_ref()
            .ok_or(super::super::LifecycleError::NoPlacer)?;
        placer
            .place(
                &super::effects::operation(&transition.draft, "site"),
                ("liminal.service", &act.target),
                ("liminal.network", &self.network),
            )
            .map_err(SiteError::Pass)?;
        for subject in &spec.subjects {
            placer
                .place(
                    &super::effects::operation(&transition.draft, &format!("subject:{subject}")),
                    ("liminal.subject", subject),
                    ("liminal.service", &act.target),
                )
                .map_err(SiteError::Pass)?;
        }
        let plan_subject = format!("services.{}.plan", spec.name);
        placer
            .place(
                &super::effects::operation(&transition.draft, "plan-subject"),
                ("liminal.subject", &plan_subject),
                ("liminal.network", &self.network),
            )
            .map_err(SiteError::Pass)?;
        let mut record = ServiceRecord {
            id: act.target.clone(),
            name: spec.name.clone(),
            template: None,
            subjects: spec.subjects.clone(),
            purpose: Some(spec.purpose.clone()),
            contracts: spec.contracts.clone(),
            builder: None,
            builder_grants: None,
            builder_bound: false,
            approved_by: transition.approved_by.clone(),
            responsible: Some(act.responsible.clone()),
            plan: super::super::plan::Plan::default(),
            approvals: vec![transition.clone().into()],
            state: ServiceState::Building,
            created_by: act.holder.clone(),
            transitions: Vec::new(),
        };
        Self::site_transition(
            &mut record,
            &act.holder,
            transition.executed_at_ms,
            "site created",
        );
        snapshot.records.insert(act.target.clone(), record);
        Ok(())
    }

    fn apply_template(
        &self,
        snapshot: &mut Snapshot,
        act: &SiteAct,
        transition: &ApprovalTransition,
    ) -> Result<(), SiteError> {
        let bindings = self.site_bindings()?;
        let policy = bindings
            .templates
            .as_ref()
            .ok_or(SiteError::Invalid("[auth.lys.templates] roots"))?;
        if self.templates.contains_key(&act.target) {
            return Err(SiteError::Refused(format!(
                "template {} is read-only configuration",
                act.target
            )));
        }
        if matches!(act.change, SiteChange::RetireTemplate) {
            let record = snapshot
                .construction
                .templates
                .get_mut(&act.target)
                .ok_or(SiteError::Invalid("template absent"))?;
            if record.retired {
                return Err(SiteError::Invalid("template retired"));
            }
            if record.transitions.len() >= bindings.limits.transitions.get() {
                return Err(SiteError::Bound("transitions"));
            }
            record.retired = true;
            record.transitions.push(transition.clone().into());
            return Ok(());
        }
        let (proposal, creating) = match &act.change {
            SiteChange::CreateTemplate(proposal) => (proposal, true),
            SiteChange::UpdateTemplate(proposal) => (proposal, false),
            _ => return Err(SiteError::Invalid("template act")),
        };
        let old = snapshot.construction.templates.get(&act.target);
        self.check_template_act(snapshot, act, proposal, creating, old)?;
        let mut transitions = old.map_or_else(Vec::new, |template| template.transitions.clone());
        if transitions.len() >= bindings.limits.transitions.get() {
            return Err(SiteError::Bound("transitions"));
        }
        let (_, actual) = policy.digest(&proposal.name, &proposal.command)?;
        if actual != proposal.digest {
            return Err(SiteError::Refused(format!(
                "template {}: command changed since proposal (digest {} != {actual})",
                proposal.name, proposal.digest
            )));
        }
        transitions.push(transition.clone().into());
        let template = super::super::templates::TemplateRecord {
            proposal: proposal.clone(),
            approved_by: transition.approved_by.clone(),
            approved_at_ms: transition.executed_at_ms,
            retired: false,
            transitions,
        };
        let placer = self
            .placer
            .as_ref()
            .ok_or(super::super::LifecycleError::NoPlacer)?;
        if creating {
            placer
                .place(
                    &super::effects::operation(&transition.draft, "template"),
                    ("liminal.template", &act.target),
                    ("liminal.network", &self.network),
                )
                .map_err(SiteError::Pass)?;
        }
        snapshot
            .construction
            .templates
            .insert(act.target.clone(), template);
        snapshot
            .records
            .get_mut(&proposal.service)
            .ok_or(SiteError::Store)?
            .template = Some(act.target.clone());
        Ok(())
    }
    fn check_template_act(
        &self,
        snapshot: &Snapshot,
        act: &SiteAct,
        proposal: &super::super::templates::TemplateProposal,
        creating: bool,
        old: Option<&super::super::templates::TemplateRecord>,
    ) -> Result<(), SiteError> {
        let bindings = self.site_bindings()?;
        let policy = bindings
            .templates
            .as_ref()
            .ok_or(SiteError::Invalid("[auth.lys.templates] roots"))?;
        policy.validate(proposal)?;
        if proposal.name != act.target {
            return Err(SiteError::Invalid("template target"));
        }
        let site = snapshot
            .records
            .get(&proposal.service)
            .ok_or(SiteError::Invalid("template site absent"))?;
        if (creating && site.state != ServiceState::Building)
            || site.state == ServiceState::Retired
            || proposal.subjects != site.subjects
        {
            return Err(SiteError::Invalid(
                "template subjects differ from building site",
            ));
        }

        if creating && old.is_some() {
            return Err(SiteError::Refused(format!(
                "template {} already exists",
                act.target
            )));
        }
        if !creating && old.is_none_or(|template| template.retired) {
            return Err(SiteError::Invalid("template absent or retired"));
        }
        if creating && snapshot.construction.templates.len() >= policy.records.get() {
            return Err(SiteError::Bound("template records"));
        }
        if old.is_some_and(|template| template.proposal.service != proposal.service) {
            return Err(SiteError::Invalid("template belongs to another site"));
        }
        Ok(())
    }
}