Skip to content

Commit 1b0c2d4

Browse files
authored
feat: Implement net.cidr_contains builtin (microsoft#471)
Major changes: - Implement the `net.cidr_contains` builtin - Enable the v0 and v1 test for `net.cidr_contains` - Add the `netip` crate to standardize CIDR searching and other operations Key Concept: - Allow users to leverage the `net.cidr_contains` builtin to check whether an IPv4 or IPv6 CIDR contains a specified IP address or subnet. Testing: - All tests passing. Signed-off-by: tjons <tylerschade99@gmail.com>
1 parent 85753aa commit 1b0c2d4

10 files changed

Lines changed: 149 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ glob = ["dep:globset"]
3333
graph = []
3434
jsonschema = ["dep:jsonschema"]
3535
mimalloc = ["dep:mimalloc"]
36-
net = []
36+
net = ["dep:ipnet"]
3737
no_std = ["lazy_static/spin_no_std"]
3838
opa-runtime = []
3939
regex = ["dep:regex"]
@@ -108,6 +108,7 @@ uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-r
108108
jsonschema = { version = "0.30.0", default-features = false, optional = true }
109109
chrono = { version = "0.4.40", optional = true }
110110
chrono-tz = { version = "0.10.1", optional = true }
111+
ipnet = { version = "2.11.0", optional = true, default-features = false }
111112

112113
serde_yaml = {version = "0.9.16", default-features = false, optional = true }
113114
# Specify thread_rng for in order to use random_range

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,11 +303,9 @@ The following test suites don't pass fully due to missing builtins:
303303
- `jwtverifyhs384`
304304
- `jwtverifyhs512`
305305
- `jwtverifyrsa`
306-
- `netcidrcontains`
307306
- `netcidrcontainsmatches`
308307
- `netcidrexpand`
309308
- `netcidrintersects`
310-
- `netcidrisvalid`
311309
- `netcidrmerge`
312310
- `netcidroverlap`
313311
- `netlookupipaddr`

bindings/ffi/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/java/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/python/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/ruby/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/wasm/Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/builtins/net.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use core::net::IpAddr;
2+
use ipnet::IpNet;
3+
use std::format;
24
use std::sync::Arc;
35

46
use crate::ast::{Expr, Ref};
@@ -7,12 +9,13 @@ use crate::builtins::utils::ensure_args_count;
79
use crate::lexer::Span;
810
use crate::value::Value;
911

10-
use anyhow::Result;
12+
use anyhow::{anyhow, bail, Result};
1113

1214
use super::utils::ensure_string;
1315

1416
pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
1517
m.insert("net.cidr_is_valid", (cidr_is_valid, 1));
18+
m.insert("net.cidr_contains", (cidr_contains, 2));
1619
}
1720

1821
/// Checks if a CIDR string is valid or invalid. Uses the
@@ -59,6 +62,50 @@ fn is_valid_cidr(cidr: Arc<str>) -> bool {
5962
}
6063
}
6164

