Decompose oversized Rust files by responsibility
This commit is contained in:
parent
03bfe92ca0
commit
ee9201b057
1 changed files with 18648 additions and 18513 deletions
308
crates/lazaret-core/src/configuration/validation.rs
Normal file
308
crates/lazaret-core/src/configuration/validation.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
/// Derives the specification-framed v1 Domain ID.
|
||||
///
|
||||
/// # Errors
|
||||
/// A field longer than `u32::MAX` cannot be framed.
|
||||
pub fn derive_domain_id(
|
||||
selected_profile: &str,
|
||||
canonical_workspace: &[u8],
|
||||
) -> Result<DomainId, ConfigurationError> {
|
||||
let mut preimage = b"lazaret-domain-id-v1\0".to_vec();
|
||||
push_bytes(&mut preimage, selected_profile.as_bytes())?;
|
||||
push_bytes(&mut preimage, canonical_workspace)?;
|
||||
Ok(DomainId::new(Sha256::digest(preimage).into()))
|
||||
}
|
||||
|
||||
/// Derives the specification-framed v1 Configuration Fingerprint.
|
||||
///
|
||||
/// # Errors
|
||||
/// A field or collection longer than `u32::MAX` cannot be framed.
|
||||
pub fn derive_configuration_fingerprint(
|
||||
record: &ConfigurationFingerprintRecordV1,
|
||||
) -> Result<ConfigurationFingerprint, ConfigurationError> {
|
||||
let mut preimage = b"lazaret-configuration-fingerprint-v1\0".to_vec();
|
||||
push_bytes(&mut preimage, record.selected_profile.as_bytes())?;
|
||||
push_bytes(&mut preimage, record.storage_root.as_bytes())?;
|
||||
push_bytes(&mut preimage, record.runtime_root.as_bytes())?;
|
||||
push_bytes(&mut preimage, record.image.as_bytes())?;
|
||||
push_bool(&mut preimage, record.isolate_gitignored);
|
||||
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.persistent_dirs.iter().map(|path| {
|
||||
let mut item = Vec::new();
|
||||
push_bytes(&mut item, path.as_bytes())?;
|
||||
Ok(item)
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.environment_references.iter().map(|reference| {
|
||||
let mut item = Vec::new();
|
||||
push_bytes(&mut item, reference.sandbox_name.as_bytes())?;
|
||||
push_bytes(&mut item, reference.host_name.as_bytes())?;
|
||||
Ok(item)
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(&mut preimage, record.volumes.iter().map(encode_volume))?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record
|
||||
.effective_allow_prefixes
|
||||
.iter()
|
||||
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record
|
||||
.effective_deny_prefixes
|
||||
.iter()
|
||||
.map(|prefix| Ok([prefix.0.as_slice(), &[prefix.1]].concat())),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.denied.iter().map(|target| {
|
||||
Ok([
|
||||
target.address.as_slice(),
|
||||
target.port.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat())
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.permitted.iter().map(|target| {
|
||||
Ok([
|
||||
target.address.as_slice(),
|
||||
target.port.to_be_bytes().as_slice(),
|
||||
]
|
||||
.concat())
|
||||
}),
|
||||
)?;
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.network_readiness.dns.iter().map(|target| {
|
||||
let mut item = target.resolver.to_vec();
|
||||
push_bytes(&mut item, target.query.as_bytes())?;
|
||||
Ok(item)
|
||||
}),
|
||||
)?;
|
||||
push_bool(&mut preimage, record.escalation_enabled);
|
||||
push_sorted_list(
|
||||
&mut preimage,
|
||||
record.auto_approval_rules.iter().map(encode_auto_approval),
|
||||
)?;
|
||||
Ok(ConfigurationFingerprint::new(
|
||||
Sha256::digest(preimage).into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn validate_structural_configuration(
|
||||
configuration: &TrustedHostConfigurationV1,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
if configuration.schema_id() != TRUSTED_HOST_CONFIGURATION_SCHEMA_ID {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidToml,
|
||||
"unknown configuration schema",
|
||||
));
|
||||
}
|
||||
for (name, profile) in configuration.profiles() {
|
||||
validate_profile_name(name)?;
|
||||
validate_text(&profile.image, "profile image")?;
|
||||
validate_image_reference(&profile.image)?;
|
||||
require_pairwise_non_ancestral(
|
||||
profile.persistent_dirs.iter().map(String::as_str),
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
)?;
|
||||
for persistent in &profile.persistent_dirs {
|
||||
validate_sandbox_path(persistent)?;
|
||||
}
|
||||
for (sandbox_name, reference) in &profile.env {
|
||||
validate_environment_name(sandbox_name)?;
|
||||
let host_name = reference.strip_prefix("env:").ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"environment value is not an env: reference",
|
||||
)
|
||||
})?;
|
||||
validate_environment_name(host_name)?;
|
||||
}
|
||||
for volume in &profile.volumes {
|
||||
validate_declared_host_path(&volume.host, true)?;
|
||||
validate_sandbox_path(&volume.container)?;
|
||||
}
|
||||
if !profile.escalation.enabled && !profile.escalation.auto_approve.is_empty() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidAutoApproval,
|
||||
"disabled escalation profile contains auto-approval rules",
|
||||
));
|
||||
}
|
||||
for rule in &profile.escalation.auto_approve {
|
||||
validate_declared_host_path(&rule.executable, false)?;
|
||||
for argument in &rule.args {
|
||||
validate_text(argument, "auto-approval argument")?;
|
||||
}
|
||||
if rule.allow_extra_args && rule.args.is_empty() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidAutoApproval,
|
||||
"allow_extra_args requires a nonempty configured prefix",
|
||||
));
|
||||
}
|
||||
}
|
||||
for prefix in configuration
|
||||
.network()
|
||||
.default_allow
|
||||
.iter()
|
||||
.chain(configuration.network().default_deny.iter())
|
||||
.chain(profile.allow_egress.iter())
|
||||
.chain(profile.deny_egress.iter())
|
||||
{
|
||||
parse_ipv4_prefix(prefix)?;
|
||||
}
|
||||
}
|
||||
if let Some(data_root) = &configuration.storage().data_root {
|
||||
validate_declared_host_path(data_root, false)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_image_reference(reference: &str) -> Result<(), ConfigurationError> {
|
||||
let valid = !reference.is_empty()
|
||||
&& reference.is_ascii()
|
||||
&& !reference
|
||||
.bytes()
|
||||
.any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
|
||||
&& !reference.contains("://")
|
||||
&& !reference.starts_with(['/', ':', '@'])
|
||||
&& !reference.ends_with(['/', ':', '@'])
|
||||
&& !reference.contains("//")
|
||||
&& reference.split('@').count() <= 2;
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"profile image is not a valid v1 OCI reference",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_profile_name(name: &str) -> Result<(), ConfigurationError> {
|
||||
let bytes = name.as_bytes();
|
||||
let valid = (1..=64).contains(&bytes.len())
|
||||
&& bytes[0].is_ascii_alphanumeric()
|
||||
&& bytes
|
||||
.iter()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
|
||||
&& !matches!(name, "." | "..");
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"profile name violates the v1 grammar",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_environment_name(name: &str) -> Result<(), ConfigurationError> {
|
||||
let mut bytes = name.bytes();
|
||||
let valid = bytes
|
||||
.next()
|
||||
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
|
||||
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"environment name violates the v1 grammar",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_text(value: &str, field: &str) -> Result<(), ConfigurationError> {
|
||||
if value.contains('\0') {
|
||||
Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
format!("{field} contains NUL"),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_declared_host_path(
|
||||
declaration: &str,
|
||||
allow_profile: bool,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
validate_text(declaration, "host path")?;
|
||||
let suffix = if let Some(suffix) = declaration.strip_prefix('/') {
|
||||
suffix
|
||||
} else if let Some(suffix) = declaration.strip_prefix("~/") {
|
||||
suffix
|
||||
} else if allow_profile {
|
||||
declaration.strip_prefix("{profile_dir}/").ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"host path is not absolute or an allowed anchored form",
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"host path is not absolute or ~/ anchored",
|
||||
));
|
||||
};
|
||||
validate_path_suffix(suffix)
|
||||
}
|
||||
|
||||
fn validate_sandbox_path(path: &str) -> Result<(), ConfigurationError> {
|
||||
validate_text(path, "sandbox path")?;
|
||||
let suffix = path.strip_prefix('/').ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"sandbox path is not absolute",
|
||||
)
|
||||
})?;
|
||||
validate_path_suffix(suffix)
|
||||
}
|
||||
|
||||
fn validate_path_suffix(suffix: &str) -> Result<(), ConfigurationError> {
|
||||
if suffix.is_empty()
|
||||
|| suffix.ends_with('/')
|
||||
|| suffix
|
||||
.split('/')
|
||||
.any(|component| component.is_empty() || matches!(component, "." | ".."))
|
||||
|| suffix.bytes().any(|byte| {
|
||||
byte.is_ascii_control()
|
||||
|| matches!(
|
||||
byte,
|
||||
b'*' | b'?'
|
||||
| b'['
|
||||
| b']'
|
||||
| b'{'
|
||||
| b'}'
|
||||
| b'$'
|
||||
| b'~'
|
||||
| b'\\'
|
||||
| b'`'
|
||||
| b'\''
|
||||
| b'"'
|
||||
| b';'
|
||||
| b'|'
|
||||
| b'&'
|
||||
| b'<'
|
||||
| b'>'
|
||||
| b'('
|
||||
| b')'
|
||||
| b'!'
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"path is empty, non-normalized, or contains expansion syntax",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Reference in a new issue