Skip to main content

cloudiful_redactor/
service.rs

1use anyhow::{Context, Result};
2
3use crate::{
4    InputKind, RedactionArtifact, RedactionSession, Redactor, RestoreResult,
5    decrypt_session_from_str, encrypt_session_to_string, ensure_restore_valid,
6    inspect_encrypted_session, require_external_id, StoredSession,
7};
8use crate::session::SessionStore;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EncryptedRedactionArtifact {
12    pub artifact: RedactionArtifact,
13    pub encrypted_session: String,
14    pub session_summary: crate::SessionSummary,
15}
16
17pub fn redact_text_artifact(
18    redactor: &Redactor,
19    text: &str,
20    input_kind: InputKind,
21) -> Result<RedactionArtifact> {
22    redactor
23        .redact_artifact_with_input_kind(text, input_kind)
24        .map_err(anyhow::Error::new)
25}
26
27pub fn redact_text_artifact_with_source(
28    redactor: &Redactor,
29    text: &str,
30    input_kind: InputKind,
31    source_path: &str,
32) -> Result<RedactionArtifact> {
33    redactor
34        .redact_artifact_with_input_kind_and_source(text, input_kind, Some(source_path))
35        .map_err(anyhow::Error::new)
36}
37
38pub fn redact_text_artifact_with_stateful_session(
39    redactor: &Redactor,
40    text: &str,
41    input_kind: InputKind,
42    external_id: &str,
43    store: &dyn SessionStore,
44) -> Result<RedactionArtifact> {
45    let (prior_session, expected_version) = load_stateful_session(store, external_id)?;
46    let artifact = redactor
47        .redact_artifact_with_prior_session(text, input_kind, prior_session.as_ref(), Some(external_id))
48        .map_err(anyhow::Error::new)?;
49    save_stateful_session(store, external_id, &artifact.session, expected_version.as_deref())?;
50    Ok(artifact)
51}
52
53pub fn redact_text_artifact_with_source_and_stateful_session(
54    redactor: &Redactor,
55    text: &str,
56    input_kind: InputKind,
57    source_path: &str,
58    external_id: &str,
59    store: &dyn SessionStore,
60) -> Result<RedactionArtifact> {
61    let (prior_session, expected_version) = load_stateful_session(store, external_id)?;
62    let artifact = redactor
63        .redact_artifact_with_input_kind_source_and_prior_session(
64            text,
65            input_kind,
66            Some(source_path),
67            prior_session.as_ref(),
68            Some(external_id),
69        )
70        .map_err(anyhow::Error::new)?;
71    save_stateful_session(store, external_id, &artifact.session, expected_version.as_deref())?;
72    Ok(artifact)
73}
74
75pub fn redact_text_with_encrypted_session(
76    redactor: &Redactor,
77    text: &str,
78    input_kind: InputKind,
79    passphrase: &str,
80) -> Result<EncryptedRedactionArtifact> {
81    let artifact =
82        redact_text_artifact(redactor, text, input_kind).context("failed to redact text input")?;
83    let encrypted_session = encrypt_session_to_string(&artifact.session, passphrase)
84        .context("failed to encrypt redaction session")?;
85    let session_summary = inspect_encrypted_session(&encrypted_session)
86        .context("failed to inspect encrypted session")?;
87
88    Ok(EncryptedRedactionArtifact {
89        artifact,
90        encrypted_session,
91        session_summary,
92    })
93}
94
95pub fn redact_text_with_encrypted_session_and_source(
96    redactor: &Redactor,
97    text: &str,
98    input_kind: InputKind,
99    source_path: &str,
100    passphrase: &str,
101) -> Result<EncryptedRedactionArtifact> {
102    let artifact = redact_text_artifact_with_source(redactor, text, input_kind, source_path)
103        .context("failed to redact text input")?;
104    let encrypted_session = encrypt_session_to_string(&artifact.session, passphrase)
105        .context("failed to encrypt redaction session")?;
106    let session_summary = inspect_encrypted_session(&encrypted_session)
107        .context("failed to inspect encrypted session")?;
108
109    Ok(EncryptedRedactionArtifact {
110        artifact,
111        encrypted_session,
112        session_summary,
113    })
114}
115
116pub fn decrypt_redaction_session(
117    encrypted_session: &str,
118    passphrase: &str,
119) -> Result<RedactionSession> {
120    decrypt_session_from_str(encrypted_session, passphrase)
121        .context("failed to decrypt provided session")
122}
123
124pub fn restore_text_from_encrypted_session(
125    redactor: &Redactor,
126    text: &str,
127    encrypted_session: &str,
128    passphrase: &str,
129) -> Result<RestoreResult> {
130    let session = decrypt_redaction_session(encrypted_session, passphrase)?;
131    let restored = redactor.restore_text(text, &session);
132    ensure_restore_valid(&restored)?;
133    Ok(restored)
134}
135
136fn load_stateful_session(
137    store: &dyn SessionStore,
138    external_id: &str,
139) -> Result<(Option<RedactionSession>, Option<String>)> {
140    let prior = store
141        .load_latest(external_id)
142        .with_context(|| format!("failed to load latest session for external_id `{external_id}`"))?;
143    Ok(split_stored_session(prior))
144}
145
146fn save_stateful_session(
147    store: &dyn SessionStore,
148    external_id: &str,
149    session: &RedactionSession,
150    expected_version: Option<&str>,
151) -> Result<()> {
152    store
153        .save_latest(external_id, session, expected_version)
154        .with_context(|| format!("failed to save latest session for external_id `{external_id}`"))?;
155    Ok(())
156}
157
158fn split_stored_session(
159    stored: Option<StoredSession>,
160) -> (Option<RedactionSession>, Option<String>) {
161    match stored {
162        Some(stored) => (Some(stored.session), stored.version),
163        None => (None, None),
164    }
165}
166
167pub fn restore_text_from_store(
168    redactor: &Redactor,
169    text: &str,
170    external_id: &str,
171    store: &dyn SessionStore,
172) -> Result<RestoreResult> {
173    let session = store
174        .load_latest(require_external_id(Some(external_id))?)
175        .with_context(|| format!("failed to load latest session for external_id `{external_id}`"))?
176        .ok_or_else(|| anyhow::anyhow!("no latest session found for external_id `{external_id}`"))?;
177    let restored = redactor.restore_text(text, &session.session);
178    ensure_restore_valid(&restored)?;
179    Ok(restored)
180}
181
182#[cfg(test)]
183mod tests {
184    use super::{
185        redact_text_artifact, redact_text_with_encrypted_session,
186        restore_text_from_encrypted_session,
187    };
188    use crate::types::FindingKind;
189    use crate::{InputKind, RedactionPolicy, RedactorBuilder};
190
191    fn full_redactor() -> crate::Redactor {
192        RedactorBuilder::new()
193            .with_redaction_policy(
194                RedactionPolicy::default()
195                    .with_kind(FindingKind::Domain, true)
196                    .with_kind(FindingKind::Secret, true)
197                    .with_kind(FindingKind::Url, true),
198            )
199            .build()
200    }
201
202    #[test]
203    fn encrypted_redaction_matches_plain_artifact_output() {
204        let redactor = full_redactor();
205        let text = "host=service.example.com secret=EJ2QEVC6AKELW0k2kkVY4NgGKONC";
206        let plain = redact_text_artifact(&redactor, text, InputKind::Text).expect("plain");
207        let encrypted =
208            redact_text_with_encrypted_session(&redactor, text, InputKind::Text, "pass")
209                .expect("encrypted");
210
211        assert_eq!(encrypted.artifact.session.entries.len(), plain.session.entries.len());
212        assert_eq!(
213            encrypted
214                .artifact
215                .session
216                .entries
217                .iter()
218                .map(|entry| (&entry.kind, &entry.original))
219                .collect::<Vec<_>>(),
220            plain
221                .session
222                .entries
223                .iter()
224                .map(|entry| (&entry.kind, &entry.original))
225                .collect::<Vec<_>>()
226        );
227        assert_ne!(
228            encrypted.artifact.result.redacted_text,
229            plain.result.redacted_text
230        );
231    }
232
233    #[test]
234    fn encrypted_session_restore_round_trips() {
235        let redactor = full_redactor();
236        let text = "host=service.example.com";
237        let encrypted =
238            redact_text_with_encrypted_session(&redactor, text, InputKind::Text, "pass")
239                .expect("encrypted");
240
241        let restored = restore_text_from_encrypted_session(
242            &redactor,
243            &encrypted.artifact.result.redacted_text,
244            &encrypted.encrypted_session,
245            "pass",
246        )
247        .expect("restore");
248
249        assert_eq!(restored.restored_text, text);
250    }
251}