use nucleus_substrate_core::Projection;
use serde::{Deserialize, Serialize};
pub const FLOW_BODY_VERSION: u32 = 1;
pub const CONF_LEVELS: &[&str] = &["public", "internal", "secret"];
pub const INTEG_LEVELS: &[&str] = &["adversarial", "untrusted", "trusted"];
pub const AUTHORITY_LEVELS: &[&str] = &[
"no_authority",
"informational",
"suggestive",
"directive",
];
pub const TAINT_LEVELS: &[&str] = &["clean", "user_derived", "ai_generated"];
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FlowBody {
pub version: u32,
pub node_count: usize,
pub session_confidentiality_ceiling: String,
pub session_integrity_ceiling: String,
pub session_authority_ceiling: String,
pub session_taint_ceiling: String,
pub has_adversarial_bid: bool,
pub has_ai_derived: bool,
pub has_confidential_data: bool,
}
#[allow(clippy::too_many_arguments)]
pub fn flow_projection(
node_count: usize,
session_confidentiality_ceiling: impl Into<String>,
session_integrity_ceiling: impl Into<String>,
session_authority_ceiling: impl Into<String>,
session_taint_ceiling: impl Into<String>,
has_adversarial_bid: bool,
has_ai_derived: bool,
has_confidential_data: bool,
) -> Projection {
let body = FlowBody {
version: FLOW_BODY_VERSION,
node_count,
session_confidentiality_ceiling: session_confidentiality_ceiling.into(),
session_integrity_ceiling: session_integrity_ceiling.into(),
session_authority_ceiling: session_authority_ceiling.into(),
session_taint_ceiling: session_taint_ceiling.into(),
has_adversarial_bid,
has_ai_derived,
has_confidential_data,
};
Projection::Flow(serde_json::to_value(body).expect("FlowBody serializes"))
}
pub fn verify_flow_projection_shape(body: &FlowBody) -> Result<(), FlowVerifyError> {
if body.version != FLOW_BODY_VERSION {
return Err(FlowVerifyError::UnsupportedBodyVersion(body.version));
}
if !CONF_LEVELS.contains(&body.session_confidentiality_ceiling.as_str()) {
return Err(FlowVerifyError::UnknownLevel {
axis: "confidentiality",
value: body.session_confidentiality_ceiling.clone(),
});
}
if !INTEG_LEVELS.contains(&body.session_integrity_ceiling.as_str()) {
return Err(FlowVerifyError::UnknownLevel {
axis: "integrity",
value: body.session_integrity_ceiling.clone(),
});
}
if !AUTHORITY_LEVELS.contains(&body.session_authority_ceiling.as_str()) {
return Err(FlowVerifyError::UnknownLevel {
axis: "authority",
value: body.session_authority_ceiling.clone(),
});
}
if !TAINT_LEVELS.contains(&body.session_taint_ceiling.as_str()) {
return Err(FlowVerifyError::UnknownLevel {
axis: "taint",
value: body.session_taint_ceiling.clone(),
});
}
if body.has_adversarial_bid && body.session_integrity_ceiling != "adversarial" {
return Err(FlowVerifyError::CeilingInconsistent {
flag: "has_adversarial_bid",
level: "integrity",
actual: body.session_integrity_ceiling.clone(),
});
}
if body.session_integrity_ceiling == "trusted" && body.has_adversarial_bid {
return Err(FlowVerifyError::CeilingInconsistent {
flag: "has_adversarial_bid",
level: "integrity",
actual: body.session_integrity_ceiling.clone(),
});
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum FlowVerifyError {
#[error("flow body version {0} not supported by this lifter")]
UnsupportedBodyVersion(u32),
#[error("unknown {axis} level: {value}")]
UnknownLevel {
axis: &'static str,
value: String,
},
#[error("flag `{flag}` is inconsistent with {level} ceiling = {actual}")]
CeilingInconsistent {
flag: &'static str,
level: &'static str,
actual: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn happy_body() -> FlowBody {
FlowBody {
version: FLOW_BODY_VERSION,
node_count: 4,
session_confidentiality_ceiling: "internal".into(),
session_integrity_ceiling: "trusted".into(),
session_authority_ceiling: "informational".into(),
session_taint_ceiling: "user_derived".into(),
has_adversarial_bid: false,
has_ai_derived: false,
has_confidential_data: true,
}
}
#[test]
fn happy_body_verifies() {
let body = happy_body();
verify_flow_projection_shape(&body).expect("happy body verifies");
}
#[test]
fn unknown_integrity_level_rejected() {
let mut body = happy_body();
body.session_integrity_ceiling = "questionable".into();
let err = verify_flow_projection_shape(&body).unwrap_err();
assert!(matches!(err, FlowVerifyError::UnknownLevel { axis: "integrity", .. }));
}
#[test]
fn adversarial_flag_with_trusted_ceiling_rejected() {
let mut body = happy_body();
body.has_adversarial_bid = true;
let err = verify_flow_projection_shape(&body).unwrap_err();
assert!(matches!(
err,
FlowVerifyError::CeilingInconsistent {
flag: "has_adversarial_bid",
..
}
));
}
#[test]
fn adversarial_flag_with_adversarial_ceiling_accepted() {
let mut body = happy_body();
body.has_adversarial_bid = true;
body.session_integrity_ceiling = "adversarial".into();
verify_flow_projection_shape(&body).expect("g8 path");
}
#[test]
fn body_version_mismatch_rejected() {
let mut body = happy_body();
body.version = 99;
let err = verify_flow_projection_shape(&body).unwrap_err();
assert!(matches!(err, FlowVerifyError::UnsupportedBodyVersion(99)));
}
#[test]
fn flow_projection_helper_packs_correct_wire_shape() {
let projection = flow_projection(
7,
"internal",
"adversarial",
"informational",
"ai_generated",
true,
true,
true,
);
assert_eq!(projection.kind(), "flow");
let v = serde_json::to_value(&projection).unwrap();
assert_eq!(v["kind"], "flow");
assert_eq!(v["body"]["node_count"], 7);
assert_eq!(v["body"]["session_integrity_ceiling"], "adversarial");
assert_eq!(v["body"]["has_adversarial_bid"], true);
assert_eq!(v["body"]["has_ai_derived"], true);
assert_eq!(v["body"]["version"], FLOW_BODY_VERSION);
}
}