use std::sync::Arc;
use async_trait::async_trait;
use chrono::SecondsFormat;
use tokio::sync::Mutex;
use cpex_core::delegation::{
payload::{AuthEnforcedBy, TargetType},
DelegationPayload, TokenDelegateHook,
};
use cpex_core::extensions::raw_credentials::TokenRole;
use cpex_core::hooks::payload::Extensions;
use cpex_core::manager::PluginManager;
use apl_core::evaluator::Decision;
use apl_core::step::{DelegateStep, DelegationError, DelegationInvoker, DelegationOutcome};
use crate::dispatch_plan::RouteDispatchPlan;
pub struct DelegationPluginInvoker {
manager: Arc<PluginManager>,
extensions: Arc<Mutex<Extensions>>,
plan: Arc<RouteDispatchPlan>,
}
impl DelegationPluginInvoker {
pub fn new(
manager: Arc<PluginManager>,
extensions: Arc<Mutex<Extensions>>,
plan: Arc<RouteDispatchPlan>,
) -> Self {
Self {
manager,
extensions,
plan,
}
}
}
#[async_trait]
impl DelegationInvoker for DelegationPluginInvoker {
async fn delegate(&self, step: &DelegateStep) -> Result<DelegationOutcome, DelegationError> {
let entry = self
.plan
.delegation_entries
.get(&step.plugin_name)
.ok_or_else(|| DelegationError::NotFound(step.plugin_name.clone()))?
.clone();
let current_extensions = self.extensions.lock().await.clone();
let bearer_token = current_extensions
.raw_credentials
.as_ref()
.and_then(|rc| rc.inbound_tokens.get(&TokenRole::User))
.map(|tok| (*tok.token).clone())
.unwrap_or_default();
let cfg = step.config_override.as_ref().and_then(|v| v.as_mapping());
let target_name: String = cfg
.and_then(|m| m.get(serde_yaml::Value::String("target".into())))
.and_then(|v| v.as_str())
.unwrap_or(&step.plugin_name)
.to_string();
let mut payload = DelegationPayload::new(bearer_token, target_name);
if let Some(audience) = cfg
.and_then(|m| m.get(serde_yaml::Value::String("audience".into())))
.and_then(|v| v.as_str())
{
payload = payload.with_target_audience(audience);
}
if let Some(perms) = cfg
.and_then(|m| m.get(serde_yaml::Value::String("permissions".into())))
.and_then(|v| v.as_sequence())
{
let list: Vec<String> = perms
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect();
if !list.is_empty() {
payload = payload.with_required_permissions(list);
}
}
if let Some(t_kind) = cfg
.and_then(|m| m.get(serde_yaml::Value::String("target_type".into())))
.and_then(|v| v.as_str())
{
payload = payload.with_target_type(target_type_from_str(t_kind));
}
if let Some(enforcer) = cfg
.and_then(|m| m.get(serde_yaml::Value::String("auth_enforced_by".into())))
.and_then(|v| v.as_str())
{
payload = payload.with_auth_enforced_by(auth_enforced_by_from_str(enforcer));
}
let (result, _bg) = self
.manager
.invoke_entries::<TokenDelegateHook>(
std::slice::from_ref(&entry),
payload,
current_extensions,
None,
)
.await;
if !result.continue_processing {
let decision = match result.violation {
Some(v) => Decision::Deny {
reason: Some(v.reason),
rule_source: v.code,
},
None => Decision::Deny {
reason: Some(format!(
"delegate `{}` denied without violation detail",
step.plugin_name
)),
rule_source: step.source.clone(),
},
};
return Ok(DelegationOutcome::deny(decision));
}
let resolved = DelegationPayload::from_pipeline_result(&result).ok_or_else(|| {
DelegationError::Dispatch(format!(
"plugin `{}` returned allow but no DelegationPayload",
step.plugin_name,
))
})?;
{
let mut ext_lock = self.extensions.lock().await;
let merged = resolved.clone().apply_to_extensions(ext_lock.clone());
*ext_lock = merged;
}
let (granted_permissions, granted_audience, granted_expires_at) =
match resolved.delegated_token {
Some(tok) => (
tok.scopes,
Some(tok.audience),
Some(tok.expires_at.to_rfc3339_opts(SecondsFormat::Secs, true)),
),
None => (Vec::new(), None, None),
};
Ok(DelegationOutcome {
decision: Decision::Allow,
granted_permissions,
granted_audience,
granted_expires_at,
})
}
}
fn target_type_from_str(s: &str) -> TargetType {
match s.to_ascii_lowercase().as_str() {
"tool" => TargetType::Tool,
"agent" => TargetType::Agent,
"resource" => TargetType::Resource,
"service" => TargetType::Service,
other => TargetType::Custom(other.to_string()),
}
}
fn auth_enforced_by_from_str(s: &str) -> AuthEnforcedBy {
match s.to_ascii_lowercase().as_str() {
"caller" => AuthEnforcedBy::Caller,
"target" => AuthEnforcedBy::Target,
_ => AuthEnforcedBy::Caller,
}
}