use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use keepsake::{
ActorRef, ApplyKeepsake, AuditContext, AuditDecision, AuditEvent, AuditEventType,
CommandContext, ExpiryCause, Keepsake, KeepsakeId, LifecycleState, RelationId, RevokeBySubject,
RevokeKeepsake, SubjectRef,
};
use uuid::Uuid;
#[cfg(any(feature = "mysql", feature = "sqlite"))]
use keepsake::ExpiryPolicy;
use super::{AuditEventRecord, RepositoryError, RepositoryResult};
pub(super) fn parse_state(value: String) -> RepositoryResult<LifecycleState> {
match value.as_str() {
"applied" => Ok(LifecycleState::Applied),
"revoked" => Ok(LifecycleState::Revoked),
"expired" => Ok(LifecycleState::Expired),
_ => Err(RepositoryError::InvalidLifecycleState { state: value }),
}
}
#[cfg(any(feature = "mysql", feature = "sqlite"))]
pub(super) fn parse_uuid(value: &str) -> RepositoryResult<Uuid> {
Ok(Uuid::parse_str(value).map_err(|error| sqlx::Error::Decode(Box::new(error)))?)
}
#[cfg(any(feature = "mysql", feature = "sqlite"))]
pub(super) const fn expires_at(expiry: &ExpiryPolicy) -> Option<DateTime<Utc>> {
match expiry {
ExpiryPolicy::At { timestamp } => Some(*timestamp),
ExpiryPolicy::ManualOnly | ExpiryPolicy::WhenFulfilled { .. } => None,
}
}
pub(super) fn audit_context_from_command(context: &CommandContext) -> AuditContext {
let mut attributes = context.metadata.clone();
if let Some(idempotency_key) = &context.idempotency_key {
attributes
.entry("idempotency_key".to_owned())
.or_insert_with(|| idempotency_key.clone());
}
AuditContext { attributes }
}
pub(super) fn apply_event(
command: &ApplyKeepsake,
keepsake: &Keepsake,
duplicate_prevented: bool,
) -> AuditEvent {
AuditEvent {
event_type: if duplicate_prevented {
AuditEventType::DuplicateApply
} else {
AuditEventType::Apply
},
at: command.at,
actor: command.context.actor.clone(),
keepsake_id: keepsake.id(),
subject: keepsake.subject().clone(),
relation_id: command.relation_id,
decision: AuditDecision::Applied {
duplicate_prevented,
},
context: audit_context_from_command(&command.context),
}
}
pub(super) struct AuditEventParts {
pub id: i64,
pub event_type: String,
pub at: DateTime<Utc>,
pub actor_kind: String,
pub actor_id: String,
pub keepsake_id: Uuid,
pub subject_kind: String,
pub subject_id: String,
pub relation_id: Uuid,
pub decision: AuditDecision,
pub attributes: BTreeMap<String, String>,
}
pub(super) fn audit_event_record(parts: AuditEventParts) -> RepositoryResult<AuditEventRecord> {
let event_type = AuditEventType::from_storage_label(&parts.event_type).ok_or_else(|| {
RepositoryError::InvalidAuditEventType {
event_type: parts.event_type.clone(),
}
})?;
Ok(AuditEventRecord {
id: parts.id,
event: AuditEvent {
event_type,
at: parts.at,
actor: ActorRef::new(parts.actor_kind, parts.actor_id)?,
keepsake_id: parts.keepsake_id,
subject: SubjectRef::new(parts.subject_kind, parts.subject_id)?,
relation_id: parts.relation_id,
decision: parts.decision,
context: AuditContext {
attributes: parts.attributes,
},
},
})
}
fn revoke_audit_event(
at: DateTime<Utc>,
context: &CommandContext,
keepsake: &Keepsake,
) -> AuditEvent {
AuditEvent {
event_type: AuditEventType::Revoke,
at,
actor: context.actor.clone(),
keepsake_id: keepsake.id(),
subject: keepsake.subject().clone(),
relation_id: keepsake.relation_id(),
decision: AuditDecision::Revoked,
context: audit_context_from_command(context),
}
}
pub(super) fn revoke_event(command: &RevokeKeepsake, keepsake: &Keepsake) -> AuditEvent {
revoke_audit_event(command.at, &command.context, keepsake)
}
pub(super) fn revoke_by_subject_event(
command: &RevokeBySubject,
keepsake: &Keepsake,
) -> AuditEvent {
revoke_audit_event(command.at, &command.context, keepsake)
}
pub(super) fn expiry_event(
at: DateTime<Utc>,
cause: ExpiryCause,
keepsake_id: KeepsakeId,
relation_id: RelationId,
subject_kind: impl Into<String>,
subject_id: impl Into<String>,
) -> RepositoryResult<AuditEvent> {
Ok(AuditEvent {
event_type: match cause {
ExpiryCause::Timed => AuditEventType::TimedExpiry,
ExpiryCause::Fulfilled => AuditEventType::FulfillmentExpiry,
},
at,
actor: ActorRef::new("system", "keepsake-expiry")?,
keepsake_id,
subject: SubjectRef::new(subject_kind, subject_id)?,
relation_id,
decision: AuditDecision::Expired { cause },
context: AuditContext::default(),
})
}