use std::sync::Arc;
use axum::{
body::Body,
extract::State,
http::{header::AUTHORIZATION, Request, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
};
use sha2::{Digest, Sha256};
use crate::error::ProblemDetails;
pub mod scopes {
pub const DATA_PLANE_CONNECT: &str = "data-plane:connect";
}
pub fn key_has_scope(scopes: &[String], required: &str) -> bool {
scopes.iter().any(|s| s == required)
}
#[derive(Clone)]
pub enum AdminAuth {
Token(Arc<[u8; 32]>),
#[cfg_attr(not(test), allow(dead_code))]
Disabled,
}
impl AdminAuth {
pub fn from_token(token: &str) -> Self {
AdminAuth::Token(Arc::new(sha256(token.as_bytes())))
}
fn accepts(&self, presented: &str) -> bool {
match self {
AdminAuth::Disabled => true,
AdminAuth::Token(expected) => ct_eq(&sha256(presented.as_bytes()), expected),
}
}
}
fn sha256(bytes: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(bytes);
hasher.finalize().into()
}
fn ct_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut diff = 0u8;
for i in 0..32 {
diff |= a[i] ^ b[i];
}
diff == 0
}
pub async fn require_admin(
State(auth): State<AdminAuth>,
req: Request<Body>,
next: Next,
) -> Response {
if let AdminAuth::Disabled = auth {
return next.run(req).await;
}
let authorized = req
.headers()
.get(AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.map(|token| auth.accepts(token))
.unwrap_or(false);
if authorized {
next.run(req).await
} else {
let mut response = ProblemDetails {
error_type: "urn:barbacane:error:unauthorized".into(),
title: "Unauthorized".into(),
status: StatusCode::UNAUTHORIZED.as_u16(),
detail: Some("a valid admin bearer token is required".into()),
instance: None,
errors: vec![],
}
.into_response();
response.headers_mut().insert(
axum::http::header::WWW_AUTHENTICATE,
axum::http::HeaderValue::from_static("Bearer"),
);
response
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_has_scope_matches_exactly() {
let scopes = vec!["data-plane:connect".to_string(), "project:read".to_string()];
assert!(key_has_scope(&scopes, scopes::DATA_PLANE_CONNECT));
assert!(key_has_scope(&scopes, "project:read"));
assert!(!key_has_scope(&scopes, "data-plane"));
assert!(!key_has_scope(&scopes, "project:write"));
assert!(!key_has_scope(
&["project:read".to_string()],
scopes::DATA_PLANE_CONNECT
));
assert!(!key_has_scope(&[], scopes::DATA_PLANE_CONNECT));
}
}