use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::{
DynamicPluginAttestationMode, DynamicPluginCheckState, DynamicPluginFailure,
DynamicPluginFailurePhase, DynamicPluginKind, DynamicPluginManifest, DynamicPluginStartupClass,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DynamicPluginHostPolicy {
pub defaults: DynamicPluginHostPolicyEffect,
pub rules: Vec<DynamicPluginHostPolicyRule>,
pub overrides: BTreeMap<String, DynamicPluginHostPolicyEffect>,
}
impl DynamicPluginHostPolicy {
pub fn merge_from(&mut self, other: Self) {
self.defaults.merge_from(other.defaults);
self.rules.extend(other.rules);
for (plugin_id, effect) in other.overrides {
self.overrides
.entry(plugin_id)
.or_default()
.merge_from(effect);
}
}
pub fn apply_secure_defaults(&mut self) {
self.defaults
.startup
.get_or_insert(DynamicPluginStartupClass::Required);
self.defaults
.attestation
.get_or_insert(DynamicPluginAttestationMode::SignatureRequired);
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DynamicPluginHostPolicyEffect {
pub allowed: Option<bool>,
pub startup: Option<DynamicPluginStartupClass>,
pub attestation: Option<DynamicPluginAttestationMode>,
pub trusted_public_keys: Option<Vec<String>>,
}
impl DynamicPluginHostPolicyEffect {
fn merge_from(&mut self, other: Self) {
if other.allowed.is_some() {
self.allowed = other.allowed;
}
if other.startup.is_some() {
self.startup = other.startup;
}
if other.attestation.is_some() {
self.attestation = other.attestation;
}
if other.trusted_public_keys.is_some() {
self.trusted_public_keys = other.trusted_public_keys;
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DynamicPluginHostPolicyRule {
pub match_kind: Option<DynamicPluginKind>,
pub match_plugin_id: Option<String>,
pub effect: DynamicPluginHostPolicyEffect,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct FileDynamicPluginHostPolicy {
#[serde(default)]
pub defaults: FileDynamicPluginHostPolicyEffect,
#[serde(default)]
pub rules: Vec<FileDynamicPluginHostPolicyRule>,
#[serde(default)]
pub overrides: BTreeMap<String, FileDynamicPluginHostPolicyEffect>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct FileDynamicPluginHostPolicyEffect {
allowed: Option<bool>,
startup: Option<DynamicPluginStartupClass>,
attestation: Option<DynamicPluginAttestationMode>,
trusted_public_keys: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct FileDynamicPluginHostPolicyRule {
match_kind: Option<DynamicPluginKind>,
match_plugin_id: Option<String>,
allowed: Option<bool>,
startup: Option<DynamicPluginStartupClass>,
attestation: Option<DynamicPluginAttestationMode>,
trusted_public_keys: Option<Vec<String>>,
}
impl From<FileDynamicPluginHostPolicy> for DynamicPluginHostPolicy {
fn from(value: FileDynamicPluginHostPolicy) -> Self {
Self {
defaults: value.defaults.into(),
rules: value.rules.into_iter().map(Into::into).collect(),
overrides: value
.overrides
.into_iter()
.map(|(id, effect)| (id.trim().to_owned(), effect.into()))
.collect(),
}
}
}
impl From<FileDynamicPluginHostPolicyEffect> for DynamicPluginHostPolicyEffect {
fn from(value: FileDynamicPluginHostPolicyEffect) -> Self {
Self {
allowed: value.allowed,
startup: value.startup,
attestation: value.attestation,
trusted_public_keys: value
.trusted_public_keys
.map(|keys| keys.into_iter().map(|key| key.trim().to_owned()).collect()),
}
}
}
impl From<FileDynamicPluginHostPolicyRule> for DynamicPluginHostPolicyRule {
fn from(value: FileDynamicPluginHostPolicyRule) -> Self {
Self {
match_kind: value.match_kind,
match_plugin_id: value.match_plugin_id.map(|id| id.trim().to_owned()),
effect: DynamicPluginHostPolicyEffect {
allowed: value.allowed,
startup: value.startup,
attestation: value.attestation,
trusted_public_keys: value
.trusted_public_keys
.map(|keys| keys.into_iter().map(|key| key.trim().to_owned()).collect()),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvaluatedDynamicPluginHostPolicy {
pub policy_satisfied: bool,
pub startup_class: DynamicPluginStartupClass,
pub attestation_mode: DynamicPluginAttestationMode,
pub trusted_public_keys: Vec<String>,
pub failure: Option<DynamicPluginHostPolicyFailure>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DynamicPluginHostPolicyFailure {
Blocked,
}
impl DynamicPluginHostPolicyFailure {
pub fn display(self, plugin_id: &str) -> String {
match self {
Self::Blocked => format!("dynamic plugin '{plugin_id}' is blocked by host policy"),
}
}
}
impl EvaluatedDynamicPluginHostPolicy {
pub fn check_state(&self) -> DynamicPluginCheckState {
if self.policy_satisfied {
DynamicPluginCheckState::Valid
} else {
DynamicPluginCheckState::Invalid
}
}
pub fn last_error(&self, plugin_id: &str) -> Option<DynamicPluginFailure> {
self.failure.map(|failure| DynamicPluginFailure {
phase: DynamicPluginFailurePhase::Policy,
code: "policy_blocked".into(),
message: failure.display(plugin_id),
})
}
pub fn failure(&self) -> Option<&DynamicPluginHostPolicyFailure> {
self.failure.as_ref()
}
}
pub fn evaluate_dynamic_plugin_host_policy(
policy: &DynamicPluginHostPolicy,
manifest: &DynamicPluginManifest,
) -> EvaluatedDynamicPluginHostPolicy {
let mut effect = DynamicPluginHostPolicyEffect {
allowed: Some(true),
startup: Some(DynamicPluginStartupClass::Optional),
attestation: Some(DynamicPluginAttestationMode::IntegrityOnly),
trusted_public_keys: None,
};
effect.merge_from(policy.defaults.clone());
for rule in &policy.rules {
if rule
.match_kind
.is_some_and(|kind| kind != manifest.plugin.kind)
|| rule
.match_plugin_id
.as_deref()
.is_some_and(|id| id != manifest.plugin.id.trim())
{
continue;
}
effect.merge_from(rule.effect.clone());
}
if let Some(effect_override) = policy.overrides.get(manifest.plugin.id.trim()) {
effect.merge_from(effect_override.clone());
}
let failure =
(effect.allowed == Some(false)).then_some(DynamicPluginHostPolicyFailure::Blocked);
EvaluatedDynamicPluginHostPolicy {
policy_satisfied: failure.is_none(),
startup_class: effect
.startup
.unwrap_or(DynamicPluginStartupClass::Optional),
attestation_mode: effect
.attestation
.unwrap_or(DynamicPluginAttestationMode::IntegrityOnly),
trusted_public_keys: effect.trusted_public_keys.unwrap_or_default(),
failure,
}
}
#[cfg(test)]
#[path = "../../../tests/unit/plugin_dynamic_policy_tests.rs"]
mod tests;