Skip to main content

openmoat_core/policy/
require.rs

1//! The `require:` section (ADR-064): the least protection level an agent must
2//! be at for OpenMoat to let any of its calls through. The level itself is
3//! worked out by the CLI, which can see the hook and the sandboxes; this
4//! module only holds what the policies ask for and which of them asks most.
5//!
6//! A repository policy and a policy pack can add a requirement, never take one
7//! away: [`Policy::required_for`] is the strictest of every source, so a lower
8//! level from one of them leaves a higher one from another in force.
9
10use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use super::{Policy, PolicyError};
15
16/// A protection level a policy can require, weakest first, so the derived
17/// order is "at least as protected as".
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum RequiredLevel {
21    /// OpenMoat's hook decides the agent's calls (`openmoat status`'s
22    /// `hook-only` or better).
23    HookOnly,
24    /// The hook decides, and an OS sandbox from OpenMoat holds the commands
25    /// the agent runs (`hook-and-os-sandbox`): the host's own sandbox as
26    /// OpenMoat configured it, or `openmoat run`.
27    HookAndOsSandbox,
28}
29
30impl RequiredLevel {
31    /// The level as `require:` and `openmoat status --format json` spell it.
32    #[must_use]
33    pub fn as_str(self) -> &'static str {
34        match self {
35            Self::HookOnly => "hook-only",
36            Self::HookAndOsSandbox => "hook-and-os-sandbox",
37        }
38    }
39}
40
41/// The `require:` section of a policy, a repository policy or a pack.
42#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct Require {
45    /// The least level of every agent; absent, none.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub protection: Option<RequiredLevel>,
48    /// The least level of one agent, by its host id (as `openmoat status` names it), in
49    /// place of [`Self::protection`] for that agent in this file. Another
50    /// source's higher level still applies ([`Policy::required_for`]).
51    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
52    pub agents: BTreeMap<String, RequiredLevel>,
53}
54
55impl Require {
56    /// What this section asks of `host` (a host id), or of a program that is
57    /// no known agent when `host` is `None`: only [`Self::protection`].
58    #[must_use]
59    pub fn for_agent(&self, host: Option<&str>) -> Option<RequiredLevel> {
60        host.and_then(|h| self.agents.get(h).copied())
61            .or(self.protection)
62    }
63
64    /// Every host id [`Self::agents`] names, for the CLI to check against the
65    /// hosts it knows.
66    pub fn agent_ids(&self) -> impl Iterator<Item = &str> {
67        self.agents.keys().map(String::as_str)
68    }
69}
70
71/// Where a requirement in force comes from.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum RequiredBy {
74    /// The person's own `policy.yaml`.
75    Policy,
76    /// The project's repository policy (`.moat/policy.yaml`, ADR-022).
77    Repository,
78    /// The installed policy pack of this name (ADR-031).
79    Pack(String),
80}
81
82/// The level an agent must be at, and which policy asks for it.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Requirement {
85    /// The least level.
86    pub level: RequiredLevel,
87    /// The source that asks for it; of two that ask for the same level, the
88    /// person's own policy, then the first merged.
89    pub by: RequiredBy,
90}
91
92impl Policy {
93    /// The level `host` (a host id; `None` for a program that is no known
94    /// agent) must be at: the strictest that the policy and every repository
95    /// policy and pack merged into it ask for, or `None` when none does.
96    #[must_use]
97    pub fn required_for(&self, host: Option<&str>) -> Option<Requirement> {
98        let own = self.require.iter().map(|r| (RequiredBy::Policy, r));
99        let added = self.require_added.iter().map(|(by, r)| (by.clone(), r));
100        own.chain(added)
101            .filter_map(|(by, r)| r.for_agent(host).map(|level| Requirement { level, by }))
102            .fold(
103                None,
104                |strictest: Option<Requirement>, next| match strictest {
105                    Some(kept) if kept.level >= next.level => Some(kept),
106                    _ => Some(next),
107                },
108            )
109    }
110
111    /// Add the requirement `require` of `by` (a repository policy or a pack)
112    /// to the ones [`Self::required_for`] reads. It can only raise a level.
113    pub fn add_requirement(&mut self, by: RequiredBy, require: &Require) {
114        self.require_added.push((by, require.clone()));
115    }
116}
117
118/// An agent id must look like a host id; whether a host has it is the CLI's to
119/// check, since the core does not know the hosts.
120pub(super) fn check(require: Option<&Require>) -> Result<(), PolicyError> {
121    for id in require.iter().flat_map(|r| r.agent_ids()) {
122        let valid = !id.is_empty()
123            && id
124                .chars()
125                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
126        if !valid {
127            return Err(PolicyError::Rule {
128                rule: "require.agents".to_owned(),
129                problem: format!(
130                    "`{id}` is not an agent id (lowercase letters, digits and `-`, as `openmoat status` names agents)"
131                ),
132            });
133        }
134    }
135    Ok(())
136}
137
138#[cfg(test)]
139mod tests;