Skip to main content

helm_schema/
fetch_policy.rs

1use std::net::{IpAddr, Ipv6Addr};
2
3/// Explicit policy for schema/document retrieval during input assembly.
4///
5/// This governs chart-authored and override-authored external references that
6/// helm-schema may load while preparing a self-contained schema document.
7/// Knowledge-provider fetching remains controlled separately by
8/// [`crate::provider_builder::ProviderOptions`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct FetchPolicy {
11    allow_file: bool,
12    allow_network: bool,
13}
14
15impl FetchPolicy {
16    /// Creates an explicit local-file and network retrieval policy.
17    #[must_use]
18    pub const fn new(allow_file: bool, allow_network: bool) -> Self {
19        Self {
20            allow_file,
21            allow_network,
22        }
23    }
24
25    /// Policy for chart-local and override-local input assembly. Local files
26    /// remain readable; network refs depend on the caller's offline policy.
27    #[must_use]
28    pub const fn input_assembly(allow_network: bool) -> Self {
29        Self::new(true, allow_network)
30    }
31
32    pub(crate) fn validate_file_host(self, host: &str) -> Result<(), String> {
33        if !self.allow_file {
34            return Err("local file access is disabled by fetch policy".to_string());
35        }
36        if host.is_empty() {
37            return Ok(());
38        }
39        Err(format!(
40            "file:// authority host is not allowed by fetch policy: {host}"
41        ))
42    }
43
44    pub(crate) fn validate_network_host(self, host: Option<&str>) -> Result<(), String> {
45        if !self.allow_network {
46            return Err("network access is disabled by fetch policy".to_string());
47        }
48
49        let Some(host) = host else {
50            return Err("network URI is missing an authority host".to_string());
51        };
52        let normalized = host.trim_end_matches('.').to_ascii_lowercase();
53        if normalized == "localhost" || normalized.ends_with(".localhost") {
54            return Err(format!(
55                "network host is denied by fetch policy because it is loopback-local: {host}"
56            ));
57        }
58
59        let ip = parse_ip_literal(host);
60        if let Some(ip) = ip
61            && is_denied_ip(ip)
62        {
63            return Err(format!(
64                "network host is denied by fetch policy because it is loopback/link-local: {host}"
65            ));
66        }
67
68        Ok(())
69    }
70}
71
72fn parse_ip_literal(host: &str) -> Option<IpAddr> {
73    if let Some(inner) = host
74        .strip_prefix('[')
75        .and_then(|rest| rest.strip_suffix(']'))
76    {
77        return inner.parse::<Ipv6Addr>().ok().map(IpAddr::V6);
78    }
79    host.parse::<IpAddr>().ok()
80}
81
82fn is_denied_ip(ip: IpAddr) -> bool {
83    match ip {
84        IpAddr::V4(addr) => addr.is_loopback() || addr.is_link_local() || addr.is_unspecified(),
85        IpAddr::V6(addr) => {
86            addr.is_loopback() || addr.is_unicast_link_local() || addr == Ipv6Addr::UNSPECIFIED
87        }
88    }
89}