Skip to main content

polars_io/cloud/
credential_provider.rs

1use std::collections::BTreeMap;
2use std::fmt::Debug;
3use std::future::Future;
4use std::hash::Hash;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9use async_trait::async_trait;
10#[cfg(feature = "aws")]
11pub use object_store::aws::AwsCredential;
12#[cfg(feature = "azure")]
13pub use object_store::azure::AzureCredential;
14#[cfg(feature = "gcp")]
15pub use object_store::gcp::GcpCredential;
16use polars_core::config;
17use polars_error::{PolarsResult, polars_bail, to_compute_err};
18use polars_utils::pl_str::PlSmallStr;
19#[cfg(feature = "python")]
20use polars_utils::python_function::PythonObject;
21#[cfg(feature = "python")]
22use python_impl::PythonCredentialProvider;
23
24#[derive(Clone, Debug, PartialEq, Hash, Eq)]
25pub enum PlCredentialProvider {
26    /// Prefer using [`PlCredentialProvider::from_func`] instead of constructing this directly
27    Function(CredentialProviderFunction),
28    #[cfg(feature = "python")]
29    Python(PythonCredentialProvider),
30    /// Configuration for `credential_provider="auto"`. This is plain data, the provider is built
31    /// from it by name on the side that executes the query.
32    AutoInit(AutoInitConfig),
33}
34
35/// Inputs of an auto-initialized credential provider, extracted from `storage_options` and the
36/// path on the client side.
37///
38/// Note: The JSON form of this is also the interface to the Python-side
39/// `_build_auto_init_credential_provider()`.
40#[derive(Clone, Debug, PartialEq, Hash, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
43#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
44pub enum AutoInitConfig {
45    Aws {
46        profile_name: Option<PlSmallStr>,
47        region_name: Option<PlSmallStr>,
48        auto_init_unhandled_key: Option<PlSmallStr>,
49        storage_options: BTreeMap<PlSmallStr, Option<PlSmallStr>>,
50    },
51    Azure {
52        tenant_id: Option<PlSmallStr>,
53        storage_account: Option<PlSmallStr>,
54    },
55    Gcp {},
56    GcpToken {
57        token: PlSmallStr,
58    },
59}
60
61impl PlCredentialProvider {
62    /// Accepts a function that returns (credential, expiry time as seconds since UNIX_EPOCH)
63    ///
64    /// This functionality is unstable.
65    pub fn from_func(
66        // Internal notes
67        // * This function is exposed as the Rust API for `PlCredentialProvider`
68        func: impl Fn() -> Pin<
69            Box<dyn Future<Output = PolarsResult<(ObjectStoreCredential, u64)>> + Send + Sync>,
70        > + Send
71        + Sync
72        + 'static,
73    ) -> Self {
74        Self::Function(CredentialProviderFunction(Arc::new(func)))
75    }
76
77    /// Intended to be called with an internal `CredentialProviderBuilder` from
78    /// py-polars.
79    #[cfg(feature = "python")]
80    pub fn from_python_builder(func: pyo3::Py<pyo3::PyAny>) -> Self {
81        Self::Python(python_impl::PythonCredentialProvider::Builder(Arc::new(
82            PythonObject(func),
83        )))
84    }
85
86    #[cfg(feature = "python")]
87    pub fn from_auto_init_json(json: &str) -> PolarsResult<Self> {
88        serde_json::from_str(json).map(Self::AutoInit).map_err(
89            |e| polars_error::polars_err!(ComputeError: "invalid auto-init credential config: {e}"),
90        )
91    }
92
93    /// Python passes a `CredentialProviderBuilder`, this calls the builder to build the final
94    /// credential provider.
95    ///
96    /// This returns `Option` as the auto-initialization case is fallible and falls back to None.
97    pub(crate) fn try_into_initialized(
98        self,
99        clear_cached_credentials: bool,
100    ) -> PolarsResult<Option<Self>> {
101        match self {
102            Self::Function(_) => Ok(Some(self)),
103            #[cfg(feature = "python")]
104            Self::Python(v) => Ok(v
105                .try_into_initialized(clear_cached_credentials)?
106                .map(Self::Python)),
107            Self::AutoInit(config) => config.try_into_initialized(clear_cached_credentials),
108        }
109    }
110
111    pub fn stable_cache_key(&self) -> PolarsResult<Vec<u8>> {
112        match self {
113            Self::Function(CredentialProviderFunction(v)) => Ok((Arc::as_ptr(v) as *const ()
114                as usize)
115                .to_ne_bytes()
116                .to_vec()),
117            #[cfg(feature = "python")]
118            Self::Python(v) => v.stable_cache_key(),
119            Self::AutoInit(config) => {
120                // Hashed, as the config can contain a token.
121                let json = serde_json::to_vec(config).map_err(to_compute_err)?;
122                let mut out = b"auto_init:".to_vec();
123                out.extend_from_slice(blake3::hash(&json).as_bytes());
124                Ok(out)
125            },
126        }
127    }
128}
129
130impl AutoInitConfig {
131    fn try_into_initialized(
132        self,
133        clear_cached_credentials: bool,
134    ) -> PolarsResult<Option<PlCredentialProvider>> {
135        // A token given in `storage_options` needs no Python.
136        #[cfg(feature = "gcp")]
137        if let Self::GcpToken { token } = &self {
138            let credential = Arc::new(object_store::gcp::GcpCredential {
139                bearer: token.to_string(),
140            });
141
142            return Ok(Some(PlCredentialProvider::from_func(move || {
143                let credential = credential.clone();
144                Box::pin(async move { Ok((ObjectStoreCredential::Gcp(credential), u64::MAX)) })
145            })));
146        }
147
148        #[cfg(feature = "python")]
149        {
150            Ok(
151                PythonCredentialProvider::build_auto_init(&self, clear_cached_credentials)?
152                    .map(PlCredentialProvider::Python),
153            )
154        }
155
156        // Without Python, fall back to the object_store's own environment-based credentials, as
157        // when the Python provider's dependencies are not installed. A requested profile cannot
158        // be loaded that way.
159        #[cfg(not(feature = "python"))]
160        {
161            let _ = clear_cached_credentials;
162
163            if let Self::Aws {
164                profile_name: Some(profile_name),
165                ..
166            } = &self
167            {
168                polars_bail!(
169                    ComputeError:
170                    "cannot load requested aws_profile '{profile_name}': requires Python"
171                )
172            }
173
174            Ok(None)
175        }
176    }
177}
178
179pub enum ObjectStoreCredential {
180    #[cfg(feature = "aws")]
181    Aws(Arc<object_store::aws::AwsCredential>),
182    #[cfg(feature = "azure")]
183    Azure(Arc<object_store::azure::AzureCredential>),
184    #[cfg(feature = "gcp")]
185    Gcp(Arc<object_store::gcp::GcpCredential>),
186    /// For testing purposes
187    None,
188}
189
190impl ObjectStoreCredential {
191    fn variant_name(&self) -> &'static str {
192        match self {
193            #[cfg(feature = "aws")]
194            Self::Aws(_) => "Aws",
195            #[cfg(feature = "azure")]
196            Self::Azure(_) => "Azure",
197            #[cfg(feature = "gcp")]
198            Self::Gcp(_) => "Gcp",
199            Self::None => "None",
200        }
201    }
202
203    fn panic_type_mismatch(&self, expected: &str) {
204        panic!(
205            "impl error: credential type mismatch: expected {}, got {} instead",
206            expected,
207            self.variant_name()
208        )
209    }
210
211    #[cfg(feature = "aws")]
212    fn unwrap_aws(self) -> Arc<object_store::aws::AwsCredential> {
213        let Self::Aws(v) = self else {
214            self.panic_type_mismatch("aws");
215            unreachable!()
216        };
217        v
218    }
219
220    #[cfg(feature = "azure")]
221    fn unwrap_azure(self) -> Arc<object_store::azure::AzureCredential> {
222        let Self::Azure(v) = self else {
223            self.panic_type_mismatch("azure");
224            unreachable!()
225        };
226        v
227    }
228
229    #[cfg(feature = "gcp")]
230    fn unwrap_gcp(self) -> Arc<object_store::gcp::GcpCredential> {
231        let Self::Gcp(v) = self else {
232            self.panic_type_mismatch("gcp");
233            unreachable!()
234        };
235        v
236    }
237}
238
239pub trait IntoCredentialProvider: Sized {
240    #[cfg(feature = "aws")]
241    fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
242        unimplemented!()
243    }
244
245    #[cfg(feature = "azure")]
246    fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
247        unimplemented!()
248    }
249
250    #[cfg(feature = "gcp")]
251    fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
252        unimplemented!()
253    }
254
255    /// Note, technically shouldn't be under the `IntoCredentialProvider` trait, but it's here
256    /// for convenience.
257    fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>>;
258}
259
260impl IntoCredentialProvider for PlCredentialProvider {
261    #[cfg(feature = "aws")]
262    fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
263        match self {
264            Self::Function(v) => v.into_aws_provider(),
265            #[cfg(feature = "python")]
266            Self::Python(v) => v.into_aws_provider(),
267            Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
268        }
269    }
270
271    #[cfg(feature = "azure")]
272    fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
273        match self {
274            Self::Function(v) => v.into_azure_provider(),
275            #[cfg(feature = "python")]
276            Self::Python(v) => v.into_azure_provider(),
277            Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
278        }
279    }
280
281    #[cfg(feature = "gcp")]
282    fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
283        match self {
284            Self::Function(v) => v.into_gcp_provider(),
285            #[cfg(feature = "python")]
286            Self::Python(v) => v.into_gcp_provider(),
287            Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
288        }
289    }
290
291    fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>> {
292        match self {
293            Self::Function(v) => v.storage_update_options(),
294            #[cfg(feature = "python")]
295            Self::Python(v) => v.storage_update_options(),
296            Self::AutoInit(_) => unreachable!("auto-init credential provider was not initialized"),
297        }
298    }
299}
300
301type CredentialProviderFunctionImpl = Arc<
302    dyn Fn() -> Pin<
303            Box<dyn Future<Output = PolarsResult<(ObjectStoreCredential, u64)>> + Send + Sync>,
304        > + Send
305        + Sync,
306>;
307
308/// Wrapper that implements [`IntoCredentialProvider`], [`Debug`], [`PartialEq`], [`Hash`] etc.
309#[derive(Clone)]
310pub struct CredentialProviderFunction(CredentialProviderFunctionImpl);
311
312macro_rules! build_to_object_store_err {
313    ($s:expr) => {{
314        fn to_object_store_err(
315            e: impl std::error::Error + Send + Sync + 'static,
316        ) -> object_store::Error {
317            object_store::Error::Generic {
318                store: $s,
319                source: Box::new(e),
320            }
321        }
322
323        to_object_store_err
324    }};
325}
326
327impl IntoCredentialProvider for CredentialProviderFunction {
328    #[cfg(feature = "aws")]
329    fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
330        #[derive(Debug)]
331        struct S(
332            CredentialProviderFunction,
333            FetchedCredentialsCache<Arc<object_store::aws::AwsCredential>>,
334        );
335
336        #[async_trait]
337        impl object_store::CredentialProvider for S {
338            type Credential = object_store::aws::AwsCredential;
339
340            async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
341                self.1
342                    .get_maybe_update(async {
343                        let (creds, expiry) = self.0.0().await?;
344                        PolarsResult::Ok((creds.unwrap_aws(), expiry))
345                    })
346                    .await
347                    .map_err(build_to_object_store_err!("credential-provider-aws"))
348            }
349        }
350
351        Arc::new(S(
352            self,
353            FetchedCredentialsCache::new(Arc::new(AwsCredential {
354                key_id: String::new(),
355                secret_key: String::new(),
356                token: None,
357            })),
358        ))
359    }
360
361    #[cfg(feature = "azure")]
362    fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
363        #[derive(Debug)]
364        struct S(
365            CredentialProviderFunction,
366            FetchedCredentialsCache<Arc<object_store::azure::AzureCredential>>,
367        );
368
369        #[async_trait]
370        impl object_store::CredentialProvider for S {
371            type Credential = object_store::azure::AzureCredential;
372
373            async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
374                self.1
375                    .get_maybe_update(async {
376                        let (creds, expiry) = self.0.0().await?;
377                        PolarsResult::Ok((creds.unwrap_azure(), expiry))
378                    })
379                    .await
380                    .map_err(build_to_object_store_err!("credential-provider-azure"))
381            }
382        }
383
384        Arc::new(S(
385            self,
386            FetchedCredentialsCache::new(Arc::new(AzureCredential::BearerToken(String::new()))),
387        ))
388    }
389
390    #[cfg(feature = "gcp")]
391    fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
392        #[derive(Debug)]
393        struct S(
394            CredentialProviderFunction,
395            FetchedCredentialsCache<Arc<object_store::gcp::GcpCredential>>,
396        );
397
398        #[async_trait]
399        impl object_store::CredentialProvider for S {
400            type Credential = object_store::gcp::GcpCredential;
401
402            async fn get_credential(&self) -> object_store::Result<Arc<Self::Credential>> {
403                self.1
404                    .get_maybe_update(async {
405                        let (creds, expiry) = self.0.0().await?;
406                        PolarsResult::Ok((creds.unwrap_gcp(), expiry))
407                    })
408                    .await
409                    .map_err(build_to_object_store_err!("credential-provider-gcp"))
410            }
411        }
412
413        Arc::new(S(
414            self,
415            FetchedCredentialsCache::new(Arc::new(GcpCredential {
416                bearer: String::new(),
417            })),
418        ))
419    }
420
421    fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>> {
422        Ok(vec![])
423    }
424}
425
426impl Debug for CredentialProviderFunction {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        write!(
429            f,
430            "credential provider function at 0x{:016x}",
431            self.0.as_ref() as *const _ as *const () as usize
432        )
433    }
434}
435
436impl Eq for CredentialProviderFunction {}
437
438impl PartialEq for CredentialProviderFunction {
439    fn eq(&self, other: &Self) -> bool {
440        Arc::ptr_eq(&self.0, &other.0)
441    }
442}
443
444impl Hash for CredentialProviderFunction {
445    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
446        state.write_usize(Arc::as_ptr(&self.0) as *const () as usize)
447    }
448}
449
450/// Serialized form of [`PlCredentialProvider`].
451///
452/// Note: Variant order is part of the serialized format. `Builder` and `Provider` keep the
453/// positions they had when this was `PythonCredentialProvider`.
454#[cfg(feature = "serde")]
455#[derive(serde::Serialize, serde::Deserialize)]
456enum SerializedCredentialProvider {
457    #[cfg(feature = "python")]
458    #[serde(
459        serialize_with = "PythonObject::serialize_with_pyversion",
460        deserialize_with = "PythonObject::deserialize_with_pyversion"
461    )]
462    Builder(Arc<PythonObject>),
463    #[cfg(not(feature = "python"))]
464    Builder(RequiresPython),
465    #[cfg(feature = "python")]
466    #[serde(
467        serialize_with = "PythonObject::serialize_with_pyversion",
468        deserialize_with = "PythonObject::deserialize_with_pyversion"
469    )]
470    Provider(Arc<PythonObject>),
471    #[cfg(not(feature = "python"))]
472    Provider(RequiresPython),
473    AutoInit(AutoInitConfig),
474}
475
476#[cfg(all(feature = "serde", not(feature = "python")))]
477enum RequiresPython {}
478
479#[cfg(all(feature = "serde", not(feature = "python")))]
480impl serde::Serialize for RequiresPython {
481    fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
482    where
483        S: serde::Serializer,
484    {
485        match *self {}
486    }
487}
488
489#[cfg(all(feature = "serde", not(feature = "python")))]
490impl<'de> serde::Deserialize<'de> for RequiresPython {
491    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
492    where
493        D: serde::Deserializer<'de>,
494    {
495        use serde::de::Error;
496        Err(D::Error::custom(
497            "cannot deserialize Python credential provider without Python",
498        ))
499    }
500}
501
502#[cfg(feature = "serde")]
503impl<'de> serde::Deserialize<'de> for PlCredentialProvider {
504    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
505    where
506        D: serde::Deserializer<'de>,
507    {
508        Ok(
509            match SerializedCredentialProvider::deserialize(deserializer)? {
510                #[cfg(feature = "python")]
511                SerializedCredentialProvider::Builder(v) => {
512                    Self::Python(PythonCredentialProvider::Builder(v))
513                },
514                #[cfg(feature = "python")]
515                SerializedCredentialProvider::Provider(v) => {
516                    Self::Python(PythonCredentialProvider::Provider(v))
517                },
518                #[cfg(not(feature = "python"))]
519                SerializedCredentialProvider::Builder(v)
520                | SerializedCredentialProvider::Provider(v) => match v {},
521                SerializedCredentialProvider::AutoInit(v) => Self::AutoInit(v),
522            },
523        )
524    }
525}
526
527#[cfg(feature = "serde")]
528impl serde::Serialize for PlCredentialProvider {
529    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
530    where
531        S: serde::Serializer,
532    {
533        use serde::ser::Error;
534
535        let v = match self {
536            Self::Function(_) => {
537                return Err(S::Error::custom(format!("cannot serialize {self:?}")));
538            },
539            #[cfg(feature = "python")]
540            Self::Python(PythonCredentialProvider::Builder(v)) => {
541                SerializedCredentialProvider::Builder(v.clone())
542            },
543            #[cfg(feature = "python")]
544            Self::Python(PythonCredentialProvider::Provider(v)) => {
545                SerializedCredentialProvider::Provider(v.clone())
546            },
547            Self::AutoInit(v) => SerializedCredentialProvider::AutoInit(v.clone()),
548        };
549
550        v.serialize(serializer)
551    }
552}
553
554#[cfg(feature = "dsl-schema")]
555impl schemars::JsonSchema for PlCredentialProvider {
556    fn schema_name() -> std::borrow::Cow<'static, str> {
557        "PlCredentialProvider".into()
558    }
559
560    fn schema_id() -> std::borrow::Cow<'static, str> {
561        std::borrow::Cow::Borrowed(concat!(module_path!(), "::", "PlCredentialProvider"))
562    }
563
564    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
565        /// Schema of [`SerializedCredentialProvider`], with Python objects as bytes.
566        #[derive(schemars::JsonSchema)]
567        #[allow(dead_code)]
568        enum CredentialProviderSchema {
569            Builder(Vec<u8>),
570            Provider(Vec<u8>),
571            AutoInit(AutoInitConfig),
572        }
573
574        CredentialProviderSchema::json_schema(generator)
575    }
576}
577
578/// Avoids calling the credential provider function if we have not yet passed the expiry time.
579#[derive(Debug)]
580struct FetchedCredentialsCache<C>(tokio::sync::Mutex<(C, u64, bool)>);
581
582impl<C: Clone> FetchedCredentialsCache<C> {
583    fn new(init_creds: C) -> Self {
584        Self(tokio::sync::Mutex::new((init_creds, 0, true)))
585    }
586
587    async fn get_maybe_update(
588        &self,
589        // Taking an `impl Future` here allows us to potentially avoid a `Box::pin` allocation from
590        // a `Fn() -> Pin<Box<dyn Future>>` by having it wrapped in an `async { f() }` block. We
591        // will not poll that block if the credentials have not yet expired.
592        update_func: impl Future<Output = PolarsResult<(C, u64)>>,
593    ) -> PolarsResult<C> {
594        let verbose = config::verbose();
595
596        fn expiry_msg(last_fetched_expiry: u64, now: u64) -> String {
597            if last_fetched_expiry == u64::MAX {
598                "expiry = (never expires)".into()
599            } else {
600                format!(
601                    "expiry = {} (in {} seconds)",
602                    last_fetched_expiry,
603                    last_fetched_expiry.saturating_sub(now)
604                )
605            }
606        }
607
608        let mut inner = self.0.lock().await;
609        let (last_fetched_credentials, last_fetched_expiry, log_use_cached) = &mut *inner;
610
611        let current_time = SystemTime::now()
612            .duration_since(UNIX_EPOCH)
613            .unwrap()
614            .as_secs();
615
616        if *last_fetched_expiry <= current_time {
617            if verbose {
618                eprintln!(
619                    "[FetchedCredentialsCache]: \
620                    Call update_func: current_time = {}, \
621                    last_fetched_expiry = {}",
622                    current_time, *last_fetched_expiry
623                )
624            }
625
626            let (credentials, expiry) = update_func.await?;
627
628            *last_fetched_credentials = credentials;
629            *last_fetched_expiry = expiry;
630            *log_use_cached = true;
631
632            if expiry < current_time && expiry != 0 {
633                polars_bail!(
634                    ComputeError:
635                    "credential expiry time {} is older than system time {} \
636                     by {} seconds",
637                    expiry,
638                    current_time,
639                    current_time - expiry
640                )
641            }
642
643            if verbose {
644                eprintln!(
645                    "[FetchedCredentialsCache]: Finish update_func: new {}",
646                    expiry_msg(
647                        *last_fetched_expiry,
648                        SystemTime::now()
649                            .duration_since(UNIX_EPOCH)
650                            .unwrap()
651                            .as_secs()
652                    )
653                )
654            }
655        } else if verbose && *log_use_cached {
656            *log_use_cached = false;
657            let now = SystemTime::now()
658                .duration_since(UNIX_EPOCH)
659                .unwrap()
660                .as_secs();
661            eprintln!(
662                "[FetchedCredentialsCache]: Using cached credentials: \
663                current_time = {}, {}",
664                now,
665                expiry_msg(*last_fetched_expiry, now)
666            )
667        }
668
669        Ok(last_fetched_credentials.clone())
670    }
671}
672
673#[cfg(feature = "python")]
674mod python_impl {
675    use std::hash::Hash;
676    use std::sync::Arc;
677
678    use polars_error::{PolarsError, PolarsResult, polars_err, to_compute_err};
679    use polars_utils::pl_str::PlSmallStr;
680    use polars_utils::python_function::PythonObject;
681    use pyo3::exceptions::PyValueError;
682    use pyo3::pybacked::PyBackedStr;
683    use pyo3::types::{PyAnyMethods, PyDict, PyDictMethods};
684    use pyo3::{Python, intern};
685
686    use super::{AutoInitConfig, IntoCredentialProvider};
687
688    #[derive(Clone, Debug)]
689    pub enum PythonCredentialProvider {
690        /// Indicates `py_object` is a `CredentialProviderBuilder`.
691        Builder(Arc<PythonObject>),
692        /// Indicates `py_object` is an instantiated credential provider
693        Provider(Arc<PythonObject>),
694    }
695
696    impl PythonCredentialProvider {
697        /// Builds the Python credential provider for an auto-init config, using the Python
698        /// that executes the query. Returns `None` if it could not be auto-initialized.
699        pub(super) fn build_auto_init(
700            config: &AutoInitConfig,
701            clear_cached_credentials: bool,
702        ) -> PolarsResult<Option<Self>> {
703            let config_json = serde_json::to_string(config).map_err(to_compute_err)?;
704
705            let opt_initialized_py_object = Python::attach(|py| {
706                let v = py
707                    .import(intern!(py, "polars.io.cloud.credential_provider._builder"))?
708                    .call_method1(
709                        intern!(py, "_build_auto_init_credential_provider"),
710                        (config_json, clear_cached_credentials),
711                    )?;
712
713                pyo3::PyResult::Ok((!v.is_none()).then(|| v.unbind()))
714            })?;
715
716            Ok(opt_initialized_py_object
717                .map(PythonObject)
718                .map(Arc::new)
719                .map(Self::Provider))
720        }
721
722        /// Performs initialization if necessary.
723        ///
724        /// This exists as a separate step that must be called beforehand. This approach is easier
725        /// as the alternative is to refactor the `IntoCredentialProvider` trait to return
726        /// `PolarsResult<Option<T>>` for every single function.
727        pub(super) fn try_into_initialized(
728            self,
729            clear_cached_credentials: bool,
730        ) -> PolarsResult<Option<Self>> {
731            match self {
732                Self::Builder(py_object) => {
733                    let opt_initialized_py_object = Python::attach(|py| {
734                        let build_fn =
735                            py_object.getattr(py, intern!(py, "build_credential_provider"))?;
736
737                        let v = build_fn.call1(py, (clear_cached_credentials,))?;
738                        let v = (!v.is_none(py)).then_some(v);
739
740                        pyo3::PyResult::Ok(v)
741                    })?;
742
743                    Ok(opt_initialized_py_object
744                        .map(PythonObject)
745                        .map(Arc::new)
746                        .map(Self::Provider))
747                },
748                Self::Provider(_) => {
749                    // Note: We don't expect to hit here.
750                    Ok(Some(self))
751                },
752            }
753        }
754
755        fn unwrap_as_provider(self) -> Arc<PythonObject> {
756            match self {
757                Self::Builder(_) => panic!(),
758                Self::Provider(v) => v,
759            }
760        }
761
762        pub(crate) fn unwrap_as_provider_ref(&self) -> &Arc<PythonObject> {
763            match self {
764                Self::Builder(_) => panic!(),
765                Self::Provider(v) => v,
766            }
767        }
768
769        pub(super) fn func_addr(&self) -> usize {
770            (match self {
771                Self::Builder(v) => Arc::as_ptr(v),
772                Self::Provider(v) => Arc::as_ptr(v),
773            }) as *const () as usize
774        }
775
776        pub fn stable_cache_key(&self) -> PolarsResult<Vec<u8>> {
777            let obj = match self {
778                Self::Builder(obj) | Self::Provider(obj) => obj,
779            };
780            let err = |e| {
781                polars_err!(ComputeError:
782                "failed to extract stable_cache_key: {e}")
783            };
784            Python::attach(|py| {
785                obj.call_method0(py, "stable_cache_key")
786                    .and_then(|r| r.extract::<Vec<u8>>(py))
787            })
788            .map_err(err)
789        }
790    }
791
792    impl IntoCredentialProvider for PythonCredentialProvider {
793        #[cfg(feature = "aws")]
794        fn into_aws_provider(self) -> object_store::aws::AwsCredentialProvider {
795            use polars_error::PolarsResult;
796
797            use crate::cloud::credential_provider::{
798                CredentialProviderFunction, ObjectStoreCredential,
799            };
800
801            let func = self.unwrap_as_provider();
802
803            CredentialProviderFunction(Arc::new(move || {
804                let func = func.clone();
805                Box::pin(async move {
806                    let mut credentials = object_store::aws::AwsCredential {
807                        key_id: String::new(),
808                        secret_key: String::new(),
809                        token: None,
810                    };
811
812                    let expiry = Python::attach(|py| {
813                        let v = func.0.call0(py)?.into_bound(py);
814                        let (storage_options, expiry) =
815                            v.extract::<(pyo3::Bound<'_, PyDict>, Option<u64>)>()?;
816
817                        for (k, v) in storage_options.iter() {
818                            let k = k.extract::<PyBackedStr>()?;
819                            let v = v.extract::<Option<String>>()?;
820
821                            match k.as_ref() {
822                                "aws_access_key_id" => {
823                                    credentials.key_id = v.ok_or_else(|| {
824                                        PyValueError::new_err("aws_access_key_id was None")
825                                    })?;
826                                },
827                                "aws_secret_access_key" => {
828                                    credentials.secret_key = v.ok_or_else(|| {
829                                        PyValueError::new_err("aws_secret_access_key was None")
830                                    })?
831                                },
832                                "aws_session_token" => credentials.token = v,
833                                v => {
834                                    return pyo3::PyResult::Err(PyValueError::new_err(format!(
835                                        "unknown configuration key for aws: {}, \
836                                    valid configuration keys are: \
837                                    {}, {}, {}",
838                                        v,
839                                        "aws_access_key_id",
840                                        "aws_secret_access_key",
841                                        "aws_session_token"
842                                    )));
843                                },
844                            }
845                        }
846
847                        pyo3::PyResult::Ok(expiry.unwrap_or(u64::MAX))
848                    })?;
849
850                    if credentials.key_id.is_empty() {
851                        return Err(PolarsError::ComputeError(
852                            "aws_access_key_id was empty or not given".into(),
853                        ));
854                    }
855
856                    if credentials.secret_key.is_empty() {
857                        return Err(PolarsError::ComputeError(
858                            "aws_secret_access_key was empty or not given".into(),
859                        ));
860                    }
861
862                    PolarsResult::Ok((ObjectStoreCredential::Aws(Arc::new(credentials)), expiry))
863                })
864            }))
865            .into_aws_provider()
866        }
867
868        #[cfg(feature = "azure")]
869        fn into_azure_provider(self) -> object_store::azure::AzureCredentialProvider {
870            use object_store::azure::AzureAccessKey;
871            use percent_encoding::percent_decode_str;
872            use polars_core::config::verbose_print_sensitive;
873            use polars_error::PolarsResult;
874
875            use crate::cloud::credential_provider::{
876                CredentialProviderFunction, ObjectStoreCredential,
877            };
878
879            let func = self.unwrap_as_provider();
880
881            return CredentialProviderFunction(Arc::new(move || {
882                let func = func.clone();
883                Box::pin(async move {
884                    let mut credentials = None;
885
886                    static VALID_KEYS_MSG: &str =
887                        "valid configuration keys are: ('account_key', 'bearer_token', 'sas_token')";
888
889                    let expiry = Python::attach(|py| {
890                        let v = func.0.call0(py)?.into_bound(py);
891                        let (storage_options, expiry) =
892                            v.extract::<(pyo3::Bound<'_, PyDict>, Option<u64>)>()?;
893
894                        for (k, v) in storage_options.iter() {
895                            let k = k.extract::<PyBackedStr>()?;
896                            let v = v.extract::<String>()?;
897
898                            match k.as_ref() {
899                                "account_key" => {
900                                    credentials =
901                                        Some(object_store::azure::AzureCredential::AccessKey(
902                                            AzureAccessKey::try_new(v.as_str()).map_err(|e| {
903                                                PyValueError::new_err(e.to_string())
904                                            })?,
905                                        ))
906                                },
907                                "bearer_token" => {
908                                    credentials =
909                                        Some(object_store::azure::AzureCredential::BearerToken(v))
910                                },
911                                "sas_token" => {
912                                    credentials =
913                                        Some(object_store::azure::AzureCredential::SASToken(
914                                            split_sas(&v).map_err(|err_msg| {
915                                                verbose_print_sensitive(|| {
916                                                    format!("error decoding SAS token: {err_msg} (token: {v})")
917                                                });
918
919                                                PyValueError::new_err(format!(
920                                                    "error decoding SAS token: {err_msg}. \
921                                                    Set POLARS_VERBOSE_SENSITIVE=1 to print the value"
922                                                ))
923                                            })?,
924                                        ))
925                                },
926                                v => {
927                                    return pyo3::PyResult::Err(PyValueError::new_err(format!(
928                                        "unknown configuration key for azure: {v}, {VALID_KEYS_MSG}"
929                                    )));
930                                },
931                            }
932                        }
933
934                        pyo3::PyResult::Ok(expiry.unwrap_or(u64::MAX))
935                    })?;
936
937                    let Some(credentials) = credentials else {
938                        return Err(PolarsError::ComputeError(
939                            format!(
940                                "did not find a valid configuration key for azure, {VALID_KEYS_MSG}"
941                            )
942                            .into(),
943                        ));
944                    };
945
946                    PolarsResult::Ok((ObjectStoreCredential::Azure(Arc::new(credentials)), expiry))
947                })
948            }))
949            .into_azure_provider();
950
951            /// Copied and adjusted from object-store.
952            ///
953            /// https://github.com/apache/arrow-rs-object-store/blob/7a0504b4924fcecee17d768fd7190b8f71b0877f/src/azure/builder.rs#L1072-L1089
954            fn split_sas(sas: &str) -> Result<Vec<(String, String)>, &'static str> {
955                let sas = percent_decode_str(sas)
956                    .decode_utf8()
957                    .map_err(|_| "UTF-8 decode error")?;
958
959                let kv_str_pairs = sas
960                    .trim_start_matches('?')
961                    .split('&')
962                    .filter(|s| !s.chars().all(char::is_whitespace));
963
964                let mut pairs = Vec::new();
965
966                for kv_pair_str in kv_str_pairs {
967                    let (k, v) = kv_pair_str
968                        .trim()
969                        .split_once('=')
970                        .ok_or("missing SAS component")?;
971                    pairs.push((k.into(), v.into()))
972                }
973
974                Ok(pairs)
975            }
976        }
977
978        #[cfg(feature = "gcp")]
979        fn into_gcp_provider(self) -> object_store::gcp::GcpCredentialProvider {
980            use polars_error::PolarsResult;
981
982            use crate::cloud::credential_provider::{
983                CredentialProviderFunction, ObjectStoreCredential,
984            };
985
986            let func = self.unwrap_as_provider();
987
988            CredentialProviderFunction(Arc::new(move || {
989                let func = func.clone();
990                Box::pin(async move {
991                    let mut credentials = object_store::gcp::GcpCredential {
992                        bearer: String::new(),
993                    };
994
995                    let expiry = Python::attach(|py| {
996                        let v = func.0.call0(py)?.into_bound(py);
997                        let (storage_options, expiry) =
998                            v.extract::<(pyo3::Bound<'_, PyDict>, Option<u64>)>()?;
999
1000                        for (k, v) in storage_options.iter() {
1001                            let k = k.extract::<PyBackedStr>()?;
1002                            let v = v.extract::<String>()?;
1003
1004                            match k.as_ref() {
1005                                "bearer_token" => credentials.bearer = v,
1006                                v => {
1007                                    return pyo3::PyResult::Err(PyValueError::new_err(format!(
1008                                        "unknown configuration key for gcp: {}, \
1009                                    valid configuration keys are: {}",
1010                                        v, "bearer_token",
1011                                    )));
1012                                },
1013                            }
1014                        }
1015
1016                        pyo3::PyResult::Ok(expiry.unwrap_or(u64::MAX))
1017                    })?;
1018
1019                    if credentials.bearer.is_empty() {
1020                        return Err(PolarsError::ComputeError(
1021                            "bearer was empty or not given".into(),
1022                        ));
1023                    }
1024
1025                    PolarsResult::Ok((ObjectStoreCredential::Gcp(Arc::new(credentials)), expiry))
1026                })
1027            }))
1028            .into_gcp_provider()
1029        }
1030
1031        /// # Panics
1032        /// Panics if `self` is not an initialized provider.
1033        fn storage_update_options(&self) -> PolarsResult<Vec<(PlSmallStr, PlSmallStr)>> {
1034            let py_object = self.unwrap_as_provider_ref();
1035
1036            Python::attach(|py| {
1037                py_object
1038                    .getattr(py, "_storage_update_options")
1039                    .map_or(Ok(vec![]), |f| {
1040                        let v = f
1041                            .call0(py)?
1042                            .extract::<pyo3::Bound<'_, PyDict>>(py)
1043                            .map_err(pyo3::PyErr::from)?;
1044
1045                        let mut out = Vec::with_capacity(v.len());
1046
1047                        for dict_item in v.call_method0("items")?.try_iter()? {
1048                            let (key, value) =
1049                                dict_item?.extract::<(PyBackedStr, PyBackedStr)>()?;
1050
1051                            out.push(((&*key).into(), (&*value).into()))
1052                        }
1053
1054                        Ok(out)
1055                    })
1056            })
1057        }
1058    }
1059
1060    // Note: We don't consider `is_builder` for hash/eq - we don't expect the same Arc<PythonObject>
1061    // to be referenced as both true and false from the `is_builder` field.
1062
1063    impl Eq for PythonCredentialProvider {}
1064
1065    impl PartialEq for PythonCredentialProvider {
1066        fn eq(&self, other: &Self) -> bool {
1067            self.func_addr() == other.func_addr()
1068        }
1069    }
1070
1071    impl Hash for PythonCredentialProvider {
1072        fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1073            // # Safety
1074            // * Inner is an `Arc`
1075            // * Visibility is limited to super
1076            // * No code in `mod python_impl` or `super` mutates the Arc inner.
1077            state.write_usize(self.func_addr())
1078        }
1079    }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084    #[cfg(feature = "serde")]
1085    #[allow(clippy::redundant_pattern_matching)]
1086    #[test]
1087    fn test_serde() {
1088        use super::*;
1089
1090        assert!(matches!(
1091            serde_json::to_string(&Some(PlCredentialProvider::from_func(|| {
1092                Box::pin(core::future::ready(PolarsResult::Ok((
1093                    ObjectStoreCredential::None,
1094                    0,
1095                ))))
1096            }))),
1097            Err(_)
1098        ));
1099
1100        assert!(matches!(
1101            serde_json::to_string(&Option::<PlCredentialProvider>::None),
1102            Ok(String { .. })
1103        ));
1104
1105        assert!(matches!(
1106            serde_json::from_str::<Option<PlCredentialProvider>>(
1107                serde_json::to_string(&Option::<PlCredentialProvider>::None)
1108                    .unwrap()
1109                    .as_str()
1110            ),
1111            Ok(None)
1112        ));
1113    }
1114
1115    #[cfg(feature = "serde")]
1116    #[test]
1117    fn test_serde_auto_init() {
1118        use super::*;
1119
1120        let provider = PlCredentialProvider::AutoInit(AutoInitConfig::Aws {
1121            profile_name: Some("p".into()),
1122            region_name: None,
1123            auto_init_unhandled_key: None,
1124            storage_options: [("region".into(), Some("eu-west-1".into()))]
1125                .into_iter()
1126                .collect(),
1127        });
1128
1129        let mut bytes = vec![];
1130        polars_utils::pl_serialize::serialize_dsl(&mut bytes, &provider).unwrap();
1131        let out: PlCredentialProvider =
1132            polars_utils::pl_serialize::deserialize_dsl(&mut bytes.as_slice()).unwrap();
1133        assert_eq!(out, provider);
1134
1135        let out: PlCredentialProvider =
1136            serde_json::from_str(&serde_json::to_string(&provider).unwrap()).unwrap();
1137        assert_eq!(out, provider);
1138
1139        // The format Python passes in.
1140        let PlCredentialProvider::AutoInit(config) = &provider else {
1141            unreachable!()
1142        };
1143        assert_eq!(
1144            serde_json::to_string(config).unwrap(),
1145            r#"{"aws":{"profile_name":"p","region_name":null,"auto_init_unhandled_key":null,"storage_options":{"region":"eu-west-1"}}}"#
1146        );
1147        assert_eq!(
1148            serde_json::from_str::<AutoInitConfig>(r#"{"gcp":{}}"#).unwrap(),
1149            AutoInitConfig::Gcp {}
1150        );
1151    }
1152
1153    #[cfg(feature = "gcp")]
1154    #[test]
1155    fn test_auto_init_gcp_token_is_native() {
1156        use super::*;
1157
1158        let provider =
1159            PlCredentialProvider::AutoInit(AutoInitConfig::GcpToken { token: "A".into() })
1160                .try_into_initialized(false)
1161                .unwrap()
1162                .unwrap();
1163
1164        let PlCredentialProvider::Function(CredentialProviderFunction(f)) = provider else {
1165            panic!()
1166        };
1167        let (credential, expiry) = futures::executor::block_on(f()).unwrap();
1168        assert_eq!(credential.unwrap_gcp().bearer, "A");
1169        assert_eq!(expiry, u64::MAX);
1170    }
1171}