use axum::{
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Json,
};
use constant_time_eq::constant_time_eq;
use serde_json::json;
use crate::AppState;
use crate::store::permissions::{PermBit, Permissions};
#[derive(Debug, Clone)]
pub enum ResolvedAuth {
Master,
Principal {
principal_id: String,
org_id: String,
key_id: String,
key_name: String,
permissions: Permissions,
},
}
impl ResolvedAuth {
pub fn is_master(&self) -> bool {
matches!(self, Self::Master)
}
pub fn org_id(&self) -> Option<&str> {
match self {
Self::Master => None,
Self::Principal { org_id, .. } => Some(org_id.as_str()),
}
}
pub fn principal_id(&self) -> Option<&str> {
match self {
Self::Master => None,
Self::Principal { principal_id, .. } => Some(principal_id.as_str()),
}
}
pub fn key_name(&self) -> Option<&str> {
match self {
Self::Master => None,
Self::Principal { key_name, .. } => Some(key_name.as_str()),
}
}
pub fn has(&self, bit: PermBit) -> bool {
match self {
Self::Master => bit == PermBit::SirrAdmin,
Self::Principal { permissions, .. } => permissions.has(bit),
}
}
pub fn can_read_my(&self) -> bool {
self.has(PermBit::ReadMy)
}
pub fn can_read_org(&self) -> bool {
self.has(PermBit::ReadOrg)
}
pub fn can_list_my(&self) -> bool {
self.has(PermBit::ListMy)
}
pub fn can_list_org(&self) -> bool {
self.has(PermBit::ListOrg)
}
pub fn can_create(&self) -> bool {
self.has(PermBit::Create)
}
pub fn can_create_on_behalf(&self) -> bool {
self.has(PermBit::CreateOnBehalf)
}
pub fn can_patch_my(&self) -> bool {
self.has(PermBit::PatchMy)
}
pub fn can_patch_org(&self) -> bool {
self.has(PermBit::PatchOrg)
}
pub fn can_account_read(&self) -> bool {
self.has(PermBit::AccountRead)
}
pub fn can_account_read_org(&self) -> bool {
self.has(PermBit::AccountReadOrg)
}
pub fn can_account_manage(&self) -> bool {
self.has(PermBit::AccountManage)
}
pub fn can_manage_org(&self) -> bool {
self.has(PermBit::ManageOrg)
}
pub fn can_sirr_admin(&self) -> bool {
self.has(PermBit::SirrAdmin)
}
pub fn can_delete_my(&self) -> bool {
self.has(PermBit::DeleteMy)
}
pub fn can_delete_org(&self) -> bool {
self.has(PermBit::DeleteOrg)
}
pub fn can_access_secret(
&self,
secret_owner_id: Option<&str>,
my_bit: PermBit,
org_bit: PermBit,
) -> bool {
match self {
Self::Master => false,
Self::Principal {
principal_id,
permissions,
..
} => {
if permissions.has(org_bit) {
return true;
}
if permissions.has(my_bit) {
if let Some(owner) = secret_owner_id {
return owner == principal_id.as_str();
}
}
false
}
}
}
}
fn unauthorized() -> Response {
(
StatusCode::UNAUTHORIZED,
Json(json!({"error": "unauthorized — valid API key required for this endpoint"})),
)
.into_response()
}
pub async fn require_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Response {
let has_admin_key = state.api_key.is_some();
let token = request
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
if !has_admin_key && token.is_none() {
request.extensions_mut().insert(ResolvedAuth::Master);
return next.run(request).await;
}
let Some(token) = token else {
return unauthorized();
};
if let Some(ref expected) = state.api_key {
if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
request.extensions_mut().insert(ResolvedAuth::Master);
return next.run(request).await;
}
}
let token_hash = {
use sha2::{Digest, Sha256};
Sha256::digest(token.as_bytes()).to_vec()
};
if let Ok(Some(key_record)) = state.store.find_principal_key_by_hash(&token_hash) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
if now < key_record.valid_after || now >= key_record.valid_before {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "principal key expired or not yet valid"})),
)
.into_response();
}
let principal = match state
.store
.get_principal(&key_record.org_id, &key_record.principal_id)
{
Ok(Some(p)) => p,
_ => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "principal not found for key"})),
)
.into_response();
}
};
let permissions = match state
.store
.get_role(Some(&key_record.org_id), &principal.role)
{
Ok(Some(role)) => role.permissions,
_ => {
match state.store.get_role(None, &principal.role) {
Ok(Some(role)) => role.permissions,
_ => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "role not found for principal"})),
)
.into_response();
}
}
}
};
request.extensions_mut().insert(ResolvedAuth::Principal {
principal_id: key_record.principal_id.clone(),
org_id: key_record.org_id.clone(),
key_id: key_record.id.clone(),
key_name: key_record.name.clone(),
permissions,
});
return next.run(request).await;
}
unauthorized()
}
pub async fn require_master_key(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Response {
if state.api_key.is_none() {
request.extensions_mut().insert(ResolvedAuth::Master);
return next.run(request).await;
}
let token = request
.headers()
.get("Authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
let Some(token) = token else {
return unauthorized();
};
if let Some(ref expected) = state.api_key {
if constant_time_eq(token.as_bytes(), expected.as_bytes()) {
request.extensions_mut().insert(ResolvedAuth::Master);
return next.run(request).await;
}
}
unauthorized()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolved_auth_master_has_no_secret_perms() {
let auth = ResolvedAuth::Master;
assert!(auth.is_master());
assert!(auth.can_sirr_admin());
assert!(!auth.can_read_my());
assert!(!auth.can_read_org());
assert!(!auth.can_create());
assert!(!auth.can_list_my());
assert!(!auth.can_list_org());
assert!(!auth.can_delete_my());
assert!(!auth.can_delete_org());
assert!(!auth.can_patch_my());
assert!(!auth.can_patch_org());
assert!(!auth.can_manage_org());
assert!(auth.org_id().is_none());
assert!(auth.principal_id().is_none());
assert!(auth.key_name().is_none());
assert!(!auth.can_access_secret(Some("p1"), PermBit::ReadMy, PermBit::ReadOrg));
}
#[test]
fn resolved_auth_principal_checks() {
let writer_perms = Permissions::parse("rlcpdam").unwrap();
let auth = ResolvedAuth::Principal {
principal_id: "p_1".into(),
org_id: "org_1".into(),
key_id: "pk_1".into(),
key_name: "default".into(),
permissions: writer_perms,
};
assert!(!auth.is_master());
assert_eq!(auth.org_id(), Some("org_1"));
assert_eq!(auth.principal_id(), Some("p_1"));
assert_eq!(auth.key_name(), Some("default"));
assert!(auth.can_read_my());
assert!(auth.can_list_my());
assert!(auth.can_create());
assert!(auth.can_patch_my());
assert!(auth.can_delete_my());
assert!(auth.can_account_read());
assert!(auth.can_account_manage());
assert!(!auth.can_read_org());
assert!(!auth.can_list_org());
assert!(!auth.can_create_on_behalf());
assert!(!auth.can_patch_org());
assert!(!auth.can_manage_org());
assert!(!auth.can_sirr_admin());
assert!(!auth.can_delete_org());
assert!(auth.can_access_secret(Some("p_1"), PermBit::ReadMy, PermBit::ReadOrg));
assert!(!auth.can_access_secret(Some("p_2"), PermBit::ReadMy, PermBit::ReadOrg));
assert!(!auth.can_access_secret(None, PermBit::ReadMy, PermBit::ReadOrg));
}
#[test]
fn resolved_auth_admin_has_org_access() {
let admin_perms = Permissions::parse("rRlLcCpPaAmMdD").unwrap();
let auth = ResolvedAuth::Principal {
principal_id: "p_admin".into(),
org_id: "org_1".into(),
key_id: "pk_admin".into(),
key_name: "admin-key".into(),
permissions: admin_perms,
};
assert!(auth.can_access_secret(Some("p_other"), PermBit::ReadMy, PermBit::ReadOrg));
assert!(auth.can_access_secret(None, PermBit::ReadMy, PermBit::ReadOrg));
assert!(auth.can_read_org());
assert!(auth.can_list_org());
assert!(auth.can_delete_org());
assert!(auth.can_patch_org());
assert!(auth.can_manage_org());
assert!(!auth.can_sirr_admin());
}
}