use std::marker::PhantomData;
use std::time::SystemTime;
use crate::audit::{MalformedRecordReason, SubstrateAuditEvent, SubstrateAuditSink};
use crate::authority::v1::ViewPrivate;
use crate::authority::BoundUserProof;
use crate::identity::TraceId;
use crate::proto::{Did, Nsid};
use crate::sealed;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReadPipelineStage {
AudienceCheck,
ContentValidation,
Decode,
}
#[derive(Debug, Clone)]
pub struct ReadAuthorization {
reader: Did,
_token: PhantomData<sealed::Token>,
}
impl ReadAuthorization {
#[must_use]
pub fn from_view_private(proof: &BoundUserProof<'_, ViewPrivate>) -> Self {
ReadAuthorization {
reader: proof.requester().clone(),
_token: PhantomData,
}
}
#[must_use]
pub fn reader(&self) -> &Did {
&self.reader
}
#[cfg(test)]
pub(crate) fn new_for_test(reader: Did) -> Self {
ReadAuthorization {
reader,
_token: PhantomData,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordValidation {
PostPrivate {
has_text: bool,
has_encoded_content: bool,
has_encoded_content_codec: bool,
has_encoded_content_generation: bool,
},
Audience {
mode_is_list: bool,
has_members: bool,
},
}
impl RecordValidation {
#[must_use]
pub fn post_private(
has_text: bool,
has_encoded_content: bool,
has_encoded_content_codec: bool,
has_encoded_content_generation: bool,
) -> Self {
RecordValidation::PostPrivate {
has_text,
has_encoded_content,
has_encoded_content_codec,
has_encoded_content_generation,
}
}
#[must_use]
pub fn audience(mode_is_list: bool, has_members: bool) -> Self {
RecordValidation::Audience {
mode_is_list,
has_members,
}
}
}
pub fn validate_record(input: &RecordValidation) -> Result<(), MalformedRecordReason> {
use MalformedRecordReason as R;
match *input {
RecordValidation::PostPrivate {
has_text,
has_encoded_content,
has_encoded_content_codec,
has_encoded_content_generation,
} => {
if has_text && has_encoded_content {
return Err(R::BothTextAndEncodedContent);
}
if has_encoded_content {
if !has_encoded_content_codec {
return Err(R::EncodedContentWithoutCodec);
}
return Ok(());
}
if has_text {
if has_encoded_content_codec {
return Err(R::EncodedContentCodecWithoutEncodedContent);
}
if has_encoded_content_generation {
return Err(R::TextWithEncodedContentGeneration);
}
return Ok(());
}
if has_encoded_content_codec {
return Err(R::EncodedContentCodecWithoutEncodedContent);
}
if has_encoded_content_generation {
return Err(R::EncodedContentGenerationWithoutEncodedContent);
}
Err(R::NeitherTextNorEncodedContent)
}
RecordValidation::Audience {
mode_is_list,
has_members,
} => {
if mode_is_list && !has_members {
return Err(R::ListModeWithoutMembers);
}
Ok(())
}
}
}
pub fn validate_record_for_write(
input: &RecordValidation,
nsid: Nsid,
requester: Did,
trace_id: TraceId,
sink: &dyn SubstrateAuditSink,
at: SystemTime,
) -> Result<(), MalformedRecordReason> {
validate_record(input).inspect_err(|&reason| {
emit_rejected(sink, trace_id, nsid, requester, reason, at);
})
}
pub fn validate_record_for_read(
authz: &ReadAuthorization,
input: &RecordValidation,
nsid: Nsid,
trace_id: TraceId,
sink: &dyn SubstrateAuditSink,
at: SystemTime,
) -> Result<(), MalformedRecordReason> {
validate_record(input).inspect_err(|&reason| {
emit_rejected(sink, trace_id, nsid, authz.reader().clone(), reason, at);
})
}
fn emit_rejected(
sink: &dyn SubstrateAuditSink,
trace_id: TraceId,
nsid: Nsid,
requester: Did,
reason: MalformedRecordReason,
at: SystemTime,
) {
let _ = sink.record(SubstrateAuditEvent::MalformedRecordRejected {
trace_id,
nsid,
requester,
reason,
at,
});
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
use crate::audit::AuditError;
fn pp(text: bool, ec: bool, codec: bool, generation: bool) -> RecordValidation {
RecordValidation::post_private(text, ec, codec, generation)
}
#[test]
fn post_private_valid_records_pass() {
assert!(validate_record(&pp(true, false, false, false)).is_ok());
assert!(validate_record(&pp(false, true, true, false)).is_ok());
assert!(validate_record(&pp(false, true, true, true)).is_ok());
}
#[test]
fn post_private_every_reason_is_reachable() {
use MalformedRecordReason as R;
assert_eq!(validate_record(&pp(true, true, false, false)), Err(R::BothTextAndEncodedContent));
assert_eq!(validate_record(&pp(false, false, false, false)), Err(R::NeitherTextNorEncodedContent));
assert_eq!(validate_record(&pp(false, true, false, false)), Err(R::EncodedContentWithoutCodec));
assert_eq!(validate_record(&pp(true, false, true, false)), Err(R::EncodedContentCodecWithoutEncodedContent));
assert_eq!(validate_record(&pp(false, false, true, false)), Err(R::EncodedContentCodecWithoutEncodedContent));
assert_eq!(validate_record(&pp(true, false, false, true)), Err(R::TextWithEncodedContentGeneration));
assert_eq!(validate_record(&pp(false, false, false, true)), Err(R::EncodedContentGenerationWithoutEncodedContent));
}
#[test]
fn audience_members_rule() {
use MalformedRecordReason as R;
assert_eq!(
validate_record(&RecordValidation::audience(true, false)),
Err(R::ListModeWithoutMembers)
);
assert!(validate_record(&RecordValidation::audience(true, true)).is_ok());
assert!(validate_record(&RecordValidation::audience(false, false)).is_ok());
}
#[derive(Default)]
struct CapturingSubstrateSink {
events: Mutex<Vec<SubstrateAuditEvent>>,
}
impl SubstrateAuditSink for CapturingSubstrateSink {
fn record(&self, event: SubstrateAuditEvent) -> Result<(), AuditError> {
self.events.lock().unwrap().push(event);
Ok(())
}
}
fn sample_did() -> Did {
Did::new("did:plc:exampleexampleexample").unwrap()
}
#[test]
fn validate_for_write_emits_on_violation_no_emit_on_ok() {
let sink = CapturingSubstrateSink::default();
let nsid = Nsid::new("tools.kryphocron.feed.postPrivate").unwrap();
let now = SystemTime::now();
let err = validate_record_for_write(
&pp(true, true, false, false),
nsid.clone(),
sample_did(),
TraceId::from_bytes([1; 16]),
&sink,
now,
)
.unwrap_err();
assert_eq!(err, MalformedRecordReason::BothTextAndEncodedContent);
assert!(matches!(
sink.events.lock().unwrap().as_slice(),
[SubstrateAuditEvent::MalformedRecordRejected { .. }]
));
let sink2 = CapturingSubstrateSink::default();
validate_record_for_write(
&pp(true, false, false, false),
nsid,
sample_did(),
TraceId::from_bytes([1; 16]),
&sink2,
now,
)
.unwrap();
assert!(sink2.events.lock().unwrap().is_empty());
}
#[test]
fn validate_for_read_uses_witness_reader_as_requester() {
let sink = CapturingSubstrateSink::default();
let reader = sample_did();
let authz = ReadAuthorization::new_for_test(reader.clone());
let err = validate_record_for_read(
&authz,
&pp(false, false, false, false),
Nsid::new("tools.kryphocron.feed.postPrivate").unwrap(),
TraceId::from_bytes([2; 16]),
&sink,
SystemTime::now(),
)
.unwrap_err();
assert_eq!(err, MalformedRecordReason::NeitherTextNorEncodedContent);
let events = sink.events.lock().unwrap();
let SubstrateAuditEvent::MalformedRecordRejected { requester, .. } = &events[0] else {
panic!("expected MalformedRecordRejected");
};
assert_eq!(requester, &reader);
}
}