use std::net::IpAddr;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuditEvent {
CertificateIssued,
CertificateIssueFailed,
CertificateRevoked,
CertificateRevokeFailed,
AccountDeactivated,
AccountContactUpdated,
AccountDeleted,
OrderDeleted,
EabCreated,
EabRevoked,
EabDeleted,
OperatorCreated,
OperatorRoleChanged,
OperatorContactUpdated,
OperatorPasswordChanged,
OperatorDisabled,
OperatorEnabled,
OperatorDeleted,
OperatorTotpEnrolled,
OperatorTotpDisabled,
OperatorRecoveryCodesRegenerated,
SessionRevoked,
JobCancelled,
JobAdvanced,
NonceCleanupCompleted,
AuditPruned,
DatabaseTransferred,
}
impl AuditEvent {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::CertificateIssued => "certificate_issued",
Self::CertificateIssueFailed => "certificate_issue_failed",
Self::CertificateRevoked => "certificate_revoked",
Self::CertificateRevokeFailed => "certificate_revoke_failed",
Self::AccountDeactivated => "account_deactivated",
Self::AccountContactUpdated => "account_contact_updated",
Self::AccountDeleted => "account_deleted",
Self::OrderDeleted => "order_deleted",
Self::EabCreated => "eab_created",
Self::EabRevoked => "eab_revoked",
Self::EabDeleted => "eab_deleted",
Self::OperatorCreated => "operator_created",
Self::OperatorRoleChanged => "operator_role_changed",
Self::OperatorContactUpdated => "operator_contact_updated",
Self::OperatorPasswordChanged => "operator_password_changed",
Self::OperatorDisabled => "operator_disabled",
Self::OperatorEnabled => "operator_enabled",
Self::OperatorDeleted => "operator_deleted",
Self::OperatorTotpEnrolled => "operator_totp_enrolled",
Self::OperatorTotpDisabled => "operator_totp_disabled",
Self::OperatorRecoveryCodesRegenerated => "operator_recovery_codes_regenerated",
Self::SessionRevoked => "session_revoked",
Self::JobCancelled => "job_cancelled",
Self::JobAdvanced => "job_advanced",
Self::NonceCleanupCompleted => "nonce_cleanup_completed",
Self::AuditPruned => "audit_pruned",
Self::DatabaseTransferred => "database_transferred",
}
}
#[must_use]
pub fn outcome(&self) -> &'static str {
match self {
Self::CertificateIssueFailed | Self::CertificateRevokeFailed => "failure",
Self::CertificateIssued
| Self::CertificateRevoked
| Self::AccountDeactivated
| Self::AccountContactUpdated
| Self::AccountDeleted
| Self::OrderDeleted
| Self::EabCreated
| Self::EabRevoked
| Self::EabDeleted
| Self::OperatorCreated
| Self::OperatorRoleChanged
| Self::OperatorContactUpdated
| Self::OperatorPasswordChanged
| Self::OperatorDisabled
| Self::OperatorEnabled
| Self::OperatorDeleted
| Self::OperatorTotpEnrolled
| Self::OperatorTotpDisabled
| Self::OperatorRecoveryCodesRegenerated
| Self::SessionRevoked
| Self::JobCancelled
| Self::JobAdvanced
| Self::NonceCleanupCompleted
| Self::AuditPruned
| Self::DatabaseTransferred => "success",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
ALL_AUDIT_EVENTS
.iter()
.copied()
.find(|event| event.as_str() == value)
}
}
pub const ALL_AUDIT_EVENTS: &[AuditEvent] = &[
AuditEvent::CertificateIssued,
AuditEvent::CertificateIssueFailed,
AuditEvent::CertificateRevoked,
AuditEvent::CertificateRevokeFailed,
AuditEvent::AccountDeactivated,
AuditEvent::AccountContactUpdated,
AuditEvent::AccountDeleted,
AuditEvent::OrderDeleted,
AuditEvent::EabCreated,
AuditEvent::EabRevoked,
AuditEvent::EabDeleted,
AuditEvent::OperatorCreated,
AuditEvent::OperatorRoleChanged,
AuditEvent::OperatorContactUpdated,
AuditEvent::OperatorPasswordChanged,
AuditEvent::OperatorDisabled,
AuditEvent::OperatorEnabled,
AuditEvent::OperatorDeleted,
AuditEvent::OperatorTotpEnrolled,
AuditEvent::OperatorTotpDisabled,
AuditEvent::OperatorRecoveryCodesRegenerated,
AuditEvent::SessionRevoked,
AuditEvent::JobCancelled,
AuditEvent::JobAdvanced,
AuditEvent::NonceCleanupCompleted,
AuditEvent::AuditPruned,
AuditEvent::DatabaseTransferred,
];
const EVENT_COUNT: usize = 27;
const _: () = assert!(
ALL_AUDIT_EVENTS.len() == EVENT_COUNT,
"ALL_AUDIT_EVENTS and EVENT_COUNT disagree: a variant was added to one and not the other"
);
#[allow(dead_code)]
const fn event_count_is_exhaustive(event: AuditEvent) -> usize {
match event {
AuditEvent::CertificateIssued => 1,
AuditEvent::CertificateIssueFailed => 2,
AuditEvent::CertificateRevoked => 3,
AuditEvent::CertificateRevokeFailed => 4,
AuditEvent::AccountDeactivated => 5,
AuditEvent::AccountContactUpdated => 6,
AuditEvent::AccountDeleted => 7,
AuditEvent::OrderDeleted => 8,
AuditEvent::EabCreated => 9,
AuditEvent::EabRevoked => 10,
AuditEvent::EabDeleted => 11,
AuditEvent::OperatorCreated => 12,
AuditEvent::OperatorRoleChanged => 13,
AuditEvent::OperatorContactUpdated => 14,
AuditEvent::OperatorPasswordChanged => 15,
AuditEvent::OperatorDisabled => 16,
AuditEvent::OperatorEnabled => 17,
AuditEvent::OperatorDeleted => 18,
AuditEvent::OperatorTotpEnrolled => 19,
AuditEvent::OperatorTotpDisabled => 20,
AuditEvent::OperatorRecoveryCodesRegenerated => 21,
AuditEvent::SessionRevoked => 22,
AuditEvent::JobCancelled => 23,
AuditEvent::JobAdvanced => 24,
AuditEvent::NonceCleanupCompleted => 25,
AuditEvent::AuditPruned => 26,
AuditEvent::DatabaseTransferred => EVENT_COUNT,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActorKind {
Acme,
Admin,
Cli,
System,
}
impl ActorKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Acme => "acme",
Self::Admin => "admin",
Self::Cli => "cli",
Self::System => "system",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Actor {
pub kind: ActorKind,
pub id: Option<String>,
}
impl Actor {
#[must_use]
pub fn acme(account_id: impl Into<String>) -> Self {
Self {
kind: ActorKind::Acme,
id: Some(account_id.into()),
}
}
#[must_use]
pub fn acme_certificate_key() -> Self {
Self {
kind: ActorKind::Acme,
id: None,
}
}
#[must_use]
pub fn admin(username: impl Into<String>) -> Self {
Self {
kind: ActorKind::Admin,
id: Some(username.into()),
}
}
#[must_use]
pub fn cli() -> Self {
let id = std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.ok()
.filter(|value| !value.is_empty());
Self {
kind: ActorKind::Cli,
id,
}
}
#[must_use]
pub fn system() -> Self {
Self {
kind: ActorKind::System,
id: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ClientContext {
pub ip: Option<String>,
pub ptr: Option<String>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
}
impl ClientContext {
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"ip": self.ip,
"ptr": self.ptr,
"user_agent": self.user_agent,
"request_id": self.request_id,
})
}
#[must_use]
pub fn from_json(value: &serde_json::Value) -> Self {
let field = |name: &str| value.get(name).and_then(|v| v.as_str()).map(str::to_string);
Self {
ip: field("ip"),
ptr: field("ptr"),
user_agent: field("user_agent"),
request_id: field("request_id"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RequestContext {
pub ip: Option<IpAddr>,
pub user_agent: Option<String>,
pub request_id: Option<String>,
}
pub const USER_AGENT_MAX: usize = 256;
impl RequestContext {
pub fn from_parts(parts: &Parts) -> Self {
Self::gather(&parts.headers, &parts.extensions)
}
pub fn from_request<B>(request: &axum::http::Request<B>) -> Self {
Self::gather(request.headers(), request.extensions())
}
fn gather(headers: &axum::http::HeaderMap, extensions: &axum::http::Extensions) -> Self {
let ip = extensions
.get::<crate::client::ClientIp>()
.and_then(|client| client.0);
let user_agent = headers
.get(axum::http::header::USER_AGENT)
.and_then(|value| value.to_str().ok())
.map(|value| value.chars().take(USER_AGENT_MAX).collect::<String>())
.filter(|value| !value.is_empty());
let request_id = extensions
.get::<crate::client::RequestId>()
.map(|id| id.0.clone());
Self {
ip,
user_agent,
request_id,
}
}
}
impl<S: Send + Sync> FromRequestParts<S> for RequestContext {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self::from_parts(parts))
}
}
#[derive(Debug, Clone)]
pub struct AuditRecord {
pub event: AuditEvent,
pub profile: String,
pub actor: Actor,
pub account_id: Option<String>,
pub order_id: Option<String>,
pub cert_serial: Option<String>,
pub identifiers: Vec<String>,
pub client: ClientContext,
pub reason: Option<String>,
pub detail: Option<String>,
}
impl AuditRecord {
#[must_use]
pub fn new(event: AuditEvent, profile: impl Into<String>, actor: Actor) -> Self {
Self {
event,
profile: profile.into(),
actor,
account_id: None,
order_id: None,
cert_serial: None,
identifiers: Vec::new(),
client: ClientContext::default(),
reason: None,
detail: None,
}
}
#[must_use]
pub fn admin(event: AuditEvent, actor: Actor) -> Self {
Self::new(event, String::new(), actor)
}
#[must_use]
pub fn with_order(
mut self,
order_id: uuid::Uuid,
account_id: uuid::Uuid,
identifiers: &[crate::identifier::Identifier],
) -> Self {
self.order_id = Some(order_id.to_string());
self.account_id = Some(account_id.to_string());
self.identifiers = identifiers
.iter()
.map(|identifier| identifier.value.clone())
.collect();
self
}
#[must_use]
pub fn with_account(mut self, account_id: impl Into<String>) -> Self {
self.account_id = Some(account_id.into());
self
}
#[must_use]
pub fn with_serial(mut self, serial: impl Into<String>) -> Self {
self.cert_serial = Some(serial.into());
self
}
#[must_use]
pub fn with_client(mut self, client: ClientContext) -> Self {
self.client = client;
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
#[cfg(test)]
mod tests;