use std::collections::{HashMap, HashSet};
use std::time::Duration;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Profile {
ReadOnly,
VmControl,
Unrestricted,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommandMode {
Plain,
Mutex {
key: String,
},
Idempotent {
key: Option<String>,
mutex_key: Option<String>,
ttl: Option<Duration>,
},
}
#[derive(Debug, Clone)]
pub struct CommandRule {
pub mode: CommandMode,
pub retry: crate::ops::RetryPolicy,
pub timeout: Option<Duration>,
}
impl Default for CommandRule {
fn default() -> Self {
Self {
mode: CommandMode::Plain,
retry: crate::ops::RetryPolicy::none(),
timeout: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Policy {
pub(crate) enforce_allow_list: bool,
allowed: HashSet<String>,
rules: HashMap<String, CommandRule>,
default_rule: CommandRule,
}
impl Policy {
#[must_use]
pub fn is_allowed(&self, command: &str) -> bool {
self.allowed.contains(command)
}
pub(crate) fn rule_for(&self, command: &str) -> Result<CommandRule> {
if self.enforce_allow_list && !self.is_allowed(command) {
return Err(Error::policy(command, "command is not allowed"));
}
Ok(self
.rules
.get(command)
.cloned()
.unwrap_or_else(|| self.default_rule.clone()))
}
}
#[derive(Debug, Clone)]
pub struct PolicyBuilder {
profile: Profile,
enforce_allow_list: bool,
allowed: HashSet<String>,
rules: HashMap<String, CommandRule>,
default_rule: CommandRule,
}
impl PolicyBuilder {
#[must_use]
pub fn new() -> Self {
Self {
profile: Profile::ReadOnly,
enforce_allow_list: true,
allowed: HashSet::new(),
rules: HashMap::new(),
default_rule: CommandRule::default(),
}
}
#[must_use]
pub fn profile(mut self, profile: Profile) -> Self {
self.profile = profile;
self
}
#[must_use]
pub fn enforce_allow_list(mut self, enabled: bool) -> Self {
self.enforce_allow_list = enabled;
self
}
#[must_use]
pub fn allow(mut self, command: impl Into<String>) -> Self {
self.allowed.insert(command.into());
self
}
#[must_use]
pub fn rule(mut self, command: impl Into<String>, rule: CommandRule) -> Self {
self.rules.insert(command.into(), rule);
self
}
#[must_use]
pub fn default_rule(mut self, rule: CommandRule) -> Self {
self.default_rule = rule;
self
}
#[must_use]
pub fn build(mut self) -> Policy {
match self.profile {
Profile::ReadOnly => {
self.allowed
.extend(read_only_allow_list().into_iter().map(|s| s.to_string()));
}
Profile::VmControl => {
self.allowed
.extend(read_only_allow_list().into_iter().map(|s| s.to_string()));
self.allowed
.extend(vm_control_allow_list().into_iter().map(|s| s.to_string()));
self.rules
.entry("stop".to_string())
.or_insert_with(|| CommandRule {
mode: CommandMode::Mutex {
key: "vm-control".to_string(),
},
retry: crate::ops::RetryPolicy::conservative(),
timeout: Some(Duration::from_secs(10)),
});
self.rules
.entry("cont".to_string())
.or_insert_with(|| CommandRule {
mode: CommandMode::Mutex {
key: "vm-control".to_string(),
},
retry: crate::ops::RetryPolicy::conservative(),
timeout: Some(Duration::from_secs(10)),
});
}
Profile::Unrestricted => {
self.enforce_allow_list = false;
}
}
Policy {
enforce_allow_list: self.enforce_allow_list,
allowed: self.allowed,
rules: self.rules,
default_rule: self.default_rule,
}
}
}
impl Default for PolicyBuilder {
fn default() -> Self {
Self::new()
}
}
fn read_only_allow_list() -> [&'static str; 6] {
[
"query-status",
"query-version",
"query-kvm",
"query-machines",
"query-cpus-fast",
"query-block",
]
}
fn vm_control_allow_list() -> [&'static str; 4] {
["stop", "cont", "system_powerdown", "system_reset"]
}