#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
mod capability;
mod diagnostic;
mod execution;
mod requirements;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsString;
use std::fmt;
use std::marker::PhantomData;
use std::net::{IpAddr, SocketAddr};
use std::path::Path;
use std::sync::Arc;
use cageforge_command::{CommandRequest, CommandSpec, StdioSpec, TimeoutPolicy};
use cageforge_path::normalize_lexical_path;
use cageforge_policy::{
ConnectionAuthorization, FilesystemDecision, NetworkDecision, PathResolutionContext,
PathSelector, ResolvedNetworkTarget,
};
use cageforge_policy_compose::{
EffectiveFilesystemLowering, EffectiveNetworkLowering, EffectivePathContext, EffectiveSandbox,
EnvironmentInput,
};
mod model;
pub use diagnostic::{BackendDiagnostic, BackendDiagnosticMetadata};
pub use execution::{DynSandbox, Sandbox, SandboxChild, SandboxExecutionError};
pub use model::{
BackendCapabilities, BackendCapability, BackendContractError, BackendIdentity, BackendRequest,
PreparedBackendRequest, SandboxBackend,
};
impl BackendCapabilities {
pub const fn new() -> Self {
Self {
capabilities: BTreeSet::new(),
}
}
pub fn from_capabilities<I>(capabilities: I) -> Self
where
I: IntoIterator<Item = BackendCapability>,
{
Self {
capabilities: capabilities.into_iter().collect(),
}
}
pub fn with(mut self, capability: BackendCapability) -> Self {
self.capabilities.insert(capability);
self
}
pub fn supports(&self, capability: BackendCapability) -> bool {
self.capabilities.contains(&capability)
}
pub fn iter(&self) -> impl Iterator<Item = &BackendCapability> {
self.capabilities.iter()
}
}
impl FromIterator<BackendCapability> for BackendCapabilities {
fn from_iter<T: IntoIterator<Item = BackendCapability>>(iter: T) -> Self {
Self::from_capabilities(iter)
}
}
impl<'a> BackendRequest<'a> {
pub const fn new(command: &'a CommandRequest, sandbox: &'a EffectiveSandbox) -> Self {
Self { command, sandbox }
}
pub const fn command(&self) -> &'a CommandRequest {
self.command
}
pub const fn sandbox(&self) -> &'a EffectiveSandbox {
self.sandbox
}
pub fn prepare_for<B: SandboxBackend>(
self,
backend: &B,
base_context: &PathResolutionContext,
) -> Result<PreparedBackendRequest<'a, B>, BackendContractError> {
self.validate::<B>(&backend.capabilities(), backend.identity(), base_context)
}
pub fn required_capabilities(&self) -> BackendCapabilities {
let mut required = BackendCapabilities::new().with(BackendCapability::CommandExecution);
requirements::add_command_capabilities(&mut required, self.command);
requirements::add_filesystem_capabilities(&mut required, self.sandbox);
requirements::add_network_capabilities(&mut required, self.sandbox);
requirements::add_environment_capabilities(&mut required, self.sandbox);
required
}
fn validate<B: SandboxBackend>(
self,
capabilities: &BackendCapabilities,
backend_identity: &BackendIdentity,
base_context: &PathResolutionContext,
) -> Result<PreparedBackendRequest<'a, B>, BackendContractError> {
if !self
.sandbox
.environment()
.requested_matches(self.command.environment())
{
return Err(BackendContractError::CommandEnvironmentMismatch);
}
for capability in self.required_capabilities().iter().copied() {
if !capabilities.supports(capability) {
return Err(BackendContractError::UnsupportedCapability { capability });
}
}
let path_context = self
.sandbox
.path_context(base_context)
.map_err(|source| BackendContractError::InvalidRuntimeContext { source })?;
if !path_context.executable_roots().is_empty()
&& !capabilities.supports(BackendCapability::FilesystemExecutableMapping)
{
return Err(BackendContractError::UnsupportedCapability {
capability: BackendCapability::FilesystemExecutableMapping,
});
}
let working_directory = match self.command.working_directory() {
Some(path) if path.is_absolute() => normalize_lexical_path(path).into_owned(),
Some(path) => {
let current_directory = base_context.current_directory().ok_or_else(|| {
BackendContractError::WorkingDirectoryResolution {
path: path.to_path_buf(),
}
})?;
normalize_lexical_path(¤t_directory.join(path)).into_owned()
}
None => base_context
.current_directory()
.map(normalize_lexical_path)
.map(std::borrow::Cow::into_owned)
.ok_or(BackendContractError::MissingRuntimeCurrentDirectory)?,
};
match self
.sandbox
.filesystem()
.access_for_path(&working_directory, &path_context)
.map_err(|source| BackendContractError::FilesystemEvaluation { source })?
{
FilesystemDecision::Read
| FilesystemDecision::Write
| FilesystemDecision::ExternallyEnforced => {}
FilesystemDecision::Deny => {
return Err(BackendContractError::WorkingDirectoryDenied {
path: working_directory.clone(),
});
}
}
Ok(PreparedBackendRequest {
request: self,
path_context,
working_directory,
capabilities: capabilities.clone(),
backend_identity: backend_identity.clone(),
backend: PhantomData,
})
}
}
impl BackendIdentity {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self(Arc::new(()))
}
}
impl fmt::Debug for BackendIdentity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BackendIdentity")
.finish_non_exhaustive()
}
}
impl PartialEq for BackendIdentity {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for BackendIdentity {}
impl<'a, B: SandboxBackend> Clone for PreparedBackendRequest<'a, B> {
fn clone(&self) -> Self {
Self {
request: self.request,
path_context: self.path_context.clone(),
working_directory: self.working_directory.clone(),
capabilities: self.capabilities.clone(),
backend_identity: self.backend_identity.clone(),
backend: PhantomData,
}
}
}
impl<'a, B: SandboxBackend> fmt::Debug for PreparedBackendRequest<'a, B> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("PreparedBackendRequest")
.field("request", &self.request)
.field("path_context", &self.path_context)
.field("working_directory", &self.working_directory)
.field("capabilities", &self.capabilities)
.finish()
}
}
impl<'a, B: SandboxBackend> PreparedBackendRequest<'a, B> {
fn ensure_backend(&self, backend: &B) -> Result<(), BackendContractError> {
if self.backend_identity != *backend.identity() {
Err(BackendContractError::BackendIdentityMismatch)
} else if self.capabilities != backend.capabilities() {
Err(BackendContractError::BackendCapabilitiesMismatch)
} else {
Ok(())
}
}
pub fn command_spec(&self, backend: &B) -> Result<&'a CommandSpec, BackendContractError> {
self.ensure_backend(backend)?;
Ok(self.request.command().command())
}
pub fn sandbox(&self, backend: &B) -> Result<&'a EffectiveSandbox, BackendContractError> {
self.ensure_backend(backend)?;
Ok(self.request.sandbox())
}
pub fn filesystem_lowering(
&self,
backend: &B,
) -> Result<EffectiveFilesystemLowering<'_>, BackendContractError> {
self.ensure_backend(backend)?;
Ok(self.request.sandbox().filesystem().lowering())
}
pub fn network_lowering(
&self,
backend: &B,
) -> Result<EffectiveNetworkLowering<'_>, BackendContractError> {
self.ensure_backend(backend)?;
Ok(self.request.sandbox().network().lowering())
}
pub fn path_context(&self, backend: &B) -> Result<&EffectivePathContext, BackendContractError> {
self.ensure_backend(backend)?;
Ok(&self.path_context)
}
pub fn working_directory(&self, backend: &B) -> Result<&Path, BackendContractError> {
self.ensure_backend(backend)?;
Ok(&self.working_directory)
}
pub fn stdio(&self, backend: &B) -> Result<StdioSpec, BackendContractError> {
self.ensure_backend(backend)?;
Ok(self.request.command().stdio())
}
pub fn timeout_policy(&self, backend: &B) -> Result<TimeoutPolicy, BackendContractError> {
self.ensure_backend(backend)?;
Ok(self.request.command().timeout_policy())
}
pub fn apply_environment(
&self,
backend: &B,
input: EnvironmentInput,
) -> Result<BTreeMap<OsString, OsString>, BackendContractError> {
self.ensure_backend(backend)?;
self.request
.sandbox()
.environment()
.apply_to(input)
.map_err(|source| BackendContractError::EnvironmentPreparation { source })
}
pub fn filesystem_access_for_path(
&self,
backend: &B,
path: &Path,
) -> Result<FilesystemDecision, BackendContractError> {
self.ensure_backend(backend)?;
self.request
.sandbox()
.filesystem()
.access_for_path(path, &self.path_context)
.map_err(|source| BackendContractError::FilesystemEvaluation { source })
}
pub fn filesystem_access_for(
&self,
backend: &B,
selector: &PathSelector,
) -> Result<FilesystemDecision, BackendContractError> {
self.ensure_backend(backend)?;
self.request
.sandbox()
.filesystem()
.access_for(selector, &self.path_context)
.map_err(|source| BackendContractError::FilesystemEvaluation { source })
}
pub fn network_decision_for_domain_with_resolved_ips(
&self,
backend: &B,
domain: &str,
resolved_ips: &[IpAddr],
) -> Result<NetworkDecision, BackendContractError> {
self.ensure_backend(backend)?;
self.request
.sandbox()
.network()
.decision_for_domain_with_resolved_ips(domain, resolved_ips)
.map_err(|source| BackendContractError::NetworkEvaluation { source })
}
pub fn authorize_connection(
&self,
backend: &B,
target: &ResolvedNetworkTarget,
connected: SocketAddr,
) -> Result<ConnectionAuthorization, BackendContractError> {
self.ensure_backend(backend)?;
self.request
.sandbox()
.network()
.authorize_connection(target, connected)
.map_err(|source| BackendContractError::NetworkEvaluation { source })
}
pub fn network_decision_for_unix_socket(
&self,
backend: &B,
socket: &Path,
) -> Result<NetworkDecision, BackendContractError> {
self.ensure_backend(backend)?;
self.request
.sandbox()
.network()
.decision_for_unix_socket(socket)
.map_err(|source| BackendContractError::NetworkEvaluation { source })
}
}