use std::fmt;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use jiff::Timestamp;
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesText, Event};
use secrecy::{ExposeSecret, SecretSlice, SecretString};
use crate::escape;
use crate::redact::REDACTED;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodedValue {
pub code: String,
pub system_name: String,
pub original_text: String,
}
impl CodedValue {
#[must_use]
pub fn new(code: &str, system_name: &str, original_text: &str) -> Self {
Self {
code: code.to_owned(),
system_name: system_name.to_owned(),
original_text: original_text.to_owned(),
}
}
#[must_use]
pub fn of((code, system_name, original_text): (&str, &str, &str)) -> Self {
Self::new(code, system_name, original_text)
}
}
#[derive(Debug, Clone)]
pub struct EventIdentification {
pub event_id: CodedValue,
pub action: &'static str,
pub date_time: Timestamp,
pub outcome: &'static str,
pub type_codes: Vec<CodedValue>,
pub outcome_description: Option<String>,
pub purposes_of_use: Vec<CodedValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessPoint {
pub type_code: &'static str,
pub id: String,
}
#[derive(Clone)]
pub struct ActiveParticipant {
pub user_id: String,
pub alternative_user_id: Option<String>,
pub user_name: Option<String>,
pub user_is_requestor: bool,
pub role_id_codes: Vec<CodedValue>,
pub access_point: Option<AccessPoint>,
}
impl fmt::Debug for ActiveParticipant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ActiveParticipant")
.field("user_id", &REDACTED)
.field("alternative_user_id", &self.alternative_user_id)
.field("user_name", &self.user_name.as_ref().map(|_| REDACTED))
.field("user_is_requestor", &self.user_is_requestor)
.field("role_id_codes", &self.role_id_codes)
.field("access_point", &self.access_point)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditSource {
pub id: String,
pub enterprise_site: Option<String>,
pub type_codes: Vec<SourceType>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceType {
pub code: String,
pub system_name: Option<String>,
pub original_text: Option<String>,
}
impl SourceType {
#[must_use]
pub fn of(code: &str) -> Self {
Self {
code: code.to_owned(),
system_name: None,
original_text: None,
}
}
}
#[derive(Clone)]
pub struct ParticipantObject {
pub id: SecretString,
pub type_code: Option<&'static str>,
pub type_code_role: Option<&'static str>,
pub data_life_cycle: Option<&'static str>,
pub id_type_code: CodedValue,
pub name: Option<SecretString>,
pub query: Option<SecretString>,
pub details: Vec<(String, SecretString)>,
}
impl fmt::Debug for ParticipantObject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParticipantObject")
.field("id", &REDACTED)
.field("type_code", &self.type_code)
.field("type_code_role", &self.type_code_role)
.field("data_life_cycle", &self.data_life_cycle)
.field("id_type_code", &self.id_type_code)
.field("name", &self.name.as_ref().map(|_| REDACTED))
.field("query", &self.query.as_ref().map(|_| REDACTED))
.field(
"details",
&self
.details
.iter()
.map(|(kind, _)| (kind.as_str(), REDACTED))
.collect::<Vec<_>>(),
)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct AuditMessage {
pub event: EventIdentification,
pub participants: Vec<ActiveParticipant>,
pub source: AuditSource,
pub objects: Vec<ParticipantObject>,
}
#[derive(Debug, thiserror::Error)]
#[error("the audit message could not be written")]
pub struct MessageError(#[source] std::io::Error);
impl AuditMessage {
pub fn to_xml(&self) -> Result<SecretSlice<u8>, MessageError> {
let mut writer = Writer::new(Vec::new());
writer
.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.map_err(MessageError)?;
writer
.create_element("AuditMessage")
.write_inner_content(|writer| {
self.write_event(writer)?;
for participant in &self.participants {
write_participant(writer, participant)?;
}
self.write_source(writer)?;
for object in &self.objects {
write_object(writer, object)?;
}
Ok(())
})
.map_err(MessageError)?;
Ok(SecretSlice::from(writer.into_inner()))
}
fn write_event(&self, writer: &mut Writer<Vec<u8>>) -> std::io::Result<()> {
let event = &self.event;
let date_time = event.date_time.to_string();
writer
.create_element("EventIdentification")
.with_attributes([
("EventActionCode", escape::untrusted(event.action).as_ref()),
("EventDateTime", date_time.as_str()),
(
"EventOutcomeIndicator",
escape::untrusted(event.outcome).as_ref(),
),
])
.write_inner_content(|writer| {
write_coded(writer, "EventID", &event.event_id)?;
for code in &event.type_codes {
write_coded(writer, "EventTypeCode", code)?;
}
if let Some(description) = &event.outcome_description {
writer
.create_element("EventOutcomeDescription")
.write_text_content(BytesText::new(&escape::untrusted(description)))?;
}
for purpose in &event.purposes_of_use {
write_coded(writer, "PurposeOfUse", purpose)?;
}
Ok(())
})?;
Ok(())
}
fn write_source(&self, writer: &mut Writer<Vec<u8>>) -> std::io::Result<()> {
let source = &self.source;
let mut attributes = Vec::new();
if let Some(site) = &source.enterprise_site {
attributes.push(("AuditEnterpriseSiteID", escape::untrusted(site)));
}
attributes.push(("AuditSourceID", escape::untrusted(&source.id)));
let attributes: Vec<(&str, &str)> = attributes
.iter()
.map(|(name, value)| (*name, value.as_ref()))
.collect();
let element = writer
.create_element("AuditSourceIdentification")
.with_attributes(attributes);
if source.type_codes.is_empty() {
element.write_empty()?;
return Ok(());
}
element.write_inner_content(|writer| {
for kind in &source.type_codes {
let mut attributes = vec![("csd-code", escape::untrusted(&kind.code))];
if let Some(system) = &kind.system_name {
attributes.push(("codeSystemName", escape::untrusted(system)));
}
if let Some(text) = &kind.original_text {
attributes.push(("originalText", escape::untrusted(text)));
}
writer
.create_element("AuditSourceTypeCode")
.with_attributes(
attributes
.iter()
.map(|(name, value)| (*name, value.as_ref())),
)
.write_empty()?;
}
Ok(())
})?;
Ok(())
}
}
fn write_coded(
writer: &mut Writer<Vec<u8>>,
element: &str,
code: &CodedValue,
) -> std::io::Result<()> {
writer
.create_element(element)
.with_attributes([
("csd-code", escape::untrusted(&code.code).as_ref()),
(
"codeSystemName",
escape::untrusted(&code.system_name).as_ref(),
),
(
"originalText",
escape::untrusted(&code.original_text).as_ref(),
),
])
.write_empty()?;
Ok(())
}
fn write_participant(
writer: &mut Writer<Vec<u8>>,
participant: &ActiveParticipant,
) -> std::io::Result<()> {
let mut attributes = vec![("UserID", escape::untrusted(&participant.user_id))];
if let Some(alternative) = &participant.alternative_user_id {
attributes.push(("AlternativeUserID", escape::untrusted(alternative)));
}
if let Some(name) = &participant.user_name {
attributes.push(("UserName", escape::untrusted(name)));
}
attributes.push((
"UserIsRequestor",
std::borrow::Cow::Borrowed(if participant.user_is_requestor {
"true"
} else {
"false"
}),
));
if let Some(point) = &participant.access_point {
attributes.push(("NetworkAccessPointID", escape::untrusted(&point.id)));
attributes.push((
"NetworkAccessPointTypeCode",
escape::untrusted(point.type_code),
));
}
writer
.create_element("ActiveParticipant")
.with_attributes(
attributes
.iter()
.map(|(name, value)| (*name, value.as_ref())),
)
.write_inner_content(|writer| {
for code in &participant.role_id_codes {
write_coded(writer, "RoleIDCode", code)?;
}
Ok(())
})?;
Ok(())
}
fn write_object(writer: &mut Writer<Vec<u8>>, object: &ParticipantObject) -> std::io::Result<()> {
let mut attributes = vec![(
"ParticipantObjectID",
escape::untrusted(object.id.expose_secret()),
)];
for (name, value) in [
("ParticipantObjectTypeCode", object.type_code),
("ParticipantObjectTypeCodeRole", object.type_code_role),
("ParticipantObjectDataLifeCycle", object.data_life_cycle),
] {
if let Some(value) = value {
attributes.push((name, escape::untrusted(value)));
}
}
let details: Vec<(std::borrow::Cow<'_, str>, String)> = object
.details
.iter()
.map(|(kind, value)| {
(
escape::untrusted(kind),
STANDARD.encode(value.expose_secret()),
)
})
.collect();
writer
.create_element("ParticipantObjectIdentification")
.with_attributes(
attributes
.iter()
.map(|(name, value)| (*name, value.as_ref())),
)
.write_inner_content(|writer| {
write_coded(writer, "ParticipantObjectIDTypeCode", &object.id_type_code)?;
if let Some(query) = &object.query {
writer
.create_element("ParticipantObjectQuery")
.write_text_content(BytesText::new(&STANDARD.encode(query.expose_secret())))?;
} else if let Some(name) = &object.name {
writer
.create_element("ParticipantObjectName")
.write_text_content(BytesText::new(&escape::untrusted(name.expose_secret())))?;
}
for (kind, value) in &details {
writer
.create_element("ParticipantObjectDetail")
.with_attributes([("type", kind.as_ref()), ("value", value.as_str())])
.write_empty()?;
}
Ok(())
})?;
Ok(())
}