cloudiful-redactor 0.5.0

Structured text redaction with reversible sessions for secrets, domains, URLs, and related sensitive values.
Documentation
use crate::{
    FindingKind, RedactionPolicy, RedactorBuilder, RestoreContext, create_restore_permit,
    decrypt_restore_permit, encrypt_restore_permit,
};

fn artifact(external_id: Option<&str>) -> crate::RedactionArtifact {
    RedactorBuilder::new()
        .with_redaction_policy(RedactionPolicy::default().with_kind(FindingKind::Domain, true))
        .build()
        .redact_artifact_with_input_kind_source_and_prior_session(
            "host=private.example.com",
            crate::InputKind::Text,
            None,
            None,
            external_id,
        )
        .expect("artifact")
}

#[test]
fn encrypted_permit_round_trips_and_rejects_tampering() {
    let permit = create_restore_permit(&artifact(None).session);
    let encrypted = encrypt_restore_permit(&permit, "permit-passphrase").expect("encrypt");
    assert_eq!(
        decrypt_restore_permit(&encrypted, "permit-passphrase").expect("decrypt"),
        permit
    );
    let mut value: serde_json::Value = serde_json::from_str(&encrypted).expect("json");
    value["ciphertext_b64"] = serde_json::Value::String("AAAA".to_string());
    assert!(decrypt_restore_permit(&value.to_string(), "permit-passphrase").is_err());
}

#[test]
fn copied_token_is_skipped_without_its_operation_permit() {
    let owned = artifact(Some("thread-a"));
    let copied = owned.session.entries[0].token.clone();
    let empty = crate::RestorePermit {
        version: 1,
        permit_id: "empty".to_string(),
        scope_id: owned.session.scope_id.clone(),
        external_id: owned.session.external_id.clone(),
        issued_tokens: Vec::new(),
    };
    let result = RestoreContext::with_permits(&owned.session, &[empty])
        .expect("context")
        .restore_text(&format!("log={copied}"));
    assert!(result.is_valid());
    assert_eq!(result.restored_count, 0);
    assert_eq!(result.skipped_tokens, vec![copied]);
}

#[test]
fn multiple_permits_union_authorized_tokens() {
    let first = artifact(Some("thread-a"));
    let redactor = RedactorBuilder::new()
        .with_redaction_policy(RedactionPolicy::default().with_kind(FindingKind::Domain, true))
        .build();
    let second = redactor
        .redact_artifact_with_prior_session(
            "backup=second.example.com",
            crate::InputKind::Text,
            Some(&first.session),
            Some("thread-a"),
        )
        .expect("second");
    let permits = [
        create_restore_permit(&first.session),
        create_restore_permit(&second.session),
    ];
    let text = format!(
        "{} {}",
        first.session.issued_tokens[0], second.session.issued_tokens[0]
    );
    let result = RestoreContext::with_permits(&second.session, &permits)
        .expect("context")
        .restore_text(&text);
    assert!(result.is_valid());
    assert_eq!(result.restored_count, 2);
}

#[test]
fn permit_from_other_external_id_is_rejected() {
    let session = artifact(Some("thread-a"));
    let permit = create_restore_permit(&artifact(Some("thread-b")).session);
    assert!(RestoreContext::with_permits(&session.session, &[permit]).is_err());
}

#[test]
fn unknown_permit_version_is_rejected_by_restore_context() {
    let session = artifact(Some("thread-a"));
    let mut permit = create_restore_permit(&session.session);
    permit.version += 1;
    assert!(RestoreContext::with_permits(&session.session, &[permit]).is_err());
}