use async_trait::async_trait;
use std::sync::Arc;
use serde::Deserialize;
use crate::BoxliteResult;
#[derive(Debug, Deserialize, Clone)]
pub struct Principal {
pub sub: String,
pub principal_type: String,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub path_prefix: Option<String>,
pub scopes: Vec<String>,
#[serde(default)]
pub expires_at: Option<String>,
}
#[async_trait]
pub(crate) trait AuthBackend: Send + Sync {
async fn whoami(&self) -> BoxliteResult<Principal>;
}
#[derive(Clone)]
pub struct AuthHandle {
backend: Arc<dyn AuthBackend>,
}
impl AuthHandle {
pub(crate) fn new(backend: Arc<dyn AuthBackend>) -> Self {
Self { backend }
}
pub async fn whoami(&self) -> BoxliteResult<Principal> {
self.backend.whoami().await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_principal_deserialization() {
let json = r#"{
"sub": "usr_01ABC",
"principal_type": "user",
"path_prefix": "acme",
"scopes": ["box:read", "box:write"]
}"#;
let p: Principal = serde_json::from_str(json).unwrap();
assert_eq!(p.sub, "usr_01ABC");
assert_eq!(p.principal_type, "user");
assert_eq!(p.path_prefix.as_deref(), Some("acme"));
assert_eq!(p.scopes, vec!["box:read", "box:write"]);
assert_eq!(p.email, None);
assert_eq!(p.display_name, None);
assert_eq!(p.expires_at, None);
let full = r#"{
"sub": "svc_1",
"principal_type": "service_account",
"email": "ci@acme.test",
"display_name": "CI",
"path_prefix": "acme",
"scopes": [],
"expires_at": "2027-01-01T00:00:00Z"
}"#;
let p: Principal = serde_json::from_str(full).unwrap();
assert_eq!(p.email.as_deref(), Some("ci@acme.test"));
assert_eq!(p.display_name.as_deref(), Some("CI"));
assert_eq!(p.expires_at.as_deref(), Some("2027-01-01T00:00:00Z"));
}
#[test]
fn principal_accepts_null_or_absent_path_prefix() {
let explicit_null = r#"{
"sub": "google-oauth2|123",
"principal_type": "user",
"path_prefix": null,
"scopes": []
}"#;
let p: Principal = serde_json::from_str(explicit_null).unwrap();
assert!(p.path_prefix.is_none(), "explicit null should yield None");
let omitted = r#"{
"sub": "google-oauth2|123",
"principal_type": "user",
"scopes": []
}"#;
let p: Principal = serde_json::from_str(omitted).unwrap();
assert!(p.path_prefix.is_none(), "omitted field should yield None");
}
}