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