use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use cageforge_policy::{
AccessMode, ConnectionAuthorization, FilesystemDecision, FilesystemMode, NetworkDecision,
NetworkMode, PathSelector, ResolvedNetworkTarget,
};
use crate::context::{ContextIdentity, EffectivePathContext};
use crate::environment::EffectiveEnvironment;
use crate::error::{CompositionBoundary, CompositionError};
use crate::filesystem::EffectiveFilesystemPolicy;
use crate::model::{
CompositionRequest, EffectiveNetworkPolicy, EffectiveSandbox, normalize_roots, root_is_within,
};
use crate::ownership::ExternalOwner;
struct ComposedWorkspaceRoots {
selected: Option<Vec<PathBuf>>,
limit: Option<Vec<PathBuf>>,
}
trait EnforcementMode {
fn delegates_enforcement(self) -> bool;
}
pub fn compose(request: CompositionRequest<'_>) -> Result<EffectiveSandbox, CompositionError> {
let requested_policy = request
.requested_policy
.normalized()
.map_err(|source| CompositionError::InvalidRequestedPolicy { source })?;
let ceiling_policy = request
.ceiling
.policy()
.normalized()
.map_err(|source| CompositionError::InvalidCeiling { source })?;
validate_ownership(
requested_policy.filesystem().mode(),
ceiling_policy.filesystem().mode(),
CompositionBoundary::Filesystem,
)?;
validate_ownership(
requested_policy.network().mode(),
ceiling_policy.network().mode(),
CompositionBoundary::Network,
)?;
validate_external_owner(
requested_policy.filesystem().mode() == FilesystemMode::External,
request.external_owner.as_ref(),
request.ceiling.external_owner(),
CompositionBoundary::Filesystem,
)?;
validate_external_owner(
requested_policy.network().mode() == NetworkMode::External,
request.external_owner.as_ref(),
request.ceiling.external_owner(),
CompositionBoundary::Network,
)?;
if requested_policy.filesystem().mode() != FilesystemMode::External
&& requested_policy.network().mode() != NetworkMode::External
&& (request.external_owner.is_some() || request.ceiling.external_owner().is_some())
{
return Err(CompositionError::UnexpectedExternalOwner {
boundary: CompositionBoundary::Filesystem,
});
}
let workspace_roots = compose_workspace_roots(
request.requested_workspace_roots.as_deref(),
request.ceiling.workspace_roots(),
)?;
let context_identity = ContextIdentity::new();
Ok(EffectiveSandbox::new(
EffectiveFilesystemPolicy::new(
requested_policy.filesystem().clone(),
ceiling_policy.filesystem().clone(),
context_identity,
),
EffectiveNetworkPolicy::new(
requested_policy.network().clone(),
ceiling_policy.network().clone(),
),
EffectiveEnvironment::new(
request.requested_environment.clone(),
request.ceiling.environment().clone(),
),
workspace_roots.selected,
workspace_roots.limit,
))
}
impl EffectiveFilesystemPolicy {
pub fn access_for(
&self,
selector: &PathSelector,
context: &EffectivePathContext,
) -> Result<FilesystemDecision, CompositionError> {
if !self.owns_context(context) {
return Err(CompositionError::PathContextMismatch);
}
if self.requested_policy().mode() == FilesystemMode::External
&& self.ceiling_policy().mode() == FilesystemMode::External
{
return Ok(FilesystemDecision::ExternallyEnforced);
}
let mut result = None;
for path in context.resolve(selector) {
let decision = self.access_for_path(&path, context)?;
result = Some(match result {
Some(previous) => combine_filesystem_decisions(previous, decision),
None => decision,
});
}
Ok(result.unwrap_or(FilesystemDecision::Deny))
}
pub fn access_for_path(
&self,
path: &Path,
context: &EffectivePathContext,
) -> Result<FilesystemDecision, CompositionError> {
if !self.owns_context(context) {
return Err(CompositionError::PathContextMismatch);
}
let requested = self
.requested_policy()
.access_for_path(path, context.raw())
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Filesystem,
source,
})?;
let ceiling = self
.ceiling_policy()
.access_for_path(path, context.raw())
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Filesystem,
source,
})?;
Ok(combine_filesystem_decisions(requested, ceiling))
}
}
impl EffectiveNetworkPolicy {
pub fn decision_for_domain(&self, domain: &str) -> Result<NetworkDecision, CompositionError> {
let requested = self
.requested_policy()
.decision_for_domain(domain)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
let ceiling = self
.ceiling_policy()
.decision_for_domain(domain)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
Ok(combine_network_decisions(requested, ceiling))
}
pub fn decision_for_domain_with_resolved_ips(
&self,
domain: &str,
resolved_ips: &[IpAddr],
) -> Result<NetworkDecision, CompositionError> {
let requested = self
.requested_policy()
.decision_for_domain_with_resolved_ips(domain, resolved_ips)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
let ceiling = self
.ceiling_policy()
.decision_for_domain_with_resolved_ips(domain, resolved_ips)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
Ok(combine_network_decisions(requested, ceiling))
}
pub fn authorize_connection(
&self,
target: &ResolvedNetworkTarget,
connected: SocketAddr,
) -> Result<ConnectionAuthorization, CompositionError> {
let requested = self
.requested_policy()
.authorize_connection(target, connected)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
let ceiling = self
.ceiling_policy()
.authorize_connection(target, connected)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
Ok(combine_connection_authorizations(requested, ceiling))
}
pub fn decision_for_unix_socket(
&self,
socket: &Path,
) -> Result<NetworkDecision, CompositionError> {
let requested = self
.requested_policy()
.decision_for_unix_socket(socket)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
let ceiling = self
.ceiling_policy()
.decision_for_unix_socket(socket)
.map_err(|source| CompositionError::PolicyEvaluation {
boundary: CompositionBoundary::Network,
source,
})?;
Ok(combine_network_decisions(requested, ceiling))
}
}
fn validate_ownership(
requested: impl EnforcementMode,
ceiling: impl EnforcementMode,
boundary: CompositionBoundary,
) -> Result<(), CompositionError> {
if requested.delegates_enforcement() != ceiling.delegates_enforcement() {
return Err(CompositionError::EnforcementOwnershipConflict { boundary });
}
Ok(())
}
fn validate_external_owner(
external: bool,
requested_owner: Option<&ExternalOwner>,
ceiling_owner: Option<&ExternalOwner>,
boundary: CompositionBoundary,
) -> Result<(), CompositionError> {
if external && (requested_owner != ceiling_owner || requested_owner.is_none()) {
return Err(CompositionError::ExternalOwnerMismatch { boundary });
}
Ok(())
}
impl EnforcementMode for FilesystemMode {
fn delegates_enforcement(self) -> bool {
self == Self::External
}
}
impl EnforcementMode for NetworkMode {
fn delegates_enforcement(self) -> bool {
self == Self::External
}
}
fn combine_filesystem_decisions(
requested: FilesystemDecision,
ceiling: FilesystemDecision,
) -> FilesystemDecision {
match (requested, ceiling) {
(FilesystemDecision::ExternallyEnforced, FilesystemDecision::ExternallyEnforced) => {
FilesystemDecision::ExternallyEnforced
}
(FilesystemDecision::ExternallyEnforced, _)
| (_, FilesystemDecision::ExternallyEnforced) => FilesystemDecision::Deny,
(left, right) => match (left.as_access_mode(), right.as_access_mode()) {
(Some(left), Some(right)) => AccessMode::most_restrictive(left, right).into(),
_ => FilesystemDecision::Deny,
},
}
}
fn combine_network_decisions(
requested: NetworkDecision,
ceiling: NetworkDecision,
) -> NetworkDecision {
match (requested, ceiling) {
(NetworkDecision::ExternallyEnforced, NetworkDecision::ExternallyEnforced) => {
NetworkDecision::ExternallyEnforced
}
(NetworkDecision::ExternallyEnforced, _) | (_, NetworkDecision::ExternallyEnforced) => {
NetworkDecision::Deny
}
(NetworkDecision::Allow, NetworkDecision::Allow) => NetworkDecision::Allow,
_ => NetworkDecision::Deny,
}
}
fn combine_connection_authorizations(
requested: ConnectionAuthorization,
ceiling: ConnectionAuthorization,
) -> ConnectionAuthorization {
match (requested, ceiling) {
(ConnectionAuthorization::Allowed(requested), ConnectionAuthorization::Allowed(_)) => {
ConnectionAuthorization::Allowed(requested)
}
(
ConnectionAuthorization::ExternallyEnforced,
ConnectionAuthorization::ExternallyEnforced,
) => ConnectionAuthorization::ExternallyEnforced,
_ => ConnectionAuthorization::Denied,
}
}
fn compose_workspace_roots(
requested_roots: Option<&[PathBuf]>,
ceiling_roots: Option<&[PathBuf]>,
) -> Result<ComposedWorkspaceRoots, CompositionError> {
let requested_roots = requested_roots
.map(|roots| normalize_roots(roots.iter().cloned()))
.transpose()?;
let ceiling_roots = ceiling_roots
.map(|roots| normalize_roots(roots.iter().cloned()))
.transpose()?;
match (requested_roots, ceiling_roots) {
(Some(requested), Some(ceiling)) => {
for root in &requested {
if !root_is_within(root, &ceiling) {
return Err(CompositionError::WorkspaceRootNotGranted { path: root.clone() });
}
}
Ok(ComposedWorkspaceRoots {
selected: Some(requested),
limit: Some(ceiling),
})
}
(Some(requested), None) => Ok(ComposedWorkspaceRoots {
selected: Some(requested),
limit: None,
}),
(None, Some(ceiling)) => Ok(ComposedWorkspaceRoots {
selected: None,
limit: Some(ceiling),
}),
(None, None) => Ok(ComposedWorkspaceRoots {
selected: None,
limit: None,
}),
}
}