use std::collections::HashSet;
use axum::http::HeaderMap;
use super::{AuthError, Authenticator, Caller};
use crate::core::{Secret, TenantId};
#[derive(Debug)]
pub struct TokenAuthenticator {
entries: Vec<(Secret, Caller)>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TokenFileError {
#[error("the token file is not valid YAML: {0}")]
Syntax(String),
#[error(
"the token file lists no callers — a server with no accepted credential \
accepts nobody, which is an outage that reads like a configuration"
)]
Empty,
#[error(
"entry {index} has an empty token; a blank credential would be presented \
by anyone who presented none"
)]
BlankToken { index: usize },
#[error(
"entry {index} has no actor; a decision recorded against an unnamed \
caller is not a decision anybody can answer for"
)]
BlankActor { index: usize },
#[error(
"two entries share one token, so one of them silently never applies and \
which depends on the order of the file"
)]
DuplicateToken,
#[error("entry {index} names an invalid tenant: {detail}")]
Tenant { index: usize, detail: String },
}
impl TokenAuthenticator {
pub fn new(entries: Vec<TokenEntry>) -> Result<Self, TokenFileError> {
if entries.is_empty() {
return Err(TokenFileError::Empty);
}
let mut seen: HashSet<String> = HashSet::new();
let mut built = Vec::with_capacity(entries.len());
for (index, entry) in entries.into_iter().enumerate() {
if entry.token.trim().is_empty() {
return Err(TokenFileError::BlankToken { index });
}
if entry.actor.trim().is_empty() {
return Err(TokenFileError::BlankActor { index });
}
if !seen.insert(entry.token.clone()) {
return Err(TokenFileError::DuplicateToken);
}
let mut caller = Caller::new(entry.actor, entry.roles);
if let Some(tenant) = entry.tenant {
let tenant = TenantId::new(tenant).map_err(|e| TokenFileError::Tenant {
index,
detail: e.to_string(),
})?;
caller = caller.in_tenant(tenant);
}
built.push((Secret::new(entry.token), caller));
}
Ok(Self { entries: built })
}
#[cfg(feature = "manifest")]
pub fn from_yaml(source: &str) -> Result<Self, TokenFileError> {
let entries: Vec<TokenEntry> =
serde_yaml_ng::from_str(source).map_err(|e| TokenFileError::Syntax(e.to_string()))?;
Self::new(entries)
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TokenEntry {
pub token: String,
pub actor: String,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub tenant: Option<String>,
}
#[async_trait::async_trait]
impl Authenticator for TokenAuthenticator {
async fn authenticate(&self, headers: &HeaderMap) -> Result<Caller, AuthError> {
let presented = headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(AuthError::Missing)?;
let presented = Secret::new(presented);
let mut found: Option<&Caller> = None;
for (token, caller) in &self.entries {
if *token == presented {
found = Some(caller);
}
}
found.cloned().ok_or(AuthError::Rejected)
}
}