use std::collections::HashSet;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use cpex_core::cmf::{CmfHook, MessagePayload};
use cpex_core::hooks::payload::Extensions;
use cpex_core::hooks::HookPhase;
use cpex_core::manager::PluginManager;
use apl_core::attributes::AttributeBag;
use apl_core::evaluator::Decision;
use apl_core::pipeline::{TaintEvent, TaintScope};
use apl_core::step::{DispatchPhase, PluginError, PluginInvocation, PluginInvoker, PluginOutcome};
use crate::dispatch_plan::RouteDispatchPlan;
use crate::session_store::{SessionStore, SessionStoreError};
pub struct CmfPluginInvoker {
manager: Arc<PluginManager>,
extensions: Arc<Mutex<Extensions>>,
payload: Arc<Mutex<MessagePayload>>,
plan: Arc<RouteDispatchPlan>,
session_id: Option<String>,
session_store: Arc<dyn SessionStore>,
initial_labels: HashSet<String>,
}
impl CmfPluginInvoker {
pub async fn for_request(
manager: Arc<PluginManager>,
mut extensions: Extensions,
payload: MessagePayload,
plan: Arc<RouteDispatchPlan>,
session_store: Arc<dyn SessionStore>,
) -> Result<Self, SessionStoreError> {
let session_id: Option<String> =
crate::session_resolver::resolve_session(&extensions).map(|(sid, _src)| sid);
if let Some(sid) = &session_id {
let stored = session_store.load_labels(sid).await?;
if !stored.is_empty() {
extensions = hydrate_labels(extensions, &stored);
}
}
let initial_labels = snapshot_labels(&extensions);
Ok(Self {
manager,
extensions: Arc::new(Mutex::new(extensions)),
payload: Arc::new(Mutex::new(payload)),
plan,
session_id,
session_store,
initial_labels,
})
}
pub async fn current_payload(&self) -> MessagePayload {
self.payload.lock().await.clone()
}
pub async fn current_extensions(&self) -> Extensions {
self.extensions.lock().await.clone()
}
pub fn extensions_arc(&self) -> Arc<Mutex<Extensions>> {
Arc::clone(&self.extensions)
}
pub fn plan_arc(&self) -> Arc<RouteDispatchPlan> {
Arc::clone(&self.plan)
}
pub async fn apply_session_taints(&self, taints: &[apl_core::pipeline::TaintEvent]) {
use apl_core::pipeline::TaintScope;
use cpex_core::extensions::SecurityExtension;
let session_labels: Vec<&str> = taints
.iter()
.filter(|t| t.scopes.contains(&TaintScope::Session))
.map(|t| t.label.as_str())
.collect();
if session_labels.is_empty() {
return;
}
let mut current = self.extensions.lock().await;
let arc = current
.security
.get_or_insert_with(|| Arc::new(SecurityExtension::default()));
let sec = Arc::make_mut(arc);
for label in session_labels {
sec.add_label(label);
}
}
pub async fn persist_session(&self) -> Result<(), SessionStoreError> {
let Some(sid) = &self.session_id else {
return Ok(());
};
let current = self.extensions.lock().await;
let Some(security) = current.security.as_ref() else {
return Ok(());
};
let new_labels: Vec<String> = security
.labels
.iter()
.filter(|l| !self.initial_labels.contains(l.as_str()))
.cloned()
.collect();
drop(current); if !new_labels.is_empty() {
self.session_store.append_labels(sid, &new_labels).await?;
}
Ok(())
}
}
#[async_trait]
impl PluginInvoker for CmfPluginInvoker {
async fn invoke(
&self,
plugin_name: &str,
_bag: &AttributeBag,
invocation: PluginInvocation<'_>,
) -> Result<PluginOutcome, PluginError> {
let resolved = self
.plan
.get(plugin_name)
.ok_or_else(|| PluginError::NotFound(plugin_name.to_string()))?;
let request_entity_type: Option<String> = {
let ext = self.extensions.lock().await;
ext.meta.as_ref().and_then(|m| m.entity_type.clone())
};
let dispatch_phase = match invocation.phase() {
DispatchPhase::Pre => HookPhase::Pre,
DispatchPhase::Post => HookPhase::Post,
};
let entry = resolved
.pick_entry(request_entity_type.as_deref(), dispatch_phase)
.ok_or_else(|| {
PluginError::Dispatch(format!(
"plugin '{plugin_name}' has no hook matching dispatch \
context (entity_type={:?}, phase={:?}); declared hooks: {:?}",
request_entity_type,
dispatch_phase,
resolved.entries_by_hook.keys().collect::<Vec<_>>(),
))
})?;
let current_payload = self.payload.lock().await.clone();
let current_extensions = self.extensions.lock().await.clone();
let before_labels = snapshot_labels(¤t_extensions);
let (result, _bg) = self
.manager
.invoke_entries::<CmfHook>(
std::slice::from_ref(entry),
current_payload,
current_extensions,
None,
)
.await;
let decision = if result.is_denied() {
let (reason, rule_source) = match result.violation {
Some(v) => (Some(v.reason), v.code),
None => (None, "policy.forbidden".to_string()),
};
Decision::Deny {
reason,
rule_source,
}
} else {
Decision::Allow
};
let modified_value = if let Some(mp_boxed) = result.modified_payload.as_ref() {
match mp_boxed.as_any().downcast_ref::<MessagePayload>() {
Some(modified) => {
*self.payload.lock().await = modified.clone();
match invocation {
PluginInvocation::Field { .. } => Some(serde_json::Value::String(
modified.message.get_text_content(),
)),
PluginInvocation::Step { .. } => None,
}
},
None => {
tracing::warn!(
plugin = %plugin_name,
"CmfPluginInvoker: modified_payload was not MessagePayload \
(downcast failed) — dropping the mutation"
);
None
},
}
} else {
None
};
let taints = if let Some(modified_ext) = result.modified_extensions {
let after_labels = snapshot_labels(&modified_ext);
let new_labels: Vec<String> =
after_labels.difference(&before_labels).cloned().collect();
*self.extensions.lock().await = modified_ext;
new_labels
.into_iter()
.map(|label| TaintEvent {
label,
scopes: vec![TaintScope::Session],
})
.collect()
} else {
Vec::new()
};
Ok(PluginOutcome {
decision,
taints,
modified_value,
})
}
}
fn snapshot_labels(extensions: &Extensions) -> HashSet<String> {
extensions
.security
.as_ref()
.map(|s| s.labels.iter().cloned().collect())
.unwrap_or_default()
}
fn hydrate_labels(mut extensions: Extensions, labels: &[String]) -> Extensions {
let mut security = extensions
.security
.as_ref()
.map(|s| (**s).clone())
.unwrap_or_default();
for l in labels {
security.add_label(l.clone());
}
extensions.security = Some(Arc::new(security));
extensions
}