use blindplane_core::{Author, open, seal};
use blindplane_crypto::aead::Suite;
use blindplane_wire::{RecordContext, SealedRecord};
use crate::AccessError;
use crate::codec::{
AccessValidationPolicy, Cursor, push_bytes, push_header, push_string, validate_identifier,
};
use crate::grant::RoleKeypair;
const AUDIT_EVENT_TAG: u8 = 5;
const AUDIT_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AuditEventKind {
Request,
Response,
CapabilityUsage,
PolicyDecision,
Custom,
}
impl AuditEventKind {
const fn code(self) -> u8 {
match self {
Self::Request => 1,
Self::Response => 2,
Self::CapabilityUsage => 3,
Self::PolicyDecision => 4,
Self::Custom => 5,
}
}
fn from_code(code: u8) -> Result<Self, AccessError> {
match code {
1 => Ok(Self::Request),
2 => Ok(Self::Response),
3 => Ok(Self::CapabilityUsage),
4 => Ok(Self::PolicyDecision),
5 => Ok(Self::Custom),
_ => Err(AccessError::WrongObjectType),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuditEvent {
pub tenant_id: String,
pub subject_id: String,
pub session_id: String,
pub sequence: u64,
pub timestamp: u64,
pub kind: AuditEventKind,
pub media_type: String,
pub body: Vec<u8>,
}
impl AuditEvent {
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(
96 + self.tenant_id.len()
+ self.subject_id.len()
+ self.session_id.len()
+ self.media_type.len()
+ self.body.len(),
);
push_header(&mut out, AUDIT_EVENT_TAG);
push_string(&mut out, &self.tenant_id);
push_string(&mut out, &self.subject_id);
push_string(&mut out, &self.session_id);
out.extend_from_slice(&self.sequence.to_be_bytes());
out.extend_from_slice(&self.timestamp.to_be_bytes());
out.push(self.kind.code());
push_string(&mut out, &self.media_type);
push_bytes(&mut out, &self.body);
out
}
pub fn decode(bytes: &[u8]) -> Result<Self, AccessError> {
Self::decode_with(bytes, &AccessValidationPolicy::default())
}
pub fn decode_with(bytes: &[u8], limits: &AccessValidationPolicy) -> Result<Self, AccessError> {
let mut cursor = Cursor::new(bytes);
cursor.take_header(AUDIT_EVENT_TAG)?;
let event = Self {
tenant_id: cursor.take_string(limits.max_identifier_bytes)?,
subject_id: cursor.take_string(limits.max_identifier_bytes)?,
session_id: cursor.take_string(limits.max_identifier_bytes)?,
sequence: cursor.take_u64()?,
timestamp: cursor.take_u64()?,
kind: AuditEventKind::from_code(cursor.take_u8()?)?,
media_type: cursor.take_string(limits.max_identifier_bytes)?,
body: cursor.take_bytes(limits.max_event_body_bytes)?.to_vec(),
};
if !cursor.is_empty() {
return Err(AccessError::TrailingBytes);
}
event.validate(limits)?;
if event.encode() != bytes {
return Err(AccessError::NonCanonicalEncoding);
}
Ok(event)
}
fn validate(&self, limits: &AccessValidationPolicy) -> Result<(), AccessError> {
validate_identifier(&self.tenant_id, limits.max_identifier_bytes)?;
validate_identifier(&self.subject_id, limits.max_identifier_bytes)?;
validate_identifier(&self.session_id, limits.max_identifier_bytes)?;
validate_identifier(&self.media_type, limits.max_identifier_bytes)?;
if self.sequence == 0 {
return Err(AccessError::InvalidEpoch);
}
if self.body.len() > limits.max_event_body_bytes {
return Err(AccessError::LengthLimit(self.body.len()));
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuditContext {
pub tenant_id: String,
pub subject_id: String,
pub stream_id: String,
pub epoch: u64,
pub sequence: u64,
}
pub fn seal_audit_event(
author: &Author,
context: AuditContext,
event: &AuditEvent,
subject_role: &RoleKeypair,
administrator_role: &RoleKeypair,
suite: Suite,
) -> Result<SealedRecord, AccessError> {
let limits = AccessValidationPolicy::default();
validate_context(&context, &limits)?;
event.validate(&limits)?;
if event.tenant_id != context.tenant_id
|| event.subject_id != context.subject_id
|| event.session_id != context.stream_id
|| event.sequence != context.sequence
|| subject_role.tenant_id() != context.tenant_id
|| administrator_role.tenant_id() != context.tenant_id
|| subject_role.scope() != administrator_role.scope()
{
return Err(AccessError::SubjectMismatch);
}
if subject_role.role_id() == administrator_role.role_id()
&& subject_role.key_epoch() == administrator_role.key_epoch()
{
return Err(AccessError::InvalidKeyIdentity);
}
let recipients = [subject_role.recipient()?, administrator_role.recipient()?];
seal(
author,
RecordContext {
tenant: context.tenant_id,
object_id: context.subject_id,
field: context.stream_id,
epoch: context.epoch,
version: context.sequence,
schema_version: AUDIT_SCHEMA_VERSION,
},
&event.encode(),
&recipients,
vec![],
suite,
)
.map_err(|_| AccessError::CryptographicFailure)
}
pub fn open_audit_event(
record: &SealedRecord,
role: &RoleKeypair,
expected_author: [u8; 32],
) -> Result<AuditEvent, AccessError> {
let recipient = role.recipient_keypair()?;
let plaintext =
open(record, &recipient, expected_author).map_err(|_| AccessError::CryptographicFailure)?;
let event = AuditEvent::decode(plaintext.as_bytes())?;
if record.context.schema_version != AUDIT_SCHEMA_VERSION
|| record.context.tenant != event.tenant_id
|| record.context.object_id != event.subject_id
|| record.context.field != event.session_id
|| record.context.version != event.sequence
{
return Err(AccessError::SubjectMismatch);
}
Ok(event)
}
fn validate_context(
context: &AuditContext,
limits: &AccessValidationPolicy,
) -> Result<(), AccessError> {
validate_identifier(&context.tenant_id, limits.max_identifier_bytes)?;
validate_identifier(&context.subject_id, limits.max_identifier_bytes)?;
validate_identifier(&context.stream_id, limits.max_identifier_bytes)?;
if context.epoch == 0 || context.sequence == 0 {
return Err(AccessError::InvalidEpoch);
}
Ok(())
}