use std::sync::Arc;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::error::AuthError;
use super::metadata::AuthMetadata;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum AuthRefreshReason {
StartupValidation,
Preflight,
Unauthorized,
ExpiringSoon,
Manual,
ConnectionChanged,
}
#[derive(Clone)]
pub enum ResolvedAuthKind {
InlineSecret(Arc<String>),
StaticHeaders(Vec<(String, String)>),
DynamicAuthorizer(Arc<dyn HttpAuthorizer>),
None,
}
impl std::fmt::Debug for ResolvedAuthKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InlineSecret(_) => f.debug_tuple("InlineSecret").field(&"<redacted>").finish(),
Self::StaticHeaders(headers) => f
.debug_tuple("StaticHeaders")
.field(&headers.len())
.finish(),
Self::DynamicAuthorizer(auth) => f
.debug_tuple("DynamicAuthorizer")
.field(&auth.label())
.finish(),
Self::None => f.debug_struct("None").finish(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResolvedAuthEnvelope {
InlineSecret {
secret: String,
metadata: AuthMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
expires_at: Option<DateTime<Utc>>,
},
StaticHeaders {
headers: Vec<(String, String)>,
metadata: AuthMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
expires_at: Option<DateTime<Utc>>,
},
DynamicAuthorizer {
metadata: AuthMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
expires_at: Option<DateTime<Utc>>,
},
None {
metadata: AuthMetadata,
},
}
pub struct HttpAuthorizationRequest<'a> {
pub method: &'a str,
pub url: &'a str,
pub headers: &'a mut Vec<(String, String)>,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HttpAuthorizer: Send + Sync {
async fn authorize(&self, req: &mut HttpAuthorizationRequest<'_>) -> Result<(), AuthError>;
fn label(&self) -> &str;
fn expires_at(&self) -> Option<DateTime<Utc>> {
None
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait AuthLease: Send + Sync {
fn kind(&self) -> &ResolvedAuthKind;
fn metadata(&self) -> &AuthMetadata;
fn expires_at(&self) -> Option<DateTime<Utc>>;
fn source_label(&self) -> &str;
async fn refresh(&self, reason: AuthRefreshReason) -> Result<(), AuthError>;
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct AuthConstraints {
#[serde(default)]
pub require_workspace_id: bool,
#[serde(default)]
pub require_account_id: bool,
#[serde(default)]
pub allow_interactive_login: bool,
#[serde(default = "default_true")]
pub allow_refresh: bool,
}
impl Default for AuthConstraints {
fn default() -> Self {
Self {
require_workspace_id: false,
require_account_id: false,
allow_interactive_login: false,
allow_refresh: true,
}
}
}
fn default_true() -> bool {
true
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn refresh_reason_roundtrip() {
let r = AuthRefreshReason::ExpiringSoon;
let s = serde_json::to_string(&r).unwrap();
assert_eq!(s, "\"expiring_soon\"");
}
#[test]
fn resolved_auth_kind_debug_smoke() {
let k = ResolvedAuthKind::StaticHeaders(vec![("k".into(), "v".into())]);
assert!(format!("{k:?}").contains("StaticHeaders"));
let n = ResolvedAuthKind::None;
assert!(format!("{n:?}").contains("None"));
}
#[test]
fn auth_constraints_defaults() {
let c = AuthConstraints::default();
assert!(!c.require_workspace_id);
assert!(!c.require_account_id);
assert!(!c.allow_interactive_login);
assert!(c.allow_refresh, "allow_refresh defaults to true");
}
#[test]
fn resolved_auth_envelope_serde_roundtrip() {
let meta = AuthMetadata {
account_id: Some("acct_x".into()),
..AuthMetadata::default()
};
let env = ResolvedAuthEnvelope::StaticHeaders {
headers: vec![("Authorization".into(), "Bearer xyz".into())],
metadata: meta,
expires_at: None,
};
let s = serde_json::to_string(&env).unwrap();
assert!(s.contains("\"kind\":\"static_headers\""));
let back: ResolvedAuthEnvelope = serde_json::from_str(&s).unwrap();
match back {
ResolvedAuthEnvelope::StaticHeaders {
headers, metadata, ..
} => {
assert_eq!(headers.len(), 1);
assert_eq!(metadata.account_id.as_deref(), Some("acct_x"));
}
other => panic!("unexpected variant: {other:?}"),
}
let dyn_env = ResolvedAuthEnvelope::DynamicAuthorizer {
metadata: AuthMetadata::default(),
expires_at: None,
};
let s = serde_json::to_string(&dyn_env).unwrap();
assert!(s.contains("dynamic_authorizer"));
let _back: ResolvedAuthEnvelope = serde_json::from_str(&s).unwrap();
let none_env = ResolvedAuthEnvelope::None {
metadata: AuthMetadata::default(),
};
let s = serde_json::to_string(&none_env).unwrap();
assert!(s.contains("\"kind\":\"none\""));
let _back: ResolvedAuthEnvelope = serde_json::from_str(&s).unwrap();
}
struct StubLease {
kind: ResolvedAuthKind,
metadata: AuthMetadata,
source_label: String,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AuthLease for StubLease {
fn kind(&self) -> &ResolvedAuthKind {
&self.kind
}
fn metadata(&self) -> &AuthMetadata {
&self.metadata
}
fn expires_at(&self) -> Option<DateTime<Utc>> {
None
}
fn source_label(&self) -> &str {
&self.source_label
}
async fn refresh(&self, _reason: AuthRefreshReason) -> Result<(), AuthError> {
Ok(())
}
}
#[tokio::test]
async fn stub_lease_is_object_safe() {
let lease: Arc<dyn AuthLease> = Arc::new(StubLease {
kind: ResolvedAuthKind::None,
metadata: AuthMetadata::default(),
source_label: "stub".into(),
});
assert_eq!(lease.source_label(), "stub");
assert!(matches!(lease.kind(), ResolvedAuthKind::None));
assert!(lease.refresh(AuthRefreshReason::Manual).await.is_ok());
}
}