use super::super::kdl::*;
use super::super::PolicyRuleDef;
use super::node_line;
use crate::access::{PolicyAction, Predicate};
use ::kdl::KdlNode;
use anyhow::{anyhow, bail};
use ipnet::IpNet;
use std::net::IpAddr;
pub(super) fn parse_policy_statements(
node: &KdlNode,
src: &str,
name: &str,
tcp_only: bool,
) -> anyhow::Result<Vec<PolicyRuleDef>> {
let children = node.children().map(|d| d.nodes()).unwrap_or_default();
let mut stmts = Vec::new();
for child in children {
let child_line = node_line(src, child);
let stmt_name = child.name().value();
if stmt_name == "apply" {
let policy_name = arg_str(child, 0).ok_or_else(|| {
anyhow!(
"{name}:{child_line}: 'apply' requires a \
policy name argument"
)
})?;
stmts.push(PolicyRuleDef::Apply { name: policy_name });
continue;
}
let action = match stmt_name {
"allow" => PolicyAction::Allow,
"deny" => {
let code = child
.get("code")
.and_then(|e| e.as_integer())
.map(|n| n as u16)
.unwrap_or(403);
PolicyAction::Deny { code }
}
"redirect" => {
let to = child
.get("to")
.and_then(|e| e.as_string())
.map(String::from)
.ok_or_else(|| {
anyhow!(
"{name}:{child_line}: 'redirect' \
requires a 'to' property"
)
})?;
let code = child
.get("code")
.and_then(|e| e.as_integer())
.map(|n| n as u16)
.unwrap_or(302);
PolicyAction::Redirect { to, code }
}
other => bail!(
"{name}:{child_line}: unknown policy statement \
'{other}'; expected 'allow', 'deny', 'redirect', \
or 'apply'"
),
};
let predicate =
parse_predicate(child, src, name, child_line, tcp_only)?;
stmts.push(PolicyRuleDef::Rule { predicate, action });
}
Ok(stmts)
}
fn parse_predicate(
node: &KdlNode,
src: &str,
name: &str,
line: usize,
tcp_only: bool,
) -> anyhow::Result<Option<Predicate>> {
let pos_args: Vec<String> = node
.entries()
.iter()
.filter(|e| e.name().is_none())
.filter_map(|e| e.value().as_string().map(String::from))
.collect();
let has_block = node
.children()
.map(|d| !d.nodes().is_empty())
.unwrap_or(false);
if !pos_args.is_empty() && has_block {
bail!(
"{name}:{line}: '{}' cannot have both an inline \
predicate and a child block; use one or the other",
node.name().value()
);
}
if !pos_args.is_empty() {
let pred = parse_inline_predicate(&pos_args, name, line, tcp_only)?;
return Ok(Some(pred));
}
if has_block {
let cond_nodes = node.children().map(|d| d.nodes()).unwrap_or_default();
let mut preds = Vec::with_capacity(cond_nodes.len());
for cond in cond_nodes {
let cond_line = node_line(src, cond);
preds.push(parse_predicate_node(cond, name, cond_line, tcp_only)?);
}
return Ok(Some(if preds.len() == 1 {
preds.remove(0)
} else {
Predicate::And(preds)
}));
}
Ok(None)
}
fn parse_inline_predicate(
args: &[String],
name: &str,
line: usize,
tcp_only: bool,
) -> anyhow::Result<Predicate> {
if args[0] == "not" {
if args.len() < 2 {
bail!(
"{name}:{line}: 'not' requires a predicate type \
(e.g. not address \"10.0.0.0/8\")"
);
}
let inner =
build_simple_predicate(&args[1], &args[2..], name, line, tcp_only)?;
if tcp_only && inner.needs_auth() {
bail!(
"{name}:{line}: identity predicates are not supported \
in stream listener policy blocks \
(no HTTP authentication available)"
);
}
return Ok(Predicate::Not(Box::new(inner)));
}
build_simple_predicate(&args[0], &args[1..], name, line, tcp_only)
}
fn parse_predicate_node(
node: &KdlNode,
name: &str,
line: usize,
tcp_only: bool,
) -> anyhow::Result<Predicate> {
let pred_name = node.name().value();
let values: Vec<String> = node
.entries()
.iter()
.filter(|e| e.name().is_none())
.filter_map(|e| e.value().as_string().map(String::from))
.collect();
if pred_name == "not" {
if values.is_empty() {
bail!(
"{name}:{line}: 'not' requires a predicate type \
(e.g. not address \"10.0.0.0/8\")"
);
}
let inner = build_simple_predicate(
&values[0],
&values[1..],
name,
line,
tcp_only,
)?;
if tcp_only && inner.needs_auth() {
bail!(
"{name}:{line}: identity predicates are not \
supported in stream listener policy blocks \
(no HTTP authentication available)"
);
}
return Ok(Predicate::Not(Box::new(inner)));
}
build_simple_predicate(pred_name, &values, name, line, tcp_only)
}
fn build_simple_predicate(
pred_type: &str,
values: &[String],
name: &str,
line: usize,
tcp_only: bool,
) -> anyhow::Result<Predicate> {
match pred_type {
"address" => {
if values.is_empty() {
bail!(
"{name}:{line}: 'address' requires at least \
one CIDR or IP address argument"
);
}
let nets = values
.iter()
.map(|s| {
s.parse::<IpNet>()
.or_else(|_| s.parse::<IpAddr>().map(IpNet::from))
.map_err(|_| {
anyhow!(
"{name}:{line}: invalid IP address or \
CIDR '{s}'"
)
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(Predicate::Address(nets))
}
"country" => {
if values.is_empty() {
bail!(
"{name}:{line}: 'country' requires at least \
one country code argument"
);
}
Ok(Predicate::Country(
values.iter().map(|s| s.to_uppercase()).collect(),
))
}
"user" => {
if tcp_only {
bail!(
"{name}:{line}: 'user' predicates are not \
supported in stream listener policy blocks \
(no HTTP authentication available)"
);
}
if values.is_empty() {
bail!(
"{name}:{line}: 'user' requires at least \
one username argument"
);
}
Ok(Predicate::User(values.to_vec()))
}
"group" => {
if tcp_only {
bail!(
"{name}:{line}: 'group' predicates are not \
supported in stream listener policy blocks \
(no HTTP authentication available)"
);
}
if values.is_empty() {
bail!(
"{name}:{line}: 'group' requires at least \
one group name argument"
);
}
Ok(Predicate::Group(values.to_vec()))
}
"authenticated" => {
if tcp_only {
bail!(
"{name}:{line}: 'authenticated' predicates are \
not supported in stream listener policy blocks \
(no HTTP authentication available)"
);
}
Ok(Predicate::Authenticated)
}
other => bail!(
"{name}:{line}: unknown predicate type '{other}'; \
expected 'address', 'country', 'user', 'group', \
'authenticated', or 'not'"
),
}
}