diff --git a/Cargo.lock b/Cargo.lock index 566576d9..1701dd96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,69 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "lock_api" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + [[package]] name = "proc-macro2" version = "1.0.43" @@ -20,6 +83,47 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + [[package]] name = "syn" version = "1.0.99" @@ -31,6 +135,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.3" @@ -41,6 +154,7 @@ checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" name = "veil" version = "0.1.0" dependencies = [ + "lazy_static", "veil-macros", ] @@ -48,9 +162,12 @@ dependencies = [ name = "veil-macros" version = "0.1.0" dependencies = [ + "lazy_static", "proc-macro2", "quote", + "serde", "syn", + "toml", ] [[package]] @@ -59,3 +176,86 @@ version = "0.1.0" dependencies = [ "veil", ] + +[[package]] +name = "veil-tests-environment-aware" +version = "0.1.0" +dependencies = [ + "parking_lot", + "veil", + "veil-tests", +] + +[[package]] +name = "veil-tests-environment-aware-disable" +version = "0.1.0" +dependencies = [ + "veil", + "veil-tests", +] + +[[package]] +name = "veil-tests-environment-aware-fallback-off" +version = "0.1.0" +dependencies = [ + "veil", + "veil-tests", +] + +[[package]] +name = "veil-tests-environment-aware-fallback-on" +version = "0.1.0" +dependencies = [ + "veil", + "veil-tests", +] + +[[package]] +name = "veil-tests-environment-aware-fallback-panic" +version = "0.1.0" +dependencies = [ + "veil", +] + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/Cargo.toml b/Cargo.toml index 3c7bb596..20ffee6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,19 @@ license = "MIT" repository = "https://github.com/primait/veil" [workspace] -members = ["veil-macros", "veil-tests"] +members = [ + "veil-macros", + "veil-tests", + "veil-tests/environment-aware", + "veil-tests/environment-aware-fallback-on", + "veil-tests/environment-aware-fallback-off", + "veil-tests/environment-aware-fallback-panic", + "veil-tests/environment-aware-disable", +] + +[features] +environment-aware = ["veil-macros/environment-aware", "lazy_static"] [dependencies] -veil-macros = { path = "veil-macros" } \ No newline at end of file +veil-macros = { path = "veil-macros" } +lazy_static = { version = "1", optional = true } diff --git a/README.md b/README.md index 58b9a5b4..b28ba882 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,40 @@ enum InsuranceStatus { }, } ``` + +# Environment Awareness + +You can configure Veil to redact or skip redacting data based on environment variables. Enable the `environment-aware` Cargo feature like so in your Cargo.toml: + +```toml +[dependencies] +veil = { version = "0.1", features = ["environment-aware"] } +``` + +## `VEIL_DISABLE_REDACTION` + +Redaction can be completely disabled by setting the `VEIL_DISABLE_REDACTION` environment variable. This is only checked once during the program lifetime for security purposes. + +## `.veil.toml` + +Redaction can also be configured on a per-project basis using a `.veil.toml` file. Put this file in your crate or workspace root and Veil will read it at compile time. + +**Please note, if you change the file, Veil won't see the changes until you do a clean build of your crate.** + +### Example + +`APP_ENV` is just an example here. You can match multiple environment variables with any UTF-8 name and value(s). + +```toml +[env.APP_ENV] +redact = ["production", "staging"] # redact data if "APP_ENV" is set to any of these values +skip-redact = ["dev", "qa"] # SKIP redacting data if "APP_ENV" is set to any of these values + +# If "APP_ENV" isn't set or isn't recognised... +[fallback] +redact = true # do redact data (default) +# OR +redact = false # don't redact data +# OR +redact = "panic" # panic at runtime +``` diff --git a/src/lib.rs b/src/lib.rs index 28c2e9f0..e67ab326 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,6 +199,43 @@ //! } //! ``` //! +//! # Environment Awareness +//! +//! You can configure Veil to redact or skip redacting data based on environment variables. Enable the `environment-aware` Cargo feature like so in your Cargo.toml: +//! +//! ```toml +//! [dependencies] +//! veil = { version = "0.1", features = ["environment-aware"] } +//! ``` +//! +//! ## `VEIL_DISABLE_REDACTION` +//! +//! Redaction can be completely disabled by setting the `VEIL_DISABLE_REDACTION` environment variable. This is only checked once during the program lifetime for security purposes. +//! +//! ## `.veil.toml` +//! +//! Redaction can also be configured on a per-project basis using a `.veil.toml` file. Put this file in your crate or workspace root and Veil will read it at compile time. +//! +//! **Please note, if you change the file, Veil won't see the changes until you do a clean build of your crate.** +//! +//! ### Example +//! +//! `APP_ENV` is just an example here. You can match multiple environment variables with any UTF-8 name and value(s). +//! +//! ```toml +//! [env.APP_ENV] +//! redact = ["production", "staging"] # redact data if "APP_ENV" is set to any of these values +//! skip-redact = ["dev", "qa"] # SKIP redacting data if "APP_ENV" is set to any of these values +//! +//! ## If "APP_ENV" isn't set or isn't recognised... +//! [fallback] +//! redact = true # do redact data (default) +//! ## OR +//! redact = false # don't redact data +//! ## OR +//! redact = "panic" # panic at runtime +//! ``` +//! //! # Limitations //! //! Currently, this macro only supports [`std::fmt::Debug`] formatting with no modifiers (`{:?}`) or the "alternate" modifier (`{:#?}`). diff --git a/src/private.rs b/src/private.rs index f9220aed..e8ff231f 100644 --- a/src/private.rs +++ b/src/private.rs @@ -107,9 +107,22 @@ impl RedactFlags { } } -pub fn redact(this: &dyn Debug, flags: RedactFlags) -> DisplayDebug { +pub fn redact( + this: &dyn Debug, + flags: RedactFlags, + #[cfg(feature = "environment-aware")] env_is_redaction_enabled: bool, +) -> DisplayDebug { let mut redacted = String::new(); + #[cfg(feature = "environment-aware")] + if !env_is_redaction_enabled { + return DisplayDebug(if flags.debug_alternate { + format!("{:#?}", this) + } else { + format!("{:?}", this) + }); + } + (|| { if flags.fixed > 0 { flags.redact_fixed(flags.fixed as usize, &mut redacted); @@ -152,3 +165,25 @@ pub fn redact(this: &dyn Debug, flags: RedactFlags) -> DisplayDebug { DisplayDebug(redacted) } + +#[cfg(feature = "environment-aware")] +pub fn env_is_redaction_enabled() -> Option { + // First check VEIL_DISABLE_REDACTION, which overrides any config file + lazy_static::lazy_static! { + // We deliberately only look this up once. + // If an attacker somehow is able to change environment variables, we don't want to give them a way of revealing sensitive data. + static ref IS_REDACTION_DISABLED: bool = std::env::var("VEIL_DISABLE_REDACTION").is_ok(); + } + if *IS_REDACTION_DISABLED { + return Some(false); + } + + // We'll run the `env_is_redaction_enabled!` macro here + // This is handled by the `fmt` module + // This is needed because we need CARGO_MANIFEST_DIR to be set by the crate being built, + // rather than this crate itself! + None +} + +#[cfg(feature = "environment-aware")] +pub use veil_macros::env_is_redaction_enabled; diff --git a/veil-macros/Cargo.toml b/veil-macros/Cargo.toml index 2d5573c8..6cf530ea 100644 --- a/veil-macros/Cargo.toml +++ b/veil-macros/Cargo.toml @@ -3,10 +3,17 @@ name = "veil-macros" version = "0.1.0" edition = "2021" +[features] +environment-aware = ["lazy_static", "toml", "serde"] + [lib] proc-macro = true [dependencies] syn = { version = "1", features = ["full"] } quote = "1" -proc-macro2 = "1" \ No newline at end of file +proc-macro2 = "1" + +lazy_static = { version = "1", optional = true } +toml = { version = "0.5", optional = true } +serde = { version = "1", features = ["derive"], optional = true } diff --git a/veil-macros/src/enums.rs b/veil-macros/src/enums.rs index c1057d3c..dfb595ad 100644 --- a/veil-macros/src/enums.rs +++ b/veil-macros/src/enums.rs @@ -1,4 +1,7 @@ -use crate::{flags::FieldFlags, fmt::FormatData}; +use crate::{ + flags::FieldFlags, + fmt::{self, FormatData}, +}; use proc_macro::TokenStream; use quote::ToTokens; use syn::spanned::Spanned; @@ -148,13 +151,7 @@ pub fn derive_redact( // Variant name redacting let variant_name = variant.ident.to_string(); let variant_name = if let Some(flags) = &flags.variant_flags { - quote! { - ::veil::private::redact(&#variant_name, ::veil::private::RedactFlags { - debug_alternate, - is_option: false, - #flags - }) - } + fmt::generate_redact_call(quote! { &#variant_name }, false, flags) } else { variant_name.into_token_stream() }; @@ -170,13 +167,21 @@ pub fn derive_redact( }); } + // Generate the `__veil_env_is_redaction_enabled` function + // Used by the `environment-aware` feature + // See the `env` module + let __veil_env_is_redaction_enabled = fmt::__veil_env_is_redaction_enabled(); + Ok(quote! { impl ::std::fmt::Debug for #name_ident { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + #__veil_env_is_redaction_enabled + let debug_alternate = f.alternate(); match self { #(Self::#variant_idents #variant_destructures => { #variant_bodies; },)* } + Ok(()) } } diff --git a/veil-macros/src/env.rs b/veil-macros/src/env.rs new file mode 100644 index 00000000..593883f0 --- /dev/null +++ b/veil-macros/src/env.rs @@ -0,0 +1,235 @@ +//! If the `environment-aware` feature is enabled, the user can configure Veil's behaviour in different environments. +//! +//! If the environment variable `VEIL_DISBALE_REDACTION` is set, Veil will not redact any data. +//! +//! If the user's project contains a `.veil.toml` file, Veil will use the configuration in that file to figure out whether to redact data. +//! This is done at compile time. Runtime changes to this file will have no effect. +//! +//! The configuration file allows the user to specify what environment variables and their values should enable or disable redaction. +//! +//! For example, I can configure the file to redact when APP_ENV="production", and not redact when APP_ENV="development". + +use lazy_static::lazy_static; +use proc_macro::TokenStream; +use quote::ToTokens; +use serde::Deserialize; +use std::{collections::BTreeMap, path::Path}; +use syn::spanned::Spanned; + +#[derive(Deserialize)] +#[serde(untagged)] +enum FallbackBehavior { + Redact(bool), + Panic(String), +} +impl ToTokens for FallbackBehavior { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Self::Redact(redact) => redact.to_tokens(tokens), + Self::Panic(_) => quote! { panic!("Veil expected environment variables to be set that determine whether sensitive data should be redacted or not. Please refer to .veil.toml in the project files to see what Veil was expecting.") }.to_tokens(tokens) + } + } +} + +#[derive(Deserialize)] +/// Should we redact data based on the values of environment variables? +struct EnvRedactConfig { + #[serde(default)] + /// Redaction should be ON if the environment variable is set to one of these values. + redact: Vec, + + #[serde(default)] + #[serde(rename = "skip-redact")] + /// Redaction should be OFF if the environment variable is set to one of these values. + skip_redact: Vec, +} + +#[derive(Deserialize)] +/// If none of those environment variables are present... +struct FallbackRedactConfig { + /// ...then we should [redact|not redact] the data. + redact: FallbackBehavior, +} +impl Default for FallbackRedactConfig { + fn default() -> Self { + Self { + redact: FallbackBehavior::Redact(true), + } + } +} + +#[derive(Deserialize)] +struct TomlVeilConfig { + #[serde(default)] + fallback: FallbackRedactConfig, + env: Option>, +} + +#[derive(Default)] +pub struct VeilConfig { + fallback: FallbackRedactConfig, + env: BTreeMap, +} +impl VeilConfig { + pub fn read(path: &Path) -> Result { + let config = std::fs::read_to_string(path)?; + let config: TomlVeilConfig = toml::from_str(&config)?; + + // Ensure there are no duplicate key-value environment variable pairs. + if let Some(env) = &config.env { + let mut pairs = Vec::new(); + for (key, config) in env { + // Ensure there are no empty environment variable configs. + if config.redact.is_empty() && config.skip_redact.is_empty() { + return Err(VeilConfigError::Custom(format!( + "Environment variable {key:?} has an empty configuration" + ))); + } + + for value in [&config.redact, &config.skip_redact].into_iter().flatten() { + let pair = (key.as_str(), value.as_str()); + if pairs.contains(&pair) { + return Err(VeilConfigError::Custom(format!( + "duplicate key-value environment variable pair: {pair:?}" + ))); + } else { + pairs.push(pair); + } + } + } + } + + // Ensure the fallback is configured correctly. + if let FallbackBehavior::Panic(value) = &config.fallback.redact { + if value != "panic" { + return Err(VeilConfigError::Custom( + "fallback redaction behavior must be \"panic\"".to_string(), + )); + } + } + + Ok(Self { + env: config.env.unwrap_or_default(), + fallback: config.fallback, + }) + } +} + +#[derive(Debug)] +pub enum VeilConfigError { + IoError(std::io::Error), + TomlError(toml::de::Error), + Custom(String), +} +impl std::error::Error for VeilConfigError {} +impl std::fmt::Display for VeilConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(err) => write!(f, "I/O error reading .veil.toml: {err}"), + Self::TomlError(err) => write!(f, "TOML error reading .veil.toml: {err:#?}"), + Self::Custom(err) => write!(f, ".veil.toml error: {err}"), + } + } +} +impl From for VeilConfigError { + #[inline] + fn from(err: std::io::Error) -> Self { + Self::IoError(err) + } +} +impl From for VeilConfigError { + #[inline] + fn from(err: toml::de::Error) -> Self { + Self::TomlError(err) + } +} + +lazy_static! { + static ref CACHED_VEIL_CONFIG: Result = find_config(); +} +fn find_config() -> Result { + let manifest_dir = match std::env::var_os("CARGO_MANIFEST_DIR") { + Some(manifest_dir) => manifest_dir, + None => return Ok(Default::default()), + }; + + let mut manifest_dir = Path::new(&manifest_dir); + + // Walk up the directory tree until we find a .veil.toml file + // We should stop at the workspace root + let config_path = loop { + let config_path = manifest_dir.join(".veil.toml"); + if config_path.is_file() { + break Some(config_path); + } + + // HACK! We can detect the workspace root by looking for Cargo.lock + if manifest_dir.join("Cargo.lock").is_file() { + break None; + } + + manifest_dir = match manifest_dir.parent() { + Some(parent) => parent, + None => break None, + }; + }; + + let config_path = match config_path { + Some(config_path) => config_path, + None => return Ok(Default::default()), + }; + + VeilConfig::read(&config_path) +} + +/// This macro expands to a function that returns true or false depending on whether the +/// data should be redacted based on the values of environment variables, configured by +/// `.veil.toml` in the project's files, if present. +pub fn env_is_redaction_enabled(input: TokenStream) -> TokenStream { + let config = match &*CACHED_VEIL_CONFIG { + Ok(config) => config, + Err(err) => { + return syn::Error::new(proc_macro2::TokenStream::from(input).span(), err.to_string()) + .into_compile_error() + .into() + } + }; + + let env = config.env.iter().map(|(key, config)| { + let redacts = &config.redact; + let skips = &config.skip_redact; + quote! { + if let Ok(value) = ::std::env::var(#key) { + static REDACTS: &[&str] = &[#(#redacts),*]; + if REDACTS.contains(&value.as_str()) { + return true; + } + + static SKIPS: &[&str] = &[#(#skips),*]; + if SKIPS.contains(&value.as_str()) { + return false; + } + } + } + }); + + let fallback = config.fallback.redact.to_token_stream(); + if config.env.is_empty() { + quote! { + #[inline(always)] + fn __veil_env_is_redaction_enabled() -> bool { + #fallback + } + } + .into() + } else { + quote! { + #[inline(never)] + fn __veil_env_is_redaction_enabled() -> bool { + #(#env)* + #fallback + } + } + .into() + } +} diff --git a/veil-macros/src/fmt.rs b/veil-macros/src/fmt.rs index 19d5242b..2da86576 100644 --- a/veil-macros/src/fmt.rs +++ b/veil-macros/src/fmt.rs @@ -103,6 +103,7 @@ impl FormatData<'_> { // Specialization for Option let is_option = is_ty_option(&field.ty); +<<<<<<< HEAD field_bodies.push(quote! { ::veil::private::redact(#field_accessor, ::veil::private::RedactFlags { @@ -111,7 +112,10 @@ impl FormatData<'_> { #field_flags }) }); +======= +>>>>>>> 0884330 (Add redaction control based on environment variables) + field_bodies.push(generate_redact_call(field_accessor, is_option, &field_flags)); continue; } } @@ -120,11 +124,18 @@ impl FormatData<'_> { field_bodies.push(quote! { #field_accessor }); } + // Generate the `__veil_env_is_redaction_enabled` function + // Used by the `environment-aware` feature + // See the `env` module + let __veil_env_is_redaction_enabled = __veil_env_is_redaction_enabled(); + Ok(match self { Self::FieldsNamed(syn::FieldsNamed { named, .. }) => { let field_names = named.iter().map(|field| field.ident.as_ref().unwrap().to_string()); quote! { + #__veil_env_is_redaction_enabled + f.debug_struct(&#name.as_ref()) #( .field(#field_names, &#field_bodies) @@ -135,6 +146,8 @@ impl FormatData<'_> { Self::FieldsUnnamed(syn::FieldsUnnamed { .. }) => { quote! { + #__veil_env_is_redaction_enabled + f.debug_tuple(&#name.as_ref()) #( .field(&#field_bodies) @@ -145,3 +158,38 @@ impl FormatData<'_> { }) } } + +/// Generates a call to `veil::private::redact` +pub fn generate_redact_call( + field_accessor: proc_macro2::TokenStream, + is_option: bool, + field_flags: &FieldFlags, +) -> proc_macro2::TokenStream { + // Environment awareness (we assume that we injected the `__veil_env_is_redaction_enabled` function earlier) + let env_is_redaction_enabled = if cfg!(feature = "environment-aware") { + quote! { + , ::veil::private::env_is_redaction_enabled().unwrap_or_else(__veil_env_is_redaction_enabled) + } + } else { + proc_macro2::TokenStream::default() + }; + + quote! { + ::veil::private::redact(#field_accessor, ::veil::private::RedactFlags { + debug_alternate, + is_option: #is_option, + #field_flags + } #env_is_redaction_enabled) + } +} + +/// Generates the `__veil_env_is_redaction_enabled` function +pub fn __veil_env_is_redaction_enabled() -> proc_macro2::TokenStream { + if cfg!(feature = "environment-aware") { + // Generate a function that returns whether redaction is enabled based on the environment. + // The compiler will be able to deduplicate the function, so we won't be generating hundreds of copies of it in the final binary. + quote! { ::veil::private::env_is_redaction_enabled!(); } + } else { + proc_macro2::TokenStream::default() + } +} diff --git a/veil-macros/src/lib.rs b/veil-macros/src/lib.rs index b18d1dd0..ae5c9584 100644 --- a/veil-macros/src/lib.rs +++ b/veil-macros/src/lib.rs @@ -27,3 +27,12 @@ pub fn derive_redact(item: TokenStream) -> TokenStream { Err(err) => err.into_compile_error().into(), } } + +#[cfg(feature = "environment-aware")] +mod env; + +#[cfg(feature = "environment-aware")] +#[proc_macro] +pub fn env_is_redaction_enabled(input: TokenStream) -> TokenStream { + env::env_is_redaction_enabled(input) +} diff --git a/veil-tests/Cargo.toml b/veil-tests/Cargo.toml index dc15beb8..74d85121 100644 --- a/veil-tests/Cargo.toml +++ b/veil-tests/Cargo.toml @@ -5,4 +5,4 @@ edition = "2021" publish = false [dependencies] -veil = { path = "../" } \ No newline at end of file +veil = { path = "../" } diff --git a/veil-tests/environment-aware-disable/Cargo.toml b/veil-tests/environment-aware-disable/Cargo.toml new file mode 100644 index 00000000..4d1c9411 --- /dev/null +++ b/veil-tests/environment-aware-disable/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "veil-tests-environment-aware-disable" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +veil = { path = "../../", features = ["environment-aware"] } +veil-tests = { path = "../" } diff --git a/veil-tests/environment-aware-disable/src/lib.rs b/veil-tests/environment-aware-disable/src/lib.rs new file mode 100644 index 00000000..db7096b4 --- /dev/null +++ b/veil-tests/environment-aware-disable/src/lib.rs @@ -0,0 +1,21 @@ +//! This needs its own crate because VEIL_DISABLE_REDACTION is only checked once +//! and another test in the other crates might run first, causing THIS test to fail. + +#![cfg_attr(not(test), allow(unused))] + +use veil::Redact; +use veil_tests::{assert_has_sensitive_data, SENSITIVE_DATA}; + +#[derive(Redact)] +#[redact(all, partial)] +struct Redactable(&'static str); + +#[test] +fn test_disable_redact_env_var() { + std::env::set_var("VEIL_DISABLE_REDACTION", "1"); + assert_has_sensitive_data(Redactable(SENSITIVE_DATA[1])); + + // Ensure it's only checked once + std::env::remove_var("VEIL_DISABLE_REDACTION"); + assert_has_sensitive_data(Redactable(SENSITIVE_DATA[1])); +} diff --git a/veil-tests/environment-aware-fallback-off/.veil.toml b/veil-tests/environment-aware-fallback-off/.veil.toml new file mode 100644 index 00000000..d87ecc8c --- /dev/null +++ b/veil-tests/environment-aware-fallback-off/.veil.toml @@ -0,0 +1,2 @@ +[fallback] +redact = false diff --git a/veil-tests/environment-aware-fallback-off/Cargo.toml b/veil-tests/environment-aware-fallback-off/Cargo.toml new file mode 100644 index 00000000..ef56b5e8 --- /dev/null +++ b/veil-tests/environment-aware-fallback-off/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "veil-tests-environment-aware-fallback-off" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +veil = { path = "../../", features = ["environment-aware"] } +veil-tests = { path = "../" } diff --git a/veil-tests/environment-aware-fallback-off/src/lib.rs b/veil-tests/environment-aware-fallback-off/src/lib.rs new file mode 100644 index 00000000..b5ca97c8 --- /dev/null +++ b/veil-tests/environment-aware-fallback-off/src/lib.rs @@ -0,0 +1,13 @@ +#![cfg_attr(not(test), allow(unused))] + +use veil::Redact; +use veil_tests::{assert_has_sensitive_data, SENSITIVE_DATA}; + +#[derive(Redact)] +#[redact(all, partial)] +struct Redactable(&'static str); + +#[test] +fn test_fallback_redact_off() { + assert_has_sensitive_data(Redactable(SENSITIVE_DATA[1])); +} diff --git a/veil-tests/environment-aware-fallback-on/.veil.toml b/veil-tests/environment-aware-fallback-on/.veil.toml new file mode 100644 index 00000000..39ad76ac --- /dev/null +++ b/veil-tests/environment-aware-fallback-on/.veil.toml @@ -0,0 +1,2 @@ +[fallback] +redact = true diff --git a/veil-tests/environment-aware-fallback-on/Cargo.toml b/veil-tests/environment-aware-fallback-on/Cargo.toml new file mode 100644 index 00000000..d4bd71fc --- /dev/null +++ b/veil-tests/environment-aware-fallback-on/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "veil-tests-environment-aware-fallback-on" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +veil = { path = "../../", features = ["environment-aware"] } +veil-tests = { path = "../" } diff --git a/veil-tests/environment-aware-fallback-on/src/lib.rs b/veil-tests/environment-aware-fallback-on/src/lib.rs new file mode 100644 index 00000000..21b68719 --- /dev/null +++ b/veil-tests/environment-aware-fallback-on/src/lib.rs @@ -0,0 +1,13 @@ +#![cfg_attr(not(test), allow(unused))] + +use veil::Redact; +use veil_tests::{assert_no_sensitive_data, SENSITIVE_DATA}; + +#[derive(Redact)] +#[redact(all, partial)] +struct Redactable(&'static str); + +#[test] +fn test_fallback_redact_on() { + assert_no_sensitive_data(Redactable(SENSITIVE_DATA[1])); +} diff --git a/veil-tests/environment-aware-fallback-panic/.veil.toml b/veil-tests/environment-aware-fallback-panic/.veil.toml new file mode 100644 index 00000000..371629c0 --- /dev/null +++ b/veil-tests/environment-aware-fallback-panic/.veil.toml @@ -0,0 +1,2 @@ +[fallback] +redact = "panic" diff --git a/veil-tests/environment-aware-fallback-panic/Cargo.toml b/veil-tests/environment-aware-fallback-panic/Cargo.toml new file mode 100644 index 00000000..1371ba0b --- /dev/null +++ b/veil-tests/environment-aware-fallback-panic/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "veil-tests-environment-aware-fallback-panic" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +veil = { path = "../../", features = ["environment-aware"] } diff --git a/veil-tests/environment-aware-fallback-panic/src/lib.rs b/veil-tests/environment-aware-fallback-panic/src/lib.rs new file mode 100644 index 00000000..cbd849f4 --- /dev/null +++ b/veil-tests/environment-aware-fallback-panic/src/lib.rs @@ -0,0 +1,13 @@ +#![cfg_attr(not(test), allow(unused))] + +use veil::Redact; + +#[derive(Redact)] +#[redact(all, partial)] +struct Redactable(&'static str); + +#[test] +#[should_panic] +fn test_fallback_redact_on() { + format!("{:?}", Redactable("")); +} diff --git a/veil-tests/environment-aware/.veil.toml b/veil-tests/environment-aware/.veil.toml new file mode 100644 index 00000000..352e18e5 --- /dev/null +++ b/veil-tests/environment-aware/.veil.toml @@ -0,0 +1,3 @@ +[env.APP_ENV] +skip-redact = ["dev", "qa"] +redact = ["production", "staging"] diff --git a/veil-tests/environment-aware/Cargo.toml b/veil-tests/environment-aware/Cargo.toml new file mode 100644 index 00000000..f4b8e3b5 --- /dev/null +++ b/veil-tests/environment-aware/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "veil-tests-environment-aware" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +parking_lot = "0.12" # needed for non-poisoning mutex +veil = { path = "../../", features = ["environment-aware"] } +veil-tests = { path = "../" } diff --git a/veil-tests/environment-aware/src/lib.rs b/veil-tests/environment-aware/src/lib.rs new file mode 100644 index 00000000..58483204 --- /dev/null +++ b/veil-tests/environment-aware/src/lib.rs @@ -0,0 +1,53 @@ +#![cfg_attr(not(test), allow(unused))] + +use parking_lot::Mutex; +use veil::Redact; +use veil_tests::{assert_has_sensitive_data, assert_no_sensitive_data}; + +const SENSITIVE_DATA: &str = veil_tests::SENSITIVE_DATA[1]; + +static ENVIRONMENT_LOCK: Mutex<()> = parking_lot::const_mutex(()); + +#[derive(Redact)] +#[redact(all, partial)] +struct Redactable(&'static str); + +#[test] +fn test_production() { + // We've set up redaction to happen when APP_ENV="production" + let _lock = ENVIRONMENT_LOCK.lock(); + + std::env::set_var("APP_ENV", "production"); + + assert_no_sensitive_data(Redactable(SENSITIVE_DATA)); +} + +#[test] +fn test_staging() { + // We've set up redaction to happen when APP_ENV="staging" + let _lock = ENVIRONMENT_LOCK.lock(); + + std::env::set_var("APP_ENV", "staging"); + + assert_no_sensitive_data(Redactable(SENSITIVE_DATA)); +} + +#[test] +fn test_dev() { + // We've set up redaction to NOT happen when APP_ENV="dev" + let _lock = ENVIRONMENT_LOCK.lock(); + + std::env::set_var("APP_ENV", "dev"); + + assert_has_sensitive_data(Redactable(SENSITIVE_DATA)); +} + +#[test] +fn test_qa() { + // We've set up redaction to NOT happen when APP_ENV="qa" + let _lock = ENVIRONMENT_LOCK.lock(); + + std::env::set_var("APP_ENV", "qa"); + + assert_has_sensitive_data(Redactable(SENSITIVE_DATA)); +} diff --git a/veil-tests/src/lib.rs b/veil-tests/src/lib.rs index 10bc16c4..0cb96300 100644 --- a/veil-tests/src/lib.rs +++ b/veil-tests/src/lib.rs @@ -1,2 +1,11 @@ mod compile_tests; mod redaction_tests; +<<<<<<< HEAD +======= + +<<<<<<< HEAD +pub use redaction_tests::{SENSITIVE_DATA, assert_has_sensitive_data, assert_no_sensitive_data}; +>>>>>>> 0884330 (Add redaction control based on environment variables) +======= +pub use redaction_tests::{assert_has_sensitive_data, assert_no_sensitive_data, SENSITIVE_DATA}; +>>>>>>> 34db55f (cargo fmt) diff --git a/veil-tests/src/redaction_tests.rs b/veil-tests/src/redaction_tests.rs index 94d0bdf7..eecc67f6 100644 --- a/veil-tests/src/redaction_tests.rs +++ b/veil-tests/src/redaction_tests.rs @@ -4,7 +4,7 @@ use veil::Redact; -const SENSITIVE_DATA: &[&str] = &[ +pub const SENSITIVE_DATA: &[&str] = &[ "William", "Assicurazioni", "039845734895", @@ -12,7 +12,16 @@ const SENSITIVE_DATA: &[&str] = &[ "SensitiveVariant", ]; -fn assert_no_sensitive_data(data: T) { +pub fn assert_has_sensitive_data(data: T) { + for redacted in [format!("{data:?}"), format!("{data:#?}")] { + assert!( + SENSITIVE_DATA.iter().any(|sensitive| redacted.contains(sensitive)), + "{redacted:?} doesn't contain any sensitive data" + ); + } +} + +pub fn assert_no_sensitive_data(data: T) { for redacted in [format!("{data:?}"), format!("{data:#?}")] { for sensitive in SENSITIVE_DATA { assert!(