gatekeep_sqlx/audit/
attempt.rs1use dovecote::{EventData, EventId, EventType, NewEvent, PagedEvent, TenantId};
2use gatekeep::AuthorizationAttempt;
3use thiserror::Error;
4
5use super::{DECISION_AUDIT_CONTENT_TYPE, DecisionAuditConfig};
6
7pub const ATTEMPT_AUDIT_EVENT_TYPE: &str = "gatekeep.authorization_attempt_failed";
9
10#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum AttemptAuditEventError {
14 #[error("authorization attempt payload exceeds size limit")]
16 PayloadTooLarge,
17 #[error(transparent)]
19 Attempt(#[from] gatekeep::AttemptValidationError),
20 #[error(transparent)]
22 Event(#[from] dovecote::ValidationError),
23 #[error(transparent)]
25 Json(#[from] serde_json::Error),
26 #[error("authorization attempt event does not match its payload")]
28 Shape,
29}
30
31pub(super) fn event_from_attempt(
32 config: &DecisionAuditConfig,
33 entry: &AuthorizationAttempt,
34) -> Result<(TenantId, NewEvent), AttemptAuditEventError> {
35 entry.validate()?;
36 let payload = serde_json::to_vec(entry)?;
37 if payload.len() > super::MAX_AUDIT_PAYLOAD_BYTES {
38 return Err(AttemptAuditEventError::PayloadTooLarge);
39 }
40
41 let event = NewEvent::builder(
42 config.stream().clone(),
43 EventId::new(format!(
44 "gatekeep-attempt-{}",
45 entry.occurrence().decision_audit_id()
46 ))?,
47 config.source().clone(),
48 EventType::new(ATTEMPT_AUDIT_EVENT_TYPE)?,
49 )
50 .time(entry.occurrence().occurred_at())
51 .datacontenttype(dovecote::ContentType::new(DECISION_AUDIT_CONTENT_TYPE)?)
52 .data(EventData::json(payload)?)
53 .build()?;
54 Ok((TenantId::new(entry.tenant().as_str())?, event))
55}
56
57pub fn decode_authorization_attempt(
63 config: &DecisionAuditConfig,
64 paged: &PagedEvent,
65) -> Result<AuthorizationAttempt, AttemptAuditEventError> {
66 let event = paged.event();
67 if event.stream() != config.stream()
68 || event.source() != config.source()
69 || event.event_type().as_str() != ATTEMPT_AUDIT_EVENT_TYPE
70 || event.datacontenttype().map(dovecote::ContentType::as_str)
71 != Some(DECISION_AUDIT_CONTENT_TYPE)
72 {
73 return Err(AttemptAuditEventError::Shape);
74 }
75
76 let Some(EventData::Json(payload)) = event.data() else {
77 return Err(AttemptAuditEventError::Shape);
78 };
79
80 if payload.as_bytes().len() > super::MAX_AUDIT_PAYLOAD_BYTES {
81 return Err(AttemptAuditEventError::Shape);
82 }
83
84 let entry: AuthorizationAttempt = serde_json::from_slice(payload.as_bytes())?;
85 if entry.tenant().as_str() != paged.tenant_id().as_str()
86 || event.time() != Some(entry.occurrence().occurred_at())
87 || event.id().as_str()
88 != format!(
89 "gatekeep-attempt-{}",
90 entry.occurrence().decision_audit_id()
91 )
92 {
93 return Err(AttemptAuditEventError::Shape);
94 }
95
96 entry.validate()?;
97 Ok(entry)
98}