use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::sync::{Arc, OnceLock, Weak};
use async_trait::async_trait;
use futures_util::FutureExt;
use tokio_util::sync::CancellationToken;
use tracing::warn;
pub use crate::rpc::EntraTokenAcquireRequest as EntraTokenProviderArgs;
use crate::rpc::EntraTokenGetTokenRequest;
use crate::{
Client, ClientInner, JsonRpcError, JsonRpcRequest, JsonRpcResponse, Result, error_codes,
};
pub(crate) const ACQUIRE_METHOD: &str = "entraToken.getToken";
#[derive(Clone, serde::Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum EntraTokenProviderResult {
Identity {
#[serde(rename = "accountId")]
account_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
username: Option<String>,
},
Token {
#[serde(rename = "accountId")]
account_id: String,
#[serde(rename = "accessToken")]
access_token: String,
},
Unavailable,
Cancelled,
}
impl std::fmt::Debug for EntraTokenProviderResult {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Identity { .. } => formatter.write_str("Identity { .. }"),
Self::Token { .. } => formatter.write_str("Token { .. }"),
Self::Unavailable => formatter.write_str("Unavailable"),
Self::Cancelled => formatter.write_str("Cancelled"),
}
}
}
#[derive(Clone)]
pub struct EntraTokenProviderContext {
cancellation: CancellationToken,
}
impl EntraTokenProviderContext {
pub fn cancellation(&self) -> CancellationToken {
self.cancellation.child_token()
}
}
#[async_trait]
pub trait EntraTokenProvider: Send + Sync + 'static {
async fn get_token(
&self,
request: EntraTokenProviderArgs,
context: EntraTokenProviderContext,
) -> Result<EntraTokenProviderResult>;
}
#[async_trait]
impl<F, Fut> EntraTokenProvider for F
where
F: Fn(EntraTokenProviderArgs, EntraTokenProviderContext) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<EntraTokenProviderResult>> + Send,
{
async fn get_token(
&self,
request: EntraTokenProviderArgs,
context: EntraTokenProviderContext,
) -> Result<EntraTokenProviderResult> {
(self)(request, context).await
}
}
pub(crate) struct EntraTokenDispatcher {
provider: Option<Arc<dyn EntraTokenProvider>>,
client: OnceLock<Weak<ClientInner>>,
}
impl EntraTokenDispatcher {
pub(crate) fn new(provider: Option<Arc<dyn EntraTokenProvider>>) -> Self {
Self {
provider,
client: OnceLock::new(),
}
}
pub(crate) fn set_client(&self, client: Weak<ClientInner>) {
let _ = self.client.set(client);
}
pub(crate) fn dispatch(self: &Arc<Self>, request: JsonRpcRequest) {
let Some(client) = self.client.get().and_then(Weak::upgrade) else {
return;
};
let Some(pending) = client.rpc.cancellable_requests.claim(request.id) else {
return;
};
let request_cancelled = pending.cancellation().clone();
let connection_closed = client.rpc.connection_closed_token();
let context = EntraTokenProviderContext {
cancellation: connection_closed.child_token(),
};
drop(client);
let dispatcher = self.clone();
tokio::spawn(async move {
let outcome = tokio::select! {
biased;
_ = connection_closed.cancelled() => return,
_ = request_cancelled.cancelled() => {
context.cancellation.cancel();
Err((error_codes::REQUEST_CANCELLED, "Entra acquisition cancelled"))
}
result = dispatcher.acquire(request.params, context.clone()) => result,
};
if connection_closed.is_cancelled() {
return;
}
let outcome = if request_cancelled.is_cancelled() {
context.cancellation.cancel();
Err((
error_codes::REQUEST_CANCELLED,
"Entra acquisition cancelled",
))
} else {
outcome
};
let (result, error) = match outcome {
Ok(result) => (Some(result), None),
Err((code, message)) => (
None,
Some(JsonRpcError {
code: code.into(),
message: message.to_owned(),
data: None,
}),
),
};
let Some(client) = dispatcher.client.get().and_then(Weak::upgrade) else {
return;
};
if Client::from_inner(client)
.send_response(&JsonRpcResponse {
jsonrpc: "2.0".to_owned(),
id: request.id,
result,
error,
})
.await
.is_err()
{
warn!("failed to send Entra acquisition response");
}
drop(pending);
});
}
async fn acquire(
&self,
params: Option<serde_json::Value>,
context: EntraTokenProviderContext,
) -> std::result::Result<serde_json::Value, (i32, &'static str)> {
let provider = self.provider.as_ref().ok_or((
error_codes::METHOD_NOT_FOUND,
"No host-owned Entra token provider configured",
))?;
let request: EntraTokenGetTokenRequest =
serde_json::from_value(params.unwrap_or(serde_json::Value::Null)).map_err(|_| {
(
error_codes::INVALID_PARAMS,
"Invalid Entra acquisition request",
)
})?;
let result = match AssertUnwindSafe(provider.get_token(request.request, context))
.catch_unwind()
.await
{
Ok(Ok(result)) => result,
Ok(Err(_)) => return Err((error_codes::INTERNAL_ERROR, "Entra token provider failed")),
Err(_) => return Err((error_codes::INTERNAL_ERROR, "Entra token provider panicked")),
};
serde_json::to_value(result).map_err(|_| {
(
error_codes::INTERNAL_ERROR,
"Invalid Entra token provider result",
)
})
}
}
#[cfg(test)]
mod tests;