///|
/// Narrows this policy by intersecting its permissions with `restriction`.
///
/// Allow-lists and path roots are intersected. Protected paths and approval
/// requirements are combined, while numeric limits take the stricter value.
/// A rule present in only one policy cannot grant a permission absent from the
/// other policy.
pub fn Policy::restrict(self : Policy, restriction : Policy) -> Policy {
{
allowed_tools: intersect_strings(
self.allowed_tools,
restriction.allowed_tools,
),
read_roots: intersect_path_roots(self.read_roots, restriction.read_roots),
write_roots: intersect_path_roots(self.write_roots, restriction.write_roots),
protected_paths: union_strings(
self.protected_paths,
restriction.protected_paths,
),
network_rules: intersect_network_rules(
self.network_rules,
restriction.network_rules,
),
command_rules: intersect_command_rules(
self.command_rules,
restriction.command_rules,
),
resource_rules: intersect_resource_rules(
self.resource_rules,
restriction.resource_rules,
),
tool_quotas: intersect_tool_quotas(
self.tool_quotas,
restriction.tool_quotas,
),
approval_required_tools: union_strings(
self.approval_required_tools,
restriction.approval_required_tools,
),
max_calls: minimum(self.max_calls, restriction.max_calls),
max_operation_bytes: minimum(
self.max_operation_bytes,
restriction.max_operation_bytes,
),
max_io_bytes: minimum(self.max_io_bytes, restriction.max_io_bytes),
}
}
///|
fn intersect_strings(
left : Array[String],
right : Array[String],
) -> Array[String] {
let result = []
for value in left {
if right.contains(value) && !result.contains(value) {
result.push(value)
}
}
result
}
///|
fn union_strings(left : Array[String], right : Array[String]) -> Array[String] {
let result = []
for value in left {
if !result.contains(value) {
result.push(value)
}
}
for value in right {
if !result.contains(value) {
result.push(value)
}
}
result
}
///|
fn intersect_path_roots(
left : Array[String],
right : Array[String],
) -> Array[String] {
let result = []
for left_root in left {
guard normalize_path(left_root) is Some(normalized_left) else { continue }
for right_root in right {
guard normalize_path(right_root) is Some(normalized_right) else {
continue
}
let narrower = if normalized_left == normalized_right ||
path_is_within(normalized_left, [normalized_right]) {
Some(normalized_left)
} else if path_is_within(normalized_right, [normalized_left]) {
Some(normalized_right)
} else {
None
}
match narrower {
Some(root) if !result.contains(root) => result.push(root)
_ => ()
}
}
}
result
}
///|
fn intersect_network_rules(
left : Array[NetworkRule],
right : Array[NetworkRule],
) -> Array[NetworkRule] {
let result = []
for left_rule in left {
for right_rule in right {
if left_rule.host == right_rule.host {
let ports = intersect_ints(
left_rule.allowed_ports,
right_rule.allowed_ports,
)
if ports.length() > 0 {
result.push({ host: left_rule.host, allowed_ports: ports, })
}
}
}
}
result
}
///|
fn intersect_ints(left : Array[Int], right : Array[Int]) -> Array[Int] {
let result = []
for value in left {
if right.contains(value) && !result.contains(value) {
result.push(value)
}
}
result
}
///|
fn intersect_command_rules(
left : Array[CommandRule],
right : Array[CommandRule],
) -> Array[CommandRule] {
let result = []
for left_rule in left {
for right_rule in right {
if left_rule.program == right_rule.program {
let left_prefix = left_rule.argument_prefix
let right_prefix = right_rule.argument_prefix
let prefix = if left_prefix.length() == 0 && right_prefix.length() == 0 {
Some([])
} else if left_prefix.length() == 0 || right_prefix.length() == 0 {
None
} else if left_prefix.length() >= right_prefix.length() &&
left_prefix.starts_with(right_prefix) {
Some(left_prefix.copy())
} else if right_prefix.length() >= left_prefix.length() &&
right_prefix.starts_with(left_prefix) {
Some(right_prefix.copy())
} else {
None
}
match prefix {
Some(argument_prefix) =>
result.push({ program: left_rule.program, argument_prefix, })
None => ()
}
}
}
}
result
}
///|
fn intersect_resource_rules(
left : Array[ResourceRule],
right : Array[ResourceRule],
) -> Array[ResourceRule] {
let result = []
for left_rule in left {
for right_rule in right {
if left_rule.tool == right_rule.tool &&
left_rule.action == right_rule.action {
let prefix = if left_rule.resource_prefix == right_rule.resource_prefix {
Some(left_rule.resource_prefix)
} else if resource_prefix_is_within(
left_rule.resource_prefix,
right_rule.resource_prefix,
) {
Some(left_rule.resource_prefix)
} else if resource_prefix_is_within(
right_rule.resource_prefix,
left_rule.resource_prefix,
) {
Some(right_rule.resource_prefix)
} else {
None
}
match prefix {
Some(resource_prefix) =>
result.push({
tool: left_rule.tool,
action: left_rule.action,
resource_prefix,
})
None => ()
}
}
}
}
result
}
///|
fn resource_prefix_is_within(candidate : String, parent : String) -> Bool {
candidate.has_prefix(parent + "/")
}
///|
fn intersect_tool_quotas(
left : Array[ToolQuota],
right : Array[ToolQuota],
) -> Array[ToolQuota] {
let tools = []
for quota in left {
if !tools.contains(quota.tool) {
tools.push(quota.tool)
}
}
for quota in right {
if !tools.contains(quota.tool) {
tools.push(quota.tool)
}
}
let result = []
for tool in tools {
match minimum_tool_quota(tool, left, right) {
Some(max_calls) => result.push({ tool, max_calls, })
None => ()
}
}
result
}
///|
fn minimum_tool_quota(
tool : String,
left : Array[ToolQuota],
right : Array[ToolQuota],
) -> Int? {
let mut limit : Int? = None
for quota in left {
if quota.tool == tool {
limit = match limit {
Some(current) => Some(minimum(current, quota.max_calls))
None => Some(quota.max_calls)
}
}
}
for quota in right {
if quota.tool == tool {
limit = match limit {
Some(current) => Some(minimum(current, quota.max_calls))
None => Some(quota.max_calls)
}
}
}
limit
}
///|
fn minimum(left : Int, right : Int) -> Int {
if left < right {
left
} else {
right
}
}