use crate::{
HammerworkError, Result,
job::{Job, JobStatus},
};
#[cfg(feature = "encryption")]
use std::sync::Arc;
#[derive(Clone, Default)]
pub(crate) struct PayloadEncryption {
#[cfg(feature = "encryption")]
pub(crate) engine: Option<Arc<crate::encryption::EncryptionEngine>>,
#[cfg(feature = "encryption")]
pub(crate) encrypted_queues: Vec<String>,
pub(crate) plaintext_guard: bool,
}
impl std::fmt::Debug for PayloadEncryption {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("PayloadEncryption");
#[cfg(feature = "encryption")]
s.field("engine", &self.engine)
.field("encrypted_queues", &self.encrypted_queues);
s.field("plaintext_guard", &self.plaintext_guard);
s.finish()
}
}
impl PayloadEncryption {
#[cfg_attr(not(any(feature = "postgres", feature = "mysql")), allow(dead_code))]
pub(crate) fn guarded_queues(&self, jobs: &[Job]) -> Vec<String> {
if !self.plaintext_guard {
return Vec::new();
}
let mut queues: Vec<String> = jobs
.iter()
.filter(|job| !job.is_encrypted)
.map(|job| job.queue_name.clone())
.collect();
queues.sort();
queues.dedup();
queues
}
#[cfg_attr(not(any(feature = "postgres", feature = "mysql")), allow(dead_code))]
pub(crate) fn plaintext_refused(queue_name: &str) -> HammerworkError {
HammerworkError::Encryption {
message: format!(
"Queue '{}' holds encrypted jobs, but this queue handle would store the new \
job in plaintext; refusing. Give it the application's encryption settings \
(the [encryption] section and its key), or encrypt the job explicitly",
queue_name
),
}
}
#[cfg(feature = "encryption")]
fn encrypts_queue(&self, queue_name: &str) -> bool {
self.encrypted_queues
.iter()
.any(|queue| queue == "*" || queue == queue_name)
}
pub(crate) async fn seal(&self, jobs: &mut [Job]) -> Result<()> {
for job in jobs.iter_mut() {
if job.is_encrypted {
continue;
}
#[cfg(feature = "encryption")]
if job.encryption_config.is_none()
&& let Some(engine) = &self.engine
&& self.encrypts_queue(&job.queue_name)
{
let config = engine.config();
let mut job_config =
crate::encryption::EncryptionConfig::new(config.algorithm.clone())
.with_key_id(engine.key_id());
job_config.compression_enabled = config.compression_enabled;
job.encryption_config = Some(job_config);
}
if !job.has_encryption() {
continue;
}
#[cfg(feature = "encryption")]
{
let engine = self
.engine
.as_ref()
.ok_or_else(|| HammerworkError::Encryption {
message: format!(
"Job {} has an encryption config but the queue has no encryption \
engine; configure one with JobQueue::with_encryption",
job.id
),
})?;
crate::encryption::job_payload::seal_job(engine, job).await?;
}
}
Ok(())
}
pub(crate) async fn open(&self, job: Job) -> Result<Job> {
if !job.is_encrypted {
return Ok(job);
}
if job.status == JobStatus::Archived && !has_ciphertext(&job) {
return Err(HammerworkError::Encryption {
message: format!(
"Job {} is archived and its ciphertext stays in the archive; restore it \
with restore_archived_job and decrypt the restored job",
job.id
),
});
}
#[cfg(feature = "encryption")]
{
let engine = self
.engine
.as_ref()
.ok_or_else(|| HammerworkError::Encryption {
message: format!(
"Job {} has an encrypted payload but the queue has no encryption \
engine; configure one with JobQueue::with_encryption",
job.id
),
})?;
let job_id = job.id;
crate::encryption::job_payload::open_job(engine, job)
.await
.map_err(|e| HammerworkError::Encryption {
message: format!("Cannot decrypt the payload of job {}: {}", job_id, e),
})
}
#[cfg(not(feature = "encryption"))]
{
Err(HammerworkError::Encryption {
message: format!(
"Job {} has an encrypted payload; decrypting it needs the `encryption` \
feature and an encryption engine (JobQueue::with_encryption)",
job.id
),
})
}
}
}
fn has_ciphertext(#[allow(unused_variables)] job: &Job) -> bool {
#[cfg(feature = "encryption")]
{
job.encrypted_payload.is_some()
}
#[cfg(not(feature = "encryption"))]
{
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn plaintext_guard_checks_each_unencrypted_queue_once() {
let mut sealed = Job::new("payments".into(), json!({}));
sealed.is_encrypted = true;
let jobs = vec![
Job::new("emails".into(), json!({})),
Job::new("emails".into(), json!({})),
Job::new("audit".into(), json!({})),
sealed,
];
assert!(
PayloadEncryption::default()
.guarded_queues(&jobs)
.is_empty()
);
#[allow(clippy::needless_update)]
let guard = PayloadEncryption {
plaintext_guard: true,
..Default::default()
};
assert_eq!(guard.guarded_queues(&jobs), vec!["audit", "emails"]);
let message = PayloadEncryption::plaintext_refused("payments").to_string();
assert!(
message.contains("'payments' holds encrypted jobs"),
"{message}"
);
assert!(format!("{guard:?}").contains("plaintext_guard: true"));
}
}