use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SourceId(pub String);
impl SourceId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
}
impl fmt::Display for SourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Trust {
Trusted,
Untrusted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Sensitivity {
Public,
Internal,
Confidential,
Secret,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Label {
pub provenance: BTreeSet<SourceId>,
pub trust: Trust,
pub sensitivity: Sensitivity,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProtectedField {
path: String,
#[serde(default)]
require_trusted: bool,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
allowed_sources: BTreeSet<SourceId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max_sensitivity: Option<Sensitivity>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Release {
scope: ReleaseScope,
basis: String,
destination: String,
fields: BTreeSet<String>,
evidence: BTreeSet<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReleaseScope {
trust: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
sensitivity: Option<Sensitivity>,
}
impl ReleaseScope {
#[must_use]
pub const fn trust() -> Self {
Self {
trust: true,
sensitivity: None,
}
}
#[must_use]
pub const fn sensitivity(target: Sensitivity) -> Self {
Self {
trust: false,
sensitivity: Some(target),
}
}
#[must_use]
pub const fn trust_and_sensitivity(target: Sensitivity) -> Self {
Self {
trust: true,
sensitivity: Some(target),
}
}
}
impl Release {
#[must_use]
pub fn whole(
scope: ReleaseScope,
basis: impl Into<String>,
destination: impl Into<String>,
evidence: impl IntoIterator<Item = String>,
) -> Self {
Self::new(scope, [String::new()], basis, destination, evidence)
}
#[must_use]
pub fn fields(
scope: ReleaseScope,
fields: impl IntoIterator<Item = String>,
basis: impl Into<String>,
destination: impl Into<String>,
evidence: impl IntoIterator<Item = String>,
) -> Self {
Self::new(scope, fields, basis, destination, evidence)
}
fn new(
scope: ReleaseScope,
fields: impl IntoIterator<Item = String>,
basis: impl Into<String>,
destination: impl Into<String>,
evidence: impl IntoIterator<Item = String>,
) -> Self {
let release = Self {
scope,
basis: basis.into(),
destination: destination.into(),
fields: fields.into_iter().collect::<BTreeSet<_>>(),
evidence: evidence.into_iter().collect::<BTreeSet<_>>(),
};
assert!(
release.validate().is_ok(),
"invalid release: {}",
release.validate().unwrap_err()
);
release
}
#[must_use]
pub fn basis(&self) -> &str {
&self.basis
}
#[must_use]
pub const fn scope(&self) -> ReleaseScope {
self.scope
}
#[must_use]
pub fn destination(&self) -> &str {
&self.destination
}
#[must_use]
pub fn fields_scope(&self) -> &BTreeSet<String> {
&self.fields
}
#[must_use]
pub fn evidence(&self) -> &BTreeSet<String> {
&self.evidence
}
#[must_use]
pub fn is_whole_value(&self) -> bool {
self.fields.contains("")
}
pub fn validate(&self) -> Result<(), &'static str> {
if !self.scope.trust && self.scope.sensitivity.is_none() {
return Err("scope must improve trust, sensitivity, or both");
}
if self.basis.trim().is_empty() || self.destination.trim().is_empty() {
return Err("basis and destination must be non-empty");
}
if self.fields.is_empty()
|| (self.fields.len() > 1 && self.fields.contains(""))
|| self
.fields
.iter()
.any(|path| !path.is_empty() && !is_json_pointer(path))
{
return Err("field scope is not a valid whole-value or JSON Pointer selection");
}
if self.evidence.is_empty() || self.evidence.iter().any(|item| item.trim().is_empty()) {
return Err("evidence must contain at least one non-empty reference");
}
Ok(())
}
}
impl ProtectedField {
#[must_use]
pub fn trusted(path: impl Into<String>) -> Self {
let path = path.into();
assert_json_pointer(&path);
Self {
path,
require_trusted: true,
allowed_sources: BTreeSet::new(),
max_sensitivity: None,
}
}
#[must_use]
pub fn from_sources(
path: impl Into<String>,
sources: impl IntoIterator<Item = SourceId>,
) -> Self {
let path = path.into();
assert_json_pointer(&path);
let allowed_sources = sources.into_iter().collect::<BTreeSet<_>>();
assert!(
!allowed_sources.is_empty(),
"a protected field source constraint must name at least one source"
);
Self {
path,
require_trusted: false,
allowed_sources,
max_sensitivity: None,
}
}
#[must_use]
pub const fn max_sensitivity(mut self, sensitivity: Sensitivity) -> Self {
self.max_sensitivity = Some(sensitivity);
self
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub const fn requires_trusted(&self) -> bool {
self.require_trusted
}
#[must_use]
pub fn allowed_sources(&self) -> &BTreeSet<SourceId> {
&self.allowed_sources
}
#[must_use]
pub const fn sensitivity_ceiling(&self) -> Option<Sensitivity> {
self.max_sensitivity
}
pub fn validate(&self) -> Result<(), &'static str> {
if !is_json_pointer(&self.path) {
return Err("path must be a non-empty absolute RFC 6901 JSON Pointer");
}
if self.require_trusted && !self.allowed_sources.is_empty() {
return Err("require_trusted and allowed_sources are mutually exclusive");
}
if !self.require_trusted
&& self.allowed_sources.is_empty()
&& self.max_sensitivity.is_none()
{
return Err("at least one trust, source, or sensitivity constraint is required");
}
Ok(())
}
}
fn assert_json_pointer(path: &str) {
assert!(
is_json_pointer(path),
"a protected field path must be a non-empty absolute RFC 6901 JSON Pointer"
);
}
fn is_json_pointer(path: &str) -> bool {
if !path.starts_with('/') {
return false;
}
let bytes = path.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'~' {
index += 1;
if index == bytes.len() || !matches!(bytes[index], b'0' | b'1') {
return false;
}
}
index += 1;
}
true
}
impl Label {
#[must_use]
pub fn trusted() -> Self {
Self {
provenance: BTreeSet::new(),
trust: Trust::Trusted,
sensitivity: Sensitivity::Public,
}
}
#[must_use]
pub fn untrusted(source: SourceId) -> Self {
Self {
provenance: BTreeSet::from([source]),
trust: Trust::Untrusted,
sensitivity: Sensitivity::Internal,
}
}
#[must_use]
pub fn with_sensitivity(mut self, s: Sensitivity) -> Self {
self.sensitivity = s;
self
}
#[must_use]
pub fn join(&self, other: &Self) -> Self {
Self {
provenance: self.provenance.union(&other.provenance).cloned().collect(),
trust: self.trust.max(other.trust),
sensitivity: self.sensitivity.max(other.sensitivity),
}
}
#[must_use]
pub fn is_untrusted(&self) -> bool {
self.trust == Trust::Untrusted
}
}
impl Default for Label {
fn default() -> Self {
Self::trusted()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tainted<T> {
value: T,
label: Label,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
fields: BTreeMap<String, Label>,
}
impl<T> Tainted<T> {
pub fn trusted(value: T) -> Self {
Self {
value,
label: Label::trusted(),
fields: BTreeMap::new(),
}
}
pub fn from_source(value: T, source: SourceId) -> Self {
Self {
value,
label: Label::untrusted(source),
fields: BTreeMap::new(),
}
}
pub fn with_label(value: T, label: Label) -> Self {
Self {
value,
label,
fields: BTreeMap::new(),
}
}
pub fn label(&self) -> &Label {
&self.label
}
#[cfg(feature = "manifest")]
pub(crate) fn with_joined_label(&self, other: &Label) -> Self
where
T: Clone,
{
Self {
value: self.value.clone(),
label: self.label.join(other),
fields: self.fields.clone(),
}
}
pub(crate) fn into_unlabelled(self) -> T {
self.value
}
pub fn peek(&self) -> &T {
&self.value
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Tainted<U> {
Tainted {
value: f(self.value),
label: self.label,
fields: BTreeMap::new(),
}
}
pub fn zip<U>(self, other: Tainted<U>) -> Tainted<(T, U)> {
let label = self.label.join(&other.label);
Tainted {
value: (self.value, other.value),
label,
fields: BTreeMap::new(),
}
}
}
impl Tainted<serde_json::Value> {
pub fn object(fields: impl IntoIterator<Item = (String, Self)>) -> Self {
let mut value = serde_json::Map::new();
let mut label = Label::trusted();
let mut field_labels = BTreeMap::new();
for (name, field) in fields {
let base = format!("/{}", escape_pointer_token(&name));
label = label.join(&field.label);
field_labels.insert(base.clone(), field.label);
for (path, nested) in field.fields {
field_labels.insert(format!("{base}{path}"), nested);
}
value.insert(name, field.value);
}
Self {
value: serde_json::Value::Object(value),
label,
fields: field_labels,
}
}
pub fn array(elements: impl IntoIterator<Item = Self>) -> Self {
let mut value = Vec::new();
let mut label = Label::trusted();
let mut field_labels = BTreeMap::new();
for (index, element) in elements.into_iter().enumerate() {
let base = format!("/{index}");
label = label.join(&element.label);
field_labels.insert(base.clone(), element.label);
for (path, nested) in element.fields {
field_labels.insert(format!("{base}{path}"), nested);
}
value.push(element.value);
}
Self {
value: serde_json::Value::Array(value),
label,
fields: field_labels,
}
}
#[must_use]
pub fn label_at(&self, path: &str) -> Option<&Label> {
self.value.pointer(path)?;
if path.is_empty() {
return Some(&self.label);
}
let mut candidate = path;
loop {
if let Some(label) = self.fields.get(candidate) {
return Some(label);
}
let Some(split) = candidate.rfind('/') else {
return Some(&self.label);
};
if split == 0 {
return Some(&self.label);
}
candidate = &candidate[..split];
}
}
pub fn field_labels(&self) -> impl Iterator<Item = (&str, &Label)> {
self.fields
.iter()
.map(|(path, label)| (path.as_str(), label))
}
pub(crate) fn project_field(&self, name: &str) -> Option<Self> {
self.project_pointer(&format!("/{}", escape_pointer_token(name)))
}
pub(crate) fn project_pointer(&self, pointer: &str) -> Option<Self> {
if pointer.is_empty() {
return Some(self.clone());
}
let value = self.value.pointer(pointer)?.clone();
let label = self.label_at(pointer)?.clone();
let prefix = format!("{pointer}/");
let fields = self
.fields
.iter()
.filter_map(|(path, label)| {
path.strip_prefix(&prefix)
.map(|relative| (format!("/{relative}"), label.clone()))
})
.collect();
Some(Self {
value,
label,
fields,
})
}
pub(crate) fn apply_release(mut self, release: &Release) -> Option<Self> {
if release.is_whole_value() {
self.label = apply_release_scope(&self.label, release.scope);
for label in self.fields.values_mut() {
*label = apply_release_scope(label, release.scope);
}
return Some(self);
}
if release
.fields
.iter()
.any(|path| !self.fields.contains_key(path) || self.value.pointer(path).is_none())
{
return None;
}
for released in &release.fields {
let descendants = format!("{released}/");
for (path, label) in &mut self.fields {
if path == released || path.starts_with(&descendants) {
*label = apply_release_scope(label, release.scope);
}
}
}
self.label = self
.fields
.values()
.fold(Label::trusted(), |joined, label| joined.join(label));
Some(self)
}
}
fn apply_release_scope(label: &Label, scope: ReleaseScope) -> Label {
let mut released = label.clone();
if scope.trust {
released.trust = Trust::Trusted;
}
if let Some(target) = scope.sensitivity {
released.sensitivity = label.sensitivity.min(target);
}
released
}
fn escape_pointer_token(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
#[cfg(test)]
mod tests {
use super::*;
fn src(s: &str) -> SourceId {
SourceId::new(s)
}
#[test]
fn trust_join_degrades() {
let t = Label::trusted();
let u = Label::untrusted(src("tool://tool"));
assert_eq!(t.join(&u).trust, Trust::Untrusted);
assert_eq!(u.join(&t).trust, Trust::Untrusted, "join is commutative");
}
#[test]
fn sensitivity_join_escalates() {
let a = Label::trusted().with_sensitivity(Sensitivity::Public);
let b = Label::trusted().with_sensitivity(Sensitivity::Secret);
assert_eq!(a.join(&b).sensitivity, Sensitivity::Secret);
}
#[test]
fn provenance_accumulates() {
let a = Label::untrusted(src("a"));
let b = Label::untrusted(src("b"));
let j = a.join(&b);
assert_eq!(j.provenance.len(), 2);
}
#[test]
fn join_is_idempotent_and_associative() {
let a = Label::untrusted(src("a"));
let b = Label::trusted().with_sensitivity(Sensitivity::Confidential);
let c = Label::untrusted(src("c")).with_sensitivity(Sensitivity::Secret);
assert_eq!(a.join(&a), a, "idempotent");
assert_eq!(a.join(&b).join(&c), a.join(&b.join(&c)), "associative");
}
#[test]
fn zip_propagates_untrust_to_derived_values() {
let trusted = Tainted::trusted(1);
let untrusted = Tainted::from_source(2, src("tool://tool"));
let combined = trusted.zip(untrusted).map(|(a, b)| a + b);
assert!(combined.label().is_untrusted());
assert_eq!(*combined.peek(), 3);
}
#[test]
fn map_preserves_label() {
let t = Tainted::from_source("x", src("doc"));
let mapped = t.map(str::to_uppercase);
assert!(mapped.label().is_untrusted());
assert_eq!(mapped.peek(), "X");
}
#[test]
fn field_projection_preserves_only_the_selected_lineage() {
let nested = Tainted::object([
("safe".to_owned(), Tainted::trusted(serde_json::json!("ok"))),
(
"body".to_owned(),
Tainted::from_source(serde_json::json!("outside"), src("model")),
),
]);
let value = Tainted::object([
("selected".to_owned(), nested),
(
"other".to_owned(),
Tainted::from_source(serde_json::json!("noise"), src("tool")),
),
]);
let selected = value.project_field("selected").unwrap();
assert!(selected.label_at("/safe").unwrap().provenance.is_empty());
assert_eq!(
selected.label_at("/body").unwrap().provenance,
BTreeSet::from([src("model")])
);
assert!(selected.label_at("/other").is_none());
}
#[test]
fn field_release_improves_only_its_declared_scope() {
let value = Tainted::object([
(
"recipient".to_owned(),
Tainted::from_source(serde_json::json!("treasury"), src("model")),
),
(
"memo".to_owned(),
Tainted::from_source(serde_json::json!("outside"), src("model")),
),
]);
let release = Release::fields(
ReleaseScope::trust(),
["/recipient".to_owned()],
"operator verified account",
"ledger.transfer",
["approval:42".to_owned()],
);
let released = value.apply_release(&release).unwrap();
let recipient = released.label_at("/recipient").unwrap();
assert_eq!(recipient.trust, Trust::Trusted);
assert_eq!(recipient.sensitivity, Sensitivity::Internal);
assert_eq!(recipient.provenance, BTreeSet::from([src("model")]));
assert!(released.label_at("/memo").unwrap().is_untrusted());
assert!(
released.label().is_untrusted(),
"unreleased content still taints"
);
}
#[test]
fn each_release_scope_moves_exactly_what_it_names() {
let secret = Label::untrusted(src("model")).with_sensitivity(Sensitivity::Secret);
let trust_only = apply_release_scope(&secret, ReleaseScope::trust());
assert_eq!(trust_only.trust, Trust::Trusted);
assert_eq!(
trust_only.sensitivity,
Sensitivity::Secret,
"improving trust must not also declassify"
);
let sensitivity_only =
apply_release_scope(&secret, ReleaseScope::sensitivity(Sensitivity::Internal));
assert_eq!(
sensitivity_only.trust,
Trust::Untrusted,
"lowering sensitivity must not also confer belief"
);
assert_eq!(sensitivity_only.sensitivity, Sensitivity::Internal);
let both = apply_release_scope(
&secret,
ReleaseScope::trust_and_sensitivity(Sensitivity::Internal),
);
assert_eq!(both.trust, Trust::Trusted);
assert_eq!(both.sensitivity, Sensitivity::Internal);
}
#[test]
fn a_release_never_raises_sensitivity() {
let public = Label::untrusted(src("model")).with_sensitivity(Sensitivity::Public);
let released = apply_release_scope(&public, ReleaseScope::sensitivity(Sensitivity::Secret));
assert_eq!(
released.sensitivity,
Sensitivity::Public,
"a release named a higher classification and the label followed it upward"
);
}
#[test]
fn no_release_scope_erases_provenance() {
let from_model = Label::untrusted(src("model")).with_sensitivity(Sensitivity::Secret);
for scope in [
ReleaseScope::trust(),
ReleaseScope::sensitivity(Sensitivity::Public),
ReleaseScope::trust_and_sensitivity(Sensitivity::Public),
] {
assert_eq!(
apply_release_scope(&from_model, scope).provenance,
BTreeSet::from([src("model")]),
"a release dropped the provenance it was granted against"
);
}
}
#[test]
fn an_untracked_descendant_inherits_its_nearest_labelled_ancestor() {
let nested = Tainted::object([
(
"recipient".to_owned(),
Tainted::with_label(
serde_json::json!({ "addr": "ops@example.com" }),
Label::trusted(),
),
),
(
"note".to_owned(),
Tainted::with_label(
serde_json::json!("n"),
Label::untrusted(src("tool://mail")).with_sensitivity(Sensitivity::Secret),
),
),
]);
assert_eq!(
nested.label_at("/recipient/addr").map(|l| l.sensitivity),
Some(Sensitivity::Public),
"a descendant of a tracked field inherited the whole-value label \
instead of its own ancestor's, so a field-scoped ceiling reads as \
the join of everything beside it"
);
assert_eq!(
nested.label().sensitivity,
Sensitivity::Secret,
"the fixture no longer distinguishes the two, so the assertion \
above would pass under either reading"
);
}
#[test]
fn field_provenance_is_reported_rebased_and_required() {
let value = Tainted::object([
(
"recipient".to_owned(),
Tainted::with_label(
serde_json::json!("ops@example.com"),
Label::untrusted(src("tool://mail")),
),
),
(
"amount".to_owned(),
Tainted::with_label(serde_json::json!(10), Label::trusted()),
),
]);
let reported: Vec<&str> = value.field_labels().map(|(p, _)| p).collect();
assert_eq!(
reported,
vec!["/amount", "/recipient"],
"field labels are not reported in stable pointer order, so an audit \
of who influenced which field reads differently run to run"
);
assert_eq!(
value.label_at("/recipient").map(|l| l.trust),
Some(Trust::Untrusted),
"the tracked field's own label was not returned"
);
assert_eq!(
value.label_at("/amount").map(|l| l.trust),
Some(Trust::Trusted),
"a trusted sibling inherited its untrusted neighbour's label"
);
assert!(
value.label_at("/absent").is_none(),
"a path that is not in the value reported a label, so a sink would \
check a field that is not being sent"
);
let release = Release {
scope: ReleaseScope {
trust: true,
sensitivity: None,
},
basis: "reviewed".to_owned(),
destination: "tool://ledger/post".to_owned(),
fields: ["/recipient".to_owned(), "/absent".to_owned()]
.into_iter()
.collect(),
evidence: ["ticket:1".to_owned()].into_iter().collect(),
};
assert!(
value.clone().apply_release(&release).is_none(),
"a release naming one tracked field and one untracked field was \
applied, so the untracked half was promoted on the strength of the \
whole-value label — precision the runtime does not have"
);
let untracked = Tainted::with_label(
serde_json::json!({ "a": "x" }),
Label::untrusted(src("tool://mail")),
);
let over_untracked = Release {
scope: ReleaseScope {
trust: true,
sensitivity: None,
},
basis: "reviewed".to_owned(),
destination: "tool://ledger/post".to_owned(),
fields: ["/a".to_owned()].into_iter().collect(),
evidence: ["ticket:1".to_owned()].into_iter().collect(),
};
assert!(
untracked.apply_release(&over_untracked).is_none(),
"a field present in the value but with no tracked lineage was \
released, so the promotion rests on the whole-value label while \
the decision reads as field-scoped"
);
let ok = Release {
fields: ["/recipient".to_owned()].into_iter().collect(),
..release
};
let good = value
.clone()
.apply_release(&ok)
.expect("a release over a field with real lineage was refused");
assert_eq!(
good.label_at("/recipient").map(|l| l.trust),
Some(Trust::Trusted),
"the released field was not promoted"
);
}
}
#[cfg(test)]
mod release_validation_tests {
use std::collections::BTreeSet;
use super::{ProtectedField, Release, ReleaseScope, Sensitivity, SourceId, is_json_pointer};
fn sound() -> Release {
Release::fields(
ReleaseScope::trust(),
["/recipient".to_owned()],
"operator matched the account to settlement SET-42",
"tool://ledger/transfer",
["approval:SET-42".to_owned()],
)
}
#[test]
fn the_baseline_is_accepted() {
sound()
.validate()
.expect("the baseline must pass, or every case below rejects for the wrong reason");
}
#[test]
fn a_scope_that_improves_nothing_is_refused() {
let mut r = sound();
r.scope = ReleaseScope {
trust: false,
sensitivity: None,
};
assert!(r.validate().is_err(), "a no-op release was accepted");
}
#[test]
fn an_unexplained_or_undirected_release_is_refused() {
for (field, broken) in [
("basis", {
let mut r = sound();
r.basis = " ".to_owned();
r
}),
("destination", {
let mut r = sound();
r.destination = String::new();
r
}),
] {
assert!(
broken.validate().is_err(),
"a release with an empty {field} was accepted, so the journal \
would record a decision that answers nothing"
);
}
}
#[test]
fn a_field_scope_that_is_neither_whole_nor_valid_pointers_is_refused() {
let empty = {
let mut r = sound();
r.fields.clear();
r
};
assert!(
empty.validate().is_err(),
"a release selecting nothing was accepted"
);
let both = {
let mut r = sound();
r.fields.insert(String::new());
r
};
assert!(
both.validate().is_err(),
"whole-value and field selection were accepted together, so the \
broader one applies while the declaration reads narrow"
);
let malformed = {
let mut r = sound();
r.fields.clear();
r.fields.insert("recipient".to_owned()); r
};
assert!(
malformed.validate().is_err(),
"a path that is not a JSON Pointer was accepted, so it would match \
no field and release nothing while reading as a release"
);
}
#[test]
fn a_json_pointer_is_judged_by_rfc_6901_escapes() {
for good in [
"/a", "/a/b", "/", "/a~0b", "/a~1b", "/a~0", "/a~1", "/~0~1", "/a~01b", ] {
assert!(
is_json_pointer(good),
"{good} is a valid RFC 6901 pointer and was refused, so a \
legitimate protected field would be rejected at parse"
);
}
for bad in [
"", "a", "a/b", "/a~", "/~", "/a~2b", "/a~xb", "/~9",
] {
assert!(
!is_json_pointer(bad),
"{bad:?} is not a valid RFC 6901 pointer and was accepted, so a \
rule written with it would match no field and guard nothing"
);
}
}
#[test]
fn a_protected_field_must_constrain_something_and_not_contradict_itself() {
let trusted_and_sources = ProtectedField {
path: "/recipient".to_owned(),
require_trusted: true,
allowed_sources: BTreeSet::from([SourceId::new("crm")]),
max_sensitivity: None,
};
assert!(
trusted_and_sources.validate().is_err(),
"require_trusted beside allowed_sources was accepted; one demands \
the lattice's top and the other names who may supply it, so a \
reviewer cannot tell which is in force"
);
let constrains_nothing = ProtectedField {
path: "/recipient".to_owned(),
require_trusted: false,
allowed_sources: BTreeSet::new(),
max_sensitivity: None,
};
assert!(
constrains_nothing.validate().is_err(),
"a rule with no trust, source or sensitivity constraint was \
accepted, so a field reads as protected while permitting anything"
);
for (label, field) in [
(
"trust alone",
ProtectedField {
path: "/recipient".to_owned(),
require_trusted: true,
allowed_sources: BTreeSet::new(),
max_sensitivity: None,
},
),
(
"sources alone",
ProtectedField {
path: "/recipient".to_owned(),
require_trusted: false,
allowed_sources: BTreeSet::from([SourceId::new("crm")]),
max_sensitivity: None,
},
),
(
"a sensitivity ceiling alone",
ProtectedField {
path: "/recipient".to_owned(),
require_trusted: false,
allowed_sources: BTreeSet::new(),
max_sensitivity: Some(Sensitivity::Internal),
},
),
] {
assert!(
field.validate().is_ok(),
"{label} is a legitimate rule and was refused"
);
}
let bad_path = ProtectedField {
path: "recipient".to_owned(),
require_trusted: true,
allowed_sources: BTreeSet::new(),
max_sensitivity: None,
};
assert!(
bad_path.validate().is_err(),
"a path that is not a JSON Pointer was accepted"
);
}
#[test]
fn a_single_named_field_is_a_release_and_not_a_contradiction() {
let one = {
let mut r = sound();
r.fields.clear();
r.fields.insert("/recipient".to_owned());
r
};
assert!(
one.validate().is_ok(),
"releasing exactly one named field was refused, which is the \
ordinary shape of a field-scoped release"
);
}
#[test]
fn a_release_reports_the_decision_it_was_given() {
let release = Release::fields(
ReleaseScope::trust(),
["/recipient".to_owned()],
"four-eyes approval AC-1",
"tool://ledger/post_entry",
["ticket:AC-1".to_owned(), "reviewer:sam".to_owned()],
);
assert_eq!(
release.basis(),
"four-eyes approval AC-1",
"the basis an audit reports is not the one the releaser gave"
);
assert_eq!(
release.destination(),
"tool://ledger/post_entry",
"the destination an audit reports is not the sink the release named"
);
assert_eq!(
release.fields_scope(),
&["/recipient".to_owned()].into_iter().collect(),
"the field scope an audit reports is not what was released"
);
assert_eq!(
release.evidence(),
&["reviewer:sam".to_owned(), "ticket:AC-1".to_owned()]
.into_iter()
.collect(),
"the evidence an audit reports is not what was supplied — evidence \
is the difference between a decision and an assertion"
);
assert!(
!release.is_whole_value(),
"a field-scoped release reported itself as whole-value, which is a \
broader claim than was authorized"
);
let whole = Release::whole(
ReleaseScope::trust(),
"operator override",
"tool://ledger/post_entry",
["ticket:AC-2".to_owned()],
);
assert!(
whole.is_whole_value(),
"a whole-value release did not report itself as one"
);
}
#[test]
fn a_protected_field_constructor_refuses_a_path_that_is_not_a_pointer() {
for build in [
(|| ProtectedField::trusted("recipient")) as fn() -> ProtectedField,
|| ProtectedField::from_sources("recipient", [SourceId::new("crm")]),
] {
let refused = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build));
assert!(
refused.is_err(),
"a path with no leading solidus was accepted, so the rule would \
match no field and guard nothing"
);
}
let ok = ProtectedField::trusted("/recipient");
assert_eq!(ok.path(), "/recipient");
}
#[test]
fn a_release_with_no_usable_evidence_is_refused() {
let none = {
let mut r = sound();
r.evidence.clear();
r
};
assert!(
none.validate().is_err(),
"an evidence-free release was accepted"
);
let blank = {
let mut r = sound();
r.evidence.clear();
r.evidence.insert(" ".to_owned());
r
};
assert!(
blank.validate().is_err(),
"blank evidence was accepted, which passes the count and says nothing"
);
}
#[test]
fn improving_sensitivity_alone_is_accepted() {
let mut r = sound();
r.scope = ReleaseScope {
trust: false,
sensitivity: Some(Sensitivity::Internal),
};
r.validate()
.expect("a sensitivity-only release is a legitimate decision");
}
}