use serde::{Deserialize, Serialize};
use super::effect::{NetworkPolicy, NodeTrustTier, PackageTrust, WorkloadEffect};
use crate::governance::{
GovernanceBranch, GovernanceRule, GovernanceRuleType, RuleSeverity, selector_matches,
};
pub const WORKLOAD_ACTION_PREFIX: &str = "workload.";
pub const ACTION_INSTALL: &str = "workload.install";
pub const ACTION_PLACE: &str = "workload.place";
pub const ACTION_LOAD: &str = "workload.load";
pub const ACTION_START: &str = "workload.start";
pub const ACTION_STOP: &str = "workload.stop";
pub const ACTION_UNLOAD: &str = "workload.unload";
pub const ACTION_MIGRATE: &str = "workload.migrate";
pub const ACTION_REVOKE: &str = "workload.revoke";
pub const ACTION_NODE_BIND: &str = "workload.node.bind";
pub const GOVERNED_ACTIONS: &[&str] = &[
ACTION_INSTALL,
ACTION_PLACE,
ACTION_LOAD,
ACTION_START,
ACTION_STOP,
ACTION_UNLOAD,
ACTION_MIGRATE,
ACTION_REVOKE,
ACTION_NODE_BIND,
];
pub const DEFAULT_DENY_RULE_ID: &str = "WORKLOAD-DEFAULT-DENY";
pub const EFFECT_CEILING_RULE_ID: &str = "WORKLOAD-EFFECT-CEILING";
pub const DEFAULT_DENY_CATEGORY: &str = "workload.default_deny";
pub fn is_governed_action(action: &str) -> bool {
GOVERNED_ACTIONS.contains(&action)
}
pub fn default_rules() -> Vec<GovernanceRule> {
let rule = |id: &str, desc: &str, category: &str, force: bool| GovernanceRule {
id: id.into(),
description: desc.into(),
branch: GovernanceBranch::Legislative,
severity: RuleSeverity::Blocking,
active: true,
reference_url: None,
sop_category: Some(category.into()),
rule_type: GovernanceRuleType::General,
action_selector: Some(format!("{WORKLOAD_ACTION_PREFIX}*")),
tool_selector: None,
force_on_match: force,
};
vec![
rule(
DEFAULT_DENY_RULE_ID,
"workload.* actions are denied unless an explicit workload permit rule matches (ADR-099 s4)",
DEFAULT_DENY_CATEGORY,
true,
),
rule(
EFFECT_CEILING_RULE_ID,
"workload.* actions above the effect threshold are denied even when permitted",
"workload.effect_ceiling",
false,
),
]
}
pub fn install_default_rules(
dist: &mut crate::rule_distribution::RuleDistribution,
now_unix: u64,
) {
for rule in default_rules() {
dist.upsert_local(rule, now_unix);
}
}
fn default_min_trust() -> PackageTrust {
PackageTrust::PinnedSigner
}
fn default_min_tier() -> NodeTrustTier {
NodeTrustTier::Paired
}
fn default_max_network() -> NetworkPolicy {
NetworkPolicy::Lan
}
fn default_max_cost() -> f64 {
1.0
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkloadPermitRule {
pub id: String,
#[serde(default)]
pub description: String,
pub actions: Vec<String>,
pub kinds: Vec<String>,
#[serde(default = "default_min_trust")]
pub min_package_trust: PackageTrust,
#[serde(default = "default_min_tier")]
pub min_node_tier: NodeTrustTier,
#[serde(default = "default_max_network")]
pub max_network: NetworkPolicy,
#[serde(default)]
pub allow_secrets: bool,
#[serde(default)]
pub allow_emulated: bool,
#[serde(default)]
pub accelerators: Vec<String>,
#[serde(default = "default_max_cost")]
pub max_resource_cost: f64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub projects: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub principals: Vec<String>,
}
pub const CATALOG_PRINCIPAL: &str = "catalog";
pub const SUPERVISOR_PRINCIPAL: &str = "weftos.project-supervisor";
pub fn project_supervisor_permit() -> WorkloadPermitRule {
let mut rule = WorkloadPermitRule::new(
"PROJECT-SUPERVISOR-PERMIT",
[ACTION_LOAD, ACTION_START, ACTION_STOP, ACTION_UNLOAD],
["project"],
);
rule.description = "the project supervisor may run per-project child kernels (ADR-103 A6)".into();
rule.min_package_trust = PackageTrust::ProjectCert;
rule.min_node_tier = NodeTrustTier::Paired;
rule.max_network = NetworkPolicy::None;
rule.principals = vec![SUPERVISOR_PRINCIPAL.into()];
rule
}
impl WorkloadPermitRule {
pub fn new(
id: impl Into<String>,
actions: impl IntoIterator<Item = impl Into<String>>,
kinds: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
id: id.into(),
description: String::new(),
actions: actions.into_iter().map(Into::into).collect(),
kinds: kinds.into_iter().map(Into::into).collect(),
min_package_trust: default_min_trust(),
min_node_tier: default_min_tier(),
max_network: default_max_network(),
allow_secrets: false,
allow_emulated: false,
accelerators: Vec::new(),
max_resource_cost: default_max_cost(),
projects: Vec::new(),
principals: Vec::new(),
}
}
pub fn validate(&self) -> Result<(), String> {
if self.id.trim().is_empty() {
return Err("permit rule id must not be empty".into());
}
if self.actions.is_empty() || self.kinds.is_empty() {
return Err(format!("permit rule '{}' needs at least one action and one kind", self.id));
}
for a in &self.actions {
let known = is_governed_action(a)
|| a == "workload.*"
|| (a.ends_with('*') && a.starts_with(WORKLOAD_ACTION_PREFIX));
if !known {
return Err(format!(
"permit rule '{}': action selector '{a}' is not a governed workload action",
self.id
));
}
}
if self.kinds.iter().any(|k| k.is_empty()) || self.accelerators.iter().any(|a| a.is_empty()) {
return Err(format!("permit rule '{}': empty kind or accelerator selector", self.id));
}
if self.min_package_trust == PackageTrust::Unsigned && self.principals.is_empty() {
return Err(format!(
"permit rule '{}' accepts unsigned packages, so it must name the principals it \
applies to (e.g. [\"{CATALOG_PRINCIPAL}\"] for the catalog verbs)",
self.id
));
}
if self.principals.iter().any(|p| p.is_empty()) {
return Err(format!("permit rule '{}': empty principal selector", self.id));
}
if self.projects.iter().any(|p| p.is_empty()) {
return Err(format!("permit rule '{}': empty project selector", self.id));
}
if !self.max_resource_cost.is_finite() || !(0.0..=1.0).contains(&self.max_resource_cost) {
return Err(format!("permit rule '{}': max_resource_cost must be in [0, 1]", self.id));
}
Ok(())
}
pub fn matches(&self, action: &str, effect: &WorkloadEffect) -> bool {
self.matches_in(action, effect, None)
}
pub fn matches_in(&self, action: &str, effect: &WorkloadEffect, project: Option<&str>) -> bool {
self.matches_for(action, effect, project, None)
}
pub fn matches_for(
&self,
action: &str,
effect: &WorkloadEffect,
project: Option<&str>,
principal: Option<&str>,
) -> bool {
let principal_ok =
self.principals.is_empty() || principal.is_some_and(|p| self.principals.iter().any(|x| x == p));
let project_ok = self.projects.is_empty() || project.is_some_and(|p| self.projects.iter().any(|x| x == p));
let action_ok = self.actions.iter().any(|s| selector_matches(s, action));
let kind_ok = self.kinds.iter().any(|k| k == "*" || k == &effect.kind);
let accel_ok = match &effect.accelerator {
None => true,
Some(acc) => self.accelerators.iter().any(|s| selector_matches(s, acc)),
};
principal_ok
&& project_ok
&& action_ok
&& kind_ok
&& accel_ok
&& effect.package_trust >= self.min_package_trust
&& effect.node_tier >= self.min_node_tier
&& effect.network <= self.max_network
&& (self.allow_secrets || !effect.secrets)
&& (self.allow_emulated || !effect.emulated)
&& effect.resource_cost <= self.max_resource_cost
}
}