use std::sync::Arc;
use axum::http::Extensions;
use serde::{Deserialize, Deserializer};
pub type AudienceSource = Arc<dyn Fn(&Extensions) -> Option<String> + Send + Sync>;
#[derive(Clone)]
pub(crate) struct AudienceBinding {
pub(crate) source: AudienceSource,
pub(crate) accept_unbound: bool,
}
impl AudienceBinding {
pub(crate) fn new<F>(source: F) -> Self
where
F: Fn(&Extensions) -> Option<String> + Send + Sync + 'static,
{
Self {
source: Arc::new(source),
accept_unbound: false,
}
}
pub(crate) fn admits(&self, extensions: &Extensions, token_aud: &[String]) -> bool {
admits(
(self.source)(extensions).as_deref(),
token_aud,
self.accept_unbound,
)
}
}
pub(crate) fn admits(
request_tenant: Option<&str>,
token_aud: &[String],
accept_unbound: bool,
) -> bool {
match request_tenant {
Some(tenant) if token_aud.is_empty() => {
if accept_unbound {
tracing::warn!(
target: "backbone_auth::audience",
tenant,
"accepted a session token that names no tenant; this compatibility path is deprecated and will be removed"
);
}
accept_unbound
}
Some(tenant) => token_aud.iter().any(|aud| aud == tenant),
None => token_aud.is_empty(),
}
}
pub(crate) fn deserialize_audience<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
Ok(match Option::<OneOrMany>::deserialize(deserializer)? {
None => Vec::new(),
Some(OneOrMany::One(aud)) => vec![aud],
Some(OneOrMany::Many(auds)) => auds,
})
}
#[cfg(test)]
mod tests {
use super::admits;
fn aud(values: &[&str]) -> Vec<String> {
values.iter().map(|v| v.to_string()).collect()
}
#[test]
fn the_binding_rule() {
assert!(admits(Some("acme"), &aud(&["acme"]), false));
assert!(admits(Some("acme"), &aud(&["other", "acme"]), false));
assert!(!admits(Some("acme"), &aud(&["globex"]), false));
assert!(!admits(Some("acme"), &aud(&[]), false));
assert!(admits(Some("acme"), &aud(&[]), true));
assert!(admits(None, &aud(&[]), false));
assert!(!admits(None, &aud(&["acme"]), false));
assert!(!admits(None, &aud(&["acme"]), true));
}
}