use serde::{Deserialize, Serialize};
use crate::{UseError, UseResult};
use super::host::{validate_request_identity, verify_capabilities, verify_supported_plan_schema};
use super::validation::valid_sha256;
use super::{
canonical_digest, canonical_json, contract_error, parse_contract, PlanPolicyDecision,
PluginHostCapabilities, PluginHostEnablementPlanResult, PluginHostPackageState,
PluginHostPlanResult, PluginManagedScope, PluginOperationConfirmation, PluginOperationPlan,
PluginPackageId,
};
pub const PLUGIN_HOST_APPLY_REQUEST_SCHEMA: &str = "a3s.use.plugin-host-apply-request.v1";
pub const PLUGIN_HOST_APPLY_RESULT_SCHEMA: &str = "a3s.use.plugin-host-apply-result.v1";
const APPLY_REQUEST_ERROR: &str = "use.plugin.host_apply_request_invalid";
const APPLY_RESULT_ERROR: &str = "use.plugin.host_apply_result_invalid";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PluginHostApplyRequest {
pub schema: String,
pub request_id: String,
pub assignment_generation: u64,
pub capabilities_digest: String,
pub scope: PluginManagedScope,
pub package_id: PluginPackageId,
pub operation_id: String,
pub plan_digest: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub confirmation: Option<PluginOperationConfirmation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PluginHostApplyResult {
pub schema: String,
pub request_id: String,
pub assignment_generation: u64,
pub capabilities_digest: String,
pub scope: PluginManagedScope,
pub package_id: PluginPackageId,
pub operation_id: String,
pub plan_digest: String,
pub completed_at_ms: u64,
pub operation_result_digest: String,
pub state: PluginHostPackageState,
pub replayed: bool,
}
impl PluginHostApplyRequest {
pub fn from_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"plugin host apply request",
APPLY_REQUEST_ERROR,
Self::validate,
)
}
pub fn validate(&self) -> UseResult<()> {
if self.schema != PLUGIN_HOST_APPLY_REQUEST_SCHEMA {
return Err(apply_request_error(
"The plugin host apply request schema is unsupported.",
));
}
validate_request_identity(
&self.request_id,
self.assignment_generation,
&self.capabilities_digest,
&self.scope,
)
.map_err(|_| {
apply_request_error("The plugin host apply request identity or scope is invalid.")
})?;
PluginOperationPlan::validate_operation_id(&self.operation_id).map_err(|_| {
apply_request_error("The plugin host apply operation identity is invalid.")
})?;
if !valid_sha256(&self.plan_digest) {
return Err(apply_request_error(
"The plugin host apply request plan digest is invalid.",
));
}
if let Some(confirmation) = &self.confirmation {
confirmation.validate().map_err(|_| {
apply_request_error("The plugin host apply confirmation is invalid.")
})?;
if confirmation.operation_id != self.operation_id
|| confirmation.plan_digest != self.plan_digest
{
return Err(apply_request_error(
"The confirmation does not bind the requested operation and plan digest.",
));
}
}
Ok(())
}
pub fn validate_for_plan(
&self,
plan: &PluginHostPlanResult,
capabilities: &PluginHostCapabilities,
) -> UseResult<()> {
self.validate_for_capabilities(capabilities)?;
plan.validate()?;
verify_supported_plan_schema(capabilities, &plan.plan.plan.schema)?;
if self.assignment_generation != plan.assignment_generation
|| self.capabilities_digest != plan.capabilities_digest
|| self.scope != plan.scope
|| self.package_id != plan.package_id
|| self.operation_id != plan.plan.plan.operation_id
|| self.plan_digest != plan.plan.plan_digest
{
return Err(UseError::new(
"use.plugin.host_apply_request_mismatch",
"The plugin host apply request does not bind the exact reviewed plan.",
));
}
match (plan.plan.plan.authority.decision, &self.confirmation) {
(PlanPolicyDecision::Allow, None) | (PlanPolicyDecision::Ask, Some(_)) => Ok(()),
(PlanPolicyDecision::Deny, _) => Err(UseError::new(
"use.plugin.plan_denied",
"Policy denies applying the plugin operation plan.",
)),
_ => Err(UseError::new(
"use.plugin.plan_confirmation_mismatch",
"The apply request confirmation does not match the plan's policy decision.",
)),
}
}
pub fn validate_for_capabilities(
&self,
capabilities: &PluginHostCapabilities,
) -> UseResult<()> {
self.validate()?;
verify_capabilities(&self.capabilities_digest, &self.scope, capabilities)
}
pub fn verify_apply_for_plan(
&self,
plan: &PluginHostPlanResult,
capabilities: &PluginHostCapabilities,
now_ms: u64,
) -> UseResult<()> {
self.validate_for_plan(plan, capabilities)?;
plan.plan.verify_confirmed_apply(
&self.operation_id,
&self.plan_digest,
self.confirmation.as_ref(),
now_ms,
)
}
pub fn verify_admitted_replay_for_plan(
&self,
plan: &PluginHostPlanResult,
capabilities: &PluginHostCapabilities,
) -> UseResult<()> {
let validation_time = self
.confirmation
.as_ref()
.map_or(plan.plan.plan.created_at_ms, |value| value.confirmed_at_ms);
self.verify_apply_for_plan(plan, capabilities, validation_time)
}
pub fn validate_for_enablement_plan(
&self,
plan: &PluginHostEnablementPlanResult,
capabilities: &PluginHostCapabilities,
) -> UseResult<()> {
plan.validate_for_capabilities(capabilities)?;
self.validate_for_plan(&plan.reviewed_plan()?, capabilities)
}
pub fn verify_apply_for_enablement_plan(
&self,
plan: &PluginHostEnablementPlanResult,
capabilities: &PluginHostCapabilities,
now_ms: u64,
) -> UseResult<()> {
plan.validate_for_capabilities(capabilities)?;
self.verify_apply_for_plan(&plan.reviewed_plan()?, capabilities, now_ms)
}
pub fn verify_admitted_replay_for_enablement_plan(
&self,
plan: &PluginHostEnablementPlanResult,
capabilities: &PluginHostCapabilities,
) -> UseResult<()> {
plan.validate_for_capabilities(capabilities)?;
self.verify_admitted_replay_for_plan(&plan.reviewed_plan()?, capabilities)
}
pub fn canonical_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
canonical_json(self, "plugin host apply request", APPLY_REQUEST_ERROR)
}
pub fn descriptor_digest(&self) -> UseResult<String> {
Ok(canonical_digest(&self.canonical_bytes()?))
}
}
impl PluginHostApplyResult {
pub fn from_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"plugin host apply result",
APPLY_RESULT_ERROR,
Self::validate,
)
}
pub fn validate(&self) -> UseResult<()> {
if self.schema != PLUGIN_HOST_APPLY_RESULT_SCHEMA
|| self.completed_at_ms == 0
|| !valid_sha256(&self.operation_result_digest)
|| !valid_sha256(&self.plan_digest)
{
return Err(apply_result_error(
"The plugin host apply result schema, time, or digest is invalid.",
));
}
validate_request_identity(
&self.request_id,
self.assignment_generation,
&self.capabilities_digest,
&self.scope,
)
.map_err(|_| {
apply_result_error("The plugin host apply result identity or scope is invalid.")
})?;
PluginOperationPlan::validate_operation_id(&self.operation_id).map_err(|_| {
apply_result_error("The plugin host apply result operation identity is invalid.")
})?;
self.state
.validate()
.map_err(|_| apply_result_error("The applied plugin state is invalid."))
}
pub fn validate_for(
&self,
request: &PluginHostApplyRequest,
capabilities: &PluginHostCapabilities,
) -> UseResult<()> {
self.validate()?;
request.validate()?;
verify_capabilities(&self.capabilities_digest, &self.scope, capabilities)?;
if self.request_id != request.request_id
|| self.assignment_generation != request.assignment_generation
|| self.capabilities_digest != request.capabilities_digest
|| self.scope != request.scope
|| self.package_id != request.package_id
|| self.operation_id != request.operation_id
|| self.plan_digest != request.plan_digest
{
return Err(UseError::new(
"use.plugin.host_apply_result_mismatch",
"The plugin host apply result does not bind the exact request.",
));
}
Ok(())
}
pub fn canonical_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
canonical_json(self, "plugin host apply result", APPLY_RESULT_ERROR)
}
pub fn descriptor_digest(&self) -> UseResult<String> {
Ok(canonical_digest(&self.canonical_bytes()?))
}
}
fn apply_request_error(message: impl Into<String>) -> UseError {
contract_error(APPLY_REQUEST_ERROR, message)
}
fn apply_result_error(message: impl Into<String>) -> UseError {
contract_error(APPLY_RESULT_ERROR, message)
}