mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
688 lines
23 KiB
Rust
688 lines
23 KiB
Rust
use std::sync::Arc;
|
|
use std::time::{Duration, UNIX_EPOCH};
|
|
|
|
use azure_core::cloud::{CloudConfiguration, CustomConfiguration};
|
|
use azure_core::credentials::{Secret, TokenCredential};
|
|
use azure_core::http::ClientOptions;
|
|
use azure_identity::{
|
|
ClientAssertion, ClientAssertionCredential, ClientAssertionCredentialOptions,
|
|
ClientSecretCredential, ClientSecretCredentialOptions, DeveloperToolsCredential,
|
|
ManagedIdentityCredential, ManagedIdentityCredentialOptions, UserAssignedId,
|
|
WorkloadIdentityCredential, WorkloadIdentityCredentialOptions,
|
|
};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
use litellm_auth_types::Error;
|
|
use litellm_auth_types::{InputSource, ResolvedCredential, SecretValue, Sourced};
|
|
|
|
use super::credential_provider_cache::{
|
|
AzureCredentialProviderCache, AzureCredentialProviderCacheKey,
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) enum NativeAzureRequest {
|
|
ClientSecret {
|
|
tenant_id: Sourced<String>,
|
|
client_id: Sourced<String>,
|
|
client_secret: Sourced<SecretValue>,
|
|
scope: Sourced<String>,
|
|
authority: Option<Sourced<String>>,
|
|
},
|
|
ClientAssertion {
|
|
tenant_id: Sourced<String>,
|
|
client_id: Sourced<String>,
|
|
assertion: Sourced<SecretValue>,
|
|
assertion_identity: String,
|
|
scope: Sourced<String>,
|
|
authority: Option<Sourced<String>>,
|
|
},
|
|
WorkloadIdentity {
|
|
tenant_id: Sourced<String>,
|
|
client_id: Sourced<String>,
|
|
token_file_path: Sourced<String>,
|
|
scope: Sourced<String>,
|
|
authority: Option<Sourced<String>>,
|
|
},
|
|
ManagedIdentity {
|
|
client_id: Option<Sourced<String>>,
|
|
scope: Sourced<String>,
|
|
selection_source: InputSource,
|
|
},
|
|
DeveloperTools {
|
|
scope: Sourced<String>,
|
|
selection_source: InputSource,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct ValidatedAzureRequest {
|
|
request: NativeAzureRequest,
|
|
credential_source: InputSource,
|
|
}
|
|
|
|
impl ValidatedAzureRequest {
|
|
pub(crate) fn new(request: NativeAzureRequest) -> Result<Self, Error> {
|
|
validate_authority(&request)?;
|
|
let credential_source = validate_sources(&request)?;
|
|
Ok(Self {
|
|
request,
|
|
credential_source,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn credential_source(&self) -> InputSource {
|
|
self.credential_source
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn kind(&self) -> &'static str {
|
|
match self.request {
|
|
NativeAzureRequest::ClientSecret { .. } => "client-secret",
|
|
NativeAzureRequest::ClientAssertion { .. } => "client-assertion",
|
|
NativeAzureRequest::WorkloadIdentity { .. } => "workload-identity",
|
|
NativeAzureRequest::ManagedIdentity { .. } => "managed-identity",
|
|
NativeAzureRequest::DeveloperTools { .. } => "developer-tools",
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct NativeAzureTokenAcquirer {
|
|
cache: AzureCredentialProviderCache,
|
|
transport: Option<azure_core::http::Transport>,
|
|
}
|
|
|
|
impl Default for NativeAzureTokenAcquirer {
|
|
fn default() -> Self {
|
|
Self::new(64)
|
|
}
|
|
}
|
|
|
|
impl NativeAzureTokenAcquirer {
|
|
pub(crate) fn new(cache_capacity: u64) -> Self {
|
|
Self {
|
|
cache: AzureCredentialProviderCache::new(cache_capacity),
|
|
transport: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn with_transport(
|
|
cache_capacity: u64,
|
|
transport: azure_core::http::Transport,
|
|
) -> Self {
|
|
Self {
|
|
cache: AzureCredentialProviderCache::new(cache_capacity),
|
|
transport: Some(transport),
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn acquire(
|
|
&self,
|
|
request: ValidatedAzureRequest,
|
|
) -> Result<ResolvedCredential, Error> {
|
|
let scope = request.request.scope().to_string();
|
|
let key = request.request.cache_key();
|
|
let transport = self.transport.clone();
|
|
let credential = self
|
|
.cache
|
|
.get_or_create(
|
|
key,
|
|
async move { build_credential(request.request, transport) },
|
|
)
|
|
.await?;
|
|
let token = credential
|
|
.get_token(&[scope.as_str()], None)
|
|
.await
|
|
.map_err(|error| Error::AzureTokenAcquisition(error.to_string()))?;
|
|
let expires_on = u64::try_from(token.expires_on.unix_timestamp())
|
|
.ok()
|
|
.map(|seconds| UNIX_EPOCH + Duration::from_secs(seconds));
|
|
|
|
Ok(ResolvedCredential::AccessToken {
|
|
token: SecretValue::new(token.token.secret()),
|
|
expires_on,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl NativeAzureRequest {
|
|
fn scope(&self) -> &str {
|
|
match self {
|
|
Self::ClientSecret { scope, .. }
|
|
| Self::ClientAssertion { scope, .. }
|
|
| Self::WorkloadIdentity { scope, .. }
|
|
| Self::ManagedIdentity { scope, .. }
|
|
| Self::DeveloperTools { scope, .. } => scope.value(),
|
|
}
|
|
}
|
|
|
|
fn cache_key(&self) -> AzureCredentialProviderCacheKey {
|
|
match self {
|
|
Self::ClientSecret {
|
|
tenant_id,
|
|
client_id,
|
|
client_secret,
|
|
scope,
|
|
authority,
|
|
} => AzureCredentialProviderCacheKey {
|
|
mechanism: "client-secret",
|
|
authority: authority
|
|
.as_ref()
|
|
.map(|value| value.value().clone())
|
|
.unwrap_or_default(),
|
|
tenant_id: tenant_id.value().clone(),
|
|
client_id: client_id.value().clone(),
|
|
scope: scope.value().clone(),
|
|
secret_identity: secret_digest(client_secret.value().expose()),
|
|
},
|
|
Self::ClientAssertion {
|
|
tenant_id,
|
|
client_id,
|
|
assertion,
|
|
assertion_identity,
|
|
scope,
|
|
authority,
|
|
} => AzureCredentialProviderCacheKey {
|
|
mechanism: "client-assertion",
|
|
authority: authority
|
|
.as_ref()
|
|
.map(|value| value.value().clone())
|
|
.unwrap_or_default(),
|
|
tenant_id: tenant_id.value().clone(),
|
|
client_id: client_id.value().clone(),
|
|
scope: scope.value().clone(),
|
|
secret_identity: format!(
|
|
"{assertion_identity}:{}",
|
|
secret_digest(assertion.value().expose())
|
|
),
|
|
},
|
|
Self::WorkloadIdentity {
|
|
tenant_id,
|
|
client_id,
|
|
token_file_path,
|
|
scope,
|
|
authority,
|
|
} => AzureCredentialProviderCacheKey {
|
|
mechanism: "workload-identity",
|
|
authority: authority
|
|
.as_ref()
|
|
.map(|value| value.value().clone())
|
|
.unwrap_or_default(),
|
|
tenant_id: tenant_id.value().clone(),
|
|
client_id: client_id.value().clone(),
|
|
scope: scope.value().clone(),
|
|
secret_identity: token_file_path.value().clone(),
|
|
},
|
|
Self::ManagedIdentity {
|
|
client_id, scope, ..
|
|
} => AzureCredentialProviderCacheKey {
|
|
mechanism: "managed-identity",
|
|
authority: String::new(),
|
|
tenant_id: String::new(),
|
|
client_id: client_id
|
|
.as_ref()
|
|
.map(|value| value.value().clone())
|
|
.unwrap_or_default(),
|
|
scope: scope.value().clone(),
|
|
secret_identity: String::new(),
|
|
},
|
|
Self::DeveloperTools { scope, .. } => AzureCredentialProviderCacheKey {
|
|
mechanism: "developer-tools",
|
|
authority: String::new(),
|
|
tenant_id: String::new(),
|
|
client_id: String::new(),
|
|
scope: scope.value().clone(),
|
|
secret_identity: String::new(),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_authority(request: &NativeAzureRequest) -> Result<(), Error> {
|
|
let authority = match request {
|
|
NativeAzureRequest::ClientSecret { authority, .. }
|
|
| NativeAzureRequest::ClientAssertion { authority, .. }
|
|
| NativeAzureRequest::WorkloadIdentity { authority, .. } => authority.as_ref(),
|
|
NativeAzureRequest::ManagedIdentity { .. } | NativeAzureRequest::DeveloperTools { .. } => {
|
|
None
|
|
}
|
|
};
|
|
let Some(authority) = authority else {
|
|
return Ok(());
|
|
};
|
|
let url = url::Url::parse(authority.value()).map_err(|_| Error::InvalidAzureAuthority)?;
|
|
if url.scheme() != "https"
|
|
|| url.host_str().is_none()
|
|
|| !url.username().is_empty()
|
|
|| url.password().is_some()
|
|
|| url.query().is_some()
|
|
|| url.fragment().is_some()
|
|
|| !matches!(url.path(), "" | "/")
|
|
{
|
|
return Err(Error::InvalidAzureAuthority);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_sources(request: &NativeAzureRequest) -> Result<InputSource, Error> {
|
|
match request {
|
|
NativeAzureRequest::ClientSecret {
|
|
tenant_id,
|
|
client_id,
|
|
client_secret,
|
|
scope,
|
|
authority,
|
|
} => {
|
|
let identity_sources = [
|
|
tenant_id.source(),
|
|
client_id.source(),
|
|
client_secret.source(),
|
|
];
|
|
let request_identity = identity_sources.contains(&InputSource::Request);
|
|
if request_identity
|
|
&& !identity_sources
|
|
.iter()
|
|
.all(|source| *source == InputSource::Request)
|
|
{
|
|
return mixed_sources();
|
|
}
|
|
if !request_identity && is_request_controlled(scope, authority.as_ref()) {
|
|
return mixed_sources();
|
|
}
|
|
Ok(if request_identity {
|
|
InputSource::Request
|
|
} else {
|
|
trusted_source(&identity_sources)
|
|
})
|
|
}
|
|
NativeAzureRequest::ClientAssertion {
|
|
tenant_id,
|
|
client_id,
|
|
assertion,
|
|
scope,
|
|
authority,
|
|
..
|
|
} => trusted_only(&[
|
|
tenant_id.source(),
|
|
client_id.source(),
|
|
assertion.source(),
|
|
scope.source(),
|
|
authority
|
|
.as_ref()
|
|
.map(Sourced::source)
|
|
.unwrap_or(InputSource::Environment),
|
|
]),
|
|
NativeAzureRequest::WorkloadIdentity {
|
|
tenant_id,
|
|
client_id,
|
|
token_file_path,
|
|
scope,
|
|
authority,
|
|
} => trusted_only(&[
|
|
tenant_id.source(),
|
|
client_id.source(),
|
|
token_file_path.source(),
|
|
scope.source(),
|
|
authority
|
|
.as_ref()
|
|
.map(Sourced::source)
|
|
.unwrap_or(InputSource::Environment),
|
|
]),
|
|
NativeAzureRequest::ManagedIdentity {
|
|
client_id,
|
|
scope,
|
|
selection_source,
|
|
} => trusted_only(&[
|
|
client_id
|
|
.as_ref()
|
|
.map(Sourced::source)
|
|
.unwrap_or(InputSource::Environment),
|
|
scope.source(),
|
|
*selection_source,
|
|
]),
|
|
NativeAzureRequest::DeveloperTools {
|
|
scope,
|
|
selection_source,
|
|
} => trusted_only(&[scope.source(), *selection_source]),
|
|
}
|
|
}
|
|
|
|
fn is_request_controlled<T>(value: &Sourced<T>, optional: Option<&Sourced<String>>) -> bool {
|
|
value.source() == InputSource::Request
|
|
|| optional.is_some_and(|value| value.source() == InputSource::Request)
|
|
}
|
|
|
|
fn trusted_only(sources: &[InputSource]) -> Result<InputSource, Error> {
|
|
if sources.contains(&InputSource::Request) {
|
|
return mixed_sources();
|
|
}
|
|
Ok(trusted_source(sources))
|
|
}
|
|
|
|
fn trusted_source(sources: &[InputSource]) -> InputSource {
|
|
if sources.contains(&InputSource::Deployment) {
|
|
InputSource::Deployment
|
|
} else {
|
|
InputSource::Environment
|
|
}
|
|
}
|
|
|
|
fn mixed_sources<T>() -> Result<T, Error> {
|
|
Err(Error::MixedAzureCredentialSources)
|
|
}
|
|
|
|
fn build_credential(
|
|
request: NativeAzureRequest,
|
|
transport: Option<azure_core::http::Transport>,
|
|
) -> Result<Arc<dyn TokenCredential>, Error> {
|
|
match request {
|
|
NativeAzureRequest::ClientSecret {
|
|
tenant_id,
|
|
client_id,
|
|
client_secret,
|
|
authority,
|
|
..
|
|
} => ClientSecretCredential::new(
|
|
tenant_id.value(),
|
|
client_id.into_value(),
|
|
Secret::new(client_secret.value().expose().to_string()),
|
|
Some(ClientSecretCredentialOptions {
|
|
client_options: client_options(authority.map(Sourced::into_value), transport),
|
|
}),
|
|
)
|
|
.map(|credential| credential as Arc<dyn TokenCredential>),
|
|
NativeAzureRequest::ClientAssertion {
|
|
tenant_id,
|
|
client_id,
|
|
assertion,
|
|
authority,
|
|
..
|
|
} => ClientAssertionCredential::new(
|
|
tenant_id.into_value(),
|
|
client_id.into_value(),
|
|
StaticAssertion(assertion.into_value()),
|
|
Some(ClientAssertionCredentialOptions {
|
|
client_options: client_options(authority.map(Sourced::into_value), transport),
|
|
}),
|
|
)
|
|
.map(|credential| credential as Arc<dyn TokenCredential>),
|
|
NativeAzureRequest::WorkloadIdentity {
|
|
tenant_id,
|
|
client_id,
|
|
token_file_path,
|
|
authority,
|
|
..
|
|
} => WorkloadIdentityCredential::new(Some(WorkloadIdentityCredentialOptions {
|
|
credential_options: azure_identity::ClientAssertionCredentialOptions {
|
|
client_options: client_options(authority.map(Sourced::into_value), transport),
|
|
},
|
|
client_id: Some(client_id.into_value()),
|
|
tenant_id: Some(tenant_id.into_value()),
|
|
token_file_path: Some(token_file_path.into_value().into()),
|
|
}))
|
|
.map(|credential| credential as Arc<dyn TokenCredential>),
|
|
NativeAzureRequest::ManagedIdentity { client_id, .. } => {
|
|
ManagedIdentityCredential::new(Some(ManagedIdentityCredentialOptions {
|
|
user_assigned_id: client_id
|
|
.map(Sourced::into_value)
|
|
.map(UserAssignedId::ClientId),
|
|
client_options: client_options(None, transport),
|
|
}))
|
|
.map(|credential| credential as Arc<dyn TokenCredential>)
|
|
}
|
|
NativeAzureRequest::DeveloperTools { .. } => DeveloperToolsCredential::new(None)
|
|
.map(|credential| credential as Arc<dyn TokenCredential>),
|
|
}
|
|
.map_err(|error| Error::AzureCredentialInitialization(error.to_string()))
|
|
}
|
|
|
|
fn client_options(
|
|
authority: Option<String>,
|
|
transport: Option<azure_core::http::Transport>,
|
|
) -> ClientOptions {
|
|
let cloud = authority.map(|authority_host| {
|
|
let mut custom = CustomConfiguration::default();
|
|
custom.authority_host = authority_host;
|
|
Arc::new(CloudConfiguration::from(custom))
|
|
});
|
|
ClientOptions {
|
|
cloud,
|
|
transport,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn secret_digest(secret: &str) -> String {
|
|
format!("{:x}", Sha256::digest(secret.as_bytes()))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct StaticAssertion(SecretValue);
|
|
|
|
impl ClientAssertion for StaticAssertion {
|
|
fn secret<'life0, 'life1, 'async_trait>(
|
|
&'life0 self,
|
|
_options: Option<azure_core::http::ClientMethodOptions<'life1>>,
|
|
) -> std::pin::Pin<
|
|
Box<dyn std::future::Future<Output = azure_core::Result<String>> + Send + 'async_trait>,
|
|
>
|
|
where
|
|
'life0: 'async_trait,
|
|
'life1: 'async_trait,
|
|
Self: 'async_trait,
|
|
{
|
|
Box::pin(async move { Ok(self.0.expose().to_string()) })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use azure_core::http::headers::Headers;
|
|
use azure_core::http::{AsyncRawResponse, HttpClient, Request, StatusCode, Transport};
|
|
use azure_core::{Bytes, Result};
|
|
|
|
use super::{NativeAzureRequest, NativeAzureTokenAcquirer, ValidatedAzureRequest};
|
|
use litellm_auth_types::{InputSource, SecretValue, Sourced};
|
|
|
|
fn deployment<T>(value: T) -> Sourced<T> {
|
|
Sourced::new(value, InputSource::Deployment)
|
|
}
|
|
|
|
fn sourced_client_secret(
|
|
credential_source: InputSource,
|
|
authority_source: InputSource,
|
|
authority: &str,
|
|
) -> NativeAzureRequest {
|
|
NativeAzureRequest::ClientSecret {
|
|
tenant_id: Sourced::new("tenant".to_string(), credential_source),
|
|
client_id: Sourced::new("client".to_string(), credential_source),
|
|
client_secret: Sourced::new(SecretValue::new("secret"), credential_source),
|
|
scope: Sourced::new("scope".to_string(), InputSource::Environment),
|
|
authority: Some(Sourced::new(authority.to_string(), authority_source)),
|
|
}
|
|
}
|
|
|
|
fn client_secret_request(
|
|
tenant: &str,
|
|
client: &str,
|
|
secret: &str,
|
|
scope: &str,
|
|
authority: &str,
|
|
) -> ValidatedAzureRequest {
|
|
ValidatedAzureRequest::new(NativeAzureRequest::ClientSecret {
|
|
tenant_id: deployment(tenant.to_string()),
|
|
client_id: deployment(client.to_string()),
|
|
client_secret: deployment(SecretValue::new(secret)),
|
|
scope: deployment(scope.to_string()),
|
|
authority: Some(deployment(authority.to_string())),
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct RecordingTokenClient {
|
|
requests: Mutex<Vec<(String, String)>>,
|
|
}
|
|
|
|
impl HttpClient for RecordingTokenClient {
|
|
fn execute_request<'life0, 'life1, 'async_trait>(
|
|
&'life0 self,
|
|
request: &'life1 Request,
|
|
) -> std::pin::Pin<
|
|
Box<dyn std::future::Future<Output = Result<AsyncRawResponse>> + Send + 'async_trait>,
|
|
>
|
|
where
|
|
'life0: 'async_trait,
|
|
'life1: 'async_trait,
|
|
Self: 'async_trait,
|
|
{
|
|
Box::pin(async move {
|
|
let body = Bytes::from(request.body());
|
|
self.requests.lock().unwrap().push((
|
|
request.url().to_string(),
|
|
String::from_utf8(body.to_vec()).unwrap(),
|
|
));
|
|
Ok(AsyncRawResponse::from_bytes(
|
|
StatusCode::Ok,
|
|
Headers::new(),
|
|
r#"{"token_type":"Bearer","expires_in":3600,"ext_expires_in":3600,"access_token":"native-token"}"#,
|
|
))
|
|
})
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn client_secret_uses_sdk_protocol_and_reuses_cached_credential() {
|
|
let transport = Arc::new(RecordingTokenClient::default());
|
|
let acquirer =
|
|
NativeAzureTokenAcquirer::with_transport(4, Transport::new(transport.clone()));
|
|
let request = client_secret_request(
|
|
"tenant",
|
|
"client",
|
|
"secret",
|
|
"https://service.test/.default",
|
|
"https://login.test",
|
|
);
|
|
|
|
let first = acquirer.acquire(request.clone()).await.unwrap();
|
|
let second = acquirer.acquire(request).await.unwrap();
|
|
|
|
assert_eq!(first.secret().expose(), "native-token");
|
|
assert_eq!(second.secret().expose(), "native-token");
|
|
let requests = transport.requests.lock().unwrap();
|
|
assert_eq!(requests.len(), 1);
|
|
assert_eq!(requests[0].0, "https://login.test/tenant/oauth2/v2.0/token");
|
|
assert!(requests[0].1.contains("client_id=client"));
|
|
assert!(requests[0].1.contains("client_secret=secret"));
|
|
assert!(
|
|
requests[0]
|
|
.1
|
|
.contains("scope=https%3A%2F%2Fservice.test%2F.default")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn credential_provider_cache_isolates_every_client_secret_identity_field() {
|
|
let transport = Arc::new(RecordingTokenClient::default());
|
|
let acquirer =
|
|
NativeAzureTokenAcquirer::with_transport(16, Transport::new(transport.clone()));
|
|
let request = client_secret_request;
|
|
let base = request("tenant", "client", "secret", "scope", "https://login.test");
|
|
let variants = [
|
|
base.clone(),
|
|
request(
|
|
"other-tenant",
|
|
"client",
|
|
"secret",
|
|
"scope",
|
|
"https://login.test",
|
|
),
|
|
request(
|
|
"tenant",
|
|
"other-client",
|
|
"secret",
|
|
"scope",
|
|
"https://login.test",
|
|
),
|
|
request(
|
|
"tenant",
|
|
"client",
|
|
"other-secret",
|
|
"scope",
|
|
"https://login.test",
|
|
),
|
|
request(
|
|
"tenant",
|
|
"client",
|
|
"secret",
|
|
"other-scope",
|
|
"https://login.test",
|
|
),
|
|
request(
|
|
"tenant",
|
|
"client",
|
|
"secret",
|
|
"scope",
|
|
"https://other-login.test",
|
|
),
|
|
];
|
|
|
|
acquirer.acquire(base.clone()).await.unwrap();
|
|
acquirer.acquire(base).await.unwrap();
|
|
for request in variants.into_iter().skip(1) {
|
|
acquirer.acquire(request).await.unwrap();
|
|
}
|
|
|
|
assert_eq!(transport.requests.lock().unwrap().len(), 6);
|
|
}
|
|
|
|
#[test]
|
|
fn request_authority_requires_request_owned_client_secret_identity() {
|
|
let error = ValidatedAzureRequest::new(sourced_client_secret(
|
|
InputSource::Deployment,
|
|
InputSource::Request,
|
|
"https://login.example",
|
|
))
|
|
.unwrap_err();
|
|
|
|
assert!(matches!(
|
|
error,
|
|
litellm_auth_types::Error::MixedAzureCredentialSources
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn request_owned_client_secret_identity_can_select_custom_authority() {
|
|
let request = ValidatedAzureRequest::new(sourced_client_secret(
|
|
InputSource::Request,
|
|
InputSource::Request,
|
|
"https://login.example",
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(request.credential_source(), InputSource::Request);
|
|
}
|
|
|
|
#[test]
|
|
fn authority_is_restricted_to_an_https_origin() {
|
|
for authority in [
|
|
"http://login.example",
|
|
"https://user@login.example",
|
|
"https://login.example/tenant",
|
|
"https://login.example?target=other",
|
|
] {
|
|
let error = ValidatedAzureRequest::new(sourced_client_secret(
|
|
InputSource::Deployment,
|
|
InputSource::Deployment,
|
|
authority,
|
|
))
|
|
.unwrap_err();
|
|
assert!(matches!(
|
|
error,
|
|
litellm_auth_types::Error::InvalidAzureAuthority
|
|
));
|
|
}
|
|
}
|
|
}
|