use std::collections::HashSet;
use std::path::{Path, PathBuf};
use cageforge_command::EnvironmentSpec;
use cageforge_path::{NativePathKey, contains_parent_traversal, is_within};
use cageforge_policy::{
DomainAccess, DomainMode, LocalIpcEndpoint, LocalNetworkAccess, NetworkMode, NetworkPolicy,
PathResolutionContext, SandboxPolicy, UnixSocketMode,
};
use crate::CompositionError;
use crate::context::EffectivePathContext;
use crate::environment::EffectiveEnvironment;
use crate::filesystem::EffectiveFilesystemPolicy;
use crate::lowering::EffectiveNetworkLowering;
use crate::ownership::ExternalOwner;
mod types;
pub use types::{
CompositionRequest, EffectiveNetworkPolicy, EffectiveNetworkRequirements, EffectiveSandbox,
PolicyCeiling,
};
impl PolicyCeiling {
pub fn new(policy: SandboxPolicy, environment: EnvironmentSpec) -> Self {
Self {
policy,
environment,
workspace_roots: None,
external_owner: None,
}
}
pub fn with_workspace_roots<I, P>(mut self, roots: I) -> Result<Self, CompositionError>
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
self.workspace_roots = Some(normalize_roots(roots)?);
Ok(self)
}
pub fn without_workspace_root_limit(mut self) -> Self {
self.workspace_roots = None;
self
}
pub fn with_external_owner(mut self, owner: ExternalOwner) -> Self {
self.external_owner = Some(owner);
self
}
pub fn policy(&self) -> &SandboxPolicy {
&self.policy
}
pub fn environment(&self) -> &EnvironmentSpec {
&self.environment
}
pub fn workspace_roots(&self) -> Option<&[PathBuf]> {
self.workspace_roots.as_deref()
}
pub(crate) fn external_owner(&self) -> Option<&ExternalOwner> {
self.external_owner.as_ref()
}
}
impl<'a> CompositionRequest<'a> {
pub fn new(
requested_policy: &'a SandboxPolicy,
requested_environment: &'a EnvironmentSpec,
ceiling: &'a PolicyCeiling,
) -> Self {
Self {
requested_policy,
requested_environment,
requested_workspace_roots: None,
ceiling,
external_owner: None,
}
}
pub fn with_workspace_roots<I, P>(mut self, roots: I) -> Result<Self, CompositionError>
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
self.requested_workspace_roots = Some(normalize_roots(roots)?);
Ok(self)
}
pub fn with_external_owner(mut self, owner: ExternalOwner) -> Self {
self.external_owner = Some(owner);
self
}
}
impl EffectiveSandbox {
pub(crate) fn new(
filesystem: EffectiveFilesystemPolicy,
network: EffectiveNetworkPolicy,
environment: EffectiveEnvironment,
workspace_roots: Option<Vec<PathBuf>>,
workspace_root_limit: Option<Vec<PathBuf>>,
) -> Self {
Self {
filesystem,
network,
environment,
workspace_roots,
workspace_root_limit,
}
}
pub fn filesystem(&self) -> &EffectiveFilesystemPolicy {
&self.filesystem
}
pub fn network(&self) -> &EffectiveNetworkPolicy {
&self.network
}
pub fn environment(&self) -> &EffectiveEnvironment {
&self.environment
}
pub fn workspace_roots(&self) -> Option<&[PathBuf]> {
self.workspace_roots.as_deref()
}
pub fn workspace_root_limit(&self) -> Option<&[PathBuf]> {
self.workspace_root_limit.as_deref()
}
pub fn path_context(
&self,
base: &PathResolutionContext,
) -> Result<EffectivePathContext, CompositionError> {
let mut context = PathResolutionContext::new();
for path in base.root_paths() {
context = context
.with_root(path.clone())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
let workspace_roots = self
.workspace_roots()
.unwrap_or_else(|| base.workspace_roots());
for path in workspace_roots.iter().filter(|path| {
self.workspace_root_limit()
.is_none_or(|limit| root_is_within(path, limit))
}) {
context = context
.with_workspace_root(path.clone())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
for path in base.minimal_paths() {
context = context
.with_minimal_path(path.clone())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
for path in base.executable_roots() {
context = context
.with_executable_root(path.clone())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
if let Some(path) = base.tmpdir() {
context = context
.with_tmpdir(path.to_path_buf())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
if let Some(path) = base.slash_tmp() {
context = context
.with_slash_tmp(path.to_path_buf())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
if let Some(path) = base.current_directory() {
context = context
.with_current_directory(path.to_path_buf())
.map_err(|source| CompositionError::InvalidPathContext { source })?;
}
Ok(EffectivePathContext::new(
context,
self.filesystem.context_identity(),
))
}
}
impl EffectiveNetworkPolicy {
pub(crate) fn new(requested: NetworkPolicy, ceiling: NetworkPolicy) -> Self {
Self { requested, ceiling }
}
pub(crate) fn requested_policy(&self) -> &NetworkPolicy {
&self.requested
}
pub(crate) fn ceiling_policy(&self) -> &NetworkPolicy {
&self.ceiling
}
pub fn requirements(&self) -> EffectiveNetworkRequirements {
let mode = effective_network_mode(self.requested.mode(), self.ceiling.mode());
let enabled = mode == NetworkMode::Enabled;
let domain_rules = enabled
&& [&self.requested, &self.ceiling].iter().any(|policy| {
!policy.domains().is_empty() || policy.domain_mode() != DomainMode::Enabled
});
let local_address_restrictions = enabled
&& [&self.requested, &self.ceiling]
.iter()
.any(|policy| policy.local_network_access() == LocalNetworkAccess::Deny);
let policies = [&self.requested, &self.ceiling];
let local_ipc_isolation = enabled
&& policies
.iter()
.any(|policy| denies_all_unix_sockets(policy));
let local_ipc_rules = enabled
&& !local_ipc_isolation
&& policies
.iter()
.any(|policy| policy.unix_socket_mode() == UnixSocketMode::Restricted);
let local_ipc_deny_rules = enabled
&& !local_ipc_isolation
&& policies
.iter()
.all(|policy| policy.unix_socket_mode() == UnixSocketMode::Enabled)
&& policies.iter().any(|policy| {
policy
.unix_sockets()
.iter()
.any(|rule| rule.access() == DomainAccess::Deny)
});
let windows_named_pipe_rules = policies.iter().any(|policy| {
policy
.local_ipc()
.iter()
.any(|rule| matches!(rule.endpoint(), LocalIpcEndpoint::WindowsNamedPipe(_)))
});
EffectiveNetworkRequirements {
mode,
domain_rules,
local_address_restrictions,
resolved_targets: domain_rules || local_address_restrictions,
local_ipc_isolation,
local_ipc_rules,
local_ipc_deny_rules,
windows_named_pipe_rules,
}
}
pub fn lowering(&self) -> EffectiveNetworkLowering<'_> {
EffectiveNetworkLowering::new(&self.requested, &self.ceiling)
}
}
impl EffectiveNetworkRequirements {
pub const fn mode(self) -> NetworkMode {
self.mode
}
pub const fn domain_rules(self) -> bool {
self.domain_rules
}
pub const fn local_address_restrictions(self) -> bool {
self.local_address_restrictions
}
pub const fn resolved_targets(self) -> bool {
self.resolved_targets
}
pub const fn local_ipc_isolation(self) -> bool {
self.local_ipc_isolation
}
pub const fn local_ipc_rules(self) -> bool {
self.local_ipc_rules
}
pub const fn local_ipc_deny_rules(self) -> bool {
self.local_ipc_deny_rules
}
pub const fn windows_named_pipe_rules(self) -> bool {
self.windows_named_pipe_rules
}
}
fn denies_all_unix_sockets(policy: &NetworkPolicy) -> bool {
policy.unix_socket_mode() == UnixSocketMode::Disabled
|| (policy.unix_socket_mode() == UnixSocketMode::Restricted
&& !policy
.unix_sockets()
.iter()
.any(|rule| rule.access() == DomainAccess::Allow))
}
fn effective_network_mode(left: NetworkMode, right: NetworkMode) -> NetworkMode {
match (left, right) {
(NetworkMode::External, NetworkMode::External) => NetworkMode::External,
(NetworkMode::Disabled, _) | (_, NetworkMode::Disabled) => NetworkMode::Disabled,
(NetworkMode::Enabled, NetworkMode::Enabled) => NetworkMode::Enabled,
(NetworkMode::External, _) | (_, NetworkMode::External) => {
NetworkMode::Disabled
}
}
}
pub(crate) fn normalize_roots<I, P>(roots: I) -> Result<Vec<PathBuf>, CompositionError>
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
let mut normalized: Vec<PathBuf> = Vec::new();
let mut seen = HashSet::new();
for root in roots {
let root = root.into();
validate_root(&root)?;
if seen.insert(NativePathKey::new(&root)) {
normalized.push(root);
}
}
Ok(normalized)
}
pub(crate) fn validate_root(path: &Path) -> Result<(), CompositionError> {
if path.as_os_str().is_empty() {
return Err(CompositionError::InvalidWorkspaceRoot {
path: path.to_path_buf(),
reason: "root must not be empty",
});
}
if path.to_string_lossy().contains('\0') {
return Err(CompositionError::InvalidWorkspaceRoot {
path: path.to_path_buf(),
reason: "root must not contain NUL",
});
}
if contains_parent_traversal(path) {
return Err(CompositionError::InvalidWorkspaceRoot {
path: path.to_path_buf(),
reason: "parent traversal is not allowed",
});
}
if !path.is_absolute() {
return Err(CompositionError::InvalidWorkspaceRoot {
path: path.to_path_buf(),
reason: "root must be absolute at composition time",
});
}
Ok(())
}
pub(crate) fn root_is_within(root: &Path, ceiling_roots: &[PathBuf]) -> bool {
ceiling_roots
.iter()
.any(|ceiling_root| is_within(root, ceiling_root))
}