use crate::store::{KeyRecord, SecretRecord, Visibility};
use axum::http::StatusCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Create,
Read,
Inspect, Audit, Patch,
Burn,
}
#[derive(Debug, Clone)]
pub enum Caller {
Anonymous,
Keyed(KeyRecord),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthDecision {
Allow,
Unauthorized, BadRequest(String), MethodNotAllowed, NotFound, Gone, Unavailable, }
impl AuthDecision {
pub fn into_status_code(&self) -> StatusCode {
match self {
AuthDecision::Allow => StatusCode::OK,
AuthDecision::Unauthorized => StatusCode::UNAUTHORIZED,
AuthDecision::BadRequest(_) => StatusCode::BAD_REQUEST,
AuthDecision::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
AuthDecision::NotFound => StatusCode::NOT_FOUND,
AuthDecision::Gone => StatusCode::GONE,
AuthDecision::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
}
}
}
pub fn authorize(
action: Action,
secret: Option<&SecretRecord>,
caller: &Caller,
visibility: Visibility,
now: i64,
) -> AuthDecision {
if visibility == Visibility::None {
return AuthDecision::Unavailable;
}
match action {
Action::Create => match (visibility, caller) {
(Visibility::Public, Caller::Anonymous) => AuthDecision::Allow,
(Visibility::Public, Caller::Keyed(_)) => AuthDecision::BadRequest(
"server in public mode does not accept keyed writes".to_string(),
),
(Visibility::Private, Caller::Anonymous) => AuthDecision::Unauthorized,
(Visibility::Private, Caller::Keyed(_)) => AuthDecision::Allow,
(Visibility::Both, _) => AuthDecision::Allow,
(Visibility::None, _) => unreachable!(),
},
Action::Read => match secret {
Some(s) if is_active(s, now) => AuthDecision::Allow,
Some(_) => AuthDecision::Gone,
None => AuthDecision::Gone,
},
Action::Inspect => match secret {
Some(s) if is_active(s, now) => AuthDecision::Allow,
Some(_) => AuthDecision::Gone,
None => AuthDecision::Gone,
},
Action::Audit => match secret {
None => AuthDecision::NotFound,
Some(s) => {
if s.owner_key_id.is_none() {
return AuthDecision::NotFound;
}
match caller {
Caller::Anonymous => AuthDecision::Unauthorized,
Caller::Keyed(_) if is_owner(s, caller) => AuthDecision::Allow,
Caller::Keyed(_) => AuthDecision::NotFound,
}
}
},
Action::Patch => match secret {
None => AuthDecision::NotFound,
Some(s) => {
if !is_active(s, now) {
return AuthDecision::Gone;
}
if s.owner_key_id.is_none() {
return AuthDecision::MethodNotAllowed;
}
match caller {
Caller::Anonymous => AuthDecision::Unauthorized,
Caller::Keyed(_) if is_owner(s, caller) => AuthDecision::Allow,
Caller::Keyed(_) => AuthDecision::NotFound,
}
}
},
Action::Burn => match secret {
None => AuthDecision::NotFound,
Some(s) => {
if !is_active(s, now) {
return AuthDecision::Gone;
}
if s.owner_key_id.is_none() {
return AuthDecision::Allow;
}
match caller {
Caller::Anonymous => AuthDecision::Unauthorized,
Caller::Keyed(_) if is_owner(s, caller) => AuthDecision::Allow,
Caller::Keyed(_) => AuthDecision::NotFound,
}
}
},
}
}
fn is_owner(secret: &SecretRecord, caller: &Caller) -> bool {
match (secret.owner_key_id.as_deref(), caller) {
(Some(owner_id), Caller::Keyed(key)) => owner_id == key.id,
_ => false,
}
}
fn is_active(secret: &SecretRecord, now: i64) -> bool {
!secret.burned && !secret.is_expired(now)
}