Skip to content

Commit 9291bbe

Browse files
committed
refactor(cli): extract allowed-ip option parsing
Signed-off-by: Adrien Langou <alangou@nvidia.com>
1 parent 173a0bc commit 9291bbe

2 files changed

Lines changed: 60 additions & 56 deletions

File tree

crates/openshell-cli/src/policy_update.rs

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ fn parse_add_endpoint_spec(spec: &str) -> Result<NetworkEndpoint> {
351351
Ok(endpoint)
352352
}
353353

354+
const ALLOWED_IP_OPTION_PREFIX: &str = "allowed-ip=";
355+
354356
fn apply_add_endpoint_options(
355357
spec: &str,
356358
endpoint: &mut NetworkEndpoint,
@@ -379,37 +381,40 @@ fn apply_add_endpoint_options(
379381
ensure_request_body_credential_rewrite_protocol(spec, endpoint)?;
380382
endpoint.request_body_credential_rewrite = true;
381383
}
382-
_ => {
383-
let Some(allowed_ip) = option.strip_prefix("allowed-ip=") else {
384-
return Err(miette!(
385-
"--add-endpoint options segment supports only 'allow-uninspected-credentials', 'websocket-credential-rewrite', 'request-body-credential-rewrite', and 'allowed-ip=<CIDR-or-IP>'; got '{option}' in '{spec}'"
386-
));
387-
};
388-
let allowed_ip = allowed_ip.trim();
389-
if allowed_ip.is_empty() {
390-
return Err(miette!(
391-
"--add-endpoint allowed-ip option must include a CIDR or IP value in '{spec}'"
392-
));
393-
}
394-
if allowed_ip.contains(char::is_whitespace) {
395-
return Err(miette!(
396-
"--add-endpoint allowed-ip option must not contain whitespace in '{spec}'"
397-
));
398-
}
399-
if !endpoint
400-
.allowed_ips
401-
.iter()
402-
.any(|existing| existing == allowed_ip)
403-
{
404-
endpoint.allowed_ips.push(allowed_ip.to_string());
384+
_ if option.starts_with(ALLOWED_IP_OPTION_PREFIX) => {
385+
let allowed_ip =
386+
parse_allowed_ip_value(spec, &option[ALLOWED_IP_OPTION_PREFIX.len()..])?;
387+
if !endpoint.allowed_ips.contains(&allowed_ip) {
388+
endpoint.allowed_ips.push(allowed_ip);
405389
}
406390
}
391+
_ => {
392+
return Err(miette!(
393+
"--add-endpoint options segment supports only 'allow-uninspected-credentials', 'websocket-credential-rewrite', 'request-body-credential-rewrite', and 'allowed-ip=<CIDR-or-IP>'; got '{option}' in '{spec}'"
394+
));
395+
}
407396
}
408397
}
409398

410399
Ok(())
411400
}
412401

