Skip to main content

entelix_cloud/foundry/
credential.rs

1//! Azure credential adapter — wraps an
2//! `azure_core::credentials::TokenCredential` into a
3//! [`crate::refresh::TokenRefresher`].
4//!
5//! Production callers typically pass
6//! `azure_identity::DeveloperToolsCredential::new(None)?` (which
7//! itself returns an `Arc<DeveloperToolsCredential>` and unsizes
8//! into `Arc<dyn TokenCredential>`). Tests inject a deterministic
9//! mock `TokenRefresher` directly without touching this adapter.
10
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14use async_trait::async_trait;
15use azure_core::credentials::TokenCredential;
16use secrecy::SecretString;
17
18use crate::CloudError;
19use crate::refresh::{TokenRefresher, TokenSnapshot};
20
21/// Standard scope for Azure OpenAI / Foundry data plane access.
22pub const FOUNDRY_SCOPE: &str = "https://cognitiveservices.azure.com/.default";
23
24/// Azure-credential-backed refresher. Generic over any
25/// `Arc<dyn TokenCredential>` — the caller picks the chain.
26pub struct FoundryCredentialProvider {
27    inner: Arc<dyn TokenCredential>,
28    scopes: Vec<String>,
29}
30
31impl FoundryCredentialProvider {
32    /// Wrap an externally-built `TokenCredential` (e.g.
33    /// `azure_identity::DeveloperToolsCredential::new(None)?` —
34    /// which already returns an `Arc<dyn TokenCredential>` once
35    /// unsized) plus the OAuth scopes the call needs.
36    pub fn new(cred: Arc<dyn TokenCredential>, scopes: &[&str]) -> Self {
37        Self {
38            inner: cred,
39            scopes: scopes.iter().map(|s| (*s).to_owned()).collect(),
40        }
41    }
42}
43
44#[async_trait]
45impl TokenRefresher<SecretString> for FoundryCredentialProvider {
46    async fn refresh(&self) -> Result<TokenSnapshot<SecretString>, CloudError> {
47        let scopes_ref: Vec<&str> = self.scopes.iter().map(String::as_str).collect();
48        let token =
49            self.inner
50                .get_token(&scopes_ref, None)
51                .await
52                .map_err(|e| CloudError::Credential {
53                    message: format!("get_token: {e}"),
54                    source: Some(Box::new(e)),
55                })?;
56        let value = SecretString::from(token.token.secret().to_owned());
57        let expires_at = offset_to_instant(token.expires_on);
58        Ok(TokenSnapshot { value, expires_at })
59    }
60}
61
62fn offset_to_instant(at: time::OffsetDateTime) -> Instant {
63    let now_t = time::OffsetDateTime::now_utc();
64    let now_inst = Instant::now();
65    let delta_ms = (at - now_t).whole_milliseconds();
66    if delta_ms <= 0 {
67        return now_inst;
68    }
69    now_inst + Duration::from_millis(u64::try_from(delta_ms).unwrap_or(u64::MAX)) // silent-fallback-ok: defense-in-depth, clamp duration overflow before Instant arithmetic
70}