Implement trusted-host configuration resolution
This commit is contained in:
parent
f106a9f636
commit
f9eb966341
9 changed files with 3468 additions and 39 deletions
218
Cargo.lock
generated
218
Cargo.lock
generated
|
|
@ -2,6 +2,27 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
|
|
@ -11,6 +32,41 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
|
|
@ -23,6 +79,47 @@ version = "0.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazaret-client"
|
||||
version = "0.0.0"
|
||||
|
|
@ -35,7 +132,11 @@ name = "lazaret-core"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"postcard",
|
||||
"rustix",
|
||||
"serde",
|
||||
"sha2",
|
||||
"static_assertions",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -66,6 +167,18 @@ dependencies = [
|
|||
"lazaret-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
|
|
@ -96,6 +209,19 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
|
|
@ -126,6 +252,32 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
|
|
@ -157,8 +309,74 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ license = "Apache-2.0"
|
|||
|
||||
[workspace.dependencies]
|
||||
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
||||
rustix = { version = "=1.1.4", features = ["fs", "process"] }
|
||||
serde = { version = "=1.0.228", features = ["derive"] }
|
||||
sha2 = "=0.11.0"
|
||||
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ license.workspace = true
|
|||
|
||||
[dependencies]
|
||||
postcard.workspace = true
|
||||
rustix.workspace = true
|
||||
serde.workspace = true
|
||||
sha2.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
static_assertions = "=1.1.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
1992
crates/lazaret-core/src/configuration.rs
Normal file
1992
crates/lazaret-core/src/configuration.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
#![doc = "Shared production interfaces for Lazaret processes."]
|
||||
|
||||
pub mod codec;
|
||||
pub mod configuration;
|
||||
pub mod identity;
|
||||
pub mod records;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
|
|
@ -20,6 +21,199 @@ pub const STORAGE_CATALOG_SCHEMA_ID: &str = "lazaret-storage-catalog-v1";
|
|||
pub const DOMAIN_STORE_SCHEMA_VERSION: u32 = 2;
|
||||
pub const LEGACY_DOMAIN_STORE_SCHEMA_VERSION: u32 = 1;
|
||||
pub const STORAGE_CATALOG_SCHEMA_VERSION: u32 = 1;
|
||||
pub const TRUSTED_HOST_CONFIGURATION_SCHEMA_ID: &str = "lazaret-trusted-host-config-v1";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StorageConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub data_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub default_allow: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub default_deny: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum VolumeKind {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum VolumeAccess {
|
||||
ReadOnly,
|
||||
#[default]
|
||||
ReadWrite,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VolumeConfigurationV1 {
|
||||
pub host: String,
|
||||
pub container: String,
|
||||
pub kind: VolumeKind,
|
||||
#[serde(default)]
|
||||
pub access: VolumeAccess,
|
||||
#[serde(default)]
|
||||
pub create: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AutoApprovalRuleConfigurationV1 {
|
||||
pub executable: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_extra_args: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EscalationConfigurationV1 {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub auto_approve: Vec<AutoApprovalRuleConfigurationV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ProfileConfigurationV1 {
|
||||
pub image: String,
|
||||
#[serde(default = "default_isolate_gitignored")]
|
||||
pub isolate_gitignored: bool,
|
||||
#[serde(default)]
|
||||
pub persistent_dirs: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub allow_egress: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub deny_egress: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub env: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub volumes: Vec<VolumeConfigurationV1>,
|
||||
#[serde(default)]
|
||||
pub escalation: EscalationConfigurationV1,
|
||||
}
|
||||
|
||||
const fn default_isolate_gitignored() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct TrustedHostConfigurationV1 {
|
||||
#[serde(rename = "schema")]
|
||||
schema_id: String,
|
||||
storage: StorageConfigurationV1,
|
||||
network: NetworkConfigurationV1,
|
||||
profiles: BTreeMap<String, ProfileConfigurationV1>,
|
||||
}
|
||||
|
||||
impl TrustedHostConfigurationV1 {
|
||||
#[must_use]
|
||||
pub fn schema_id(&self) -> &str {
|
||||
&self.schema_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn storage(&self) -> &StorageConfigurationV1 {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn network(&self) -> &NetworkConfigurationV1 {
|
||||
&self.network
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn profiles(&self) -> &BTreeMap<String, ProfileConfigurationV1> {
|
||||
&self.profiles
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TrustedHostConfigurationV1 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Fields {
|
||||
#[serde(rename = "schema")]
|
||||
schema_id: String,
|
||||
#[serde(default)]
|
||||
storage: StorageConfigurationV1,
|
||||
#[serde(default)]
|
||||
network: NetworkConfigurationV1,
|
||||
profiles: BTreeMap<String, ProfileConfigurationV1>,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.schema_id != TRUSTED_HOST_CONFIGURATION_SCHEMA_ID {
|
||||
return Err(serde::de::Error::custom(
|
||||
"unknown trusted-host configuration schema",
|
||||
));
|
||||
}
|
||||
if fields.profiles.is_empty() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"trusted-host configuration has no profiles",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
schema_id: fields.schema_id,
|
||||
storage: fields.storage,
|
||||
network: fields.network,
|
||||
profiles: fields.profiles,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct EnvironmentReference {
|
||||
pub sandbox_name: String,
|
||||
pub host_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct ResolvedVolume {
|
||||
pub canonical_host_source: HostPath,
|
||||
pub container_path: String,
|
||||
pub kind: VolumeKind,
|
||||
pub access: VolumeAccess,
|
||||
pub create: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub struct ResolvedAutoApprovalRule {
|
||||
pub canonical_executable: HostPath,
|
||||
pub args: Vec<String>,
|
||||
pub allow_extra_args: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct ConfigurationFingerprintRecordV1 {
|
||||
pub selected_profile: String,
|
||||
pub storage_root: HostPath,
|
||||
pub runtime_root: HostPath,
|
||||
pub image: String,
|
||||
pub isolate_gitignored: bool,
|
||||
pub persistent_dirs: Vec<String>,
|
||||
pub environment_references: Vec<EnvironmentReference>,
|
||||
pub volumes: Vec<ResolvedVolume>,
|
||||
pub effective_allow_prefixes: Vec<([u8; 4], u8)>,
|
||||
pub effective_deny_prefixes: Vec<([u8; 4], u8)>,
|
||||
pub escalation_enabled: bool,
|
||||
pub auto_approval_rules: Vec<ResolvedAutoApprovalRule>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct StoreMetadata {
|
||||
|
|
@ -1493,15 +1687,138 @@ pub struct SignedProtocolRecord {
|
|||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct MailboxRequest {
|
||||
pub protocol_version: ProtocolVersion,
|
||||
pub session_binding: SessionBinding,
|
||||
pub request_id: RequestId,
|
||||
pub session_capability: SessionRequestCapability,
|
||||
pub sandbox_credential_id: ProtocolCredentialId,
|
||||
pub command_argv: Vec<Vec<u8>>,
|
||||
pub justification: String,
|
||||
protocol_version: ProtocolVersion,
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MailboxRequestError {
|
||||
EmptyCommand,
|
||||
InvalidArgument,
|
||||
NonAbsoluteExecutable,
|
||||
}
|
||||
|
||||
impl fmt::Display for MailboxRequestError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::EmptyCommand => formatter.write_str("command argument vector is empty"),
|
||||
Self::InvalidArgument => formatter.write_str("command argument is not NUL-free UTF-8"),
|
||||
Self::NonAbsoluteExecutable => {
|
||||
formatter.write_str("command executable is not an absolute path")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MailboxRequestError {}
|
||||
|
||||
impl MailboxRequest {
|
||||
/// # Errors
|
||||
/// The command must contain an absolute executable followed by NUL-free UTF-8 arguments.
|
||||
pub fn new(
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
) -> Result<Self, MailboxRequestError> {
|
||||
let Some(executable) = command_argv.first() else {
|
||||
return Err(MailboxRequestError::EmptyCommand);
|
||||
};
|
||||
if command_argv
|
||||
.iter()
|
||||
.any(|argument| argument.contains(&0) || std::str::from_utf8(argument).is_err())
|
||||
{
|
||||
return Err(MailboxRequestError::InvalidArgument);
|
||||
}
|
||||
if !executable.starts_with(b"/") {
|
||||
return Err(MailboxRequestError::NonAbsoluteExecutable);
|
||||
}
|
||||
Ok(Self {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding,
|
||||
request_id,
|
||||
session_capability,
|
||||
sandbox_credential_id,
|
||||
command_argv,
|
||||
justification,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn protocol_version(&self) -> ProtocolVersion {
|
||||
self.protocol_version
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn session_binding(&self) -> &SessionBinding {
|
||||
&self.session_binding
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn request_id(&self) -> RequestId {
|
||||
self.request_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn session_capability(&self) -> &SessionRequestCapability {
|
||||
&self.session_capability
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn sandbox_credential_id(&self) -> ProtocolCredentialId {
|
||||
self.sandbox_credential_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn command_argv(&self) -> &[Vec<u8>] {
|
||||
&self.command_argv
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn justification(&self) -> &str {
|
||||
&self.justification
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for MailboxRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Fields {
|
||||
protocol_version: ProtocolVersion,
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
}
|
||||
|
||||
let fields = Fields::deserialize(deserializer)?;
|
||||
if fields.protocol_version != ProtocolVersion::V1 {
|
||||
return Err(serde::de::Error::custom("unknown mailbox request version"));
|
||||
}
|
||||
Self::new(
|
||||
fields.session_binding,
|
||||
fields.request_id,
|
||||
fields.session_capability,
|
||||
fields.sandbox_credential_id,
|
||||
fields.command_argv,
|
||||
fields.justification,
|
||||
)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
|
|
@ -2701,13 +3018,13 @@ schema_types!(
|
|||
"DeniedDestinationObservation" => DeniedDestinationObservation,
|
||||
"PermittedDestinationObservation" => PermittedDestinationObservation,
|
||||
"DnsObservation" => DnsObservation,
|
||||
"TrustedHostConfigurationV1" => TrustedHostConfigurationV1,
|
||||
"ConfigurationFingerprintRecordV1" => ConfigurationFingerprintRecordV1,
|
||||
"EnvironmentReference" => EnvironmentReference,
|
||||
"ResolvedVolume" => ResolvedVolume,
|
||||
"ResolvedAutoApprovalRule" => ResolvedAutoApprovalRule,
|
||||
"DomainStore" => DomainStore,
|
||||
"StorageCatalog" => StorageCatalog;
|
||||
"TrustedHostConfigurationV1",
|
||||
"ConfigurationFingerprintRecordV1",
|
||||
"EnvironmentReference",
|
||||
"ResolvedVolume",
|
||||
"ResolvedAutoApprovalRule",
|
||||
);
|
||||
|
||||
pub const DECLARED_RUNTIME_SCHEMA_TYPE_NAMES: &[&str] = &[
|
||||
|
|
|
|||
735
crates/lazaret-core/tests/configuration.rs
Normal file
735
crates/lazaret-core/tests/configuration.rs
Normal file
|
|
@ -0,0 +1,735 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use lazaret_core::codec::{CodecError, decode, encode};
|
||||
use lazaret_core::configuration::{
|
||||
ConfigurationErrorKind, ConfigurationLoadRequest, ConfinedPathPlan, ConfinedPathType,
|
||||
ResolvedDomainConfiguration, SandboxLayoutV1, derive_configuration_fingerprint,
|
||||
derive_domain_id, load_trusted_configuration, parse_trusted_configuration,
|
||||
};
|
||||
use lazaret_core::identity::HostPath;
|
||||
use lazaret_core::records::{
|
||||
ConfigurationFingerprintRecordV1, EnvironmentReference, ResolvedAutoApprovalRule,
|
||||
ResolvedVolume, VolumeAccess, VolumeKind,
|
||||
};
|
||||
use static_assertions::assert_not_impl_any;
|
||||
|
||||
assert_not_impl_any!(ResolvedDomainConfiguration: serde::Serialize, serde::de::DeserializeOwned);
|
||||
assert_not_impl_any!(SandboxLayoutV1: serde::Serialize, serde::de::DeserializeOwned);
|
||||
assert_not_impl_any!(ConfinedPathPlan: serde::Serialize, serde::de::DeserializeOwned);
|
||||
|
||||
static NEXT_TREE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestTree(PathBuf);
|
||||
|
||||
impl TestTree {
|
||||
fn new() -> Self {
|
||||
let sequence = NEXT_TREE.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"lazaret-configuration-{}-{sequence}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestTree {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn write_configuration(tree: &TestTree, body: &str) -> PathBuf {
|
||||
let directory = tree.path().join("config");
|
||||
fs::create_dir(&directory).unwrap();
|
||||
let path = directory.join("config.toml");
|
||||
fs::write(&path, body).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn layout() -> SandboxLayoutV1 {
|
||||
SandboxLayoutV1::from_production_layout([
|
||||
"/workspace".to_owned(),
|
||||
"/proc".to_owned(),
|
||||
"/sys".to_owned(),
|
||||
"/dev".to_owned(),
|
||||
"/run/lazaret".to_owned(),
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn request(
|
||||
tree: &TestTree,
|
||||
config: PathBuf,
|
||||
environment: BTreeMap<String, std::ffi::OsString>,
|
||||
) -> ConfigurationLoadRequest {
|
||||
let workspace = tree.path().join("workspace");
|
||||
let runtime = tree.path().join("runtime");
|
||||
fs::create_dir_all(&workspace).unwrap();
|
||||
fs::create_dir_all(&runtime).unwrap();
|
||||
ConfigurationLoadRequest {
|
||||
explicit_configuration_path: Some(config),
|
||||
selected_profile: "default".into(),
|
||||
workspace,
|
||||
home: tree.path().join("home"),
|
||||
xdg_config_home: None,
|
||||
xdg_data_home: None,
|
||||
xdg_runtime_dir: runtime,
|
||||
environment,
|
||||
sandbox_layout: layout(),
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_configuration(tree: &TestTree) -> String {
|
||||
format!(
|
||||
r#"schema = "lazaret-trusted-host-config-v1"
|
||||
|
||||
[storage]
|
||||
data_root = "{}"
|
||||
|
||||
[network]
|
||||
default_allow = ["192.0.2.4/24", "198.51.100.53/32"]
|
||||
|
||||
[profiles.default]
|
||||
image = "example.invalid/agent:v1"
|
||||
persistent_dirs = ["/root"]
|
||||
allow_egress = ["10.1.2.3/24"]
|
||||
env = {{ TOKEN = "env:HOST_TOKEN" }}
|
||||
volumes = [
|
||||
{{ host = "{{profile_dir}}/agent-state", container = "/root/.local/state/agent", kind = "directory", access = "read-write", create = true }},
|
||||
]
|
||||
|
||||
[profiles.default.escalation]
|
||||
enabled = false
|
||||
"#,
|
||||
tree.path().join("data").display()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_parser_applies_defaults_and_rejects_every_unknown_level() {
|
||||
let minimal = r#"schema = "lazaret-trusted-host-config-v1"
|
||||
[profiles.default]
|
||||
image = "example.invalid/agent:v1"
|
||||
"#;
|
||||
let parsed = parse_trusted_configuration(minimal.as_bytes()).unwrap();
|
||||
let profile = &parsed.profiles()["default"];
|
||||
assert!(profile.isolate_gitignored);
|
||||
assert!(profile.persistent_dirs.is_empty());
|
||||
assert!(profile.allow_egress.is_empty());
|
||||
assert!(profile.deny_egress.is_empty());
|
||||
assert!(profile.env.is_empty());
|
||||
assert!(profile.volumes.is_empty());
|
||||
assert!(!profile.escalation.enabled);
|
||||
assert!(profile.escalation.auto_approve.is_empty());
|
||||
assert!(parsed.storage().data_root.is_none());
|
||||
assert!(parsed.network().default_allow.is_empty());
|
||||
assert!(parsed.network().default_deny.is_empty());
|
||||
|
||||
for invalid in [
|
||||
minimal.replace("schema", "unknown"),
|
||||
format!("{minimal}unknown = true\n"),
|
||||
minimal.replace(
|
||||
"image = \"example.invalid/agent:v1\"",
|
||||
"image = \"example.invalid/agent:v1\"\nunknown = true",
|
||||
),
|
||||
minimal.replace(
|
||||
"example.invalid/agent:v1",
|
||||
"https://example.invalid/agent:v1",
|
||||
),
|
||||
minimal.replace("example.invalid/agent:v1", ""),
|
||||
minimal.replace("-v1", "-v2"),
|
||||
] {
|
||||
assert!(parse_trusted_configuration(invalid.as_bytes()).is_err());
|
||||
}
|
||||
|
||||
let duplicate = format!("{minimal}schema = \"lazaret-trusted-host-config-v1\"\n");
|
||||
assert!(parse_trusted_configuration(duplicate.as_bytes()).is_err());
|
||||
|
||||
for invalid in [
|
||||
format!("{minimal}\n[storage]\nunknown = true\n"),
|
||||
format!("{minimal}\n[network]\nunknown = true\n"),
|
||||
r#"schema = "lazaret-trusted-host-config-v1"
|
||||
[profiles.default]
|
||||
image = "example.invalid/agent:v1"
|
||||
volumes = [{ host = "/source", container = "/root/source", kind = "directory", unknown = true }]
|
||||
"#
|
||||
.into(),
|
||||
format!("{minimal}\n[profiles.default.escalation]\nunknown = true\n"),
|
||||
format!(
|
||||
"{minimal}\n[profiles.default.escalation]\nenabled = true\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"/bin/true\"\nunknown = true\n"
|
||||
),
|
||||
r#"schema = "lazaret-trusted-host-config-v1"
|
||||
[profiles.default]
|
||||
image = "example.invalid/agent:v1"
|
||||
volumes = [{ host = "/source", container = "/root/source", kind = "socket" }]
|
||||
"#
|
||||
.into(),
|
||||
] {
|
||||
assert!(parse_trusted_configuration(invalid.as_bytes()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_layout_is_closed_normalized_and_requires_workspace() {
|
||||
assert!(SandboxLayoutV1::from_production_layout(["/proc".into()]).is_err());
|
||||
assert!(
|
||||
SandboxLayoutV1::from_production_layout(["/workspace".into(), "/workspace".into()])
|
||||
.is_err()
|
||||
);
|
||||
for invalid in ["workspace", "/", "/workspace/", "/workspace/../control"] {
|
||||
assert!(
|
||||
SandboxLayoutV1::from_production_layout(["/workspace".into(), invalid.into(),])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repository_example_loads_with_ipv4_normalization_and_private_denies() {
|
||||
let tree = TestTree::new();
|
||||
let config = write_configuration(&tree, include_str!("../../../docs/example.toml"));
|
||||
let resolved = load_trusted_configuration(request(&tree, config, BTreeMap::new())).unwrap();
|
||||
assert_eq!(resolved.selected_profile(), "default");
|
||||
assert!(
|
||||
resolved
|
||||
.effective_allow_prefixes()
|
||||
.iter()
|
||||
.any(|prefix| prefix.address() == [1, 1, 1, 1] && prefix.prefix_length() == 32)
|
||||
);
|
||||
for (address, length) in [
|
||||
([10, 0, 0, 0], 8),
|
||||
([172, 16, 0, 0], 12),
|
||||
([192, 168, 0, 0], 16),
|
||||
] {
|
||||
assert!(
|
||||
resolved
|
||||
.effective_deny_prefixes()
|
||||
.iter()
|
||||
.any(|prefix| prefix.address() == address && prefix.prefix_length() == length)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_tilde_configuration_path_expands_only_against_home() {
|
||||
let tree = TestTree::new();
|
||||
let home = tree.path().join("home");
|
||||
fs::create_dir(&home).unwrap();
|
||||
fs::set_permissions(&home, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::write(home.join("trusted.toml"), valid_configuration(&tree)).unwrap();
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let resolved =
|
||||
load_trusted_configuration(request(&tree, PathBuf::from("~/trusted.toml"), environment))
|
||||
.unwrap();
|
||||
assert_eq!(resolved.selected_profile(), "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonselected_profiles_are_decoded_but_do_not_resolve_host_authority() {
|
||||
let tree = TestTree::new();
|
||||
let body = format!(
|
||||
"{}\n[profiles.other]\nimage = \"example.invalid/other:v1\"\nenv = {{ SECRET = \"env:UNSET\" }}\nvolumes = [{{ host = \"/does/not/exist\", container = \"/root/missing\", kind = \"directory\" }}]\n",
|
||||
valid_configuration(&tree)
|
||||
);
|
||||
let config = write_configuration(&tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
assert!(load_trusted_configuration(request(&tree, config, environment)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonselected_profiles_cannot_hide_invalid_approval_semantics() {
|
||||
for other_profile in [
|
||||
r#"[profiles.other]
|
||||
image = "example.invalid/other:v1"
|
||||
[[profiles.other.escalation.auto_approve]]
|
||||
executable = "/bin/true"
|
||||
"#,
|
||||
r#"[profiles.other]
|
||||
image = "example.invalid/other:v1"
|
||||
[profiles.other.escalation]
|
||||
enabled = true
|
||||
[[profiles.other.escalation.auto_approve]]
|
||||
executable = "/bin/true"
|
||||
allow_extra_args = true
|
||||
"#,
|
||||
] {
|
||||
let document = format!(
|
||||
"schema = \"lazaret-trusted-host-config-v1\"\n[profiles.default]\nimage = \"example.invalid/default:v1\"\n{other_profile}"
|
||||
);
|
||||
let error = parse_trusted_configuration(document.as_bytes()).unwrap_err();
|
||||
assert_eq!(error.kind(), ConfigurationErrorKind::InvalidAutoApproval);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_profile_loads_into_confined_nonserializable_authority() {
|
||||
let tree = TestTree::new();
|
||||
let config = write_configuration(&tree, &valid_configuration(&tree));
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "top-secret".into());
|
||||
let resolved = load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
||||
|
||||
assert_eq!(resolved.selected_profile(), "default");
|
||||
assert_eq!(
|
||||
resolved.effective_allow_prefixes()[0].address(),
|
||||
[1, 2, 3, 0]
|
||||
);
|
||||
assert!(
|
||||
resolved
|
||||
.effective_deny_prefixes()
|
||||
.iter()
|
||||
.any(|prefix| prefix.address() == [10, 0, 0, 0] && prefix.prefix_length() == 8)
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.ephemeral_environment()["TOKEN"].expose().to_str(),
|
||||
Some("top-secret")
|
||||
);
|
||||
assert!(!format!("{resolved:?}").contains("top-secret"));
|
||||
assert_eq!(resolved.volumes().len(), 1);
|
||||
resolved.volumes()[0].open_source().unwrap();
|
||||
assert_eq!(
|
||||
fs::metadata(tree.path().join("data"))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777,
|
||||
0o700
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_values_never_change_the_fingerprint() {
|
||||
let tree = TestTree::new();
|
||||
let config = write_configuration(&tree, &valid_configuration(&tree));
|
||||
let mut first = BTreeMap::new();
|
||||
first.insert("HOST_TOKEN".into(), "first-secret".into());
|
||||
let first = load_trusted_configuration(request(&tree, config.clone(), first)).unwrap();
|
||||
let mut second = BTreeMap::new();
|
||||
second.insert("HOST_TOKEN".into(), "second-secret".into());
|
||||
let second = load_trusted_configuration(request(&tree, config, second)).unwrap();
|
||||
assert_eq!(
|
||||
first.configuration_fingerprint(),
|
||||
second.configuration_fingerprint()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_routing_is_explicit_and_fingerprinted() {
|
||||
let first_tree = TestTree::new();
|
||||
let first_config = write_configuration(&first_tree, &valid_configuration(&first_tree));
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let first =
|
||||
load_trusted_configuration(request(&first_tree, first_config, environment)).unwrap();
|
||||
assert!(first.fingerprint_record().isolate_gitignored);
|
||||
|
||||
let second_tree = TestTree::new();
|
||||
let body = valid_configuration(&second_tree).replace(
|
||||
"image = \"example.invalid/agent:v1\"",
|
||||
"image = \"example.invalid/agent:v1\"\nisolate_gitignored = false",
|
||||
);
|
||||
let second_config = write_configuration(&second_tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let second =
|
||||
load_trusted_configuration(request(&second_tree, second_config, environment)).unwrap();
|
||||
assert!(!second.fingerprint_record().isolate_gitignored);
|
||||
let disabled = derive_configuration_fingerprint(second.fingerprint_record()).unwrap();
|
||||
let mut enabled_record = second.fingerprint_record().clone();
|
||||
enabled_record.isolate_gitignored = true;
|
||||
assert_ne!(
|
||||
disabled,
|
||||
derive_configuration_fingerprint(&enabled_record).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_ancestor_authority_survives_path_replacement() {
|
||||
let tree = TestTree::new();
|
||||
let config = write_configuration(&tree, &valid_configuration(&tree));
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let resolved = load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
||||
let original_source = tree.path().join("data/profiles/default/agent-state");
|
||||
let original_inode = fs::metadata(&original_source).unwrap().ino();
|
||||
let profile = tree.path().join("data/profiles/default");
|
||||
let displaced = tree.path().join("data/profiles/displaced");
|
||||
fs::rename(&profile, &displaced).unwrap();
|
||||
fs::create_dir(&profile).unwrap();
|
||||
fs::set_permissions(&profile, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::create_dir(profile.join("agent-state")).unwrap();
|
||||
let replacement_inode = fs::metadata(profile.join("agent-state")).unwrap().ino();
|
||||
|
||||
let opened = resolved.volumes()[0].open_source().unwrap();
|
||||
let opened_inode = rustix::fs::fstat(&opened).unwrap().st_ino;
|
||||
assert_eq!(opened_inode, original_inode);
|
||||
assert_ne!(opened_inode, replacement_inode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumer_api_is_descriptor_relative_and_refuses_final_identity_drift() {
|
||||
let tree = TestTree::new();
|
||||
let config = write_configuration(&tree, &valid_configuration(&tree));
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let load_request = request(&tree, config, environment);
|
||||
let nested = load_request.workspace.join("nested");
|
||||
fs::create_dir(&nested).unwrap();
|
||||
fs::write(nested.join("data"), b"validated").unwrap();
|
||||
symlink("data", nested.join("alias")).unwrap();
|
||||
let expected_inode = fs::metadata(nested.join("data")).unwrap().ino();
|
||||
let resolved = load_trusted_configuration(load_request).unwrap();
|
||||
|
||||
let opened = resolved
|
||||
.workspace()
|
||||
.open_beneath(Path::new("nested/data"), ConfinedPathType::RegularFile)
|
||||
.unwrap();
|
||||
assert_eq!(rustix::fs::fstat(&opened).unwrap().st_ino, expected_inode);
|
||||
for refused in ["../outside", "/absolute", "nested/./data", "nested/alias"] {
|
||||
assert!(
|
||||
resolved
|
||||
.workspace()
|
||||
.open_beneath(Path::new(refused), ConfinedPathType::RegularFile)
|
||||
.is_err(),
|
||||
"{refused} must not escape or redirect authority"
|
||||
);
|
||||
}
|
||||
|
||||
let workspace = tree.path().join("workspace");
|
||||
fs::rename(&workspace, tree.path().join("displaced-workspace")).unwrap();
|
||||
fs::create_dir(&workspace).unwrap();
|
||||
assert!(resolved.workspace().open_authority().is_err());
|
||||
|
||||
let source = tree.path().join("data/profiles/default/agent-state");
|
||||
fs::rename(&source, tree.path().join("displaced-agent-state")).unwrap();
|
||||
fs::create_dir(&source).unwrap();
|
||||
assert!(resolved.volumes()[0].open_source().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_approval_uses_only_the_canonical_direct_vector_and_safe_object() {
|
||||
let tree = TestTree::new();
|
||||
let binary_directory = tree.path().join("bin");
|
||||
fs::create_dir(&binary_directory).unwrap();
|
||||
let executable = binary_directory.join("tool");
|
||||
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
||||
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let body = valid_configuration(&tree).replace(
|
||||
"[profiles.default.escalation]\nenabled = false",
|
||||
&format!(
|
||||
"[profiles.default.escalation]\nenabled = true\n\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"\nargs = [\"fixed\"]\nallow_extra_args = false",
|
||||
executable.display()
|
||||
),
|
||||
);
|
||||
let config = write_configuration(&tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let resolved = load_trusted_configuration(request(&tree, config, environment)).unwrap();
|
||||
let rule = &resolved.auto_approval_rules()[0];
|
||||
let canonical = fs::canonicalize(&executable)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
assert!(rule.matches_command_argv(&[canonical.clone(), b"fixed".to_vec()]));
|
||||
assert!(!rule.matches_command_argv(&[b"tool".to_vec(), b"fixed".to_vec()]));
|
||||
assert!(!rule.matches_command_argv(std::slice::from_ref(&canonical)));
|
||||
assert!(
|
||||
!rule.matches_command_argv(&[canonical.clone(), b"fixed".to_vec(), b"extra".to_vec(),])
|
||||
);
|
||||
assert!(!rule.matches_command_argv(&[canonical, b"fixed\0extra".to_vec()]));
|
||||
|
||||
let prefix_tree = TestTree::new();
|
||||
let binary_directory = prefix_tree.path().join("bin");
|
||||
fs::create_dir(&binary_directory).unwrap();
|
||||
let executable = binary_directory.join("tool");
|
||||
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
||||
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let body = valid_configuration(&prefix_tree).replace(
|
||||
"[profiles.default.escalation]\nenabled = false",
|
||||
&format!(
|
||||
"[profiles.default.escalation]\nenabled = true\n\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"\nargs = [\"fixed\"]\nallow_extra_args = true",
|
||||
executable.display()
|
||||
),
|
||||
);
|
||||
let config = write_configuration(&prefix_tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let resolved = load_trusted_configuration(request(&prefix_tree, config, environment)).unwrap();
|
||||
let canonical = fs::canonicalize(executable)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
let rule = &resolved.auto_approval_rules()[0];
|
||||
assert!(rule.matches_command_argv(&[canonical.clone(), b"fixed".to_vec()]));
|
||||
assert!(rule.matches_command_argv(&[canonical, b"fixed".to_vec(), b"extra".to_vec(),]));
|
||||
|
||||
let hardlink_tree = TestTree::new();
|
||||
let binary_directory = hardlink_tree.path().join("bin");
|
||||
fs::create_dir(&binary_directory).unwrap();
|
||||
let executable = binary_directory.join("tool");
|
||||
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
||||
fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::hard_link(&executable, binary_directory.join("alias")).unwrap();
|
||||
let body = valid_configuration(&hardlink_tree).replace(
|
||||
"[profiles.default.escalation]\nenabled = false",
|
||||
&format!(
|
||||
"[profiles.default.escalation]\nenabled = true\n\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"",
|
||||
executable.display()
|
||||
),
|
||||
);
|
||||
let config = write_configuration(&hardlink_tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
assert_eq!(
|
||||
load_trusted_configuration(request(&hardlink_tree, config, environment))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ConfigurationErrorKind::InvalidAutoApproval
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_network_environment_and_volume_conflicts_refuse_loading() {
|
||||
let tree = TestTree::new();
|
||||
let base = valid_configuration(&tree);
|
||||
let cases = [
|
||||
(
|
||||
base.replace(
|
||||
"persistent_dirs = [\"/root\"]",
|
||||
"persistent_dirs = [\"/workspace/cache\"]",
|
||||
),
|
||||
ConfigurationErrorKind::PathConflict,
|
||||
),
|
||||
(
|
||||
base.replace(
|
||||
"allow_egress = [\"10.1.2.3/24\"]",
|
||||
"allow_egress = [\"10.0.0.0/8\"]",
|
||||
),
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
),
|
||||
(
|
||||
base.replace("/root/.local/state/agent", "/root"),
|
||||
ConfigurationErrorKind::InvalidVolume,
|
||||
),
|
||||
(
|
||||
base.replace(
|
||||
"persistent_dirs = [\"/root\"]",
|
||||
"persistent_dirs = [\"/root\", \"/root/cache\"]",
|
||||
),
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
),
|
||||
(
|
||||
base.replace("kind = \"directory\"", "kind = \"file\""),
|
||||
ConfigurationErrorKind::InvalidVolume,
|
||||
),
|
||||
(
|
||||
format!(
|
||||
"{base}\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"/bin/true\"\n"
|
||||
),
|
||||
ConfigurationErrorKind::InvalidAutoApproval,
|
||||
),
|
||||
(
|
||||
base.replace(
|
||||
"[profiles.default.escalation]\nenabled = false",
|
||||
"[profiles.default.escalation]\nenabled = true\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"/bin/true\"\nallow_extra_args = true",
|
||||
),
|
||||
ConfigurationErrorKind::InvalidAutoApproval,
|
||||
),
|
||||
];
|
||||
for (index, (body, expected)) in cases.into_iter().enumerate() {
|
||||
let case_tree = TestTree::new();
|
||||
let body = body.replace(
|
||||
&tree.path().join("data").display().to_string(),
|
||||
&case_tree.path().join("data").display().to_string(),
|
||||
);
|
||||
let config = write_configuration(&case_tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
let error = load_trusted_configuration(request(&case_tree, config, environment))
|
||||
.expect_err(&format!("case {index} must fail"));
|
||||
assert_eq!(error.kind(), expected);
|
||||
}
|
||||
|
||||
let config = write_configuration(&tree, &base);
|
||||
let error = load_trusted_configuration(request(&tree, config, BTreeMap::new())).unwrap_err();
|
||||
assert_eq!(error.kind(), ConfigurationErrorKind::MissingEnvironment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_environment_resolution_occurs_after_authority_validation() {
|
||||
let volume_tree = TestTree::new();
|
||||
let body = valid_configuration(&volume_tree).replace("kind = \"directory\"", "kind = \"file\"");
|
||||
let config = write_configuration(&volume_tree, &body);
|
||||
assert_eq!(
|
||||
load_trusted_configuration(request(&volume_tree, config, BTreeMap::new()))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ConfigurationErrorKind::InvalidVolume
|
||||
);
|
||||
|
||||
let executable_tree = TestTree::new();
|
||||
let executable = executable_tree.path().join("unsafe-tool");
|
||||
fs::write(&executable, b"#!/bin/sh\nexit 0\n").unwrap();
|
||||
fs::set_permissions(&executable, fs::Permissions::from_mode(0o722)).unwrap();
|
||||
let body = valid_configuration(&executable_tree).replace(
|
||||
"[profiles.default.escalation]\nenabled = false",
|
||||
&format!(
|
||||
"[profiles.default.escalation]\nenabled = true\n[[profiles.default.escalation.auto_approve]]\nexecutable = \"{}\"",
|
||||
executable.display()
|
||||
),
|
||||
);
|
||||
let config = write_configuration(&executable_tree, &body);
|
||||
assert_eq!(
|
||||
load_trusted_configuration(request(&executable_tree, config, BTreeMap::new()))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ConfigurationErrorKind::InvalidAutoApproval
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn volume_symlinks_and_lazaret_owned_mode_drift_are_rejected() {
|
||||
let tree = TestTree::new();
|
||||
let target = tree.path().join("target");
|
||||
fs::create_dir(&target).unwrap();
|
||||
let link = tree.path().join("volume-link");
|
||||
symlink(&target, &link).unwrap();
|
||||
let body = valid_configuration(&tree)
|
||||
.replace("{profile_dir}/agent-state", link.to_str().unwrap())
|
||||
.replace("create = true", "create = false");
|
||||
let config = write_configuration(&tree, &body);
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
assert_eq!(
|
||||
load_trusted_configuration(request(&tree, config, environment))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ConfigurationErrorKind::InvalidVolume
|
||||
);
|
||||
|
||||
let mode_tree = TestTree::new();
|
||||
let data = mode_tree.path().join("data");
|
||||
fs::create_dir(&data).unwrap();
|
||||
fs::set_permissions(&data, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let config = write_configuration(&mode_tree, &valid_configuration(&mode_tree));
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
assert_eq!(
|
||||
load_trusted_configuration(request(&mode_tree, config, environment))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ConfigurationErrorKind::InvalidPath
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_source_permissions_links_and_size_are_bounded() {
|
||||
for mutation in 0..3 {
|
||||
let tree = TestTree::new();
|
||||
let config = write_configuration(&tree, &valid_configuration(&tree));
|
||||
match mutation {
|
||||
0 => fs::set_permissions(&config, fs::Permissions::from_mode(0o666)).unwrap(),
|
||||
1 => fs::hard_link(&config, tree.path().join("config-link")).unwrap(),
|
||||
2 => fs::write(&config, vec![b'x'; 1_048_577]).unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let mut environment = BTreeMap::new();
|
||||
environment.insert("HOST_TOKEN".into(), "value".into());
|
||||
assert!(
|
||||
load_trusted_configuration(request(&tree, config, environment)).is_err(),
|
||||
"mutation {mutation} must fail"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn digest_vectors_are_framed_and_order_independent() {
|
||||
assert_eq!(
|
||||
derive_domain_id("default", b"/workspace/project")
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
&[
|
||||
0x62, 0x64, 0xea, 0x6c, 0x25, 0x85, 0x2f, 0x5b, 0x77, 0x03, 0x90, 0x33, 0x9b, 0xb1,
|
||||
0xd9, 0x75, 0x01, 0x9b, 0x70, 0xa6, 0x04, 0x55, 0xff, 0xfa, 0x2a, 0x1c, 0xf3, 0x0e,
|
||||
0xf1, 0xc6, 0xbe, 0xf9,
|
||||
]
|
||||
);
|
||||
|
||||
let mut record = ConfigurationFingerprintRecordV1 {
|
||||
selected_profile: "default".into(),
|
||||
storage_root: HostPath::new(b"/data".to_vec()),
|
||||
runtime_root: HostPath::new(b"/run/user/1000/lazaret".to_vec()),
|
||||
image: "example.invalid/agent:v1".into(),
|
||||
isolate_gitignored: true,
|
||||
persistent_dirs: vec!["/var".into(), "/root".into()],
|
||||
environment_references: vec![
|
||||
EnvironmentReference {
|
||||
sandbox_name: "B".into(),
|
||||
host_name: "HOST_B".into(),
|
||||
},
|
||||
EnvironmentReference {
|
||||
sandbox_name: "A".into(),
|
||||
host_name: "HOST_A".into(),
|
||||
},
|
||||
],
|
||||
volumes: vec![ResolvedVolume {
|
||||
canonical_host_source: HostPath::new(b"/source".to_vec()),
|
||||
container_path: "/root/source".into(),
|
||||
kind: VolumeKind::Directory,
|
||||
access: VolumeAccess::ReadOnly,
|
||||
create: false,
|
||||
}],
|
||||
effective_allow_prefixes: vec![([8, 8, 8, 8], 32), ([1, 1, 1, 1], 32)],
|
||||
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)],
|
||||
escalation_enabled: true,
|
||||
auto_approval_rules: vec![ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
args: vec!["a".into(), "b".into()],
|
||||
allow_extra_args: false,
|
||||
}],
|
||||
};
|
||||
let first = derive_configuration_fingerprint(&record).unwrap();
|
||||
assert_eq!(
|
||||
first.as_bytes(),
|
||||
&[
|
||||
0xd3, 0xba, 0xef, 0x91, 0x21, 0x0f, 0x98, 0x31, 0x6c, 0xea, 0x34, 0xfb, 0x41, 0xbd,
|
||||
0x6c, 0xa5, 0x07, 0xf0, 0xb8, 0x9b, 0x90, 0x77, 0xaf, 0x0a, 0x0e, 0xd0, 0x6a, 0xd6,
|
||||
0x46, 0xc7, 0x2d, 0xbf,
|
||||
]
|
||||
);
|
||||
record.persistent_dirs.reverse();
|
||||
record.environment_references.reverse();
|
||||
record.effective_allow_prefixes.reverse();
|
||||
assert_eq!(first, derive_configuration_fingerprint(&record).unwrap());
|
||||
record.auto_approval_rules[0].args.reverse();
|
||||
assert_ne!(first, derive_configuration_fingerprint(&record).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_configuration_records_use_strict_postcard() {
|
||||
let reference = EnvironmentReference {
|
||||
sandbox_name: "TOKEN".into(),
|
||||
host_name: "HOST_TOKEN".into(),
|
||||
};
|
||||
let mut encoded = encode(&reference).unwrap();
|
||||
encoded.push(0);
|
||||
assert!(matches!(
|
||||
decode::<EnvironmentReference>(&encoded),
|
||||
Err(CodecError::TrailingPayload)
|
||||
));
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
use lazaret_core::codec::{CodecError, MAX_SCHEMA_RECORD_BYTES, decode, encode};
|
||||
use lazaret_core::configuration::parse_trusted_configuration;
|
||||
use lazaret_core::identity::{
|
||||
BootId, BundleDigest, CapabilityId, CertificateDigest, ClientId, ConfigurationFingerprint,
|
||||
ContainerId, CoordinatorId, DerivedPolicyDigest, DeviceId, DomainId, FileDescriptor,
|
||||
|
|
@ -148,6 +149,25 @@ struct RawNetworkReadinessIdentity {
|
|||
enforcement_namespace: NamespaceIdentity,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RawMailboxRequest {
|
||||
protocol_version: ProtocolVersion,
|
||||
session_binding: SessionBinding,
|
||||
request_id: RequestId,
|
||||
session_capability: SessionRequestCapability,
|
||||
sandbox_credential_id: ProtocolCredentialId,
|
||||
command_argv: Vec<Vec<u8>>,
|
||||
justification: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RawTrustedHostConfigurationV1 {
|
||||
schema_id: String,
|
||||
storage: StorageConfigurationV1,
|
||||
network: NetworkConfigurationV1,
|
||||
profiles: BTreeMap<String, ProfileConfigurationV1>,
|
||||
}
|
||||
|
||||
fn opaque(byte: u8) -> [u8; 32] {
|
||||
[byte; 32]
|
||||
}
|
||||
|
|
@ -657,15 +677,15 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
vec![39, 40],
|
||||
CertificateDigest::new(opaque(41)),
|
||||
);
|
||||
let request = MailboxRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(17)),
|
||||
command_argv: vec![b"printf".to_vec(), vec![0xff]],
|
||||
justification: "build dependency".into(),
|
||||
};
|
||||
let request = MailboxRequest::new(
|
||||
session_binding(),
|
||||
RequestId::new(opaque(42)),
|
||||
session_capability(),
|
||||
ProtocolCredentialId::new(opaque(17)),
|
||||
vec![b"/usr/bin/printf".to_vec(), b"value".to_vec()],
|
||||
"build dependency".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let prepared_artifact = PreparedArtifact {
|
||||
prepared_artifact_id: PreparedArtifactId::new(opaque(24)),
|
||||
source_identity: SourceIdentity::new(vec![25, 26]),
|
||||
|
|
@ -846,7 +866,7 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
request_id: RequestId::new(opaque(42)),
|
||||
request_sha256: RequestDigest::new(opaque(59)),
|
||||
decision: DeveloperDecisionKind::Approve,
|
||||
approved_command_argv: request.command_argv.clone(),
|
||||
approved_command_argv: request.command_argv().to_vec(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
|
|
@ -1054,6 +1074,71 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
parse_trusted_configuration(
|
||||
br#"schema = "lazaret-trusted-host-config-v1"
|
||||
[profiles.trusted]
|
||||
image = "example.invalid/agent:v1"
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ConfigurationFingerprintRecordV1 {
|
||||
selected_profile: "trusted".into(),
|
||||
storage_root: HostPath::new(b"/data/lazaret".to_vec()),
|
||||
runtime_root: HostPath::new(b"/run/user/1000/lazaret".to_vec()),
|
||||
image: "example.invalid/agent:v1".into(),
|
||||
isolate_gitignored: true,
|
||||
persistent_dirs: vec!["/root".into()],
|
||||
environment_references: vec![EnvironmentReference {
|
||||
sandbox_name: "TOKEN".into(),
|
||||
host_name: "HOST_TOKEN".into(),
|
||||
}],
|
||||
volumes: vec![ResolvedVolume {
|
||||
canonical_host_source: HostPath::new(b"/data/source".to_vec()),
|
||||
container_path: "/root/source".into(),
|
||||
kind: VolumeKind::Directory,
|
||||
access: VolumeAccess::ReadOnly,
|
||||
create: false,
|
||||
}],
|
||||
effective_allow_prefixes: vec![([1, 1, 1, 1], 32)],
|
||||
effective_deny_prefixes: vec![([10, 0, 0, 0], 8)],
|
||||
escalation_enabled: true,
|
||||
auto_approval_rules: vec![ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
args: vec!["--version".into()],
|
||||
allow_extra_args: false,
|
||||
}],
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
EnvironmentReference {
|
||||
sandbox_name: "TOKEN".into(),
|
||||
host_name: "HOST_TOKEN".into(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ResolvedVolume {
|
||||
canonical_host_source: HostPath::new(b"/data/source".to_vec()),
|
||||
container_path: "/root/source".into(),
|
||||
kind: VolumeKind::Directory,
|
||||
access: VolumeAccess::ReadWrite,
|
||||
create: false,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
ResolvedAutoApprovalRule {
|
||||
canonical_executable: HostPath::new(b"/usr/bin/true".to_vec()),
|
||||
args: vec![],
|
||||
allow_extra_args: false,
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
|
||||
assert_eq!(seen.len(), IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES.len());
|
||||
assert!(
|
||||
|
|
@ -1296,27 +1381,27 @@ fn unknown_enums_and_protocol_versions_are_rejected() {
|
|||
));
|
||||
assert_unknown_protocol_version(&session_capability());
|
||||
|
||||
let request = MailboxRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(5)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(6)),
|
||||
command_argv: vec![b"true".to_vec()],
|
||||
justification: "test".into(),
|
||||
};
|
||||
let request = MailboxRequest::new(
|
||||
session_binding(),
|
||||
RequestId::new(opaque(5)),
|
||||
session_capability(),
|
||||
ProtocolCredentialId::new(opaque(6)),
|
||||
vec![b"/usr/bin/true".to_vec()],
|
||||
"test".into(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_unknown_protocol_version(&request);
|
||||
assert_unknown_protocol_version(&MailboxCancellation {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id,
|
||||
request_id: request.request_id(),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(6)),
|
||||
});
|
||||
assert_unknown_protocol_version(&MailboxResultAck {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id,
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(6)),
|
||||
|
|
@ -1325,7 +1410,7 @@ fn unknown_enums_and_protocol_versions_are_rejected() {
|
|||
assert_unknown_protocol_version(&DeveloperDecision {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id,
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
decision: DeveloperDecisionKind::Deny,
|
||||
approved_command_argv: vec![],
|
||||
|
|
@ -1333,7 +1418,7 @@ fn unknown_enums_and_protocol_versions_are_rejected() {
|
|||
assert_unknown_protocol_version(&MailboxResult {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id,
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
coordinator_credential_id: ProtocolCredentialId::new(opaque(8)),
|
||||
outcome: EscalationOutcome::Denied,
|
||||
|
|
@ -1345,7 +1430,7 @@ fn unknown_enums_and_protocol_versions_are_rejected() {
|
|||
assert_unknown_protocol_version(&MailboxResultChunk {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: request.request_id,
|
||||
request_id: request.request_id(),
|
||||
request_sha256: RequestDigest::new(opaque(7)),
|
||||
coordinator_credential_id: ProtocolCredentialId::new(opaque(8)),
|
||||
sequence: 0,
|
||||
|
|
@ -2982,6 +3067,40 @@ fn network_document_schema_identities_are_fixed_to_v1() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_host_configuration_postcard_dispatch_rejects_unknown_schema() {
|
||||
let mut profiles = BTreeMap::new();
|
||||
profiles.insert(
|
||||
"default".into(),
|
||||
ProfileConfigurationV1 {
|
||||
image: "example.invalid/agent:v1".into(),
|
||||
isolate_gitignored: true,
|
||||
persistent_dirs: vec![],
|
||||
allow_egress: vec![],
|
||||
deny_egress: vec![],
|
||||
env: BTreeMap::new(),
|
||||
volumes: vec![],
|
||||
escalation: EscalationConfigurationV1::default(),
|
||||
},
|
||||
);
|
||||
for schema_id in [
|
||||
"",
|
||||
"lazaret-trusted-host-config-v0",
|
||||
"lazaret-trusted-host-config-v2",
|
||||
] {
|
||||
let raw = RawTrustedHostConfigurationV1 {
|
||||
schema_id: schema_id.into(),
|
||||
storage: StorageConfigurationV1::default(),
|
||||
network: NetworkConfigurationV1::default(),
|
||||
profiles: profiles.clone(),
|
||||
};
|
||||
assert!(matches!(
|
||||
decode::<TrustedHostConfigurationV1>(&encode(&raw).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_terminal_dimensions_are_rejected() {
|
||||
assert!(NonZeroU16::new(0).is_none());
|
||||
|
|
@ -3015,6 +3134,41 @@ fn generic_schema_record_limit_is_enforced_before_decode() {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_request_command_vector_is_closed_and_direct() {
|
||||
for command_argv in [
|
||||
vec![],
|
||||
vec![b"true".to_vec()],
|
||||
vec![b"/usr/bin/true".to_vec(), vec![0xff]],
|
||||
vec![b"/usr/bin/true".to_vec(), b"bad\0argument".to_vec()],
|
||||
] {
|
||||
let raw = RawMailboxRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_binding: session_binding(),
|
||||
request_id: RequestId::new(opaque(42)),
|
||||
session_capability: session_capability(),
|
||||
sandbox_credential_id: ProtocolCredentialId::new(opaque(17)),
|
||||
command_argv,
|
||||
justification: "test".into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
decode::<MailboxRequest>(&encode(&raw).unwrap()),
|
||||
Err(CodecError::MalformedPayload(_))
|
||||
));
|
||||
}
|
||||
assert!(
|
||||
MailboxRequest::new(
|
||||
session_binding(),
|
||||
RequestId::new(opaque(42)),
|
||||
session_capability(),
|
||||
ProtocolCredentialId::new(opaque(17)),
|
||||
vec![b"/bin/sh".to_vec(), b"-c".to_vec(), b"fixed".to_vec()],
|
||||
"exact direct vector".into(),
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_exit_code_is_accepted_only_for_exited() {
|
||||
let encoded = encode(&InvalidSessionExit {
|
||||
|
|
|
|||
|
|
@ -247,11 +247,14 @@ def audit_dependencies(metadata: dict) -> None:
|
|||
f"unapproved dependency source: {package['name']}",
|
||||
)
|
||||
license_expression = package.get("license") or ""
|
||||
exceptions = set(
|
||||
re.findall(r"\bWITH\s+([A-Za-z0-9.-]+)", license_expression)
|
||||
)
|
||||
licenses = set(re.findall(r"[A-Za-z0-9.-]+", license_expression)) - {
|
||||
"AND",
|
||||
"OR",
|
||||
"WITH",
|
||||
}
|
||||
} - exceptions
|
||||
require(
|
||||
bool(licenses) and licenses <= allowed_licenses,
|
||||
f"unapproved dependency license: {package['name']} {license_expression}",
|
||||
|
|
|
|||
Loading…
Reference in a new issue