use crate::config::ResolvedOAuthConfig;
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum StaticTokenDecision {
StaticAndOAuth(String),
StaticOnly(String),
OAuthOnly,
StaticIgnored,
Unauthenticated,
}
impl StaticTokenDecision {
pub fn oauth_enabled(&self) -> bool {
match self {
Self::StaticAndOAuth(_) | Self::OAuthOnly | Self::StaticIgnored => true,
Self::StaticOnly(_) | Self::Unauthenticated => false,
}
}
pub fn static_token(&self) -> Option<&str> {
match self {
Self::StaticAndOAuth(t) | Self::StaticOnly(t) => Some(t),
Self::OAuthOnly | Self::StaticIgnored | Self::Unauthenticated => None,
}
}
pub fn into_static_token(self) -> Option<String> {
match self {
Self::StaticAndOAuth(t) | Self::StaticOnly(t) => Some(t),
Self::OAuthOnly | Self::StaticIgnored | Self::Unauthenticated => None,
}
}
}
impl std::fmt::Debug for StaticTokenDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::StaticAndOAuth(_) => f.write_str("StaticAndOAuth(<redacted>)"),
Self::StaticOnly(_) => f.write_str("StaticOnly(<redacted>)"),
Self::OAuthOnly => f.write_str("OAuthOnly"),
Self::StaticIgnored => f.write_str("StaticIgnored"),
Self::Unauthenticated => f.write_str("Unauthenticated"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"no authentication is configured: set a static token or enable OAuth, or explicitly \
allow unauthenticated access"
)]
#[non_exhaustive]
pub struct NoAuthConfigured;
pub fn static_token_policy(
static_token: Option<String>,
oauth: Option<&ResolvedOAuthConfig>,
allow_unauthenticated: bool,
) -> Result<StaticTokenDecision, NoAuthConfigured> {
let static_token = static_token.filter(|v| !v.is_empty());
match (static_token, oauth) {
(Some(_), Some(o)) if !o.accept_static_bearer => Ok(StaticTokenDecision::StaticIgnored),
(Some(token), Some(_)) => Ok(StaticTokenDecision::StaticAndOAuth(token)),
(Some(token), None) => Ok(StaticTokenDecision::StaticOnly(token)),
(None, Some(_)) => Ok(StaticTokenDecision::OAuthOnly),
(None, None) if allow_unauthenticated => Ok(StaticTokenDecision::Unauthenticated),
(None, None) => Err(NoAuthConfigured),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing;
use StaticTokenDecision::*;
fn tok() -> Option<String> {
Some("secret".to_string())
}
#[test]
fn static_token_resolution_matrix() {
let mut oauth = testing::resolved_config("http://127.0.0.1:1/jwks");
let d = static_token_policy(tok(), Some(&oauth), false).unwrap();
assert_eq!(d, StaticAndOAuth("secret".into()));
assert_eq!(d.static_token(), Some("secret"));
let d = static_token_policy(tok(), None, false).unwrap();
assert_eq!(d, StaticOnly("secret".into()));
assert_eq!(d.into_static_token(), tok());
assert_eq!(
static_token_policy(None, Some(&oauth), false).unwrap(),
OAuthOnly
);
assert_eq!(
static_token_policy(Some(String::new()), Some(&oauth), false).unwrap(),
OAuthOnly
);
assert_eq!(
static_token_policy(None, None, false).unwrap_err(),
NoAuthConfigured
);
assert_eq!(
static_token_policy(Some(String::new()), None, false).unwrap_err(),
NoAuthConfigured
);
let d = static_token_policy(None, None, true).unwrap();
assert_eq!(d, Unauthenticated);
assert_eq!(d.static_token(), None);
oauth.accept_static_bearer = false;
let d = static_token_policy(tok(), Some(&oauth), false).unwrap();
assert_eq!(d, StaticIgnored);
assert_eq!(d.into_static_token(), None);
assert_eq!(
static_token_policy(None, Some(&oauth), false).unwrap(),
OAuthOnly
);
}
#[test]
fn allow_unauthenticated_never_weakens_a_configured_credential() {
let mut oauth = testing::resolved_config("http://127.0.0.1:1/jwks");
assert_eq!(
static_token_policy(tok(), None, true).unwrap(),
StaticOnly("secret".into())
);
assert_eq!(
static_token_policy(None, Some(&oauth), true).unwrap(),
OAuthOnly
);
assert_eq!(
static_token_policy(tok(), Some(&oauth), true).unwrap(),
StaticAndOAuth("secret".into())
);
oauth.accept_static_bearer = false;
assert_eq!(
static_token_policy(tok(), Some(&oauth), true).unwrap(),
StaticIgnored
);
}
#[test]
fn a_whitespace_token_stays_configured() {
assert_eq!(
static_token_policy(Some(" ".into()), None, true).unwrap(),
StaticOnly(" ".into())
);
}
#[test]
fn oauth_enabled_says_whether_a_validator_must_accompany_the_decision() {
let oauth = testing::resolved_config("http://127.0.0.1:1/jwks");
let mut ignoring = oauth.clone();
ignoring.accept_static_bearer = false;
for (decision, expected) in [
(static_token_policy(tok(), Some(&oauth), false), true),
(static_token_policy(None, Some(&oauth), false), true),
(static_token_policy(tok(), Some(&ignoring), false), true),
(static_token_policy(tok(), None, false), false),
(static_token_policy(None, None, true), false),
] {
let decision = decision.unwrap();
assert_eq!(decision.oauth_enabled(), expected, "{decision:?}");
}
}
#[test]
fn debug_never_prints_the_token() {
for d in [
StaticAndOAuth("hunter2".into()),
StaticOnly("hunter2".into()),
] {
let rendered = format!("{d:?}");
assert!(!rendered.contains("hunter2"), "{rendered}");
}
}
}