Skip to main content

uv_auth/
index.rs

1use std::fmt::{self, Display, Formatter};
2
3use rustc_hash::FxHashSet;
4use uv_redacted::SafeUrl;
5
6/// When to use authentication.
7#[derive(
8    Copy,
9    Clone,
10    Debug,
11    Default,
12    Hash,
13    Eq,
14    PartialEq,
15    Ord,
16    PartialOrd,
17    serde::Serialize,
18    serde::Deserialize,
19)]
20#[serde(rename_all = "kebab-case")]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22pub enum AuthPolicy {
23    /// Authenticate when necessary.
24    ///
25    /// If credentials are provided, they will be used. Otherwise, an unauthenticated request will
26    /// be attempted first. If the request fails, uv will search for credentials. If credentials are
27    /// found, an authenticated request will be attempted.
28    #[default]
29    Auto,
30    /// Always authenticate.
31    ///
32    /// If credentials are not provided, uv will eagerly search for credentials. If credentials
33    /// cannot be found, uv will error instead of attempting an unauthenticated request.
34    Always,
35    /// Never authenticate.
36    ///
37    /// If credentials are provided, uv will error. uv will not search for credentials.
38    Never,
39}
40
41impl Display for AuthPolicy {
42    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
43        match self {
44            Self::Auto => write!(f, "auto"),
45            Self::Always => write!(f, "always"),
46            Self::Never => write!(f, "never"),
47        }
48    }
49}
50
51// TODO(john): We are not using `uv_distribution_types::Index` directly
52// here because it would cause circular crate dependencies. However, this
53// could potentially make sense for a future refactor.
54#[derive(Debug, Clone, Hash, Eq, PartialEq)]
55pub struct Index {
56    pub url: SafeUrl,
57    /// The root endpoint where authentication is applied.
58    /// For PEP 503 endpoints, this excludes `/simple`.
59    pub root_url: SafeUrl,
60    pub auth_policy: AuthPolicy,
61}
62
63impl Index {
64    fn is_prefix_for(&self, url: &SafeUrl) -> bool {
65        if self.root_url.scheme() != url.scheme()
66            || self.root_url.host_str() != url.host_str()
67            || self.root_url.port_or_known_default() != url.port_or_known_default()
68        {
69            return false;
70        }
71
72        is_path_prefix(self.root_url.path(), url.path())
73    }
74}
75
76/// Returns `true` if `prefix` is a complete path-segment prefix of `path`.
77///
78/// This rejects partial segment matches, so `/simple` matches `/simple/anyio` but not
79/// `/simpleevil`.
80pub(crate) fn is_path_prefix(prefix: &str, path: &str) -> bool {
81    if prefix == path {
82        return true;
83    }
84
85    let Some(suffix) = path.strip_prefix(prefix) else {
86        return false;
87    };
88
89    prefix.ends_with('/') || suffix.starts_with('/')
90}
91
92// TODO(john): Multiple methods in this struct need to iterate over
93// all the indexes in the set. There are probably not many URLs to
94// iterate through, but we could use a trie instead of a HashSet here
95// for more efficient search.
96#[derive(Debug, Default, Clone, Eq, PartialEq)]
97pub struct Indexes(FxHashSet<Index>);
98
99impl Indexes {
100    pub fn new() -> Self {
101        Self(FxHashSet::default())
102    }
103
104    /// Create a new [`Indexes`] instance from an iterator of [`Index`]s.
105    pub fn from_indexes(urls: impl IntoIterator<Item = Index>) -> Self {
106        let mut index_urls = Self::new();
107        for url in urls {
108            index_urls.0.insert(url);
109        }
110        index_urls
111    }
112
113    /// Get the index for a URL if one exists.
114    pub(crate) fn index_for(&self, url: &SafeUrl) -> Option<&Index> {
115        self.find_prefix_index(url)
116    }
117
118    /// Get the [`AuthPolicy`] for a URL.
119    pub(crate) fn auth_policy_for(&self, url: &SafeUrl) -> AuthPolicy {
120        self.find_prefix_index(url)
121            .map(|index| index.auth_policy)
122            .unwrap_or(AuthPolicy::Auto)
123    }
124
125    fn find_prefix_index(&self, url: &SafeUrl) -> Option<&Index> {
126        self.0.iter().find(|&index| index.is_prefix_for(url))
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    fn index(root_url: &str, auth_policy: AuthPolicy) -> Index {
135        let root_url = SafeUrl::parse(root_url).unwrap();
136        Index {
137            url: root_url.clone(),
138            root_url,
139            auth_policy,
140        }
141    }
142
143    #[test]
144    fn test_index_path_prefix_requires_segment_boundary() {
145        let index = index("https://example.com/simple", AuthPolicy::Always);
146
147        for url in [
148            "https://example.com/simple",
149            "https://example.com/simple/",
150            "https://example.com/simple/anyio",
151        ] {
152            assert!(
153                index.is_prefix_for(&SafeUrl::parse(url).unwrap()),
154                "Failed to match URL with prefix: {url}"
155            );
156        }
157
158        for url in [
159            "https://example.com/simpleevil",
160            "https://example.com/simple-evil",
161            "https://example.com/simpl",
162        ] {
163            assert!(
164                !index.is_prefix_for(&SafeUrl::parse(url).unwrap()),
165                "Should not match URL with partial path segment: {url}"
166            );
167        }
168    }
169}