blob: e0c5d7ebb80137c89ec52754a8bade857eb5631a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Additional authority requested from the user, not a replacement sandbox.
//! Host-protected resources and the caller's capability ceiling still apply.
use crate::{Error, Network, Sandbox, filesystem};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Permissions {
pub filesystem: Vec<filesystem::Rule>,
pub network: Network,
}
impl Permissions {
pub fn validate(&self) -> Result<(), Error> {
if self
.filesystem
.iter()
.any(|rule| rule.access == filesystem::Access::Deny)
{
return Err(Error::Invalid(
"additional permissions cannot contain denials".into(),
));
}
self.compile()?;
Ok(())
}
/// Partial approval may narrow paths, access and network, never expand them.
/// Checking both sides' exact/subtree boundaries also catches a broad grant
/// that would erase a read-only exception inside the requested subtree.
pub fn contains(&self, granted: &Self) -> Result<bool, Error> {
self.validate()?;
granted.validate()?;
if !self.network.contains(&granted.network) {
return Ok(false);
}
let requested = self.compile()?;
let granted_policy = granted.compile()?;
Ok(requested.contains_paths(&granted_policy))
}
fn compile(&self) -> Result<filesystem::Compiled, Error> {
filesystem::Policy {
default: filesystem::Access::Deny,
rules: self.filesystem.clone(),
deny_globs: Vec::new(),
}
.compile()
}
}
impl Sandbox {
pub fn permits(&self, requested: &Permissions) -> Result<bool, Error> {
requested.validate()?;
match self {
Self::Disabled => Ok(true),
Self::Managed {
filesystem,
network,
} => Ok(network.contains(&requested.network)
&& filesystem.deny_globs.is_empty()
&& filesystem.compile()?.contains_paths(&requested.compile()?)),
Self::External { .. } => Ok(false),
}
}
/// Apply an already-approved additive grant under an immutable Host ceiling.
/// Neither the approval nor this calculation authorizes an effect to start.
pub fn with_grant(&self, grant: &Permissions, ceiling: &Self) -> Result<Self, Error> {
grant.validate()?;
let widened = match self {
Self::Managed {
filesystem,
network,
} => Self::Managed {
filesystem: filesystem
.compile()?
.with_grants(&grant.compile()?)?
.policy()
.clone(),
network: network.union(&grant.network),
},
Self::Disabled => Self::Disabled,
Self::External { .. } => {
return Err(Error::Unsupported(
"external isolation requires executor-owned permission grants".into(),
));
}
};
widened.intersect(ceiling)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
Once,
Turn,
Session,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "decision", rename_all = "snake_case", deny_unknown_fields)]
pub enum Decision {
Deny,
Allow {
permissions: Permissions,
scope: Scope,
},
}
impl Decision {
pub fn validate_for(&self, requested: &Permissions) -> Result<(), Error> {
requested.validate()?;
if let Self::Allow { permissions, .. } = self
&& !requested.contains(permissions)?
{
return Err(Error::Invalid(
"approved permissions exceed the request".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use filesystem::{Access, Rule};
#[test]
fn partial_approval_cannot_widen_nested_or_exact_authority() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path();
let requested = Permissions {
filesystem: vec![
Rule::subtree(root, Access::Write),
Rule::subtree(root.join("restricted"), Access::Read),
],
network: Network::Denied,
};
let mut granted = Permissions {
filesystem: vec![Rule::subtree(root, Access::Read)],
network: Network::Denied,
};
assert!(requested.contains(&granted).unwrap());
granted.filesystem[0].access = Access::Write;
assert!(!requested.contains(&granted).unwrap());
granted.filesystem = vec![Rule::exact(root.join("file"), Access::Write)];
assert!(requested.contains(&granted).unwrap());
let exact = granted.clone();
granted.filesystem[0].scope = filesystem::Scope::Subtree;
assert!(!exact.contains(&granted).unwrap());
let sandbox = |grant: &Permissions| Sandbox::Managed {
filesystem: filesystem::Policy {
default: Access::Read,
rules: grant.filesystem.clone(),
deny_globs: Vec::new(),
},
network: grant.network.clone(),
};
assert!(!sandbox(&exact).contains(&sandbox(&granted)).unwrap());
assert!(sandbox(&granted).contains(&sandbox(&exact)).unwrap());
granted.network = Network::Allowed;
assert!(!sandbox(&requested).contains(&sandbox(&granted)).unwrap());
assert!(!requested.contains(&granted).unwrap());
granted.filesystem[0].path = root.join("../escape");
assert!(requested.contains(&granted).is_err());
}
}