use std::fmt;
use std::future::Future;
use std::pin::Pin;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Credential {
ApiKey {
key: String,
},
Bearer {
token: String,
#[serde(default)]
expires_at: Option<chrono::DateTime<chrono::Utc>>,
},
OAuth2 {
access_token: String,
refresh_token: Option<String>,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
token_url: String,
client_id: String,
client_secret: Option<String>,
#[serde(default)]
scopes: Vec<String>,
},
}
impl std::fmt::Debug for Credential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ApiKey { .. } => f
.debug_struct("Credential::ApiKey")
.field("key", &"[REDACTED]")
.finish(),
Self::Bearer { expires_at, .. } => f
.debug_struct("Credential::Bearer")
.field("token", &"[REDACTED]")
.field("expires_at", expires_at)
.finish(),
Self::OAuth2 {
expires_at,
client_id,
scopes,
..
} => f
.debug_struct("Credential::OAuth2")
.field("access_token", &"[REDACTED]")
.field("refresh_token", &"[REDACTED]")
.field("expires_at", expires_at)
.field("token_url", &"[REDACTED]")
.field("client_id", client_id)
.field("client_secret", &"[REDACTED]")
.field("scopes", scopes)
.finish(),
}
}
}
impl Credential {
#[must_use]
pub const fn credential_type(&self) -> CredentialType {
match self {
Self::ApiKey { .. } => CredentialType::ApiKey,
Self::Bearer { .. } => CredentialType::Bearer,
Self::OAuth2 { .. } => CredentialType::OAuth2,
}
}
}
#[non_exhaustive]
#[derive(Clone)]
pub enum ResolvedCredential {
ApiKey(String),
Bearer(String),
OAuth2AccessToken(String),
}
impl std::fmt::Debug for ResolvedCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ApiKey(_) => f
.debug_tuple("ResolvedCredential::ApiKey")
.field(&"[REDACTED]")
.finish(),
Self::Bearer(_) => f
.debug_tuple("ResolvedCredential::Bearer")
.field(&"[REDACTED]")
.finish(),
Self::OAuth2AccessToken(_) => f
.debug_tuple("ResolvedCredential::OAuth2AccessToken")
.field(&"[REDACTED]")
.finish(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub credential_key: String,
pub auth_scheme: AuthScheme,
pub credential_type: CredentialType,
}
impl AuthConfig {
#[must_use]
pub fn new(
credential_key: impl Into<String>,
auth_scheme: AuthScheme,
credential_type: CredentialType,
) -> Self {
Self {
credential_key: credential_key.into(),
auth_scheme,
credential_type,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum AuthScheme {
BearerHeader,
ApiKeyHeader(String),
ApiKeyQuery(String),
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CredentialType {
ApiKey,
Bearer,
OAuth2,
}
#[non_exhaustive]
pub enum CredentialError {
NotFound {
key: String,
},
Expired {
key: String,
},
RefreshFailed {
key: String,
reason: String,
},
TypeMismatch {
key: String,
expected: CredentialType,
actual: CredentialType,
},
StoreError(Box<dyn std::error::Error + Send + Sync>),
Timeout {
key: String,
},
AuthorizationFailed {
key: String,
reason: String,
},
AuthorizationTimeout {
key: String,
},
}
impl fmt::Debug for CredentialError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound { key } => f
.debug_struct("CredentialError::NotFound")
.field("key", key)
.finish(),
Self::Expired { key } => f
.debug_struct("CredentialError::Expired")
.field("key", key)
.finish(),
Self::RefreshFailed { key, reason } => f
.debug_struct("CredentialError::RefreshFailed")
.field("key", key)
.field("reason", reason)
.finish(),
Self::TypeMismatch {
key,
expected,
actual,
} => f
.debug_struct("CredentialError::TypeMismatch")
.field("key", key)
.field("expected", expected)
.field("actual", actual)
.finish(),
Self::StoreError(_) => f
.debug_tuple("CredentialError::StoreError")
.field(&"[REDACTED]")
.finish(),
Self::Timeout { key } => f
.debug_struct("CredentialError::Timeout")
.field("key", key)
.finish(),
Self::AuthorizationFailed { key, reason } => f
.debug_struct("CredentialError::AuthorizationFailed")
.field("key", key)
.field("reason", reason)
.finish(),
Self::AuthorizationTimeout { key } => f
.debug_struct("CredentialError::AuthorizationTimeout")
.field("key", key)
.finish(),
}
}
}
impl std::fmt::Display for CredentialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound { key } => write!(f, "credential not found: {key}"),
Self::Expired { key } => write!(f, "credential expired: {key}"),
Self::RefreshFailed { key, reason } => {
write!(f, "credential refresh failed for {key}: {reason}")
}
Self::TypeMismatch {
key,
expected,
actual,
} => write!(
f,
"credential type mismatch for {key}: expected {expected:?}, got {actual:?}"
),
Self::StoreError(_) => f.write_str("credential store error"),
Self::Timeout { key } => write!(f, "credential resolution timed out for {key}"),
Self::AuthorizationFailed { key, reason } => {
write!(f, "authorization failed for {key}: {reason}")
}
Self::AuthorizationTimeout { key } => {
write!(f, "authorization timed out for {key}")
}
}
}
}
impl std::error::Error for CredentialError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::StoreError(error) => Some(&**error),
_ => None,
}
}
}
impl Clone for CredentialError {
fn clone(&self) -> Self {
match self {
Self::NotFound { key } => Self::NotFound { key: key.clone() },
Self::Expired { key } => Self::Expired { key: key.clone() },
Self::RefreshFailed { key, reason } => Self::RefreshFailed {
key: key.clone(),
reason: reason.clone(),
},
Self::TypeMismatch {
key,
expected,
actual,
} => Self::TypeMismatch {
key: key.clone(),
expected: *expected,
actual: *actual,
},
Self::StoreError(error) => {
Self::StoreError(Box::new(std::io::Error::other(error.to_string())))
}
Self::Timeout { key } => Self::Timeout { key: key.clone() },
Self::AuthorizationFailed { key, reason } => Self::AuthorizationFailed {
key: key.clone(),
reason: reason.clone(),
},
Self::AuthorizationTimeout { key } => Self::AuthorizationTimeout { key: key.clone() },
}
}
}
pub type CredentialFuture<'a, T> =
Pin<Box<dyn Future<Output = Result<T, CredentialError>> + Send + 'a>>;
pub trait CredentialStore: Send + Sync {
fn get(&self, key: &str) -> CredentialFuture<'_, Option<Credential>>;
fn set(&self, key: &str, credential: Credential) -> CredentialFuture<'_, ()>;
fn delete(&self, key: &str) -> CredentialFuture<'_, ()>;
}
pub trait CredentialResolver: Send + Sync {
fn resolve(&self, key: &str) -> CredentialFuture<'_, ResolvedCredential>;
}
pub trait AuthorizationHandler: Send + Sync {
fn authorize(&self, auth_url: &str, state: &str) -> CredentialFuture<'_, String>;
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct DeviceCodePrompt {
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: Option<String>,
pub expires_in: Option<i64>,
}
impl DeviceCodePrompt {
#[must_use]
pub fn new(user_code: impl Into<String>, verification_uri: impl Into<String>) -> Self {
Self {
user_code: user_code.into(),
verification_uri: verification_uri.into(),
verification_uri_complete: None,
expires_in: None,
}
}
#[must_use]
pub fn with_verification_uri_complete(
mut self,
verification_uri_complete: impl Into<String>,
) -> Self {
self.verification_uri_complete = Some(verification_uri_complete.into());
self
}
#[must_use]
pub const fn with_expires_in(mut self, expires_in: i64) -> Self {
self.expires_in = Some(expires_in);
self
}
}
pub trait DeviceCodeHandler: Send + Sync {
fn present(&self, prompt: &DeviceCodePrompt) -> CredentialFuture<'_, ()>;
}
#[cfg(test)]
#[path = "credential_tests.rs"]
mod tests;