Implement trusted-host configuration resolution
This commit is contained in:
parent
f106a9f636
commit
f9eb966341
3 changed files with 3468 additions and 39 deletions
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] = &[
|
||||
|
|
|
|||
Loading…
Reference in a new issue