use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Json, Response};
use serde_json::json;
use crate::ctx::AuthCtx;
use crate::state::AdminApiKeys;
#[derive(Clone, Debug)]
pub struct Caller {
pub user_id: String,
pub source: CallerSource,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CallerSource {
SessionCookie,
Jwt,
AdminApiKey,
}
impl Caller {
pub fn is_break_glass(&self) -> bool {
matches!(self.source, CallerSource::AdminApiKey)
}
}
pub async fn extract_caller(
headers: &HeaderMap,
#[cfg_attr(
not(any(feature = "auth-session", feature = "auth-jwt")),
allow(unused_variables)
)]
ctx: &AuthCtx,
keys: &AdminApiKeys,
) -> Result<Caller, Box<Response>> {
if let Some(token) = bearer_token(headers)
&& keys.enabled()
&& keys.check(token)
{
return Ok(Caller {
user_id: short_admin_actor(token),
source: CallerSource::AdminApiKey,
});
}
#[cfg(feature = "auth-session")]
if let Some(sid) = cookie_value(headers, crate::session::SESSION_COOKIE) {
let mgr = crate::session::SessionManager::with_default_duration(ctx.sessions.clone());
if let Ok(Some(s)) = mgr.resolve(&sid).await {
return Ok(Caller {
user_id: s.user_id,
source: CallerSource::SessionCookie,
});
}
}
#[cfg(feature = "auth-jwt")]
if let Some(token) = bearer_token(headers) {
#[derive(serde::Deserialize)]
struct SubClaim {
sub: String,
}
if let Some(jwt) = ctx.jwt.as_ref()
&& let Ok(td) = jwt.verify::<SubClaim>(token)
{
return Ok(Caller {
user_id: td.claims.sub,
source: CallerSource::Jwt,
});
}
if let Some(result) =
crate::external_jwt::verify_with_any::<SubClaim>(ctx.external_issuers(), token)
&& let Ok(td) = result
{
return Ok(Caller {
user_id: td.claims.sub,
source: CallerSource::Jwt,
});
}
}
Err(unauthorized("authentication required"))
}
pub async fn require_role(
caller: &Caller,
#[cfg_attr(not(feature = "auth-zanzibar"), allow(unused_variables))] ctx: &AuthCtx,
#[cfg_attr(not(feature = "auth-zanzibar"), allow(unused_variables))] object_type: &str,
#[cfg_attr(not(feature = "auth-zanzibar"), allow(unused_variables))] object_id: &str,
#[cfg_attr(not(feature = "auth-zanzibar"), allow(unused_variables))] permission: &str,
) -> Result<(), Box<Response>> {
if caller.is_break_glass() {
return Ok(());
}
#[cfg(feature = "auth-zanzibar")]
{
use crate::zanzibar::{CheckResult, Consistency, ObjectRef, SubjectRef};
let Some(store) = ctx.zanzibar.as_ref() else {
return Err(forbidden("zanzibar store not configured"));
};
let resource = ObjectRef {
object_type: object_type.to_string(),
object_id: object_id.to_string(),
};
let subject = SubjectRef {
subject_type: "user".to_string(),
subject_id: caller.user_id.clone(),
subject_rel: String::new(),
};
match store
.check(&resource, permission, &subject, Consistency::Minimum)
.await
{
Ok(CheckResult::Allowed { .. }) => Ok(()),
Ok(_) => Err(forbidden("permission denied")),
Err(e) => Err(internal(&format!("zanzibar check: {e}"))),
}
}
#[cfg(not(feature = "auth-zanzibar"))]
{
Err(forbidden("authorization not compiled in"))
}
}
pub async fn require_role_for(
headers: &HeaderMap,
ctx: &AuthCtx,
keys: &AdminApiKeys,
object_type: &str,
object_id: &str,
permission: &str,
) -> Result<Caller, Box<Response>> {
let caller = extract_caller(headers, ctx, keys).await?;
require_role(&caller, ctx, object_type, object_id, permission).await?;
Ok(caller)
}
pub fn require_admin_bearer(headers: &HeaderMap, keys: &AdminApiKeys) -> Result<(), Box<Response>> {
if let Some(token) = bearer_token(headers)
&& keys.enabled()
&& keys.check(token)
{
return Ok(());
}
Err(unauthorized("admin bearer token required"))
}
pub async fn require_admin_or_jwt(
headers: &HeaderMap,
#[cfg_attr(not(feature = "auth-jwt"), allow(unused_variables))] ctx: &AuthCtx,
keys: &AdminApiKeys,
) -> Result<Option<String>, Box<Response>> {
if let Some(token) = bearer_token(headers)
&& keys.enabled()
&& keys.check(token)
{
return Ok(None);
}
#[cfg(feature = "auth-jwt")]
if let Some(token) = bearer_token(headers) {
#[derive(serde::Deserialize)]
struct SubClaim {
sub: String,
}
if let Some(jwt) = ctx.jwt.as_ref()
&& let Ok(td) = jwt.verify::<SubClaim>(token)
{
return Ok(Some(td.claims.sub));
}
if let Some(result) =
crate::external_jwt::verify_with_any::<SubClaim>(ctx.external_issuers(), token)
&& let Ok(td) = result
{
return Ok(Some(td.claims.sub));
}
}
Err(unauthorized("admin bearer or trusted JWT required"))
}
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|s| {
s.strip_prefix("Bearer ")
.or_else(|| s.strip_prefix("bearer "))
})
.map(str::trim)
}
#[cfg(feature = "auth-session")]
fn cookie_value(headers: &HeaderMap, name: &str) -> Option<String> {
let raw = headers.get(header::COOKIE)?.to_str().ok()?;
for kv in raw.split(';') {
let kv = kv.trim();
if let Some((k, v)) = kv.split_once('=')
&& k == name
{
return Some(v.to_string());
}
}
None
}
fn short_admin_actor(token: &str) -> String {
let t = token.trim();
if t.len() <= 6 {
return format!("admin:****{t}");
}
let tail = &t[t.len() - 6..];
format!("admin:****{tail}")
}
fn unauthorized(msg: &str) -> Box<Response> {
Box::new(
(
StatusCode::UNAUTHORIZED,
Json(json!({"error": "unauthorized", "error_description": msg})),
)
.into_response(),
)
}
fn forbidden(msg: &str) -> Box<Response> {
Box::new(
(
StatusCode::FORBIDDEN,
Json(json!({"error": "forbidden", "error_description": msg})),
)
.into_response(),
)
}
fn internal(msg: &str) -> Box<Response> {
Box::new(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "server_error", "error_description": msg})),
)
.into_response(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caller_break_glass_only_for_api_key() {
let c = Caller {
user_id: "x".into(),
source: CallerSource::AdminApiKey,
};
assert!(c.is_break_glass());
let c = Caller {
user_id: "x".into(),
source: CallerSource::SessionCookie,
};
assert!(!c.is_break_glass());
let c = Caller {
user_id: "x".into(),
source: CallerSource::Jwt,
};
assert!(!c.is_break_glass());
}
#[test]
fn short_admin_actor_truncates_long_tokens() {
assert_eq!(short_admin_actor("abcdef0123456789"), "admin:****456789");
}
#[test]
fn short_admin_actor_handles_short_tokens() {
assert_eq!(short_admin_actor("abc"), "admin:****abc");
}
}