402+
/// Validate the value part of an `allowed-ip=<CIDR-or-IP>` endpoint option.
403+
fn parse_allowed_ip_value(spec: &str, value: &str) -> Result<String> {
404+
let allowed_ip = value.trim();
405+
if allowed_ip.is_empty() {
406+
return Err(miette!(
407+
"--add-endpoint allowed-ip option must include a CIDR or IP value in '{spec}'"
408+
));
409+
}
410+
if allowed_ip.contains(char::is_whitespace) {
411+
return Err(miette!(
412+
"--add-endpoint allowed-ip option must not contain whitespace in '{spec}'"
413+
));
414+
}
415+
Ok(allowed_ip.to_string())
416+
}
417+
413418
fn parse_host(flag: &str, spec: &str, host: &str) -> Result<String> {
414419
let host = host.trim();
415420
if host.is_empty() {
@@ -457,6 +462,7 @@ fn dedup_strings(values: &[String]) -> Vec<String> {
457462
mod tests {
458463
use super::{
459464
PolicyUpdatePlan, build_policy_update_plan as build_policy_update_plan_with_options,
465+
parse_allowed_ip_value,
460466
};
461467
use openshell_policy::PolicyMergeOp;
462468

@@ -687,6 +693,28 @@ mod tests {
687693
assert!(error.to_string().contains("allowed-ip option"));
688694
}
689695

696+
#[test]
697+
fn parse_allowed_ip_value_accepts_trimmed_cidr_and_ip() {
698+
assert_eq!(
699+
parse_allowed_ip_value("spec", "10.0.0.0/8").expect("CIDR should parse"),
700+
"10.0.0.0/8"
701+
);
702+
assert_eq!(
703+
parse_allowed_ip_value("spec", " 192.168.1.10 ").expect("IP should parse"),
704+
"192.168.1.10"
705+
);
706+
}
707+
708+
#[test]
709+
fn parse_allowed_ip_value_rejects_empty_and_interior_whitespace() {
710+
let empty = parse_allowed_ip_value("spec", " ").expect_err("empty value must fail");
711+
assert!(empty.to_string().contains("must include a CIDR or IP"));
712+
713+
let spaced =
714+
parse_allowed_ip_value("spec", "10.0.0.0/8 172.16.0.0/12").expect_err("must fail");
715+
assert!(spaced.to_string().contains("must not contain whitespace"));
716+
}
717+
690718
#[test]
691719
fn websocket_credential_rewrite_rejects_l4_endpoint() {
692720
let error = build_policy_update_plan(

crates/openshell-server/src/grpc/policy.rs

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use openshell_core::telemetry::{
5252
use openshell_core::{
5353
VERSION,
5454
endpoint_path::EndpointPathPattern,
55-
host_pattern::host_matches,
55+
host_pattern::{host_matches, host_patterns_overlap},
5656
settings::{self, SettingValueKind},
5757
};
5858
use openshell_ocsf::{
@@ -2277,42 +2277,11 @@ fn endpoint_ports(endpoint: &NetworkEndpoint) -> Vec<u32> {
22772277
}
22782278
}
22792279

2280-
fn host_patterns_overlap(left: &str, right: &str) -> bool {
2281-
if left.eq_ignore_ascii_case(right) {
2282-
return true;
2283-
}
2284-
2285-
let left = left.to_ascii_lowercase();
2286-
let right = right.to_ascii_lowercase();
2287-
let left_has_wildcard = left.contains('*');
2288-
let right_has_wildcard = right.contains('*');
2289-
2290-
if !right_has_wildcard {
2291-
return host_matches(&left, &right).unwrap_or(false);
2292-
}
2293-
if !left_has_wildcard {
2294-
return host_matches(&right, &left).unwrap_or(false);
2295-
}
2296-
2297-
fn literal_suffix(pattern: &str) -> &str {
2298-
pattern
2299-
.rfind('*')
2300-
.map_or(pattern, |index| &pattern[index + 1..])
2301-
}
2302-
2303-
let left_suffix = literal_suffix(&left);
2304-
let right_suffix = literal_suffix(&right);
2305-
left_suffix.is_empty()
2306-
|| right_suffix.is_empty()
2307-
|| left_suffix.ends_with(right_suffix)
2308-
|| right_suffix.ends_with(left_suffix)
2309-
}
2310-
23112280
fn endpoint_matches_credentialed_scope(
23122281
endpoint: &NetworkEndpoint,
23132282
scope: &CredentialedEndpointScope,
23142283
) -> bool {
2315-
if !host_patterns_overlap(&endpoint.host, &scope.host) {
2284+
if !host_patterns_overlap(&endpoint.host, &scope.host).unwrap_or(false) {
23162285
return false;
23172286
}
23182287
let endpoint_ports = endpoint_ports(endpoint);
@@ -5790,6 +5759,12 @@ mod tests {
57905759
provider_credentialed: true,
57915760
..Default::default()
57925761
},
5762+
NetworkEndpoint {
5763+
host: "*.api.example.com".to_string(),
5764+
port: 443,
5765+
provider_credentialed: true,
5766+
..Default::default()
5767+
},
57935768
],
57945769
..Default::default()
57955770
},
@@ -5807,6 +5782,7 @@ mod tests {
58075782
let endpoints = &policy.network_policies["test"].endpoints;
58085783
assert!(endpoints[0].provider_credentialed);
58095784
assert!(!endpoints[1].provider_credentialed);
5785+
assert!(!endpoints[2].provider_credentialed);
58105786
}
58115787

58125788
#[test]

0 commit comments

Comments
 (0)