use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{CellosError, ExecutionCellSpec, PlacementSpec, SecretDeliveryMode};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PolicyPackDocument {
pub api_version: String,
pub kind: String,
pub spec: PolicyPackSpec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PolicyPackSpec {
pub id: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placement: Option<PlacementSpec>,
pub rules: PolicyRules,
}
pub const MIN_SUPPORTED_POLICY_PACK_VERSION: &str = "1.0.0";
pub const POLICY_ALLOW_DOWNGRADE_ENV: &str = "CELLOS_POLICY_ALLOW_DOWNGRADE";
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PolicyRules {
#[serde(default)]
pub max_lifetime_ttl_seconds: Option<u64>,
#[serde(default)]
pub max_memory_max_bytes: Option<u64>,
#[serde(default)]
pub max_run_timeout_ms: Option<u64>,
#[serde(default)]
pub require_egress_declared: bool,
#[serde(default)]
pub forbid_outbound_egress_rules: bool,
#[serde(default)]
pub allowed_egress_hosts: Vec<String>,
#[serde(default)]
pub require_runtime_secret_delivery: bool,
#[serde(default)]
pub require_resource_limits: bool,
#[serde(default)]
pub flag_dns_egress_without_acknowledgment: Option<bool>,
#[serde(default, rename = "requireDnsEgressJustification")]
pub require_dns_egress_justification: Option<bool>,
#[serde(default, rename = "secretRefAllowlist")]
pub secret_ref_allowlist: Option<HashMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyViolation {
pub rule: String,
pub message: String,
}
impl std::fmt::Display for PolicyViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.rule, self.message)
}
}
fn parse_semver_triple(value: &str) -> Result<(u64, u64, u64), CellosError> {
let core = match value.split_once('-') {
Some((core, pre)) => {
if pre.is_empty()
|| !pre
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-'))
{
return Err(CellosError::InvalidSpec(format!(
"policy pack spec.version {value:?} has malformed pre-release suffix"
)));
}
core
}
None => value,
};
let parts: Vec<&str> = core.split('.').collect();
if parts.len() != 3 {
return Err(CellosError::InvalidSpec(format!(
"policy pack spec.version {value:?} must be a MAJOR.MINOR.PATCH semver string"
)));
}
let mut triple = [0u64; 3];
for (i, p) in parts.iter().enumerate() {
if p.is_empty() || !p.chars().all(|c| c.is_ascii_digit()) {
return Err(CellosError::InvalidSpec(format!(
"policy pack spec.version {value:?} component {p:?} is not a non-negative integer"
)));
}
if p.len() > 1 && p.starts_with('0') {
return Err(CellosError::InvalidSpec(format!(
"policy pack spec.version {value:?} component {p:?} has a leading zero"
)));
}
triple[i] = p.parse::<u64>().map_err(|_| {
CellosError::InvalidSpec(format!(
"policy pack spec.version {value:?} component {p:?} overflows u64"
))
})?;
}
Ok((triple[0], triple[1], triple[2]))
}
pub fn check_policy_pack_version_compatibility(
declared: Option<&str>,
allow_downgrade: bool,
) -> Result<(), CellosError> {
let declared_triple = match declared {
Some(v) => parse_semver_triple(v)?,
None => return Ok(()),
};
let floor_triple = parse_semver_triple(MIN_SUPPORTED_POLICY_PACK_VERSION)
.expect("MIN_SUPPORTED_POLICY_PACK_VERSION must parse");
if declared_triple < floor_triple {
if allow_downgrade {
return Ok(());
}
return Err(CellosError::InvalidSpec(format!(
"policy pack spec.version {} is older than runtime-supported floor {} \
(set {}=1 to override)",
declared.unwrap_or(""),
MIN_SUPPORTED_POLICY_PACK_VERSION,
POLICY_ALLOW_DOWNGRADE_ENV
)));
}
Ok(())
}
pub fn validate_policy_pack_document(doc: &PolicyPackDocument) -> Result<(), CellosError> {
if doc.api_version != "cellos.io/v1" {
return Err(CellosError::InvalidSpec(format!(
"policy pack apiVersion must be \"cellos.io/v1\", got {:?}",
doc.api_version
)));
}
if doc.kind != "PolicyPack" {
return Err(CellosError::InvalidSpec(format!(
"policy pack kind must be \"PolicyPack\", got {:?}",
doc.kind
)));
}
if !crate::spec_validation::is_portable_identifier(&doc.spec.id) {
return Err(CellosError::InvalidSpec(format!(
"policy pack spec.id {:?} is not a valid portable identifier",
doc.spec.id
)));
}
check_policy_pack_version_compatibility(doc.spec.version.as_deref(), false)?;
let rules = &doc.spec.rules;
if let Some(v) = rules.max_lifetime_ttl_seconds {
if v == 0 {
return Err(CellosError::InvalidSpec(
"policy pack rules.maxLifetimeTtlSeconds must be > 0".into(),
));
}
}
if let Some(v) = rules.max_memory_max_bytes {
if v == 0 {
return Err(CellosError::InvalidSpec(
"policy pack rules.maxMemoryMaxBytes must be > 0".into(),
));
}
}
if let Some(v) = rules.max_run_timeout_ms {
if v == 0 {
return Err(CellosError::InvalidSpec(
"policy pack rules.maxRunTimeoutMs must be > 0".into(),
));
}
}
if rules.require_egress_declared && rules.forbid_outbound_egress_rules {
return Err(CellosError::InvalidSpec(
"policy pack rules.requireEgressDeclared and rules.forbidOutboundEgressRules \
are mutually exclusive"
.into(),
));
}
for pattern in &rules.allowed_egress_hosts {
if pattern.is_empty() {
return Err(CellosError::InvalidSpec(
"policy pack rules.allowedEgressHosts contains an empty pattern".into(),
));
}
}
Ok(())
}
pub fn spec_matches_placement_scope(
spec_placement: Option<&PlacementSpec>,
scope: &PlacementSpec,
) -> bool {
let scope_has_any = scope.pool_id.is_some()
|| scope.kubernetes_namespace.is_some()
|| scope.queue_name.is_some();
if !scope_has_any {
return true;
}
let Some(spec_placement) = spec_placement else {
return false;
};
if let Some(pool) = scope.pool_id.as_deref() {
if spec_placement.pool_id.as_deref() != Some(pool) {
return false;
}
}
if let Some(ns) = scope.kubernetes_namespace.as_deref() {
if spec_placement.kubernetes_namespace.as_deref() != Some(ns) {
return false;
}
}
if let Some(queue) = scope.queue_name.as_deref() {
if spec_placement.queue_name.as_deref() != Some(queue) {
return false;
}
}
true
}
pub fn validate_spec_against_policy(
spec: &ExecutionCellSpec,
pack: &PolicyPackSpec,
) -> Vec<PolicyViolation> {
let mut violations = Vec::new();
if let Some(scope) = &pack.placement {
if !spec_matches_placement_scope(spec.placement.as_ref(), scope) {
return violations;
}
}
let rules = &pack.rules;
if let Some(max) = rules.max_lifetime_ttl_seconds {
if spec.lifetime.ttl_seconds > max {
violations.push(PolicyViolation {
rule: "maxLifetimeTtlSeconds".into(),
message: format!(
"spec.lifetime.ttlSeconds {} exceeds policy maximum {}",
spec.lifetime.ttl_seconds, max
),
});
}
}
if let Some(max) = rules.max_memory_max_bytes {
let actual = spec
.run
.as_ref()
.and_then(|r| r.limits.as_ref())
.and_then(|l| l.memory_max_bytes);
if let Some(actual) = actual {
if actual > max {
violations.push(PolicyViolation {
rule: "maxMemoryMaxBytes".into(),
message: format!(
"spec.run.limits.memoryMaxBytes {actual} exceeds policy maximum {max}"
),
});
}
}
}
if let Some(max) = rules.max_run_timeout_ms {
let actual = spec.run.as_ref().and_then(|r| r.timeout_ms);
if let Some(actual) = actual {
if actual > max {
violations.push(PolicyViolation {
rule: "maxRunTimeoutMs".into(),
message: format!("spec.run.timeoutMs {actual} exceeds policy maximum {max}"),
});
}
}
}
let egress_rules = spec.authority.egress_rules.as_deref().unwrap_or_default();
if rules.require_egress_declared && egress_rules.is_empty() {
violations.push(PolicyViolation {
rule: "requireEgressDeclared".into(),
message: "policy requires spec.authority.egressRules to be non-empty".into(),
});
}
if rules.forbid_outbound_egress_rules && !egress_rules.is_empty() {
violations.push(PolicyViolation {
rule: "forbidOutboundEgressRules".into(),
message: format!(
"policy forbids outbound egress rules but spec declares {} rule(s)",
egress_rules.len()
),
});
}
if !rules.allowed_egress_hosts.is_empty() {
for rule in egress_rules {
if !rules
.allowed_egress_hosts
.iter()
.any(|pat| host_matches_pattern(&rule.host, pat))
{
violations.push(PolicyViolation {
rule: "allowedEgressHosts".into(),
message: format!(
"egress host {:?} does not match any allowed pattern in {:?}",
rule.host, rules.allowed_egress_hosts
),
});
}
}
}
if rules.require_runtime_secret_delivery {
let delivery = spec
.run
.as_ref()
.map(|r| &r.secret_delivery)
.unwrap_or(&SecretDeliveryMode::Env);
if *delivery == SecretDeliveryMode::Env {
violations.push(PolicyViolation {
rule: "requireRuntimeSecretDelivery".into(),
message: "policy requires spec.run.secretDelivery to be runtimeBroker or \
runtimeLeasedBroker, not env"
.into(),
});
}
}
if rules.require_resource_limits {
let has_limits = spec.run.as_ref().and_then(|r| r.limits.as_ref()).is_some();
if !has_limits {
violations.push(PolicyViolation {
rule: "requireResourceLimits".into(),
message: "policy requires spec.run.limits to be declared".into(),
});
}
}
if rules.flag_dns_egress_without_acknowledgment == Some(true) {
let mut has_dns_egress = false;
let mut all_acknowledged = true;
for rule in egress_rules {
if rule.port == 53 {
has_dns_egress = true;
let acknowledged = rule
.protocol
.as_deref()
.is_some_and(|p| p.eq_ignore_ascii_case("dns-acknowledged"));
if !acknowledged {
all_acknowledged = false;
}
}
}
if has_dns_egress && !all_acknowledged {
violations.push(PolicyViolation {
rule: "flagDnsEgressWithoutAcknowledgment".into(),
message: "spec declares port 53 (DNS) egress without acknowledgment — \
DNS can be used as a covert exfiltration channel; set \
protocol: dns-acknowledged to acknowledge this risk"
.into(),
});
}
}
if rules.require_dns_egress_justification == Some(true) {
for rule in egress_rules {
if rule.port != 53 {
continue;
}
let acknowledged = rule
.protocol
.as_deref()
.is_some_and(|p| p.eq_ignore_ascii_case("dns-acknowledged"));
if !acknowledged {
continue;
}
let justified = rule
.dns_egress_justification
.as_deref()
.is_some_and(|s| !s.trim().is_empty());
if !justified {
violations.push(PolicyViolation {
rule: "requireDnsEgressJustification".into(),
message: "port-53 egress rule with protocol dns-acknowledged \
requires a non-empty dnsEgressJustification field"
.into(),
});
}
}
}
violations
}
pub fn validate_secret_refs_against_allowlist(
spec: &ExecutionCellSpec,
rules: &PolicyRules,
caller_identity: &str,
) -> Result<(), CellosError> {
let Some(allowlist) = rules.secret_ref_allowlist.as_ref() else {
return Ok(());
};
let Some(allowed) = allowlist.get(caller_identity) else {
return Err(CellosError::InvalidSpec(format!(
"caller_unmapped: caller identity {caller_identity:?} is not present in \
policy pack rules.secretRefAllowlist; admission rejected per ADR-0007"
)));
};
let Some(requested) = spec.authority.secret_refs.as_ref() else {
return Ok(());
};
for ref_name in requested {
if !allowed.iter().any(|granted| granted == ref_name) {
return Err(CellosError::InvalidSpec(format!(
"secret_ref_denied: caller {caller_identity:?} is not granted secretRef \
{ref_name:?} by policy pack rules.secretRefAllowlist; admission rejected \
per ADR-0007"
)));
}
}
Ok(())
}
fn host_matches_pattern(host: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(suffix) = pattern.strip_prefix("*.") {
host.ends_with(&format!(".{suffix}"))
} else {
host == pattern
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationPolicyDocument {
pub api_version: String,
pub kind: String,
pub spec: AuthorizationPolicy,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationPolicy {
pub subjects: Vec<String>,
#[serde(default)]
pub allowed_pools: Vec<String>,
#[serde(default)]
pub allowed_policy_packs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_cells_per_hour: Option<u32>,
}
pub fn validate_authorization_policy(doc: &AuthorizationPolicyDocument) -> Result<(), CellosError> {
if doc.api_version != "cellos.io/v1" {
return Err(CellosError::InvalidSpec(format!(
"authorization policy apiVersion must be \"cellos.io/v1\", got {:?}",
doc.api_version
)));
}
if doc.kind != "AuthorizationPolicy" {
return Err(CellosError::InvalidSpec(format!(
"authorization policy kind must be \"AuthorizationPolicy\", got {:?}",
doc.kind
)));
}
let policy = &doc.spec;
if policy.subjects.is_empty() {
return Err(CellosError::InvalidSpec(
"authorization policy spec.subjects must be non-empty — \
an empty subjects list would reject every spec; \
remove CELLOS_AUTHZ_POLICY_PATH to disable the gate instead"
.into(),
));
}
for s in &policy.subjects {
if s.trim().is_empty() {
return Err(CellosError::InvalidSpec(
"authorization policy spec.subjects contains an empty / whitespace-only entry"
.into(),
));
}
}
for p in &policy.allowed_pools {
if p.trim().is_empty() {
return Err(CellosError::InvalidSpec(
"authorization policy spec.allowedPools contains an empty entry".into(),
));
}
}
for p in &policy.allowed_policy_packs {
if p.trim().is_empty() {
return Err(CellosError::InvalidSpec(
"authorization policy spec.allowedPolicyPacks contains an empty entry".into(),
));
}
}
if let Some(0) = policy.max_cells_per_hour {
return Err(CellosError::InvalidSpec(
"authorization policy spec.maxCellsPerHour must be > 0 when set".into(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{AuthorityBundle, EgressRule, Lifetime, RunLimits, RunSpec};
fn minimal_spec() -> ExecutionCellSpec {
ExecutionCellSpec {
id: "test-cell".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::Env,
}),
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: None,
secret_refs: None,
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 300 },
export: None,
telemetry: None,
}
}
fn minimal_pack(rules: PolicyRules) -> PolicyPackSpec {
PolicyPackSpec {
id: "test-policy".into(),
description: None,
version: None,
placement: None,
rules,
}
}
fn minimal_doc(rules: PolicyRules) -> PolicyPackDocument {
PolicyPackDocument {
api_version: "cellos.io/v1".into(),
kind: "PolicyPack".into(),
spec: minimal_pack(rules),
}
}
#[test]
fn valid_doc_passes_structural_check() {
let doc = minimal_doc(PolicyRules::default());
assert!(validate_policy_pack_document(&doc).is_ok());
}
#[test]
fn wrong_api_version_is_rejected() {
let mut doc = minimal_doc(PolicyRules::default());
doc.api_version = "v1".into();
assert!(validate_policy_pack_document(&doc).is_err());
}
#[test]
fn wrong_kind_is_rejected() {
let mut doc = minimal_doc(PolicyRules::default());
doc.kind = "ExecutionCell".into();
assert!(validate_policy_pack_document(&doc).is_err());
}
#[test]
fn invalid_spec_id_is_rejected() {
let mut doc = minimal_doc(PolicyRules::default());
doc.spec.id = "-bad".into();
assert!(validate_policy_pack_document(&doc).is_err());
}
#[test]
fn zero_max_ttl_is_rejected() {
let doc = minimal_doc(PolicyRules {
max_lifetime_ttl_seconds: Some(0),
..Default::default()
});
assert!(validate_policy_pack_document(&doc).is_err());
}
#[test]
fn require_and_forbid_egress_together_is_rejected() {
let doc = minimal_doc(PolicyRules {
require_egress_declared: true,
forbid_outbound_egress_rules: true,
..Default::default()
});
assert!(validate_policy_pack_document(&doc).is_err());
}
#[test]
fn empty_egress_host_pattern_is_rejected() {
let doc = minimal_doc(PolicyRules {
allowed_egress_hosts: vec!["".into()],
..Default::default()
});
assert!(validate_policy_pack_document(&doc).is_err());
}
#[test]
fn spec_passes_empty_policy() {
let spec = minimal_spec();
let pack = minimal_pack(PolicyRules::default());
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn ttl_exceeds_max_is_violation() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
max_lifetime_ttl_seconds: Some(60),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "maxLifetimeTtlSeconds");
}
#[test]
fn ttl_at_exact_max_passes() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
max_lifetime_ttl_seconds: Some(300),
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn memory_exceeds_max_is_violation() {
let mut spec = minimal_spec();
spec.run = Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: None,
limits: Some(RunLimits {
memory_max_bytes: Some(8 * 1024 * 1024 * 1024), cpu_max: None,
graceful_shutdown_seconds: None,
}),
secret_delivery: SecretDeliveryMode::Env,
});
let pack = minimal_pack(PolicyRules {
max_memory_max_bytes: Some(4 * 1024 * 1024 * 1024), ..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "maxMemoryMaxBytes");
}
#[test]
fn run_timeout_exceeds_max_is_violation() {
let mut spec = minimal_spec();
spec.run = Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: Some(7_200_000), limits: None,
secret_delivery: SecretDeliveryMode::Env,
});
let pack = minimal_pack(PolicyRules {
max_run_timeout_ms: Some(3_600_000), ..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "maxRunTimeoutMs");
}
#[test]
fn require_egress_declared_fails_when_no_egress_rules() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
require_egress_declared: true,
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "requireEgressDeclared");
}
#[test]
fn require_egress_declared_passes_when_egress_present() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "api.github.com".into(),
port: 443,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
require_egress_declared: true,
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn forbid_outbound_egress_fails_when_rules_declared() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "external.example.com".into(),
port: 443,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
forbid_outbound_egress_rules: true,
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "forbidOutboundEgressRules");
}
#[test]
fn forbid_outbound_egress_passes_when_no_rules() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
forbid_outbound_egress_rules: true,
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn allowed_egress_hosts_rejects_unlisted_host() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "evil.example.com".into(),
port: 443,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
allowed_egress_hosts: vec!["*.internal".into(), "api.github.com".into()],
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "allowedEgressHosts");
}
#[test]
fn allowed_egress_hosts_accepts_wildcard_subdomain() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "cache.internal".into(),
port: 443,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
allowed_egress_hosts: vec!["*.internal".into()],
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn wildcard_subdomain_does_not_match_bare_domain() {
assert!(!host_matches_pattern("internal", "*.internal"));
assert!(host_matches_pattern("foo.internal", "*.internal"));
}
#[test]
fn require_runtime_secret_delivery_rejects_env_mode() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
require_runtime_secret_delivery: true,
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "requireRuntimeSecretDelivery");
}
#[test]
fn require_runtime_secret_delivery_accepts_broker_mode() {
let mut spec = minimal_spec();
spec.run = Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: None,
limits: None,
secret_delivery: SecretDeliveryMode::RuntimeBroker,
});
let pack = minimal_pack(PolicyRules {
require_runtime_secret_delivery: true,
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn require_resource_limits_rejects_spec_without_limits() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
require_resource_limits: true,
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "requireResourceLimits");
}
#[test]
fn require_resource_limits_passes_with_limits_set() {
let mut spec = minimal_spec();
spec.run = Some(RunSpec {
argv: vec!["/usr/bin/true".into()],
working_directory: None,
timeout_ms: None,
limits: Some(RunLimits {
memory_max_bytes: Some(512 * 1024 * 1024),
cpu_max: None,
graceful_shutdown_seconds: None,
}),
secret_delivery: SecretDeliveryMode::Env,
});
let pack = minimal_pack(PolicyRules {
require_resource_limits: true,
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn multiple_violations_are_all_reported() {
let spec = minimal_spec(); let pack = minimal_pack(PolicyRules {
max_lifetime_ttl_seconds: Some(60),
require_runtime_secret_delivery: true,
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 2);
let rules: Vec<&str> = violations.iter().map(|v| v.rule.as_str()).collect();
assert!(rules.contains(&"maxLifetimeTtlSeconds"));
assert!(rules.contains(&"requireRuntimeSecretDelivery"));
}
#[test]
fn dns_egress_flagged_when_rule_enabled() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "flagDnsEgressWithoutAcknowledgment");
assert!(violations[0].message.contains("dns-acknowledged"));
}
#[test]
fn dns_egress_not_flagged_when_protocol_acknowledged() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn dns_egress_acknowledgment_is_case_insensitive() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: Some("DNS-Acknowledged".into()),
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn dns_egress_not_checked_when_rule_disabled() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules::default());
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn dns_egress_not_checked_when_rule_explicitly_false() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(false),
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn dns_egress_rule_does_not_affect_non_dns_ports() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "api.github.com".into(),
port: 443,
protocol: None,
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn dns_egress_flagged_when_some_rules_acknowledged_but_not_all() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![
EgressRule {
host: "ns1.example.com".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: None,
},
EgressRule {
host: "ns2.example.com".into(),
port: 53,
protocol: None,
dns_egress_justification: None,
},
]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "flagDnsEgressWithoutAcknowledgment");
}
#[test]
fn dns_egress_ack_gate_covers_tcp_protocol() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "1.1.1.1".into(),
port: 53,
protocol: Some("tcp".into()),
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(
violations.len(),
1,
"TCP/53 without dns-acknowledged must violate the SEC-15 gate; \
got: {violations:?}"
);
assert_eq!(violations[0].rule, "flagDnsEgressWithoutAcknowledgment");
}
#[test]
fn dns_egress_ack_gate_admits_acknowledged_tcp_53() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "1.1.1.1".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
assert!(
validate_spec_against_policy(&spec, &pack).is_empty(),
"acknowledged port-53 rule must pass the SEC-15 gate"
);
}
#[test]
fn dns_egress_ack_gate_rejects_mixed_acknowledged_and_tcp_53() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![
EgressRule {
host: "1.1.1.1".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: None,
},
EgressRule {
host: "8.8.8.8".into(),
port: 53,
protocol: Some("tcp".into()),
dns_egress_justification: None,
},
]);
let pack = minimal_pack(PolicyRules {
flag_dns_egress_without_acknowledgment: Some(true),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(
violations.len(),
1,
"mixed ack+TCP/53 must violate the SEC-15 gate; got: {violations:?}"
);
assert_eq!(violations[0].rule, "flagDnsEgressWithoutAcknowledgment");
}
#[test]
fn policy_violation_display_includes_rule_and_message() {
let v = PolicyViolation {
rule: "maxLifetimeTtlSeconds".into(),
message: "300 exceeds 60".into(),
};
let s = v.to_string();
assert!(s.contains("maxLifetimeTtlSeconds"));
assert!(s.contains("300 exceeds 60"));
}
#[test]
fn dns_justification_required_when_rule_enabled_and_acknowledged() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules {
require_dns_egress_justification: Some(true),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "requireDnsEgressJustification");
assert!(violations[0].message.contains("dnsEgressJustification"));
}
#[test]
fn dns_justification_satisfied_with_nonempty_string() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: Some("internal resolver at 10.0.0.1".into()),
}]);
let pack = minimal_pack(PolicyRules {
require_dns_egress_justification: Some(true),
..Default::default()
});
assert!(validate_spec_against_policy(&spec, &pack).is_empty());
}
#[test]
fn dns_justification_empty_string_rejected() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: Some(" ".into()),
}]);
let pack = minimal_pack(PolicyRules {
require_dns_egress_justification: Some(true),
..Default::default()
});
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].rule, "requireDnsEgressJustification");
}
#[test]
fn dns_justification_not_required_when_rule_disabled() {
let mut spec = minimal_spec();
spec.authority.egress_rules = Some(vec![EgressRule {
host: "ns.example.com".into(),
port: 53,
protocol: Some("dns-acknowledged".into()),
dns_egress_justification: None,
}]);
let pack = minimal_pack(PolicyRules::default());
let violations = validate_spec_against_policy(&spec, &pack);
assert!(
!violations
.iter()
.any(|v| v.rule == "requireDnsEgressJustification"),
"unexpected requireDnsEgressJustification violation: {violations:?}"
);
}
#[test]
fn version_absent_is_accepted() {
assert!(check_policy_pack_version_compatibility(None, false).is_ok());
}
#[test]
fn version_at_floor_is_accepted() {
assert!(check_policy_pack_version_compatibility(
Some(MIN_SUPPORTED_POLICY_PACK_VERSION),
false
)
.is_ok());
}
#[test]
fn version_above_floor_is_accepted() {
assert!(check_policy_pack_version_compatibility(Some("1.4.2"), false).is_ok());
assert!(check_policy_pack_version_compatibility(Some("2.0.0"), false).is_ok());
}
#[test]
fn version_with_prerelease_is_accepted() {
assert!(check_policy_pack_version_compatibility(Some("1.0.0-rc.1"), false).is_ok());
}
#[test]
fn malformed_version_is_rejected() {
assert!(check_policy_pack_version_compatibility(Some("v1.0"), false).is_err());
assert!(check_policy_pack_version_compatibility(Some("1.0"), false).is_err());
assert!(check_policy_pack_version_compatibility(Some("01.00.00"), false).is_err());
assert!(check_policy_pack_version_compatibility(Some(""), false).is_err());
}
#[test]
fn document_validates_with_explicit_floor_version() {
let mut doc = minimal_doc(PolicyRules::default());
doc.spec.version = Some(MIN_SUPPORTED_POLICY_PACK_VERSION.into());
assert!(validate_policy_pack_document(&doc).is_ok());
}
#[test]
fn document_rejects_malformed_version() {
let mut doc = minimal_doc(PolicyRules::default());
doc.spec.version = Some("not-a-semver".into());
assert!(validate_policy_pack_document(&doc).is_err());
}
fn pack_with_placement(rules: PolicyRules, placement: PlacementSpec) -> PolicyPackSpec {
PolicyPackSpec {
id: "scoped-policy".into(),
description: None,
version: None,
placement: Some(placement),
rules,
}
}
fn spec_with_ttl_and_placement(
ttl_seconds: u64,
placement: Option<PlacementSpec>,
) -> ExecutionCellSpec {
let mut s = minimal_spec();
s.lifetime.ttl_seconds = ttl_seconds;
s.placement = placement;
s
}
#[test]
fn placement_scoped_pack_applies_when_pool_matches() {
let pack = pack_with_placement(
PolicyRules {
max_lifetime_ttl_seconds: Some(60),
..Default::default()
},
PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: None,
queue_name: None,
},
);
let spec = spec_with_ttl_and_placement(
300,
Some(PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: None,
queue_name: None,
}),
);
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1, "scoped pack should apply on match");
assert_eq!(violations[0].rule, "maxLifetimeTtlSeconds");
}
#[test]
fn placement_scoped_pack_is_skipped_when_pool_differs() {
let pack = pack_with_placement(
PolicyRules {
max_lifetime_ttl_seconds: Some(60),
..Default::default()
},
PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: None,
queue_name: None,
},
);
let spec = spec_with_ttl_and_placement(
300,
Some(PlacementSpec {
pool_id: Some("runner-pool-arm64".into()),
kubernetes_namespace: None,
queue_name: None,
}),
);
let violations = validate_spec_against_policy(&spec, &pack);
assert!(
violations.is_empty(),
"scoped pack must not apply to mismatched placement, got {violations:?}"
);
}
#[test]
fn unscoped_pack_applies_everywhere() {
let pack = minimal_pack(PolicyRules {
max_lifetime_ttl_seconds: Some(60),
..Default::default()
});
let spec_no_placement = spec_with_ttl_and_placement(300, None);
let spec_with_pool = spec_with_ttl_and_placement(
300,
Some(PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: None,
queue_name: None,
}),
);
assert_eq!(
validate_spec_against_policy(&spec_no_placement, &pack).len(),
1,
"unscoped pack must apply to specs without placement"
);
assert_eq!(
validate_spec_against_policy(&spec_with_pool, &pack).len(),
1,
"unscoped pack must apply to specs with any placement"
);
}
#[test]
fn placement_scope_with_no_populated_fields_is_universal() {
let pack = pack_with_placement(
PolicyRules {
max_lifetime_ttl_seconds: Some(60),
..Default::default()
},
PlacementSpec::default(),
);
let spec = spec_with_ttl_and_placement(300, None);
let violations = validate_spec_against_policy(&spec, &pack);
assert_eq!(violations.len(), 1, "empty scope must behave as universal");
}
#[test]
fn scope_with_multiple_fields_requires_all_to_match() {
let pack = pack_with_placement(
PolicyRules {
max_lifetime_ttl_seconds: Some(60),
..Default::default()
},
PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: Some("cellos-prod".into()),
queue_name: None,
},
);
let half_match = spec_with_ttl_and_placement(
300,
Some(PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: Some("cellos-staging".into()),
queue_name: None,
}),
);
assert!(validate_spec_against_policy(&half_match, &pack).is_empty());
let full_match = spec_with_ttl_and_placement(
300,
Some(PlacementSpec {
pool_id: Some("runner-pool-amd64".into()),
kubernetes_namespace: Some("cellos-prod".into()),
queue_name: None,
}),
);
assert_eq!(validate_spec_against_policy(&full_match, &pack).len(), 1);
}
fn minimal_authz_doc(policy: AuthorizationPolicy) -> AuthorizationPolicyDocument {
AuthorizationPolicyDocument {
api_version: "cellos.io/v1".into(),
kind: "AuthorizationPolicy".into(),
spec: policy,
}
}
#[test]
fn authz_policy_valid_doc_passes() {
let doc = minimal_authz_doc(AuthorizationPolicy {
subjects: vec!["tenant:acme".into(), "oidc:github:foo/bar".into()],
allowed_pools: vec!["pool-a".into()],
allowed_policy_packs: vec!["strict-1".into()],
max_cells_per_hour: Some(100),
});
assert!(validate_authorization_policy(&doc).is_ok());
}
#[test]
fn authz_policy_empty_subjects_rejected() {
let doc = minimal_authz_doc(AuthorizationPolicy {
subjects: vec![],
..AuthorizationPolicy::default()
});
let err = validate_authorization_policy(&doc).expect_err("empty subjects must reject");
assert!(
err.to_string().contains("subjects must be non-empty"),
"got: {err}"
);
}
#[test]
fn authz_policy_wrong_kind_rejected() {
let mut doc = minimal_authz_doc(AuthorizationPolicy {
subjects: vec!["tenant:acme".into()],
..AuthorizationPolicy::default()
});
doc.kind = "PolicyPack".into();
assert!(validate_authorization_policy(&doc).is_err());
}
#[test]
fn authz_policy_zero_rate_limit_rejected() {
let doc = minimal_authz_doc(AuthorizationPolicy {
subjects: vec!["tenant:acme".into()],
max_cells_per_hour: Some(0),
..AuthorizationPolicy::default()
});
let err = validate_authorization_policy(&doc).expect_err("zero rate limit must reject");
assert!(err.to_string().contains("maxCellsPerHour"), "got: {err}");
}
#[test]
fn authz_policy_empty_pool_entry_rejected() {
let doc = minimal_authz_doc(AuthorizationPolicy {
subjects: vec!["tenant:acme".into()],
allowed_pools: vec!["valid".into(), " ".into()],
..AuthorizationPolicy::default()
});
assert!(validate_authorization_policy(&doc).is_err());
}
}