use std::marker::PhantomData;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicU8, Ordering};
use crate::EcosystemId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HostClass {
Loopback,
LinkLocal,
CloudMetadata,
PrivateV4,
Cgnat,
UniqueLocalV6,
Unspecified,
InternalName,
Reserved,
Global,
}
impl HostClass {
#[must_use]
pub const fn never_a_registry(self) -> bool {
matches!(
self,
Self::Loopback
| Self::LinkLocal
| Self::CloudMetadata
| Self::Unspecified
| Self::Reserved
)
}
}
impl std::fmt::Display for HostClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Loopback => "loopback",
Self::LinkLocal => "link-local",
Self::CloudMetadata => "cloud metadata",
Self::PrivateV4 => "private (RFC1918)",
Self::Cgnat => "carrier-grade NAT",
Self::UniqueLocalV6 => "unique-local IPv6",
Self::Unspecified => "unspecified",
Self::InternalName => "internal name",
Self::Reserved => "reserved (IETF special-purpose)",
Self::Global => "global",
})
}
}
fn unwrap_mapped_v4(addr: IpAddr) -> IpAddr {
match addr {
IpAddr::V6(v6) => v6
.to_ipv4_mapped()
.or_else(|| nat64_embedded_v4(v6))
.map_or(addr, IpAddr::V4),
IpAddr::V4(_) => addr,
}
}
fn nat64_embedded_v4(v6: Ipv6Addr) -> Option<Ipv4Addr> {
let segments = v6.segments();
let is_64_ff9b = segments[0] == 0x0064 && segments[1] == 0xff9b;
let is_well_known = is_64_ff9b && segments[2..6] == [0, 0, 0, 0];
let is_local_use = is_64_ff9b && segments[2] == 0x0001;
if is_well_known || is_local_use {
let [a, b] = segments[6].to_be_bytes();
let [c, d] = segments[7].to_be_bytes();
Some(Ipv4Addr::new(a, b, c, d))
} else {
None
}
}
fn classify_ip(addr: IpAddr) -> HostClass {
match addr {
IpAddr::V4(v4) => {
if v4.is_loopback() {
HostClass::Loopback
} else if v4 == Ipv4Addr::new(169, 254, 169, 254)
|| v4 == Ipv4Addr::new(100, 100, 100, 200)
{
HostClass::CloudMetadata
} else if v4.is_link_local() {
HostClass::LinkLocal
} else if v4.octets()[0] == 0 {
HostClass::Unspecified
} else if v4.is_private() {
HostClass::PrivateV4
} else if v4.octets()[0] == 100 && (v4.octets()[1] & 0b1100_0000) == 0b0100_0000 {
HostClass::Cgnat
} else if v4.octets()[0..3] == [192, 0, 0] {
HostClass::Reserved
} else {
HostClass::Global
}
}
IpAddr::V6(v6) => {
if v6.is_loopback() {
HostClass::Loopback
} else if v6.segments() == [0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x254] {
HostClass::CloudMetadata
} else if (v6.segments()[0] & 0xffc0) == 0xfe80 {
HostClass::LinkLocal
} else if v6.is_unspecified() {
HostClass::Unspecified
} else if (v6.segments()[0] & 0xfe00) == 0xfc00 {
HostClass::UniqueLocalV6
} else if (v6.segments()[0] & 0xffc0) == 0xfec0 {
HostClass::Reserved
} else {
HostClass::Global
}
}
}
}
fn classify_name(host: &str) -> HostClass {
let lower = host.to_ascii_lowercase();
let lower = lower.trim_end_matches('.');
if lower == "localhost" || lower.ends_with(".localhost") {
return HostClass::Loopback;
}
if lower == "metadata.google.internal" || lower == "metadata.goog" {
return HostClass::CloudMetadata;
}
if lower.ends_with(".internal") || lower.ends_with(".local") || lower.ends_with(".home.arpa") {
return HostClass::InternalName;
}
if !lower.contains('.') {
return HostClass::InternalName;
}
HostClass::Global
}
#[must_use]
pub fn classify_addr(addr: IpAddr) -> HostClass {
classify_ip(unwrap_mapped_v4(addr))
}
#[must_use]
pub fn classify_host(url: &url::Url) -> HostClass {
match url.host() {
Some(url::Host::Ipv4(v4)) => classify_ip(unwrap_mapped_v4(IpAddr::V4(v4))),
Some(url::Host::Ipv6(v6)) => classify_ip(unwrap_mapped_v4(IpAddr::V6(v6))),
Some(url::Host::Domain(name)) => classify_name(name),
None => HostClass::InternalName,
}
}
#[must_use]
pub fn is_trusted_prefix(candidate: &url::Url, trusted: &url::Url) -> bool {
candidate.origin() == trusted.origin() && path_under_prefix(candidate.path(), trusted.path())
}
fn path_under_prefix(path: &str, prefix: &str) -> bool {
let prefix = prefix.trim_end_matches('/');
path == prefix
|| path
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with('/'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum WorkspaceRegistryAccess {
Off,
#[default]
PublicOnly,
All,
}
impl WorkspaceRegistryAccess {
#[must_use]
pub const fn allows(self, class: HostClass) -> bool {
match self {
Self::Off => false,
Self::PublicOnly => matches!(class, HostClass::Global),
Self::All => true,
}
}
pub(crate) const fn to_u8(self) -> u8 {
match self {
Self::Off => 0,
Self::PublicOnly => 1,
Self::All => 2,
}
}
const fn from_u8(value: u8) -> Self {
match value {
0 => Self::Off,
2 => Self::All,
_ => Self::PublicOnly,
}
}
}
#[derive(Debug)]
pub struct RegistryAccessPolicy(AtomicU8);
impl RegistryAccessPolicy {
#[must_use]
pub fn new(initial: WorkspaceRegistryAccess) -> Self {
Self(AtomicU8::new(initial.to_u8()))
}
#[must_use]
pub fn get(&self) -> WorkspaceRegistryAccess {
WorkspaceRegistryAccess::from_u8(self.0.load(Ordering::Relaxed))
}
pub fn set(&self, value: WorkspaceRegistryAccess) {
self.0.store(value.to_u8(), Ordering::Relaxed);
}
}
impl Default for RegistryAccessPolicy {
fn default() -> Self {
Self::new(WorkspaceRegistryAccess::default())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IndexUrlError {
#[error("not a valid URL: {0}")]
InvalidUrl(RedactedUrl),
#[error("registry index must use https, got scheme {0:?}")]
NotHttps(String),
#[error("registry index URL must not carry userinfo")]
UserInfoPresent,
#[error("registry index host class {class} blocked by registries.workspace_registries policy")]
BlockedHost {
class: HostClass,
},
}
#[derive(Debug, Clone, Copy)]
pub enum PolicyGate<'a> {
Skip,
Enforce(&'a RegistryAccessPolicy),
}
pub use crate::redact::{
MAX_PARSE_ERROR_LOG_BYTES, RedactedName, RedactedUrl, is_credential_or_query_bearing,
parse_error_source, redact_declaration_key, redact_parse_error_for_log, redact_userinfo,
sanitize_invisible, url_for_tracing,
};
#[cfg(any(test, feature = "test-util"))]
fn is_loopback_url(url: &url::Url) -> bool {
url.scheme() == "http"
&& match url.host() {
Some(url::Host::Domain("localhost")) => true,
Some(url::Host::Ipv4(addr)) => addr == Ipv4Addr::LOCALHOST,
Some(url::Host::Ipv6(addr)) => addr == Ipv6Addr::LOCALHOST,
_ => false,
}
}
pub fn validate_index_url(
candidate: &str,
raw_for_log: &str,
ecosystem: EcosystemId,
gate: PolicyGate<'_>,
) -> Result<url::Url, IndexUrlError> {
let url = url::Url::parse(candidate)
.map_err(|_| IndexUrlError::InvalidUrl(RedactedUrl::new(raw_for_log)))?;
let is_https = url.scheme() == "https";
#[cfg(any(test, feature = "test-util"))]
let is_https = is_https || is_loopback_url(&url);
if !is_https {
return Err(IndexUrlError::NotHttps(url.scheme().to_string()));
}
if !url.username().is_empty() || url.password().is_some() {
return Err(IndexUrlError::UserInfoPresent);
}
if let PolicyGate::Enforce(policy) = gate {
let class = classify_host(&url);
if !policy.get().allows(class) {
tracing::warn!(
url = %RedactedUrl::new(raw_for_log),
?class,
%ecosystem,
"workspace-declared registry index host blocked by registries.workspace_registries policy"
);
return Err(IndexUrlError::BlockedHost { class });
}
}
Ok(url)
}
#[doc(hidden)]
pub mod private {
pub trait Sealed {}
}
pub trait RegistryUrlKind: private::Sealed {
const ECOSYSTEM: EcosystemId;
const REJECT_QUERY_FRAGMENT: bool;
type Error: From<IndexUrlError>;
}
pub trait TrustedConstantRegistryUrl: RegistryUrlKind {}
pub struct ValidatedRegistryUrl<K> {
normalized: String,
kind: PhantomData<fn() -> K>,
}
impl<K> Clone for ValidatedRegistryUrl<K> {
fn clone(&self) -> Self {
Self {
normalized: self.normalized.clone(),
kind: PhantomData,
}
}
}
impl<K: RegistryUrlKind> std::fmt::Debug for ValidatedRegistryUrl<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(K::ECOSYSTEM.id())
.field(&self.normalized)
.finish()
}
}
impl<K> PartialEq for ValidatedRegistryUrl<K> {
fn eq(&self, other: &Self) -> bool {
self.normalized == other.normalized
}
}
impl<K> Eq for ValidatedRegistryUrl<K> {}
impl<K> std::hash::Hash for ValidatedRegistryUrl<K> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.normalized.hash(state);
}
}
impl<K> std::fmt::Display for ValidatedRegistryUrl<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.normalized)
}
}
impl<K: RegistryUrlKind> ValidatedRegistryUrl<K> {
pub fn new(raw: &str, policy: &RegistryAccessPolicy) -> Result<Self, K::Error> {
Self::new_with_raw_for_log(raw, raw, policy)
}
pub fn new_with_raw_for_log(
candidate: &str,
raw_for_log: &str,
policy: &RegistryAccessPolicy,
) -> Result<Self, K::Error> {
Self::build(candidate, raw_for_log, PolicyGate::Enforce(policy))
}
fn build(candidate: &str, raw_for_log: &str, gate: PolicyGate<'_>) -> Result<Self, K::Error> {
let url = validate_index_url(candidate, raw_for_log, K::ECOSYSTEM, gate)?;
if K::REJECT_QUERY_FRAGMENT && (url.query().is_some() || url.fragment().is_some()) {
return Err(IndexUrlError::InvalidUrl(RedactedUrl::new(raw_for_log)).into());
}
let normalized = url.as_str().trim_end_matches('/').to_string();
Ok(Self {
normalized,
kind: PhantomData,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.normalized
}
}
impl<K: TrustedConstantRegistryUrl> ValidatedRegistryUrl<K> {
pub fn new_trusted_constant(raw: &'static str) -> Result<Self, K::Error> {
Self::build(raw, raw, PolicyGate::Skip)
}
}
pub trait BlockedHostReason {
fn blocked_host_class(&self) -> Option<HostClass>;
}
impl BlockedHostReason for IndexUrlError {
fn blocked_host_class(&self) -> Option<HostClass> {
match self {
Self::BlockedHost { class } => Some(*class),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegistryRejectionReason {
InvalidUrl,
NotHttps,
UserInfoPresent,
UndefinedEnvVar,
EnvVarExpansionNotPermitted,
HasCredentials,
EncryptedCredentialUnsupported,
UnsupportedFeedKind,
}
impl std::fmt::Display for RegistryRejectionReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::InvalidUrl => "not a valid URL",
Self::NotHttps => "does not use https",
Self::UserInfoPresent => "contains embedded credentials",
Self::UndefinedEnvVar => "references an undefined environment variable",
Self::EnvVarExpansionNotPermitted => {
"uses environment-variable expansion, which is not permitted for this entry"
}
Self::HasCredentials => "has credentials that cannot be used",
Self::EncryptedCredentialUnsupported => {
"uses an encrypted credential, which is not supported"
}
Self::UnsupportedFeedKind => "uses a feed kind that is not supported",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RejectionOutcome {
Reject(RegistryRejectionReason),
HandledByBlockedHostPath,
IntentionallySilent,
}
impl RejectionOutcome {
#[must_use]
pub const fn into_reason(self) -> Option<RegistryRejectionReason> {
match self {
Self::Reject(reason) => Some(reason),
Self::HandledByBlockedHostPath | Self::IntentionallySilent => None,
}
}
}
pub trait RegistryRejectionClassifier {
fn rejection_reason(&self) -> RejectionOutcome;
}
impl RegistryRejectionClassifier for IndexUrlError {
fn rejection_reason(&self) -> RejectionOutcome {
match self {
Self::InvalidUrl(_) => RejectionOutcome::Reject(RegistryRejectionReason::InvalidUrl),
Self::NotHttps(_) => RejectionOutcome::Reject(RegistryRejectionReason::NotHttps),
Self::UserInfoPresent => {
RejectionOutcome::Reject(RegistryRejectionReason::UserInfoPresent)
}
Self::BlockedHost { .. } => RejectionOutcome::HandledByBlockedHostPath,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct InvalidEntry<E = IndexUrlError> {
pub raw: RedactedUrl,
pub reason: E,
}
impl<E> InvalidEntry<E> {
#[must_use]
pub fn new(raw: RedactedUrl, reason: E) -> Self {
Self { raw, reason }
}
pub fn logged(raw: &str, reason: E, ecosystem: EcosystemId, message: &'static str) -> Self
where
E: std::fmt::Display,
{
let redacted = RedactedUrl::new(raw);
tracing::warn!(raw = %redacted, %reason, %ecosystem, "{}", message);
Self::new(redacted, reason)
}
}
impl<E: BlockedHostReason> InvalidEntry<E> {
#[must_use]
pub fn blocked_class(&self) -> Option<(HostClass, String)> {
self.reason
.blocked_host_class()
.map(|class| (class, self.raw.to_string()))
}
}
impl<E: RegistryRejectionClassifier> InvalidEntry<E> {
#[must_use]
pub fn rejection_reason(&self) -> Option<(RegistryRejectionReason, String)> {
self.reason
.rejection_reason()
.into_reason()
.map(|reason| (reason, self.raw.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
fn host_class(url: &str) -> HostClass {
classify_host(&url::Url::parse(url).unwrap())
}
fn trusted_prefix(candidate: &str, trusted: &str) -> bool {
is_trusted_prefix(
&url::Url::parse(candidate).unwrap(),
&url::Url::parse(trusted).unwrap(),
)
}
#[test]
fn test_index_url_error_rejection_reason_variants() {
assert_eq!(
IndexUrlError::InvalidUrl(RedactedUrl::new("not a url")).rejection_reason(),
RejectionOutcome::Reject(RegistryRejectionReason::InvalidUrl)
);
assert_eq!(
IndexUrlError::NotHttps("ftp".to_string()).rejection_reason(),
RejectionOutcome::Reject(RegistryRejectionReason::NotHttps)
);
assert_eq!(
IndexUrlError::UserInfoPresent.rejection_reason(),
RejectionOutcome::Reject(RegistryRejectionReason::UserInfoPresent)
);
assert_eq!(
IndexUrlError::BlockedHost {
class: HostClass::Loopback
}
.rejection_reason(),
RejectionOutcome::HandledByBlockedHostPath,
"a policy-blocked host must classify as HandledByBlockedHostPath, not IntentionallySilent \
or a generic Reject — it already has its own diagnostic path via BlockedHostReason"
);
}
#[test]
fn test_is_trusted_prefix_rejects_sibling_path_no_trailing_slash() {
let trusted = "https://artifacts.corp/cargo/index";
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/index-public/steal",
trusted
));
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/indexEVIL",
trusted
));
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/index.evil/x",
trusted
));
}
#[test]
fn test_is_trusted_prefix_accepts_self_and_child_no_trailing_slash() {
let trusted = "https://artifacts.corp/cargo/index";
assert!(trusted_prefix(
"https://artifacts.corp/cargo/index",
trusted
));
assert!(trusted_prefix(
"https://artifacts.corp/cargo/index/se/rd/serde",
trusted
));
}
#[test]
fn test_is_trusted_prefix_trailing_slash_equivalent_to_no_trailing_slash() {
let trusted = "https://artifacts.corp/cargo/index/";
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/indexEVIL",
trusted
));
assert!(trusted_prefix(
"https://artifacts.corp/cargo/index/se/rd/serde",
trusted
));
}
#[test]
fn test_is_trusted_prefix_doubled_trailing_slash_still_trusts_children() {
let trusted = "https://artifacts.corp/v3-flatcontainer//";
assert!(trusted_prefix(
"https://artifacts.corp/v3-flatcontainer/serde/index.json",
trusted
));
}
#[test]
fn test_is_trusted_prefix_rejects_origin_mismatch() {
assert!(!trusted_prefix(
"https://artifacts.corp.evil.com/cargo/index",
"https://artifacts.corp/cargo/index"
));
}
#[test]
fn test_is_trusted_prefix_root_path_trusts_everything_on_origin() {
assert!(trusted_prefix(
"https://registry.example/anything/at/all",
"https://registry.example/"
));
}
#[test]
fn test_classify_cloud_metadata_ipv4() {
assert_eq!(
host_class("https://169.254.169.254/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_ipv4_mapped_v6_bypass() {
assert_eq!(
host_class("https://[::ffff:169.254.169.254]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_nat64_bypass() {
assert_eq!(
host_class("https://[64:ff9b::a9fe:a9fe]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_nat64_local_use_bypass() {
assert_eq!(
host_class("https://[64:ff9b:1::a9fe:a9fe]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_nat64_local_use_non_zero_subnet() {
assert_eq!(
host_class("https://[64:ff9b:1:1::a9fe:a9fe]/"),
HostClass::CloudMetadata
);
assert_eq!(
host_class("https://[64:ff9b:1:ffff:0:0:a9fe:a9fe]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_alibaba_v4() {
assert_eq!(
host_class("https://100.100.100.200/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_ec2_ipv6() {
assert_eq!(
host_class("https://[fd00:ec2::254]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_link_local_ipv6() {
assert_eq!(host_class("https://[fe80::1]/"), HostClass::LinkLocal);
}
#[test]
fn test_classify_private_v4() {
assert_eq!(host_class("https://10.0.0.1/"), HostClass::PrivateV4);
}
#[test]
fn test_classify_cgnat() {
assert_eq!(host_class("https://100.64.0.1/"), HostClass::Cgnat);
}
#[test]
fn test_classify_private_v4_192_168() {
assert_eq!(host_class("https://192.168.1.1/"), HostClass::PrivateV4);
}
#[test]
fn test_classify_unique_local_v6() {
assert_eq!(host_class("https://[fc00::1]/"), HostClass::UniqueLocalV6);
}
#[test]
fn test_classify_unspecified_v4() {
assert_eq!(host_class("https://0.0.0.0/"), HostClass::Unspecified);
}
#[test]
fn test_classify_unspecified_v4_slash_8() {
assert_eq!(host_class("https://0.1.2.3/"), HostClass::Unspecified);
}
#[test]
fn test_classify_reserved_ietf_protocol_assignment_v4() {
assert_eq!(host_class("https://192.0.0.192/"), HostClass::Reserved);
}
#[test]
fn test_classify_reserved_deprecated_site_local_v6() {
assert_eq!(host_class("https://[fec0::1]/"), HostClass::Reserved);
}
#[test]
fn test_classify_localhost_name() {
assert_eq!(host_class("https://localhost/"), HostClass::Loopback);
}
#[test]
fn test_classify_localhost_subdomain() {
assert_eq!(host_class("https://foo.localhost/"), HostClass::Loopback);
}
#[test]
fn test_classify_google_metadata_name() {
assert_eq!(
host_class("https://metadata.google.internal/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_internal_suffix_name() {
assert_eq!(
host_class("https://registry.internal/"),
HostClass::InternalName
);
}
#[test]
fn test_classify_single_label_name() {
assert_eq!(host_class("https://single-label/"), HostClass::InternalName);
}
#[test]
fn test_classify_localhost_trailing_dot() {
assert_eq!(host_class("https://localhost./"), HostClass::Loopback);
}
#[test]
fn test_classify_google_metadata_trailing_dot() {
assert_eq!(
host_class("https://metadata.google.internal./"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_internal_suffix_trailing_dot() {
assert_eq!(
host_class("https://registry.internal./"),
HostClass::InternalName
);
}
#[test]
fn test_classify_localhost_double_trailing_dot() {
assert_eq!(host_class("https://localhost../"), HostClass::Loopback);
}
#[test]
fn test_classify_global_public_name() {
assert_eq!(host_class("https://index.crates.io/"), HostClass::Global);
}
#[test]
fn test_classify_addr_cloud_metadata() {
let addr: IpAddr = "169.254.169.254".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::CloudMetadata);
}
#[test]
fn test_classify_addr_private_v4() {
let addr: IpAddr = "10.0.0.1".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::PrivateV4);
}
#[test]
fn test_classify_addr_unwraps_mapped_v4() {
let addr: IpAddr = "::ffff:169.254.169.254".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::CloudMetadata);
}
#[test]
fn test_classify_addr_unwraps_nat64_cloud_metadata() {
let addr: IpAddr = "64:ff9b::a9fe:a9fe".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::CloudMetadata);
}
#[test]
fn test_classify_addr_unwraps_nat64_loopback() {
let addr: IpAddr = "64:ff9b::7f00:1".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::Loopback);
}
#[test]
fn test_classify_addr_global() {
let addr: IpAddr = "1.1.1.1".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::Global);
}
#[test]
fn test_never_a_registry_classes() {
assert!(HostClass::Loopback.never_a_registry());
assert!(HostClass::LinkLocal.never_a_registry());
assert!(HostClass::CloudMetadata.never_a_registry());
assert!(HostClass::Unspecified.never_a_registry());
assert!(HostClass::Reserved.never_a_registry());
assert!(!HostClass::PrivateV4.never_a_registry());
assert!(!HostClass::Cgnat.never_a_registry());
assert!(!HostClass::UniqueLocalV6.never_a_registry());
assert!(!HostClass::InternalName.never_a_registry());
assert!(!HostClass::Global.never_a_registry());
}
#[test]
fn test_workspace_registry_access_off_blocks_everything() {
let policy = WorkspaceRegistryAccess::Off;
assert!(!policy.allows(HostClass::Global));
assert!(!policy.allows(HostClass::PrivateV4));
assert!(!policy.allows(HostClass::Loopback));
}
#[test]
fn test_workspace_registry_access_public_only_allows_global_only() {
let policy = WorkspaceRegistryAccess::PublicOnly;
assert!(policy.allows(HostClass::Global));
assert!(!policy.allows(HostClass::PrivateV4));
assert!(!policy.allows(HostClass::CloudMetadata));
}
#[test]
fn test_workspace_registry_access_all_allows_everything() {
let policy = WorkspaceRegistryAccess::All;
assert!(policy.allows(HostClass::Global));
assert!(policy.allows(HostClass::PrivateV4));
assert!(policy.allows(HostClass::Loopback));
}
#[test]
fn test_registry_access_policy_default_is_public_only() {
let policy = RegistryAccessPolicy::default();
assert_eq!(policy.get(), WorkspaceRegistryAccess::PublicOnly);
}
#[test]
fn test_registry_access_policy_live_update() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::All);
assert_eq!(policy.get(), WorkspaceRegistryAccess::All);
policy.set(WorkspaceRegistryAccess::Off);
assert_eq!(policy.get(), WorkspaceRegistryAccess::Off);
}
#[test]
fn test_is_loopback_url_bracketed_ipv6() {
assert!(is_loopback_url(
&url::Url::parse("http://[::1]:9999/").unwrap()
));
}
#[test]
fn test_is_loopback_url_ipv6_no_port() {
assert!(is_loopback_url(&url::Url::parse("http://[::1]/").unwrap()));
}
#[test]
fn test_is_loopback_url_ipv4_and_localhost() {
assert!(is_loopback_url(
&url::Url::parse("http://127.0.0.1:9999/").unwrap()
));
assert!(is_loopback_url(
&url::Url::parse("http://localhost:9999/").unwrap()
));
}
#[test]
fn test_is_loopback_url_rejects_non_loopback_addresses() {
assert!(!is_loopback_url(
&url::Url::parse("http://127.0.0.2:9999/").unwrap()
));
assert!(!is_loopback_url(
&url::Url::parse("http://[::2]:9999/").unwrap()
));
assert!(!is_loopback_url(
&url::Url::parse("http://example.com:9999/").unwrap()
));
}
#[test]
fn test_is_loopback_url_rejects_https_scheme() {
assert!(!is_loopback_url(
&url::Url::parse("https://127.0.0.1:9999/").unwrap()
));
}
#[test]
fn test_validate_index_url_userinfo_rejected_before_policy_gate() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::Off);
let result = validate_index_url(
"https://user:pass@169.254.169.254/",
"https://user:pass@169.254.169.254/",
EcosystemId::Cargo,
PolicyGate::Enforce(&policy),
);
assert_eq!(result, Err(IndexUrlError::UserInfoPresent));
}
#[test]
fn test_validate_index_url_policy_gate_skip_vs_enforce() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::Off);
assert!(
validate_index_url(
"https://169.254.169.254/",
"https://169.254.169.254/",
EcosystemId::Cargo,
PolicyGate::Skip
)
.is_ok()
);
assert_matches!(
validate_index_url(
"https://169.254.169.254/",
"https://169.254.169.254/",
EcosystemId::Cargo,
PolicyGate::Enforce(&policy)
),
Err(IndexUrlError::BlockedHost { .. })
);
}
#[cfg(feature = "test-util")]
#[test]
fn test_validate_index_url_blocked_host_log_redacts_query_string() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::Off);
let raw = "https://169.254.169.254/?ApiKey=super-secret-value";
let log = crate::test_util::capture_tracing_output(|| {
let result =
validate_index_url(raw, raw, EcosystemId::Cargo, PolicyGate::Enforce(&policy));
assert_matches!(result, Err(IndexUrlError::BlockedHost { .. }));
});
assert!(!log.contains("super-secret-value"), "log: {log}");
}
#[test]
fn test_validate_index_url_redacts_userinfo_in_invalid_url_error() {
let raw = "https://user:hunter2@registry.example:99999/simple";
let err = validate_index_url(raw, raw, EcosystemId::Cargo, PolicyGate::Skip).unwrap_err();
let IndexUrlError::InvalidUrl(redacted) = &err else {
panic!("expected InvalidUrl, got {err:?}");
};
assert!(
!redacted.as_ref().contains("hunter2"),
"redacted: {redacted}"
);
assert!(!err.to_string().contains("hunter2"), "Display: {err}");
}
#[test]
fn test_validate_index_url_invalid_url_redacts_query_string() {
let raw = "https://user:hunter2@registry.example:99999/simple?token=super-secret-value";
for ecosystem in [EcosystemId::Cargo, EcosystemId::Npm] {
let err = validate_index_url(raw, raw, ecosystem, PolicyGate::Skip).unwrap_err();
let IndexUrlError::InvalidUrl(redacted) = &err else {
panic!("expected InvalidUrl, got {err:?}");
};
assert!(
!redacted.as_ref().contains("hunter2"),
"redacted: {redacted}"
);
assert!(
!redacted.as_ref().contains("super-secret-value"),
"redacted: {redacted}"
);
assert!(
!err.to_string().contains("super-secret-value"),
"Display: {err}"
);
}
}
enum PermissiveTestKind {}
impl private::Sealed for PermissiveTestKind {}
impl RegistryUrlKind for PermissiveTestKind {
const ECOSYSTEM: EcosystemId = EcosystemId::Cargo;
const REJECT_QUERY_FRAGMENT: bool = false;
type Error = IndexUrlError;
}
enum RejectingTestKind {}
impl private::Sealed for RejectingTestKind {}
impl RegistryUrlKind for RejectingTestKind {
const ECOSYSTEM: EcosystemId = EcosystemId::Go;
const REJECT_QUERY_FRAGMENT: bool = true;
type Error = IndexUrlError;
}
impl TrustedConstantRegistryUrl for PermissiveTestKind {}
impl TrustedConstantRegistryUrl for RejectingTestKind {}
fn permissive_policy() -> RegistryAccessPolicy {
RegistryAccessPolicy::new(WorkspaceRegistryAccess::All)
}
#[test]
fn test_validated_registry_url_accepts_query_string_when_kind_opts_out() {
let url = ValidatedRegistryUrl::<PermissiveTestKind>::new(
"https://example.com/path?token=abc",
&permissive_policy(),
)
.unwrap();
assert_eq!(url.as_str(), "https://example.com/path?token=abc");
}
#[test]
fn test_validated_registry_url_accepts_fragment_when_kind_opts_out() {
let url = ValidatedRegistryUrl::<PermissiveTestKind>::new(
"https://example.com/path#frag",
&permissive_policy(),
)
.unwrap();
assert_eq!(url.as_str(), "https://example.com/path#frag");
}
#[test]
fn test_validated_registry_url_rejects_query_string_when_kind_opts_in() {
assert_matches!(
ValidatedRegistryUrl::<RejectingTestKind>::new(
"https://example.com/path?token=abc",
&permissive_policy(),
),
Err(IndexUrlError::InvalidUrl(_))
);
}
#[test]
fn test_validated_registry_url_rejects_fragment_when_kind_opts_in() {
assert_matches!(
ValidatedRegistryUrl::<RejectingTestKind>::new(
"https://example.com/path#frag",
&permissive_policy(),
),
Err(IndexUrlError::InvalidUrl(_))
);
}
#[test]
fn test_validated_registry_url_debug_names_ecosystem() {
let url = ValidatedRegistryUrl::<PermissiveTestKind>::new(
"https://example.com",
&permissive_policy(),
)
.unwrap();
assert_eq!(format!("{url:?}"), "cargo(\"https://example.com\")");
}
#[test]
fn test_validated_registry_url_send_sync_unconditional_regardless_of_kind() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ValidatedRegistryUrl<PermissiveTestKind>>();
}
#[test]
fn test_validated_registry_url_new_trusted_constant_skips_policy() {
let url =
ValidatedRegistryUrl::<PermissiveTestKind>::new_trusted_constant("https://example.com")
.unwrap();
assert_eq!(url.as_str(), "https://example.com");
}
#[test]
fn test_validated_registry_url_new_trusted_constant_bypasses_blocked_host_policy() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::PublicOnly);
assert_matches!(
ValidatedRegistryUrl::<RejectingTestKind>::new("https://127.0.0.1:9999", &policy),
Err(IndexUrlError::BlockedHost { .. })
);
let url = ValidatedRegistryUrl::<RejectingTestKind>::new_trusted_constant(
"https://127.0.0.1:9999",
)
.unwrap();
assert_eq!(url.as_str(), "https://127.0.0.1:9999");
}
#[test]
fn test_invalid_entry_blocked_class_some_for_blocked_host() {
let entry = InvalidEntry::new(
RedactedUrl::new("https://127.0.0.1:9999"),
IndexUrlError::BlockedHost {
class: HostClass::Loopback,
},
);
assert_eq!(
entry.blocked_class(),
Some((HostClass::Loopback, "https://127.0.0.1:9999".to_string()))
);
}
#[test]
fn test_invalid_entry_blocked_class_none_for_other_reason() {
let entry = InvalidEntry::new(
RedactedUrl::new("not-a-url"),
IndexUrlError::InvalidUrl(RedactedUrl::new("not-a-url")),
);
assert_eq!(entry.blocked_class(), None);
}
}