use af_context::{SubjectId, WorkflowResourceId, WorkflowSourceId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkflowSourceGrant {
pub source: WorkflowSourceId,
pub event_type: String,
pub predicate: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkflowAdmissionPolicy {
pub enabled: bool,
pub max_active_instances: u32,
pub sources: Vec<WorkflowSourceGrant>,
pub resources: BTreeSet<WorkflowResourceId>,
pub providers: BTreeSet<String>,
pub modes: Vec<crate::ExecutionMode>,
}
impl WorkflowAdmissionPolicy {
pub fn validate(&self) -> Result<(), String> {
if self.sources.len() > 64
|| self.resources.len() > 256
|| self.providers.len() > 256
|| self.modes.len() > 4
{
return Err("workflow admission exceeds grant collection bounds".into());
}
if self
.sources
.iter()
.any(|grant| grant.event_type.trim().is_empty() || !grant.predicate.is_object())
|| self
.providers
.iter()
.any(|provider| provider.trim().is_empty())
{
return Err(
"workflow admission requires exact providers and source/object restrictions".into(),
);
}
if serde_json::to_vec(self)
.map_err(|error| error.to_string())?
.len()
> 65_536
{
return Err("workflow admission exceeds 64 KiB".into());
}
Ok(())
}
pub fn authorize_profile(
&self,
context: &af_context::RequestContext,
profile: &crate::ExecutionProfileRevision,
) -> Result<(), String> {
if !self.enabled
|| !self.modes.contains(&profile.mode)
|| [
&profile.trigger_provider,
&profile.data_provider,
&profile.action_provider,
]
.iter()
.any(|provider| !self.providers.contains(*provider))
|| profile.connection_bindings.values().any(|resource| {
!self
.resources
.iter()
.any(|allowed| allowed.as_str() == resource)
})
{
return Err("workflow profile exceeds current mode/provider/resource admission".into());
}
if let Some(permissions) = profile.policy_bundle.get("permissions") {
let permissions = permissions
.as_array()
.ok_or("workflow profile permissions must be an array")?;
if permissions.iter().any(|permission| {
permission.as_str().is_none_or(|permission| {
!context.roles.contains(permission)
&& !context.entitlements.contains(permission)
})
}) {
return Err("workflow profile exceeds current subject permissions".into());
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkflowAdmissionRecord {
pub subject_id: SubjectId,
pub version: u64,
pub policy: WorkflowAdmissionPolicy,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deployment_grants_bound_config_and_never_add_subject_permissions() {
let policy = WorkflowAdmissionPolicy {
enabled: true,
max_active_instances: 1,
sources: vec![WorkflowSourceGrant {
source: "source".parse().unwrap(),
event_type: "event".into(),
predicate: serde_json::json!({"owner":"self"}),
}],
resources: ["resource".parse().unwrap()].into(),
providers: ["provider".into()].into(),
modes: vec![crate::ExecutionMode::Paper],
};
let context = af_context::RequestContext {
tenant_id: "tenant".parse().unwrap(),
subject_id: "owner".parse().unwrap(),
request_id: "request".parse().unwrap(),
locale: "en".into(),
roles: ["allowed".into()].into(),
entitlements: Default::default(),
};
let profile:crate::ExecutionProfileRevision=serde_json::from_value(serde_json::json!({"id":"profile","revision":1,"content_digest":"digest","mode":"paper","durability_grade":"standard","trigger_provider":"provider","data_provider":"provider","action_provider":"provider","clock_model":"database","connection_bindings":{"primary":"resource"},"policy_bundle":{"permissions":["allowed"]}})).unwrap();
assert!(policy.validate().is_ok());
assert!(policy.authorize_profile(&context, &profile).is_ok());
let mut changed = policy.clone();
changed.enabled = false;
assert!(changed.authorize_profile(&context, &profile).is_err());
for field in [
"mode",
"action_provider",
"connection_bindings",
"policy_bundle",
] {
let mut changed = serde_json::to_value(&profile).unwrap();
changed[field] = match field {
"mode" => "live".into(),
"action_provider" => "other".into(),
"connection_bindings" => serde_json::json!({"primary":"other"}),
_ => serde_json::json!({"permissions":["admin"]}),
};
assert!(policy
.authorize_profile(&context, &serde_json::from_value(changed).unwrap())
.is_err());
}
for permissions in [serde_json::json!("allowed"), serde_json::json!([null])] {
let mut changed = profile.clone();
changed.policy_bundle = serde_json::json!({"permissions":permissions});
assert!(policy.authorize_profile(&context, &changed).is_err());
}
let mut changed = profile;
changed.policy_bundle = Value::Null;
assert!(policy.authorize_profile(&context, &changed).is_ok());
for variant in 0..8 {
let mut changed = policy.clone();
match variant {
0 => changed.sources = vec![policy.sources[0].clone(); 65],
1 => changed.resources = (0..257).map(|i| i.to_string().parse().unwrap()).collect(),
2 => changed.providers = (0..257).map(|i| i.to_string()).collect(),
3 => changed.modes = vec![crate::ExecutionMode::Paper; 5],
4 => changed.sources[0].event_type = " ".into(),
5 => changed.sources[0].predicate = Value::Null,
6 => {
changed.providers.insert(" ".into());
}
_ => changed.sources[0].predicate = serde_json::json!({"large":"x".repeat(65536)}),
}
assert!(changed.validate().is_err(), "case {variant}");
}
}
}