Skip to main content

uv_auth/
index.rs

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