Skip to main content

uv_auth/
providers.rs

1use std::borrow::Cow;
2use std::sync::LazyLock;
3
4use anyhow::{Context, Result};
5use reqsign::aws::DefaultSigner as AwsDefaultSigner;
6use reqsign::azure::DefaultSigner as AzureDefaultSigner;
7use reqsign::google::DefaultSigner as GcsDefaultSigner;
8use tracing::debug;
9use url::ParseError;
10
11use uv_preview::{Preview, PreviewFeature};
12use uv_redacted::SafeUrl;
13use uv_static::EnvVars;
14use uv_warnings::warn_user_once;
15
16use crate::Credentials;
17use crate::credentials::Token;
18use crate::index::is_path_prefix;
19use crate::realm::{Realm, RealmRef};
20
21/// The [`Realm`] for the Hugging Face platform.
22static HUGGING_FACE_REALM: LazyLock<Realm> = LazyLock::new(|| {
23    let url = SafeUrl::parse("https://huggingface.co").expect("Failed to parse Hugging Face URL");
24    Realm::from(&url)
25});
26
27/// The authentication token for the Hugging Face platform, if set.
28static HUGGING_FACE_TOKEN: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
29    // Extract the Hugging Face token from the environment variable, if it exists.
30    let hf_token = std::env::var(EnvVars::HF_TOKEN)
31        .ok()
32        .map(String::into_bytes)
33        .filter(|token| !token.is_empty())?;
34
35    if std::env::var_os(EnvVars::UV_NO_HF_TOKEN).is_some() {
36        debug!("Ignoring Hugging Face token from environment due to `UV_NO_HF_TOKEN`");
37        return None;
38    }
39
40    debug!("Found Hugging Face token in environment");
41    Some(hf_token)
42});
43
44/// A provider for authentication credentials for the Hugging Face platform.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub(crate) struct HuggingFaceProvider;
47
48impl HuggingFaceProvider {
49    /// Returns the credentials for the Hugging Face platform, if available.
50    pub(crate) fn credentials_for(url: &SafeUrl) -> Option<Credentials> {
51        if RealmRef::from(url) == *HUGGING_FACE_REALM {
52            if let Some(token) = HUGGING_FACE_TOKEN.as_ref() {
53                return Some(Credentials::Bearer {
54                    token: Token::new(token.clone()),
55                });
56            }
57        }
58        None
59    }
60}
61
62/// The [`SafeUrl`] for the S3 endpoint, if set.
63static S3_ENDPOINT_URL: LazyLock<Result<Option<SafeUrl>, ParseError>> =
64    LazyLock::new(|| endpoint_url(EnvVars::UV_S3_ENDPOINT_URL));
65
66/// A provider for authentication credentials for S3 endpoints.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub(crate) struct S3EndpointProvider;
69
70impl S3EndpointProvider {
71    /// Returns `true` if the URL matches the configured S3 endpoint.
72    pub(crate) fn is_s3_endpoint(url: &SafeUrl, preview: Preview) -> Result<bool> {
73        if let Some(s3_endpoint_url) = S3_ENDPOINT_URL
74            .as_ref()
75            .map_err(|error| *error)
76            .with_context(|| format!("Invalid `{}`", EnvVars::UV_S3_ENDPOINT_URL))?
77        {
78            if !preview.is_enabled(PreviewFeature::S3Endpoint) {
79                warn_user_once!(
80                    "The `s3-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
81                    PreviewFeature::S3Endpoint
82                );
83            }
84
85            // Treat any URL under the endpoint path on the same domain or subdomain as available
86            // for S3 signing.
87            if is_endpoint_url(url, s3_endpoint_url) {
88                return Ok(true);
89            }
90        }
91        Ok(false)
92    }
93
94    /// Creates a new S3 signer with the configured region.
95    ///
96    /// This is potentially expensive as it may invoke credential helpers, so the result
97    /// should be cached.
98    pub(crate) fn create_signer() -> AwsDefaultSigner {
99        // TODO(charlie): Can `reqsign` infer the region for us? Profiles, for example,
100        // often have a region set already.
101        let region = std::env::var(EnvVars::AWS_REGION)
102            .map(Cow::Owned)
103            .unwrap_or_else(|_| {
104                std::env::var(EnvVars::AWS_DEFAULT_REGION)
105                    .map(Cow::Owned)
106                    .unwrap_or_else(|_| Cow::Borrowed("us-east-1"))
107            });
108        reqsign::aws::default_signer("s3", &region)
109    }
110}
111
112/// The [`SafeUrl`] for the GCS endpoint, if set.
113static GCS_ENDPOINT_URL: LazyLock<Result<Option<SafeUrl>, ParseError>> =
114    LazyLock::new(|| endpoint_url(EnvVars::UV_GCS_ENDPOINT_URL));
115
116/// A provider for authentication credentials for GCS endpoints.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub(crate) struct GcsEndpointProvider;
119
120impl GcsEndpointProvider {
121    /// Returns `true` if the URL matches the configured GCS endpoint.
122    pub(crate) fn is_gcs_endpoint(url: &SafeUrl, preview: Preview) -> Result<bool> {
123        if let Some(gcs_endpoint_url) = GCS_ENDPOINT_URL
124            .as_ref()
125            .map_err(|error| *error)
126            .with_context(|| format!("Invalid `{}`", EnvVars::UV_GCS_ENDPOINT_URL))?
127        {
128            if !preview.is_enabled(PreviewFeature::GcsEndpoint) {
129                warn_user_once!(
130                    "The `gcs-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
131                    PreviewFeature::GcsEndpoint
132                );
133            }
134
135            // Treat any URL under the endpoint path on the same domain or subdomain as available
136            // for GCS signing.
137            if is_endpoint_url(url, gcs_endpoint_url) {
138                return Ok(true);
139            }
140        }
141        Ok(false)
142    }
143
144    /// Creates a new GCS signer.
145    ///
146    /// This is potentially expensive as it may invoke credential helpers, so the result
147    /// should be cached.
148    pub(crate) fn create_signer() -> GcsDefaultSigner {
149        reqsign::google::default_signer("storage.googleapis.com")
150    }
151}
152
153/// The [`SafeUrl`] for the Azure endpoint, if set.
154static AZURE_ENDPOINT_URL: LazyLock<Result<Option<SafeUrl>, ParseError>> =
155    LazyLock::new(|| endpoint_url(EnvVars::UV_AZURE_ENDPOINT_URL));
156
157/// A provider for authentication credentials for Azure endpoints.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct AzureEndpointProvider;
160
161impl AzureEndpointProvider {
162    /// Returns `true` if the URL matches the configured Azure endpoint.
163    pub fn is_azure_endpoint(url: &SafeUrl, preview: Preview) -> Result<bool> {
164        if let Some(azure_endpoint_url) = AZURE_ENDPOINT_URL
165            .as_ref()
166            .map_err(|error| *error)
167            .with_context(|| format!("Invalid `{}`", EnvVars::UV_AZURE_ENDPOINT_URL))?
168        {
169            if !preview.is_enabled(PreviewFeature::AzureEndpoint) {
170                warn_user_once!(
171                    "The `azure-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
172                    PreviewFeature::AzureEndpoint
173                );
174            }
175
176            // Treat any URL under the endpoint path on the same domain or subdomain as available
177            // for Azure signing.
178            if is_endpoint_url(url, azure_endpoint_url) {
179                return Ok(true);
180            }
181        }
182        Ok(false)
183    }
184
185    /// Creates a new Azure signer using the default Azure credential chain.
186    ///
187    /// This is potentially expensive as it may invoke credential helpers, so the result
188    /// should be cached.
189    pub(crate) fn create_signer() -> AzureDefaultSigner {
190        reqsign::azure::default_signer()
191    }
192}
193
194/// Returns the configured endpoint [`SafeUrl`], if set and valid.
195fn endpoint_url(env_var: &str) -> Result<Option<SafeUrl>, ParseError> {
196    let Some(endpoint_url) = std::env::var(env_var).ok() else {
197        return Ok(None);
198    };
199    SafeUrl::parse_relaxed(&endpoint_url).map(Some)
200}
201
202/// Returns `true` if `url` is within the configured S3, GCS, or Azure-compatible endpoint URL.
203///
204/// The URL must be in the same realm, or a subdomain of the endpoint realm, and must be under the
205/// endpoint path using complete path-segment prefix matching.
206fn is_endpoint_url(url: &SafeUrl, endpoint_url: &SafeUrl) -> bool {
207    let endpoint_realm = RealmRef::from(endpoint_url);
208    let realm = RealmRef::from(url);
209    if realm != endpoint_realm && !realm.is_subdomain_of(endpoint_realm) {
210        return false;
211    }
212
213    is_path_prefix(endpoint_url.path(), url.path())
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_endpoint_url_matches_path_prefix() {
222        let endpoint_url = SafeUrl::parse("https://example.com/private").unwrap();
223
224        for url in [
225            "https://example.com/private",
226            "https://example.com/private/",
227            "https://example.com/private/packages/anyio.whl",
228        ] {
229            assert!(
230                is_endpoint_url(&SafeUrl::parse(url).unwrap(), &endpoint_url),
231                "Failed to match endpoint URL prefix: {url}"
232            );
233        }
234    }
235
236    #[test]
237    fn test_endpoint_url_rejects_partial_path_segments() {
238        let endpoint_url = SafeUrl::parse("https://example.com/private").unwrap();
239
240        for url in [
241            "https://example.com/public",
242            "https://example.com/private-bucket",
243            "https://example.com/privatebucket",
244        ] {
245            assert!(
246                !is_endpoint_url(&SafeUrl::parse(url).unwrap(), &endpoint_url),
247                "Should not match URL outside endpoint path: {url}"
248            );
249        }
250    }
251
252    #[test]
253    fn test_endpoint_url_matches_subdomain_with_path_prefix() {
254        let endpoint_url = SafeUrl::parse("https://example.com/private").unwrap();
255
256        assert!(is_endpoint_url(
257            &SafeUrl::parse("https://bucket.example.com/private/package.whl").unwrap(),
258            &endpoint_url
259        ));
260        assert!(!is_endpoint_url(
261            &SafeUrl::parse("https://bucket.example.com/public/package.whl").unwrap(),
262            &endpoint_url
263        ));
264    }
265
266    #[test]
267    fn test_endpoint_url_root_path_matches_all_paths() {
268        let endpoint_url = SafeUrl::parse("https://example.com").unwrap();
269
270        for url in [
271            "https://example.com/package.whl",
272            "https://bucket.example.com/package.whl",
273        ] {
274            assert!(
275                is_endpoint_url(&SafeUrl::parse(url).unwrap(), &endpoint_url),
276                "Failed to match URL under endpoint root: {url}"
277            );
278        }
279    }
280}