From 53a2ce50f9d95d496222da5a2caa64100dc8a123 Mon Sep 17 00:00:00 2001 From: tjons Date: Sun, 22 Jun 2025 19:55:56 -0400 Subject: [PATCH 1/5] wip on parsing ipv4 addresses Signed-off-by: tjons --- Cargo.toml | 2 + src/builtins/mod.rs | 5 ++ src/builtins/net.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 src/builtins/net.rs diff --git a/Cargo.toml b/Cargo.toml index 9ca8b6f87..7e131d945 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ http = [] glob = ["dep:globset"] graph = [] jsonschema = ["dep:jsonschema"] +net = [] no_std = ["lazy_static/spin_no_std"] opa-runtime = [] regex = ["dep:regex"] @@ -49,6 +50,7 @@ full-opa = [ "hex", "http", "jsonschema", + "net", "opa-runtime", "regex", "semver", diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 73a8c4e8e..0a300c381 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -14,6 +14,9 @@ mod glob; mod graph; #[cfg(feature = "http")] mod http; +#[cfg(feature = "net")] +mod net; + pub mod numbers; mod objects; #[cfg(feature = "opa-runtime")] @@ -80,6 +83,8 @@ lazy_static! { //graphql::register(&mut m); #[cfg(feature = "http")] http::register(&mut m); + #[cfg(feature = "net")] + net::register(&mut m); //net::register(&mut m); #[cfg(feature = "uuid")] uuid::register(&mut m); diff --git a/src/builtins/net.rs b/src/builtins/net.rs new file mode 100644 index 000000000..6e3d9bd90 --- /dev/null +++ b/src/builtins/net.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use crate::ast::{Expr, Ref}; +use crate::builtins; +use crate::lexer::Span; +use crate::builtins::utils::ensure_args_count; +use crate::value::Value; + +use anyhow::{bail, Ok, Result}; + +use super::utils::ensure_string; + +pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { + m.insert("net.cidr_is_valid", (cidr_is_valid, 1)); +} + +pub fn cidr_is_valid(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + ensure_args_count(span, "cidr_is_valid", params, args, 1)?; + let cidr = ensure_string("cidr_is_valid", ¶ms[0], &args[0])?; + _cidr_is_valid(cidr) +} + +fn _cidr_is_valid(cidr: Arc) -> Result { + let mut pieces= cidr.split("/"); + let addr_piece = pieces.next(); + let mask_piece = pieces.next(); + // TODO(tjons): check if there are more segments here... + let mut v4: bool = false; + let mut v6: bool = false; + match addr_piece { + None => bail!("cidr not valid"), + Some(addr) => { + for c in addr.chars() { + match c { + '.' => { + v4 = true; + break; + }, + ':' => { + v6 = true; + break; + }, + _ => continue, + } + } + } + }; + + if v4 { + let retval = parse_ipv4(addr_piece.unwrap())?; + } else if v6 { + let retval = parse_ipv6(addr_piece.unwrap()); + } + + match mask_piece { + None => bail!("cidr not valid"), + _ => (), + } + + Ok(Value::Bool(true)) +} + + +fn parse_ipv4_or_err(addr: &str) -> Result<[u32; 4]> { + // store each octet in the array + let mut fields: [u32; 4] = [0; 4]; + let mut cur_octet: u32 = 0; + let mut octet_digits = 0; + let mut pos = 0; + let mut prev: char = '\0'; + + for c in addr.chars() { + if c >= '0' && c <= '9' { + if octet_digits == 1 && cur_octet == 0 { + bail!("IPv4 field has octet with leading zero"); + } + // safe to unwrap this value because we check above that it is indeed a digit + cur_octet = cur_octet * 10 + c.to_digit(10).unwrap(); + octet_digits += 1; + + if cur_octet > 255 { + bail!("IPv4 field has value >255"); + } + } else if c == '.' { + if pos == 0 || prev == '.' { + bail!("IPv4 field must have at least one digit"); + } + + if pos == 3 { + bail!("IPv4 address too long"); + } + fields[pos] = cur_octet; + pos += 1; + cur_octet = 0; + octet_digits = 0; + } else { + bail!("unexpected character"); + } + prev = c; + } + + Ok(fields) +} + +fn parse_ipv6(addr: &str) -> Result<(&str, i64)> { + bail!("not implemented yet") +} + +#[cfg(test)] +mod net_tests { + use super::*; + + #[test] + fn test_cidr_is_valid() { + let valids = Vec::from([ + "127.0.0.1/32", + "10.0.0.0/8", + ]); + + let invalids = Vec::from([ + "0.1.2.3/32", + "256.0.0.0/8", + ]); + + for cidr in valids { + _cidr_is_valid(Arc::from(cidr)).expect("Valid CIDR returned invalid"); + } + + for cidr in invalids { + _cidr_is_valid(Arc::from(cidr)).expect_err("Invalid CIDR returned valid"); + } + } +} From 44875294e93703ac7db5c289ad91dade467d954e Mon Sep 17 00:00:00 2001 From: tjons Date: Mon, 30 Jun 2025 09:11:56 -0400 Subject: [PATCH 2/5] finish ipv4 cidr_is_valid Signed-off-by: tjons --- src/builtins/net.rs | 83 ++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/src/builtins/net.rs b/src/builtins/net.rs index 6e3d9bd90..8a83792f9 100644 --- a/src/builtins/net.rs +++ b/src/builtins/net.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use crate::ast::{Expr, Ref}; use crate::builtins; -use crate::lexer::Span; use crate::builtins::utils::ensure_args_count; +use crate::lexer::Span; use crate::value::Value; use anyhow::{bail, Ok, Result}; @@ -14,14 +14,24 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn m.insert("net.cidr_is_valid", (cidr_is_valid, 1)); } -pub fn cidr_is_valid(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { +/// Checks if a CIDR string is valid or invalid. Based on the +/// golang standard library implementation, as that is how the +/// built-in is implemented in the Open Policy Agent rego standardt +/// lib. +/// https://github.com/golang/go/blob/master/src/net/ip.go#L550 +pub fn cidr_is_valid( + span: &Span, + params: &[Ref], + args: &[Value], + _strict: bool, +) -> Result { ensure_args_count(span, "cidr_is_valid", params, args, 1)?; let cidr = ensure_string("cidr_is_valid", ¶ms[0], &args[0])?; _cidr_is_valid(cidr) } fn _cidr_is_valid(cidr: Arc) -> Result { - let mut pieces= cidr.split("/"); + let mut pieces = cidr.split("/"); let addr_piece = pieces.next(); let mask_piece = pieces.next(); // TODO(tjons): check if there are more segments here... @@ -35,11 +45,11 @@ fn _cidr_is_valid(cidr: Arc) -> Result { '.' => { v4 = true; break; - }, + } ':' => { v6 = true; break; - }, + } _ => continue, } } @@ -47,20 +57,29 @@ fn _cidr_is_valid(cidr: Arc) -> Result { }; if v4 { - let retval = parse_ipv4(addr_piece.unwrap())?; + let _retval = parse_ipv4_or_err(addr_piece.unwrap())?; } else if v6 { - let retval = parse_ipv6(addr_piece.unwrap()); + let _retval = parse_ipv6(addr_piece.unwrap()); } - match mask_piece { - None => bail!("cidr not valid"), - _ => (), + let mask = mask_piece + .expect("CIDR mask invalid") + .parse::() + .unwrap(); + if mask < 0 { + bail!("subnet mask cannot be less than 0"); + } + if v4 && mask > 32 { + bail!("ipv4 CIDR cannot have a mask greater than 32"); + } + + if v6 && mask > 128 { + bail!("ipv6 CIDR cannot have a mask greater than 128"); } Ok(Value::Bool(true)) } - fn parse_ipv4_or_err(addr: &str) -> Result<[u32; 4]> { // store each octet in the array let mut fields: [u32; 4] = [0; 4]; @@ -70,26 +89,32 @@ fn parse_ipv4_or_err(addr: &str) -> Result<[u32; 4]> { let mut prev: char = '\0'; for c in addr.chars() { - if c >= '0' && c <= '9' { - if octet_digits == 1 && cur_octet == 0 { - bail!("IPv4 field has octet with leading zero"); - } + if c.is_ascii_digit() { // safe to unwrap this value because we check above that it is indeed a digit cur_octet = cur_octet * 10 + c.to_digit(10).unwrap(); + // if this is the second character of octets 2, 3, or 4; and the + // octet == 0, like `1.00.x.x`, the CIDR cannot be valid. + // if this is the first character of octet 1 and it is 0, + // this is an invalid CIDR. + if (octet_digits == 1 || pos == 0) && cur_octet == 0 { + bail!("IPv4 field has octet with leading zero"); + } octet_digits += 1; if cur_octet > 255 { bail!("IPv4 field has value >255"); } } else if c == '.' { - if pos == 0 || prev == '.' { + // the CIDR may not start with a `.`, and there may not + // be two consecutive `.` characters. + if octet_digits == 0 || prev == '.' { bail!("IPv4 field must have at least one digit"); } if pos == 3 { bail!("IPv4 address too long"); } - fields[pos] = cur_octet; + fields[pos] = cur_octet; pos += 1; cur_octet = 0; octet_digits = 0; @@ -102,32 +127,34 @@ fn parse_ipv4_or_err(addr: &str) -> Result<[u32; 4]> { Ok(fields) } -fn parse_ipv6(addr: &str) -> Result<(&str, i64)> { +fn parse_ipv6(_addr: &str) -> Result<(&str, i64)> { bail!("not implemented yet") } #[cfg(test)] mod net_tests { use super::*; + use std::format; + use std::vec::Vec; #[test] fn test_cidr_is_valid() { - let valids = Vec::from([ - "127.0.0.1/32", - "10.0.0.0/8", - ]); + let valids = Vec::from(["127.0.0.1/32", "10.0.0.0/8"]); - let invalids = Vec::from([ - "0.1.2.3/32", - "256.0.0.0/8", - ]); + let invalids = Vec::from(["0.1.2.3/32", "256.0.0.0/8"]); for cidr in valids { - _cidr_is_valid(Arc::from(cidr)).expect("Valid CIDR returned invalid"); + assert_eq!( + _cidr_is_valid(Arc::from(cidr)).unwrap(), + Value::Bool(true), + "Valid CIDR {} deemed invalid", + cidr + ); } for cidr in invalids { - _cidr_is_valid(Arc::from(cidr)).expect_err("Invalid CIDR returned valid"); + _cidr_is_valid(Arc::from(cidr)) + .expect_err(format!("Invalid CIDR {} deemed valid", cidr).as_str()); } } } From 5797da637e71d147e99474c66ccb2b625f18750a Mon Sep 17 00:00:00 2001 From: tjons Date: Mon, 28 Jul 2025 14:52:13 -0400 Subject: [PATCH 3/5] implement v6 and handle code review Signed-off-by: tjons --- src/builtins/net.rs | 138 +++++++++++--------------------------------- 1 file changed, 33 insertions(+), 105 deletions(-) diff --git a/src/builtins/net.rs b/src/builtins/net.rs index 8a83792f9..fb2641e9e 100644 --- a/src/builtins/net.rs +++ b/src/builtins/net.rs @@ -1,3 +1,4 @@ +use core::net::IpAddr; use std::sync::Arc; use crate::ast::{Expr, Ref}; @@ -6,7 +7,7 @@ use crate::builtins::utils::ensure_args_count; use crate::lexer::Span; use crate::value::Value; -use anyhow::{bail, Ok, Result}; +use anyhow::{bail, Ok as aOk, Result}; use super::utils::ensure_string; @@ -14,11 +15,10 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn m.insert("net.cidr_is_valid", (cidr_is_valid, 1)); } -/// Checks if a CIDR string is valid or invalid. Based on the -/// golang standard library implementation, as that is how the -/// built-in is implemented in the Open Policy Agent rego standardt -/// lib. -/// https://github.com/golang/go/blob/master/src/net/ip.go#L550 +/// Checks if a CIDR string is valid or invalid. Uses the +/// `net::IpAddr` type to determine if the string is a valid IP, +/// and checks to ensure that the mask is in bounds for the parsed +/// IP address type (v4 or v6). pub fn cidr_is_valid( span: &Span, params: &[Ref], @@ -27,108 +27,37 @@ pub fn cidr_is_valid( ) -> Result { ensure_args_count(span, "cidr_is_valid", params, args, 1)?; let cidr = ensure_string("cidr_is_valid", ¶ms[0], &args[0])?; - _cidr_is_valid(cidr) + + match is_valid_cidr(cidr) { + Ok(result) => aOk(result), + Err(_) => bail!(span.error("invalid CIDR")), + } } -fn _cidr_is_valid(cidr: Arc) -> Result { - let mut pieces = cidr.split("/"); - let addr_piece = pieces.next(); - let mask_piece = pieces.next(); - // TODO(tjons): check if there are more segments here... - let mut v4: bool = false; - let mut v6: bool = false; - match addr_piece { - None => bail!("cidr not valid"), - Some(addr) => { - for c in addr.chars() { - match c { - '.' => { - v4 = true; - break; +fn is_valid_cidr(cidr: Arc) -> Result { + let Some((ip_addr, prefix_len)) = cidr.split_once("/") else { + bail!("invalid CIDR") + }; + match ip_addr.parse::() { + Ok(addr) => { + let mask = prefix_len.parse::()?; + + match addr { + IpAddr::V4(_) => { + if !(0..=32).contains(&mask) { + bail!("invalid CIDR") } - ':' => { - v6 = true; - break; + } + IpAddr::V6(_) => { + if !(0..=128).contains(&mask) { + bail!("invalid CIDR") } - _ => continue, } } + aOk(Value::Bool(true)) } - }; - - if v4 { - let _retval = parse_ipv4_or_err(addr_piece.unwrap())?; - } else if v6 { - let _retval = parse_ipv6(addr_piece.unwrap()); - } - - let mask = mask_piece - .expect("CIDR mask invalid") - .parse::() - .unwrap(); - if mask < 0 { - bail!("subnet mask cannot be less than 0"); - } - if v4 && mask > 32 { - bail!("ipv4 CIDR cannot have a mask greater than 32"); - } - - if v6 && mask > 128 { - bail!("ipv6 CIDR cannot have a mask greater than 128"); + Err(_) => bail!("Invalid CIDR"), } - - Ok(Value::Bool(true)) -} - -fn parse_ipv4_or_err(addr: &str) -> Result<[u32; 4]> { - // store each octet in the array - let mut fields: [u32; 4] = [0; 4]; - let mut cur_octet: u32 = 0; - let mut octet_digits = 0; - let mut pos = 0; - let mut prev: char = '\0'; - - for c in addr.chars() { - if c.is_ascii_digit() { - // safe to unwrap this value because we check above that it is indeed a digit - cur_octet = cur_octet * 10 + c.to_digit(10).unwrap(); - // if this is the second character of octets 2, 3, or 4; and the - // octet == 0, like `1.00.x.x`, the CIDR cannot be valid. - // if this is the first character of octet 1 and it is 0, - // this is an invalid CIDR. - if (octet_digits == 1 || pos == 0) && cur_octet == 0 { - bail!("IPv4 field has octet with leading zero"); - } - octet_digits += 1; - - if cur_octet > 255 { - bail!("IPv4 field has value >255"); - } - } else if c == '.' { - // the CIDR may not start with a `.`, and there may not - // be two consecutive `.` characters. - if octet_digits == 0 || prev == '.' { - bail!("IPv4 field must have at least one digit"); - } - - if pos == 3 { - bail!("IPv4 address too long"); - } - fields[pos] = cur_octet; - pos += 1; - cur_octet = 0; - octet_digits = 0; - } else { - bail!("unexpected character"); - } - prev = c; - } - - Ok(fields) -} - -fn parse_ipv6(_addr: &str) -> Result<(&str, i64)> { - bail!("not implemented yet") } #[cfg(test)] @@ -139,13 +68,12 @@ mod net_tests { #[test] fn test_cidr_is_valid() { - let valids = Vec::from(["127.0.0.1/32", "10.0.0.0/8"]); - - let invalids = Vec::from(["0.1.2.3/32", "256.0.0.0/8"]); + let valids = Vec::from(["127.0.0.1/32", "10.0.0.0/8", "0.1.2.3/32", "::1/128"]); + let invalids = Vec::from(["256.0.0.0/8", "127.0.0.1/33", "::1/129"]); for cidr in valids { assert_eq!( - _cidr_is_valid(Arc::from(cidr)).unwrap(), + is_valid_cidr(Arc::from(cidr)).unwrap(), Value::Bool(true), "Valid CIDR {} deemed invalid", cidr @@ -153,7 +81,7 @@ mod net_tests { } for cidr in invalids { - _cidr_is_valid(Arc::from(cidr)) + is_valid_cidr(Arc::from(cidr)) .expect_err(format!("Invalid CIDR {} deemed valid", cidr).as_str()); } } From e322f5b3d39e04c6a0be0774fc0cba833a4c237f Mon Sep 17 00:00:00 2001 From: tjons Date: Mon, 4 Aug 2025 17:28:53 -0400 Subject: [PATCH 4/5] make errors more informative Signed-off-by: tjons --- src/builtins/net.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/builtins/net.rs b/src/builtins/net.rs index fb2641e9e..6d681c6bc 100644 --- a/src/builtins/net.rs +++ b/src/builtins/net.rs @@ -1,4 +1,5 @@ use core::net::IpAddr; +use std::format; use std::sync::Arc; use crate::ast::{Expr, Ref}; @@ -7,7 +8,7 @@ use crate::builtins::utils::ensure_args_count; use crate::lexer::Span; use crate::value::Value; -use anyhow::{bail, Ok as aOk, Result}; +use anyhow::{anyhow, bail, Result}; use super::utils::ensure_string; @@ -28,10 +29,7 @@ pub fn cidr_is_valid( ensure_args_count(span, "cidr_is_valid", params, args, 1)?; let cidr = ensure_string("cidr_is_valid", ¶ms[0], &args[0])?; - match is_valid_cidr(cidr) { - Ok(result) => aOk(result), - Err(_) => bail!(span.error("invalid CIDR")), - } + is_valid_cidr(cidr).map_err(|e| span.error(&format!("{}", e))) } fn is_valid_cidr(cidr: Arc) -> Result { @@ -40,23 +38,28 @@ fn is_valid_cidr(cidr: Arc) -> Result { }; match ip_addr.parse::() { Ok(addr) => { - let mask = prefix_len.parse::()?; + let mask = prefix_len + .parse::() + .map_err(|_| anyhow!("failed to parse prefix length"))?; match addr { IpAddr::V4(_) => { if !(0..=32).contains(&mask) { - bail!("invalid CIDR") + bail!("invalid CIDR: {} is not a valid IPv4 CIDR mask", &mask) } } IpAddr::V6(_) => { if !(0..=128).contains(&mask) { - bail!("invalid CIDR") + bail!("invalid CIDR: {} is not a valid IPv6 CIDR mask", &mask) } } } - aOk(Value::Bool(true)) + Ok(Value::Bool(true)) } - Err(_) => bail!("Invalid CIDR"), + Err(_) => bail!( + "Invalid CIDR: {} could not be parsed as a IPv4 or IPv6 address", + ip_addr + ), } } From 74b2b25060001c19551b6e30ece271a4fd26497f Mon Sep 17 00:00:00 2001 From: tjons Date: Tue, 5 Aug 2025 16:17:36 -0400 Subject: [PATCH 5/5] rework control flow to return a bool, not an error; enable builtin tests Signed-off-by: tjons --- src/builtins/net.rs | 41 ++++++++++++++++++----------------------- tests/opa.passing | 2 ++ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/builtins/net.rs b/src/builtins/net.rs index 6d681c6bc..874e2815e 100644 --- a/src/builtins/net.rs +++ b/src/builtins/net.rs @@ -1,5 +1,4 @@ use core::net::IpAddr; -use std::format; use std::sync::Arc; use crate::ast::{Expr, Ref}; @@ -8,7 +7,7 @@ use crate::builtins::utils::ensure_args_count; use crate::lexer::Span; use crate::value::Value; -use anyhow::{anyhow, bail, Result}; +use anyhow::Result; use super::utils::ensure_string; @@ -29,44 +28,40 @@ pub fn cidr_is_valid( ensure_args_count(span, "cidr_is_valid", params, args, 1)?; let cidr = ensure_string("cidr_is_valid", ¶ms[0], &args[0])?; - is_valid_cidr(cidr).map_err(|e| span.error(&format!("{}", e))) + Ok(Value::from(is_valid_cidr(cidr))) } -fn is_valid_cidr(cidr: Arc) -> Result { +fn is_valid_cidr(cidr: Arc) -> bool { let Some((ip_addr, prefix_len)) = cidr.split_once("/") else { - bail!("invalid CIDR") + return false; }; match ip_addr.parse::() { Ok(addr) => { - let mask = prefix_len - .parse::() - .map_err(|_| anyhow!("failed to parse prefix length"))?; + let Ok(mask) = prefix_len.parse::() else { + return false; + }; match addr { IpAddr::V4(_) => { if !(0..=32).contains(&mask) { - bail!("invalid CIDR: {} is not a valid IPv4 CIDR mask", &mask) + return false; } } IpAddr::V6(_) => { if !(0..=128).contains(&mask) { - bail!("invalid CIDR: {} is not a valid IPv6 CIDR mask", &mask) + return false; } } } - Ok(Value::Bool(true)) + true } - Err(_) => bail!( - "Invalid CIDR: {} could not be parsed as a IPv4 or IPv6 address", - ip_addr - ), + Err(_) => false, } } #[cfg(test)] mod net_tests { use super::*; - use std::format; use std::vec::Vec; #[test] @@ -75,17 +70,17 @@ mod net_tests { let invalids = Vec::from(["256.0.0.0/8", "127.0.0.1/33", "::1/129"]); for cidr in valids { - assert_eq!( - is_valid_cidr(Arc::from(cidr)).unwrap(), - Value::Bool(true), - "Valid CIDR {} deemed invalid", - cidr + assert!( + is_valid_cidr(Arc::from(cidr)), + "Valid CIDR {cidr} deemed invalid" ); } for cidr in invalids { - is_valid_cidr(Arc::from(cidr)) - .expect_err(format!("Invalid CIDR {} deemed valid", cidr).as_str()); + assert!( + !is_valid_cidr(Arc::from(cidr)), + "Invalid CIDR {cidr} deemed valid" + ); } } } diff --git a/tests/opa.passing b/tests/opa.passing index 170c0a85a..b81fb715a 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -45,6 +45,7 @@ v0/jsonremoveidempotent v0/jsonschema v0/negation v0/nestedreferences +v0/netcidrisvalid v0/numbersrange v0/numbersrangestep v0/objectfilter @@ -149,6 +150,7 @@ v1/jsonremoveidempotent v1/jsonschema v1/negation v1/nestedreferences +v1/netcidrisvalid v1/numbersrange v1/numbersrangestep v1/objectfilter