1use blindplane_core::{Author, open, seal};
4use blindplane_crypto::aead::Suite;
5use blindplane_wire::{RecordContext, SealedRecord};
6
7use crate::AccessError;
8use crate::codec::{
9 AccessValidationPolicy, Cursor, push_bytes, push_header, push_string, validate_identifier,
10};
11use crate::grant::RoleKeypair;
12
13const AUDIT_EVENT_TAG: u8 = 5;
14const AUDIT_SCHEMA_VERSION: u32 = 1;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum AuditEventKind {
19 Request,
21 Response,
23 CapabilityUsage,
25 PolicyDecision,
27 Custom,
29}
30
31impl AuditEventKind {
32 const fn code(self) -> u8 {
33 match self {
34 Self::Request => 1,
35 Self::Response => 2,
36 Self::CapabilityUsage => 3,
37 Self::PolicyDecision => 4,
38 Self::Custom => 5,
39 }
40 }
41
42 fn from_code(code: u8) -> Result<Self, AccessError> {
43 match code {
44 1 => Ok(Self::Request),
45 2 => Ok(Self::Response),
46 3 => Ok(Self::CapabilityUsage),
47 4 => Ok(Self::PolicyDecision),
48 5 => Ok(Self::Custom),
49 _ => Err(AccessError::WrongObjectType),
50 }
51 }
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct AuditEvent {
57 pub tenant_id: String,
59 pub subject_id: String,
61 pub session_id: String,
63 pub sequence: u64,
65 pub timestamp: u64,
67 pub kind: AuditEventKind,
69 pub media_type: String,
71 pub body: Vec<u8>,
73}
74
75impl AuditEvent {
76 pub fn encode(&self) -> Vec<u8> {
78 let mut out = Vec::with_capacity(
79 96 + self.tenant_id.len()
80 + self.subject_id.len()
81 + self.session_id.len()
82 + self.media_type.len()
83 + self.body.len(),
84 );
85 push_header(&mut out, AUDIT_EVENT_TAG);
86 push_string(&mut out, &self.tenant_id);
87 push_string(&mut out, &self.subject_id);
88 push_string(&mut out, &self.session_id);
89 out.extend_from_slice(&self.sequence.to_be_bytes());
90 out.extend_from_slice(&self.timestamp.to_be_bytes());
91 out.push(self.kind.code());
92 push_string(&mut out, &self.media_type);
93 push_bytes(&mut out, &self.body);
94 out
95 }
96
97 pub fn decode(bytes: &[u8]) -> Result<Self, AccessError> {
99 Self::decode_with(bytes, &AccessValidationPolicy::default())
100 }
101
102 pub fn decode_with(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
104 let mut cursor = Cursor::new(bytes);
105 cursor.take_header(AUDIT_EVENT_TAG)?;
106 let event = Self {
107 tenant_id: cursor.take_string(limits.max_identifier_bytes)?,
108 subject_id: cursor.take_string(limits.max_identifier_bytes)?,
109 session_id: cursor.take_string(limits.max_identifier_bytes)?,
110 sequence: cursor.take_u64()?,
111 timestamp: cursor.take_u64()?,
112 kind: AuditEventKind::from_code(cursor.take_u8()?)?,
113 media_type: cursor.take_string(limits.max_identifier_bytes)?,
114 body: cursor.take_bytes(limits.max_event_body_bytes)?.to_vec(),
115 };
116 if !cursor.is_empty() {
117 return Err(AccessError::TrailingBytes);
118 }
119 event.validate(limits)?;
120 if event.encode() != bytes {
121 return Err(AccessError::NonCanonicalEncoding);
122 }
123 Ok(event)
124 }
125
126 fn validate(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
127 validate_identifier(&self.tenant_id, limits.max_identifier_bytes)?;
128 validate_identifier(&self.subject_id, limits.max_identifier_bytes)?;
129 validate_identifier(&self.session_id, limits.max_identifier_bytes)?;
130 validate_identifier(&self.media_type, limits.max_identifier_bytes)?;
131 if self.sequence == 0 {
132 return Err(AccessError::InvalidEpoch);
133 }
134 if self.body.len() > limits.max_event_body_bytes {
135 return Err(AccessError::LengthLimit(self.body.len()));
136 }
137 Ok(())
138 }
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
143pub struct AuditContext {
144 pub tenant_id: String,
146 pub subject_id: String,
148 pub stream_id: String,
150 pub epoch: u64,
152 pub sequence: u64,
154}
155
156pub fn seal_audit_event(
158 author: &Author,
159 context: AuditContext,
160 event: &AuditEvent,
161 subject_role: &RoleKeypair,
162 administrator_role: &RoleKeypair,
163 suite: Suite,
164) -> Result<SealedRecord, AccessError> {
165 let limits = AccessValidationPolicy::default();
166 validate_context(&context, &limits)?;
167 event.validate(&limits)?;
168 if event.tenant_id != context.tenant_id
169 || event.subject_id != context.subject_id
170 || event.session_id != context.stream_id
171 || event.sequence != context.sequence
172 || subject_role.tenant_id() != context.tenant_id
173 || administrator_role.tenant_id() != context.tenant_id
174 || subject_role.scope() != administrator_role.scope()
175 {
176 return Err(AccessError::SubjectMismatch);
177 }
178 if subject_role.role_id() == administrator_role.role_id()
179 && subject_role.key_epoch() == administrator_role.key_epoch()
180 {
181 return Err(AccessError::InvalidKeyIdentity);
182 }
183 let recipients = [subject_role.recipient()?, administrator_role.recipient()?];
184 seal(
185 author,
186 RecordContext {
187 tenant: context.tenant_id,
188 object_id: context.subject_id,
189 field: context.stream_id,
190 epoch: context.epoch,
191 version: context.sequence,
192 schema_version: AUDIT_SCHEMA_VERSION,
193 },
194 &event.encode(),
195 &recipients,
196 vec![],
197 suite,
198 )
199 .map_err(|_| AccessError::CryptographicFailure)
200}
201
202pub fn open_audit_event(
204 record: &SealedRecord,
205 role: &RoleKeypair,
206 expected_author: [u8; 32],
207) -> Result<AuditEvent, AccessError> {
208 let recipient = role.recipient_keypair()?;
209 let plaintext =
210 open(record, &recipient, expected_author).map_err(|_| AccessError::CryptographicFailure)?;
211 let event = AuditEvent::decode(plaintext.as_bytes())?;
212 if record.context.schema_version != AUDIT_SCHEMA_VERSION
213 || record.context.tenant != event.tenant_id
214 || record.context.object_id != event.subject_id
215 || record.context.field != event.session_id
216 || record.context.version != event.sequence
217 {
218 return Err(AccessError::SubjectMismatch);
219 }
220 Ok(event)
221}
222
223fn validate_context(
224 context: &AuditContext,
225 limits: &AccessValidationPolicy,
226) -> Result<(), AccessError> {
227 validate_identifier(&context.tenant_id, limits.max_identifier_bytes)?;
228 validate_identifier(&context.subject_id, limits.max_identifier_bytes)?;
229 validate_identifier(&context.stream_id, limits.max_identifier_bytes)?;
230 if context.epoch == 0 || context.sequence == 0 {
231 return Err(AccessError::InvalidEpoch);
232 }
233 Ok(())
234}