use std::sync::Arc;
use async_trait::async_trait;
use crate::core::{Digest, Epoch, RunId, StoreError, TenantId};
use crate::journal::{
Append, AtomicJournal, AtomicTx, AtomicWork, Cancellation, Checkpoint, Head, Inclusion,
JournalStore, Lease, Record, payload,
};
use super::KeyRing;
#[derive(Debug)]
pub struct SealedJournal {
inner: Arc<dyn JournalStore>,
keys: Arc<dyn KeyRing>,
tenant: TenantId,
}
impl SealedJournal {
#[must_use]
pub fn wrap(
inner: Arc<dyn JournalStore>,
keys: Arc<dyn KeyRing>,
tenant: TenantId,
) -> Arc<Self> {
super::assert_serves(inner.tenant(), &tenant, "journal");
Arc::new(Self {
inner,
keys,
tenant,
})
}
fn scope_for(&self, run: RunId, case: Option<crate::core::CaseId>) -> String {
case.map_or_else(
|| super::scope(&self.tenant, &run.to_string()),
|c| super::scope(&self.tenant, &c.to_string()),
)
}
fn aad(
&self,
run: RunId,
kind: &crate::journal::RecordKind,
effect: Option<crate::core::EffectKey>,
) -> String {
journal_aad(self.tenant.as_str(), run, kind, effect)
}
}
fn sealing(e: &super::KeyError) -> StoreError {
StoreError::Backend(format!("sealing a journal payload failed: {e}"))
}
#[async_trait]
impl JournalStore for SealedJournal {
fn is_shared(&self) -> bool {
self.inner.is_shared()
}
fn seals(&self) -> bool {
true
}
fn tenant(&self) -> &str {
self.inner.tenant()
}
async fn append(&self, epoch: Epoch, batch: Vec<Append>) -> Result<Vec<Record>, StoreError> {
let (sealed, plain) = self.seal_batch(batch).await?;
let written = self.inner.append(epoch, sealed).await?;
self.reopened(written, plain).await
}
fn atomic(&self) -> Option<&dyn AtomicJournal> {
self.inner.atomic().map(|_| self as &dyn AtomicJournal)
}
async fn read(&self, run: RunId, from: crate::core::Seq) -> Result<Vec<Record>, StoreError> {
let records = self.inner.read(run, from).await?;
self.open_all(records).await
}
async fn read_page(
&self,
run: RunId,
from: crate::core::Seq,
limit: usize,
) -> Result<Vec<Record>, StoreError> {
let records = self.inner.read_page(run, from, limit).await?;
self.open_all(records).await
}
async fn case_history(
&self,
case: crate::core::CaseId,
limit: usize,
) -> Result<Vec<Record>, StoreError> {
let records = self.inner.case_history(case, limit).await?;
self.open_all(records).await
}
async fn acquire(
&self,
run: RunId,
owner: &str,
ttl: std::time::Duration,
) -> Result<Lease, StoreError> {
self.inner.acquire(run, owner, ttl).await
}
async fn renew(
&self,
run: RunId,
owner: &str,
epoch: Epoch,
ttl: std::time::Duration,
) -> Result<Lease, StoreError> {
self.inner.renew(run, owner, epoch, ttl).await
}
async fn release_lease(&self, run: RunId, epoch: Epoch) -> Result<(), StoreError> {
self.inner.release_lease(run, epoch).await
}
async fn abandoned_runs(&self, limit: usize) -> Result<Vec<RunId>, StoreError> {
self.inner.abandoned_runs(limit).await
}
async fn waiting_runs(
&self,
limit: usize,
) -> Result<Vec<crate::journal::WaitingRun>, StoreError> {
self.inner.waiting_runs(limit).await
}
async fn runs_by_outcome(&self, outcome: &str, limit: usize) -> Result<Vec<RunId>, StoreError> {
self.inner.runs_by_outcome(outcome, limit).await
}
async fn count_by_outcome(&self, outcome: &str) -> Result<u64, StoreError> {
self.inner.count_by_outcome(outcome).await
}
async fn admitted_as(&self, key: &str) -> Result<Option<RunId>, StoreError> {
self.inner.admitted_as(key).await
}
async fn forget_admissions(
&self,
older_than: crate::core::Timestamp,
) -> Result<usize, StoreError> {
self.inner.forget_admissions(older_than).await
}
async fn runs_by_id(
&self,
after: Option<RunId>,
limit: usize,
) -> Result<Vec<RunId>, StoreError> {
self.inner.runs_by_id(after, limit).await
}
async fn recent_runs(
&self,
after: Option<(u64, RunId)>,
limit: usize,
) -> Result<Vec<(RunId, u64)>, StoreError> {
self.inner.recent_runs(after, limit).await
}
async fn recent_runs_from(
&self,
source: &str,
after: Option<(u64, RunId)>,
limit: usize,
) -> Result<Vec<(RunId, u64)>, StoreError> {
self.inner.recent_runs_from(source, after, limit).await
}
async fn head(&self, run: RunId) -> Result<Head, StoreError> {
self.inner.head(run).await
}
async fn seal(&self, run: RunId, epoch: Epoch, outcome: &str) -> Result<Digest, StoreError> {
self.inner.seal(run, epoch, outcome).await
}
async fn checkpoint(&self) -> Result<Checkpoint, StoreError> {
self.inner.checkpoint().await
}
async fn consistency_proof(&self, old_size: u64) -> Result<Vec<Digest>, StoreError> {
self.inner.consistency_proof(old_size).await
}
async fn inclusion_proof(&self, run: RunId) -> Result<Option<Inclusion>, StoreError> {
self.inner.inclusion_proof(run).await
}
async fn inclusion_proof_at(
&self,
run: RunId,
size: u64,
) -> Result<Option<Inclusion>, StoreError> {
self.inner.inclusion_proof_at(run, size).await
}
async fn log_positions(
&self,
runs: &[RunId],
) -> Result<Vec<Option<(u64, crate::core::Digest)>>, StoreError> {
self.inner.log_positions(runs).await
}
async fn request_cancel(
&self,
run: RunId,
actor: &crate::core::Operator,
reason: &str,
) -> Result<bool, StoreError> {
self.inner.request_cancel(run, actor, reason).await
}
async fn cancellation(&self, run: RunId) -> Result<Option<Cancellation>, StoreError> {
self.inner.cancellation(run).await
}
}
struct SealingWork<'a> {
journal: &'a SealedJournal,
work: &'a dyn AtomicWork,
plain: std::sync::Mutex<Vec<crate::journal::RecordKind>>,
}
#[async_trait]
impl AtomicWork for SealingWork<'_> {
async fn run(&self, tx: &dyn AtomicTx) -> Result<Vec<Append>, crate::core::EffectError> {
let batch = self.work.run(tx).await?;
let (sealed, plain) = self.journal.seal_batch(batch).await.map_err(|e| {
crate::core::EffectError::Unavailable {
driver: "keyring".to_owned(),
detail: e.to_string(),
}
})?;
*self
.plain
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = plain;
Ok(sealed)
}
}
#[async_trait]
impl AtomicJournal for SealedJournal {
async fn append_atomic(
&self,
run: RunId,
epoch: Epoch,
work: &dyn AtomicWork,
) -> Result<Vec<Record>, StoreError> {
let Some(inner) = self.inner.atomic() else {
return Err(StoreError::Backend(
"the sealed store has no transaction a resource can join".to_owned(),
));
};
let sealing = SealingWork {
journal: self,
work,
plain: std::sync::Mutex::new(Vec::new()),
};
let written = inner.append_atomic(run, epoch, &sealing).await?;
let plain = sealing
.plain
.into_inner()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.reopened(written, plain).await
}
}
impl SealedJournal {
async fn seal_batch(
&self,
batch: Vec<Append>,
) -> Result<(Vec<Append>, Vec<crate::journal::RecordKind>), StoreError> {
let mut sealed = Vec::with_capacity(batch.len());
let mut plain = Vec::with_capacity(batch.len());
for mut entry in batch {
if entry.written().is_some() {
return Err(StoreError::Backend(
"a sealed journal cannot store written bytes: stored as they stand, \
their payloads would sit unsealed in a store that seals every payload, \
and sealed they would no longer hash as written — restore into the \
unwrapped store"
.to_owned(),
));
}
plain.push(entry.kind.clone());
let scope = self.scope_for(entry.run, entry.case);
let aad = self.aad(entry.run, &entry.kind, entry.effect_key);
for field in payload::payloads(&mut entry.kind) {
match field {
payload::SealedField::Value(field) => {
let plain = crate::core::canon::to_bytes(&*field).map_err(|e| {
StoreError::Backend(format!("a payload would not serialise: {e}"))
})?;
let envelope = super::envelope::seal(
self.keys.as_ref(),
&scope,
aad.as_bytes(),
&plain,
)
.await
.map_err(|e| sealing(&e))?;
*field = payload::wrap(&envelope);
}
payload::SealedField::Text(field) => {
let envelope = super::envelope::seal(
self.keys.as_ref(),
&scope,
aad.as_bytes(),
field.as_bytes(),
)
.await
.map_err(|e| sealing(&e))?;
*field = payload::wrap_text(&envelope);
}
}
}
sealed.push(entry);
}
Ok((sealed, plain))
}
async fn reopened(
&self,
written: Vec<Record>,
plain: Vec<crate::journal::RecordKind>,
) -> Result<Vec<Record>, StoreError> {
if written.len() != plain.len() {
return self.open_all(written).await;
}
Ok(written
.into_iter()
.zip(plain)
.map(|(record, kind)| record.with_opened_kind(kind))
.collect())
}
async fn open_all(&self, records: Vec<Record>) -> Result<Vec<Record>, StoreError> {
let mut out = Vec::with_capacity(records.len());
for record in records {
let mut kind = record.kind().clone();
let opened = open_payloads(
self.keys.as_ref(),
self.tenant.as_str(),
record.body.run,
record.effect_key(),
&mut kind,
)
.await?;
out.push(if opened.opened > 0 {
record.with_opened_kind(kind)
} else {
record
});
}
Ok(out)
}
}
pub(crate) fn journal_aad(
tenant: &str,
run: RunId,
kind: &crate::journal::RecordKind,
effect: Option<crate::core::EffectKey>,
) -> String {
format!(
"journal:{tenant}:{run}:{}:{}",
kind.kind_str(),
effect.map_or_else(|| "-".to_owned(), crate::core::EffectKey::to_hex),
)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct Opened {
pub(crate) opened: usize,
pub(crate) erased: usize,
}
pub(crate) async fn open_payloads(
keys: &dyn KeyRing,
tenant: &str,
run: RunId,
effect: Option<crate::core::EffectKey>,
kind: &mut crate::journal::RecordKind,
) -> Result<Opened, StoreError> {
let aad = journal_aad(tenant, run, kind, effect);
let mut found = Opened::default();
for field in payload::payloads(kind) {
match field {
payload::SealedField::Value(field) => {
let Some(envelope) = payload::unwrap(field) else {
continue;
};
match super::envelope::open_or_erased(keys, aad.as_bytes(), &envelope)
.await
.map_err(|e| StoreError::Backend(e.to_string()))?
{
Some(plain) => {
*field = serde_json::from_slice(&plain)?;
found.opened += 1;
}
None => found.erased += 1,
}
}
payload::SealedField::Text(field) => {
let Some(envelope) = payload::unwrap_text(field) else {
continue;
};
match super::envelope::open_or_erased(keys, aad.as_bytes(), &envelope)
.await
.map_err(|e| StoreError::Backend(e.to_string()))?
{
Some(plain) => {
*field = String::from_utf8(plain).map_err(|e| {
StoreError::Backend(format!(
"a sealed text payload opened to bytes that are not UTF-8: {e}"
))
})?;
found.opened += 1;
}
None => found.erased += 1,
}
}
}
}
Ok(found)
}
#[cfg(all(test, feature = "testkit"))]
mod tests {
use super::{journal_aad, open_payloads, payload};
use crate::core::RunId;
use crate::journal::RecordKind;
#[tokio::test]
async fn a_text_payload_that_opens_to_non_utf8_is_an_error() {
let keys = crate::testkit::MemoryKeyRing::default();
let run = RunId::generate();
let probe = RecordKind::Note {
text: String::new(),
};
let aad = journal_aad("t", run, &probe, None);
let envelope = super::super::envelope::seal(&keys, "t/run", aad.as_bytes(), &[0xff, 0xfe])
.await
.expect("seal");
let mut kind = RecordKind::Note {
text: payload::wrap_text(&envelope),
};
let opened = open_payloads(&keys, "t", run, None, &mut kind).await;
assert!(
opened.is_err(),
"non-UTF-8 plaintext was left sealed and reported as {opened:?}"
);
}
}