mirror of
https://github.com/block/buzz.git
synced 2026-11-09 19:02:00 +00:00
fix(workflows): execute immutable signed revisions
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -44,7 +44,9 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use buzz_core::kind::{event_kind_u32, is_workflow_execution_kind, KIND_REACTION};
|
||||
use buzz_core::kind::{
|
||||
event_kind_u32, is_workflow_execution_kind, KIND_REACTION, KIND_WORKFLOW_DEF,
|
||||
};
|
||||
use buzz_core::tenant::CommunityId;
|
||||
use buzz_db::workflow::RunStatus;
|
||||
use buzz_db::Db;
|
||||
@@ -122,6 +124,62 @@ impl WorkflowEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load and verify the exact owner-signed definition bound to a run.
|
||||
///
|
||||
/// The mutable `workflows` row supplies only immutable identity/channel
|
||||
/// binding. Definition content always comes from the run's signed event;
|
||||
/// legacy runs without a revision fail closed.
|
||||
pub async fn load_run_definition(
|
||||
&self,
|
||||
community_id: CommunityId,
|
||||
run_id: Uuid,
|
||||
) -> Result<(buzz_db::workflow::WorkflowRunRecord, WorkflowDef), WorkflowError> {
|
||||
let run = self.db.get_workflow_run(community_id, run_id).await?;
|
||||
let revision = run.definition_event_id.as_deref().ok_or_else(|| {
|
||||
WorkflowError::InvalidDefinition(
|
||||
"workflow run has no owner-signed definition revision".into(),
|
||||
)
|
||||
})?;
|
||||
let workflow = self.db.get_workflow(community_id, run.workflow_id).await?;
|
||||
let stored = self
|
||||
.db
|
||||
.get_event_by_id(community_id, revision)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
WorkflowError::InvalidDefinition(
|
||||
"workflow run definition event is unavailable".into(),
|
||||
)
|
||||
})?;
|
||||
let event = &stored.event;
|
||||
let workflow_id = run.workflow_id.to_string();
|
||||
let channel_id = workflow.channel_id.map(|id| id.to_string());
|
||||
let exact_tag = |name: &str| {
|
||||
let mut values = event.tags.iter().filter_map(|tag| {
|
||||
(tag.kind().to_string() == name)
|
||||
.then(|| tag.content())
|
||||
.flatten()
|
||||
});
|
||||
let value = values.next();
|
||||
value.filter(|_| values.next().is_none())
|
||||
};
|
||||
if event.id.as_bytes() != revision
|
||||
|| !event.verify_id()
|
||||
|| !event.verify_signature()
|
||||
|| event_kind_u32(event) != KIND_WORKFLOW_DEF
|
||||
|| event.pubkey.to_bytes().as_slice() != workflow.owner_pubkey
|
||||
|| exact_tag("d") != Some(workflow_id.as_str())
|
||||
|| channel_id.is_none()
|
||||
|| exact_tag("h") != channel_id.as_deref()
|
||||
|| stored.channel_id != workflow.channel_id
|
||||
{
|
||||
return Err(WorkflowError::InvalidDefinition(
|
||||
"workflow run definition event binding mismatch".into(),
|
||||
));
|
||||
}
|
||||
let (definition, _) = Self::parse_yaml(&event.content)?;
|
||||
Ok((run, definition))
|
||||
}
|
||||
|
||||
/// Drop the cached enabled-workflow list for a channel.
|
||||
///
|
||||
/// Must be called after any write to a workflow's trigger eligibility or
|
||||
@@ -407,6 +465,7 @@ impl WorkflowEngine {
|
||||
.create_workflow_run(
|
||||
community_id,
|
||||
workflow.id,
|
||||
workflow.definition_event_id.as_deref(),
|
||||
Some(&trigger_event_id_bytes),
|
||||
Some(&trigger_ctx_json),
|
||||
)
|
||||
@@ -426,13 +485,22 @@ impl WorkflowEngine {
|
||||
);
|
||||
|
||||
let engine = Arc::clone(self);
|
||||
let def_clone = def.clone();
|
||||
let ctx_clone = trigger_ctx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result =
|
||||
executor::execute_run(&engine, community_id, run_id, &def_clone, &ctx_clone)
|
||||
.await;
|
||||
let result = match engine.load_run_definition(community_id, run_id).await {
|
||||
Ok((_, definition)) => {
|
||||
executor::execute_run(
|
||||
&engine,
|
||||
community_id,
|
||||
run_id,
|
||||
&definition,
|
||||
&ctx_clone,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err((error, PartialProgress::default())),
|
||||
};
|
||||
engine
|
||||
.finalize_run(community_id, run_id, result, None)
|
||||
.await;
|
||||
@@ -669,6 +737,7 @@ impl WorkflowEngine {
|
||||
.create_workflow_run(
|
||||
community_id,
|
||||
workflow.id,
|
||||
workflow.definition_event_id.as_deref(),
|
||||
None, // no trigger event for cron
|
||||
trigger_ctx_json.as_ref(),
|
||||
)
|
||||
@@ -721,17 +790,21 @@ impl WorkflowEngine {
|
||||
);
|
||||
|
||||
let engine = Arc::clone(self);
|
||||
let def_clone = def.clone();
|
||||
let ctx_clone = trigger_ctx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = executor::execute_run(
|
||||
&engine,
|
||||
community_id,
|
||||
run_id,
|
||||
&def_clone,
|
||||
&ctx_clone,
|
||||
)
|
||||
.await;
|
||||
let result = match engine.load_run_definition(community_id, run_id).await {
|
||||
Ok((_, definition)) => {
|
||||
executor::execute_run(
|
||||
&engine,
|
||||
community_id,
|
||||
run_id,
|
||||
&definition,
|
||||
&ctx_clone,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err((error, PartialProgress::default())),
|
||||
};
|
||||
engine
|
||||
.finalize_run(community_id, run_id, result, None)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user