65+
pub fn cidr_contains(
66+
span: &Span,
67+
params: &[Ref<Expr>],
68+
args: &[Value],
69+
strict: bool,
70+
) -> Result<Value> {
71+
ensure_args_count(span, "cidr_contains", params, args, 2)?;
72+
let cidr = ensure_string("cidr_contains", &params[0], &args[0])?;
73+
let cidr_or_ip = ensure_string("cidr_contains", &params[1], &args[1])?;
74+
let contains = _cidr_contains(cidr, cidr_or_ip);
75+
76+
match contains {
77+
Ok(r) => Ok(Value::from(r)),
78+
// The rego implementation will retur an error in strict mode, see
79+
// https://github.com/open-policy-agent/opa/blob/main/v1/test/cases/testdata/v1/netcidrcontains/test-netcidrcontains-0100.yaml
80+
// as an example, so we will propagate the error if the builtin is
81+
// run in strict mode.
82+
Err(e) if strict => bail!(span.error(&format!("{e}"))),
83+
// If not in strict mode, an error will result in Undefined.
84+
_ => Ok(Value::Undefined),
85+
}
86+
}
87+
88+
fn _cidr_contains(cidr: Arc<str>, cidr_or_ip: Arc<str>) -> Result<bool> {
89+
let net = cidr
90+
.parse::<IpNet>()
91+
.map_err(|e| anyhow!("Error parsing {cidr}: {e}"))?;
92+
93+
if cidr_or_ip.contains("/") {
94+
let subnet = cidr_or_ip
95+
.parse::<IpNet>()
96+
.map_err(|e| anyhow!("Error parsing {cidr_or_ip} as CIDR: {e}"))?;
97+
98+
return Ok(net.contains(&subnet));
99+
}
100+
101+
// if the caller did not provide a CIDR string, try to parse
102+
// the input as an IP address.
103+
let subnet = cidr_or_ip
104+
.parse::<IpAddr>()
105+
.map_err(|e| anyhow!("Error parsing {cidr_or_ip} as IP address: {e}"))?;
106+
Ok(net.contains(&subnet))
107+
}
108+
62109
#[cfg(test)]
63110
mod net_tests {
64111
use super::*;
@@ -83,4 +130,58 @@ mod net_tests {
83130
);
84131
}
85132
}
133+
134+
#[test]
135+
fn test_cidr_contains() {
136+
let test_cases: std::vec::IntoIter<(Arc<str>, Arc<str>, bool, bool)> = Vec::from([
137+
// Each case is a tuple of (cidr, cidr_or_ip, expected Ok(result), and expected error)
138+
(
139+
Arc::from("127.0.0.1/32"),
140+
Arc::from("127.0.0.1"),
141+
true,
142+
false,
143+
),
144+
(
145+
Arc::from("10.0.0.0/8"),
146+
Arc::from("10.10.10.10"),
147+
true,
148+
false,
149+
),
150+
(
151+
Arc::from("10.0.0.0/8"),
152+
Arc::from("10.10.10.0/24"),
153+
true,
154+
false,
155+
),
156+
(Arc::from("fd00::/16"), Arc::from("fd00::/17"), true, false),
157+
(
158+
Arc::from("127.0.0.1/32"),
159+
Arc::from("127.0.0.2"),
160+
false,
161+
false,
162+
),
163+
(Arc::from("10.0.0.0/8"), Arc::from("11.0.0.1"), false, false),
164+
(Arc::from("fd00::/16"), Arc::from("fd00::/15"), false, false),
165+
(
166+
Arc::from("127.0.0.0/8"),
167+
Arc::from("not a cidr"),
168+
false,
169+
true,
170+
),
171+
])
172+
.into_iter();
173+
174+
for (cidr, sub, result, should_err) in test_cases {
175+
let got = _cidr_contains(cidr.clone(), sub.clone());
176+
match got {
177+
Err(_) if should_err => continue,
178+
Ok(res) if res == result => continue,
179+
_ => {
180+
panic!(
181+
"Expected `cidr_contains` for cidr {cidr} and subnet {sub} to be {result}"
182+
)
183+
}
184+
}
185+
}
186+
}
86187
}

tests/opa.passing

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ v0/jsonschema
4646
v0/negation
4747
v0/nestedreferences
4848
v0/netcidrisvalid
49+
v0/netcidrcontains
4950
v0/numbersrange
5051
v0/numbersrangestep
5152
v0/objectfilter
@@ -150,6 +151,7 @@ v1/jsonremoveidempotent
150151
v1/jsonschema
151152
v1/negation
152153
v1/nestedreferences
154+
v1/netcidrcontains
153155
v1/netcidrisvalid
154156
v1/numbersrange
155157
v1/numbersrangestep
@@ -206,4 +208,4 @@ v1/uuid
206208
v1/varreferences
207209
v1/virtualdocs
208210
v1/walkbuiltin
209-
v1/withkeyword
211+
v1/withkeyword

0 commit comments

Comments
 (0)