use lean_ctx_protocol::context_gateway::ClassificationV1;
use serde::{Deserialize, Serialize};
use super::carrier::{RESTRICTED_CODE, Withheld, admit_carrier_text_under, current_policy};
use super::{AdmissionPolicy, fingerprint_digest};
const BINDING_VERSION: u8 = 2;
const BINDING_MISMATCH_CODE: &str = "store.binding_mismatch";
const PROJECT_MISMATCH_CODE: &str = "store.project_mismatch";
const PROJECT_UNRESOLVED_CODE: &str = "store.project_unresolved";
const SOURCE_CHANGED_CODE: &str = "store.source_changed";
const LINEAGE_UNBOUNDED_CODE: &str = "store.lineage_unbounded";
const MAX_SOURCES: usize = crate::core::archive::authority::MAX_SOURCES;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceRef {
path: String,
tool: String,
admitted_digest: String,
classification: ClassificationV1,
}
impl SourceRef {
pub(crate) fn read(path: &str, root: &str, tool: &str) -> Result<(String, Self), Withheld> {
let mut remaining = crate::core::limits::max_read_bytes();
let read = Self::current(path, root, tool, &mut remaining)?;
let source = Self {
path: path.to_owned(),
tool: tool.to_owned(),
admitted_digest: content_digest(&read.content),
classification: read.classification,
};
Ok((read.content, source))
}
pub(crate) fn read_as_admitted(
path: &str,
root: &str,
tool: &str,
admitted_digest: &str,
) -> Option<Self> {
Self::read(path, root, tool)
.ok()
.map(|(_, source)| source)
.filter(|source| source.admitted_digest == admitted_digest)
}
pub(crate) fn read_as_admitted_within(
path: &str,
root: &str,
tool: &str,
admitted_digest: &str,
remaining: &mut usize,
) -> Option<Self> {
let read = Self::current(path, root, tool, remaining).ok()?;
let source = Self {
path: path.to_owned(),
tool: tool.to_owned(),
admitted_digest: content_digest(&read.content),
classification: read.classification,
};
(source.admitted_digest == admitted_digest).then_some(source)
}
fn current(
path: &str,
root: &str,
tool: &str,
remaining: &mut usize,
) -> Result<crate::core::rooted_read::AdmittedRootedRead, Withheld> {
let read = crate::core::rooted_read::read_file_for_tool_rooted_admitted(
path, root, tool, remaining,
)
.map_err(|_| Withheld::source_unavailable())?;
if !read.storable || !read.complete {
return Err(Withheld::new(&[RESTRICTED_CODE]));
}
Ok(read)
}
fn reauthorize(&self, root: &str, remaining: &mut usize) -> Result<(), Withheld> {
let read = Self::current(&self.path, root, &self.tool, remaining)?;
if content_digest(&read.content) != self.admitted_digest
|| read.classification > self.classification
{
return Err(Withheld::new(&[SOURCE_CHANGED_CODE]));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum Lineage {
CallerSupplied,
Sources { sources: Vec<SourceRef> },
}
impl Lineage {
pub(crate) fn of(mut sources: Vec<SourceRef>) -> Result<Self, Withheld> {
let mut unique: Vec<SourceRef> = Vec::with_capacity(sources.len());
for source in sources.drain(..) {
if !unique.contains(&source) {
unique.push(source);
}
}
if unique.len() > MAX_SOURCES {
return Err(Withheld::new(&[LINEAGE_UNBOUNDED_CODE]));
}
Ok(if unique.is_empty() {
Self::CallerSupplied
} else {
Self::Sources { sources: unique }
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoredBinding {
version: u8,
origin: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
project: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
writer_role: Option<String>,
policy_digest: String,
content_digest: String,
classification: ClassificationV1,
lineage: Lineage,
}
impl StoredBinding {
pub(crate) fn binds(&self, text: &str) -> bool {
self.version == BINDING_VERSION && self.content_digest == content_digest(text)
}
pub(crate) fn origin(&self) -> &str {
&self.origin
}
pub(crate) fn file_sources(&self) -> Option<(&str, Vec<(&str, &str, &str)>)> {
let (Lineage::Sources { sources }, Some(project)) =
(&self.lineage, self.project.as_deref())
else {
return None;
};
let files = sources
.iter()
.map(|source| {
(
source.path.as_str(),
source.tool.as_str(),
source.admitted_digest.as_str(),
)
})
.collect();
Some((project, files))
}
#[cfg(test)]
pub(crate) fn source_bound(&self) -> bool {
matches!(self.lineage, Lineage::Sources { .. })
}
}
pub fn publish_served(binding: Option<&StoredBinding>) {
use crate::core::archive::authority::{self, ArchiveAuthority};
if authority::current_capture().is_none() || !source_authority_required() {
return;
}
let bound = binding.and_then(|binding| match (&binding.lineage, &binding.project) {
(Lineage::Sources { sources }, Some(project)) => Some((project, sources)),
_ => None,
});
let Some((project, sources)) = bound else {
authority::mark_unbound();
return;
};
for source in sources {
if let Some(source) = ArchiveAuthority::admitted_source(
std::path::Path::new(project),
&source.path,
&source.tool,
&source.admitted_digest,
) {
source.publish();
}
}
}
fn canonical_project(root: &str) -> Option<String> {
crate::core::pathutil::canonicalize_secure(std::path::Path::new(root))
.ok()
.and_then(|path| path.to_str().map(str::to_owned))
}
fn resolve_project(project: Option<&str>) -> Option<String> {
project
.filter(|root| !root.is_empty())
.map(|root| canonical_project(root).unwrap_or_else(|| root.to_owned()))
}
fn content_digest(text: &str) -> String {
blake3::hash(text.as_bytes()).to_hex().to_string()
}
fn policy_digest(policy: &AdmissionPolicy) -> String {
fingerprint_digest(&policy.fingerprint()).hex().to_owned()
}
#[must_use]
pub fn source_authority_required() -> bool {
protected(¤t_policy())
}
#[must_use]
pub(crate) fn source_authority_required_under(policy: &AdmissionPolicy) -> bool {
protected(policy)
}
fn protected(policy: &AdmissionPolicy) -> bool {
policy.config_unloadable()
|| policy.detectors_mandatory()
|| crate::core::policy::runtime::is_active()
}
pub(crate) fn admit_and_bind(
text: &str,
origin: &str,
project: Option<&str>,
lineage: Lineage,
) -> Result<(String, StoredBinding), Withheld> {
let policy = current_policy();
let project = resolve_project(project);
if project.is_none() && protected(&policy) {
return Err(Withheld::new(&[PROJECT_UNRESOLVED_CODE]));
}
let admission = super::semantic_deferred::for_stored_context(|| {
admit_carrier_text_under(text, Some(&policy))
})?;
let mut classification = admission.classification;
if let Lineage::Sources { sources } = &lineage {
for source in sources {
classification = classification.join(source.classification);
}
}
if !admission.storable || classification >= ClassificationV1::Restricted {
return Err(Withheld::new(&[RESTRICTED_CODE]));
}
let binding = StoredBinding {
version: BINDING_VERSION,
origin: origin.to_owned(),
project,
writer_role: crate::core::policy::runtime::role_fingerprint(
&crate::core::roles::active_role(),
)
.ok()
.map(|fingerprint| fingerprint.to_hex().to_string()),
policy_digest: policy_digest(&policy),
content_digest: content_digest(&admission.text),
classification,
lineage,
};
Ok((admission.text, binding))
}
pub(crate) fn serve_stored(
text: &str,
binding: Option<&StoredBinding>,
project: Option<&str>,
) -> Result<String, Withheld> {
let policy = current_policy();
let protected = protected(&policy);
let Some(binding) = binding else {
if protected {
return Err(Withheld::no_source_authority());
}
return admit_stored_text(text, &policy);
};
if binding.version != BINDING_VERSION || binding.content_digest != content_digest(text) {
return Err(Withheld::new(&[BINDING_MISMATCH_CODE]));
}
let reader = resolve_project(project);
match (binding.project.as_deref(), reader.as_deref()) {
(Some(bound), Some(reader)) if bound == reader => {}
(Some(_), Some(_)) => return Err(Withheld::new(&[PROJECT_MISMATCH_CODE])),
_ if protected => return Err(Withheld::new(&[PROJECT_UNRESOLVED_CODE])),
_ => {}
}
if let Lineage::Sources { sources } = &binding.lineage {
let Some(root) = binding.project.as_deref() else {
return Err(Withheld::no_source_authority());
};
let mut remaining = crate::core::limits::max_read_bytes();
crate::core::archive::authority::without_capture(|| {
sources
.iter()
.try_for_each(|source| source.reauthorize(root, &mut remaining))
})?;
}
if binding.classification >= ClassificationV1::Restricted {
return Err(Withheld::new(&[RESTRICTED_CODE]));
}
admit_stored_text(text, &policy)
}
fn admit_stored_text(text: &str, policy: &AdmissionPolicy) -> Result<String, Withheld> {
let admission = super::semantic_deferred::for_stored_context(|| {
admit_carrier_text_under(text, Some(policy))
})?;
if admission.storable {
Ok(admission.text)
} else {
Err(Withheld::new(&[RESTRICTED_CODE]))
}
}
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "test-support"))]
#[cfg_attr(not(test), allow(unused_imports))]
pub mod lineage_support;