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/// What a policy can require of `openmoat run` itself (ADR-077).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "kebab-case")]
44pub enum RunBoundary {
45    /// `openmoat run` starts only with its whole boundary: an OS sandbox and,
46    /// where the system has one, a network of the agent's own.
47    FullBoundary,
48}
49
50/// The `require:` section of a policy, a repository policy or a pack.
51#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct Require {
54    /// The least level of every agent; absent, none.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub protection: Option<RequiredLevel>,
57    /// What `openmoat run` must have before it starts anything; absent, what
58    /// it can get. Applies to every program it starts, agent or not.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub run: Option<RunBoundary>,
61    /// The least level of one agent, by its host id (as `openmoat status` names it), in
62    /// place of [`Self::protection`] for that agent in this file. Another
63    /// source's higher level still applies ([`Policy::required_for`]).
64    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
65    pub agents: BTreeMap<String, RequiredLevel>,
66}
67
68impl Require {
69    /// What this section asks of `host` (a host id), or of a program that is
70    /// no known agent when `host` is `None`: only [`Self::protection`].
71    #[must_use]
72    pub fn for_agent(&self, host: Option<&str>) -> Option<RequiredLevel> {
73        host.and_then(|h| self.agents.get(h).copied())
74            .or(self.protection)
75    }
76
77    /// Every host id [`Self::agents`] names, for the CLI to check against the
78    /// hosts it knows.
79    pub fn agent_ids(&self) -> impl Iterator<Item = &str> {
80        self.agents.keys().map(String::as_str)
81    }
82}
83
84/// Where a requirement in force comes from.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum RequiredBy {
87    /// The person's own `policy.yaml`.
88    Policy,
89    /// The project's repository policy (`.moat/policy.yaml`, ADR-022).
90    Repository,
91    /// The installed policy pack of this name (ADR-031).
92    Pack(String),
93}
94
95/// The level an agent must be at, and which policy asks for it.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct Requirement {
98    /// The least level.
99    pub level: RequiredLevel,
100    /// The source that asks for it; of two that ask for the same level, the
101    /// person's own policy, then the first merged.
102    pub by: RequiredBy,
103}
104
105impl Policy {
106    /// The level `host` (a host id; `None` for a program that is no known
107    /// agent) must be at: the strictest that the policy and every repository
108    /// policy and pack merged into it ask for, or `None` when none does.
109    #[must_use]
110    pub fn required_for(&self, host: Option<&str>) -> Option<Requirement> {
111        let own = self.require.iter().map(|r| (RequiredBy::Policy, r));
112        let added = self.require_added.iter().map(|(by, r)| (by.clone(), r));
113        own.chain(added)
114            .filter_map(|(by, r)| r.for_agent(host).map(|level| Requirement { level, by }))
115            .fold(
116                None,
117                |strictest: Option<Requirement>, next| match strictest {
118                    Some(kept) if kept.level >= next.level => Some(kept),
119                    _ => Some(next),
120                },
121            )
122    }
123
124    /// Which policy requires `openmoat run` to start only with its full
125    /// boundary, or `None` when none does. Any one source is enough, so a
126    /// repository or a pack can turn it on and none can turn it off.
127    #[must_use]
128    pub fn run_boundary_required(&self) -> Option<RequiredBy> {
129        let own = self.require.iter().map(|r| (RequiredBy::Policy, r));
130        let added = self.require_added.iter().map(|(by, r)| (by.clone(), r));
131        own.chain(added)
132            .find_map(|(by, r)| r.run.map(|RunBoundary::FullBoundary| by))
133    }
134
135    /// Add the requirement `require` of `by` (a repository policy or a pack)
136    /// to the ones [`Self::required_for`] reads. It can only raise a level.
137    pub fn add_requirement(&mut self, by: RequiredBy, require: &Require) {
138        self.require_added.push((by, require.clone()));
139    }
140}
141
142/// An agent id must look like a host id; whether a host has it is the CLI's to
143/// check, since the core does not know the hosts.
144pub(super) fn check(require: Option<&Require>) -> Result<(), PolicyError> {
145    for id in require.iter().flat_map(|r| r.agent_ids()) {
146        let valid = !id.is_empty()
147            && id
148                .chars()
149                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
150        if !valid {
151            return Err(PolicyError::Rule {
152                rule: "require.agents".to_owned(),
153                problem: format!(
154                    "`{id}` is not an agent id (lowercase letters, digits and `-`, as `openmoat status` names agents)"
155                ),
156            });
157        }
158    }
159    Ok(())
160}
161
162#[cfg(test)]
163mod tests;