Skip to content

Commit de100b0

Browse files
geeknoidMartin Tailleferralfbiedert
authored
feat: Introduce the #[classified] macro (#48)
Co-authored-by: Martin Taillefer <mataille@microsoft.com> Co-authored-by: Ralf Biedert <rb@xr.io>
1 parent 6f907b9 commit de100b0

19 files changed

+1019
-580
lines changed

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fundle_macros = { path = "crates/fundle_macros", default-features = false, versi
2626
fundle_macros_impl = { path = "crates/fundle_macros_impl", default-features = false, version = "0.2.0" }
2727

2828
# external dependencies
29+
derive_more = { version = "2.0.1", default-features = false }
2930
insta = { version = "1.42.0", default-features = false }
3031
mutants = { version = "0.0.3", default-features = false }
3132
once_cell = { version = "1.21.3", default-features = false }

crates/data_privacy/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ serde = { workspace = true, optional = true, default-features = false, features
2222
xxhash-rust = { workspace = true, optional = true, features = ["xxh3"] }
2323

2424
[dev-dependencies]
25+
derive_more = { workspace = true, features = ["constructor", "from"] }
2526
once_cell = { workspace = true, features = ["std"] }
2627
mutants.workspace = true
2728
serde = { workspace = true, features = ["derive", "std"] }

crates/data_privacy/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ fn try_out() {
130130
// doesn't compile since `Sensitive` doesn't implement `Display`
131131
// println!("Name: {}", person.name);
132132

133-
// outputs: Name: <common/sensitive:REDACTED>"
133+
// outputs: Name: <CLASSIFIED:common/sensitive>"
134134
println!("Name: {:?}", person.name);
135135

136136
// extract the data from the `Sensitive` type and outputs: Name: John Doe
@@ -170,7 +170,7 @@ fn try_out() {
170170
let mut output_buffer = String::new();
171171

172172
// Redact the sensitive data in the person's name using the redaction engine.
173-
engine.display_redacted(&person.name, |s| output_buffer.write_str(s).unwrap());
173+
engine.redacted_display(&person.name, |s| output_buffer.write_str(s).unwrap());
174174

175175
// check that the data in the output buffer has indeed been redacted as expected.
176176
assert_eq!(output_buffer, "********");
Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
use crate::example_taxonomy::ExampleTaxonomy;
5+
use data_privacy_macros::classified;
6+
use derive_more::{Constructor, From};
47
use serde::{Deserialize, Serialize};
58

6-
use crate::example_taxonomy::{OrganizationallyIdentifiableInformation, PersonallyIdentifiableInformation};
9+
#[classified(ExampleTaxonomy::PersonallyIdentifiableInformation)]
10+
#[derive(Clone, Serialize, Deserialize, Constructor, From)]
11+
pub struct UserName(String);
12+
13+
#[classified(ExampleTaxonomy::PersonallyIdentifiableInformation)]
14+
#[derive(Clone, Serialize, Deserialize, Constructor, From)]
15+
pub struct UserAddress(String);
16+
17+
#[classified(ExampleTaxonomy::OrganizationallyIdentifiableInformation)]
18+
#[derive(Clone, Serialize, Deserialize, Constructor, From)]
19+
pub struct EmployeeID(String);
720

821
/// Holds info about a single corporate employee.
922
#[derive(Serialize, Deserialize, Clone)]
1023
pub struct Employee {
11-
pub name: PersonallyIdentifiableInformation<String>,
12-
pub address: PersonallyIdentifiableInformation<String>,
13-
pub id: OrganizationallyIdentifiableInformation<String>,
24+
pub name: UserName,
25+
pub address: UserAddress,
26+
pub id: EmployeeID,
1427
pub age: u32,
1528
}

crates/data_privacy/examples/employees/logging.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ pub fn set_redaction_engine_for_logging(engine: RedactionEngine) {
1111
REDACTION_ENGINE.set(engine).unwrap();
1212
}
1313

14-
pub fn classified_display<C, T>(value: &C) -> String
14+
pub fn classified_display<C>(value: &C) -> String
1515
where
16-
C: Classified<T>,
17-
T: Display,
16+
C: Classified,
17+
C::Payload: Display,
1818
{
1919
let engine = REDACTION_ENGINE.get().unwrap();
2020
let mut output = String::new();
21-
engine.display_redacted(value, |s| output.push_str(s));
21+
engine.redacted_display(value, |s| output.push_str(s));
2222
output
2323
}
2424

crates/data_privacy/examples/employees/main.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ mod employee;
3838
mod example_taxonomy;
3939
mod logging;
4040

41+
use crate::employee::{EmployeeID, UserAddress, UserName};
4142
use data_privacy::{RedactionEngineBuilder, SimpleRedactor, SimpleRedactorMode};
4243
use employee::Employee;
43-
use example_taxonomy::{ExampleTaxonomy, OrganizationallyIdentifiableInformation, PersonallyIdentifiableInformation};
44+
use example_taxonomy::ExampleTaxonomy;
4445
use logging::{log, set_redaction_engine_for_logging};
4546
use std::fs::{File, OpenOptions};
4647
use std::io::BufReader;
@@ -83,9 +84,9 @@ fn app_loop() {
8384

8485
// pretend some UI collected some data, and we then create an Employee struct to hold this data
8586
let employee = Employee {
86-
name: PersonallyIdentifiableInformation::new("John Doe".to_string()),
87-
address: PersonallyIdentifiableInformation::new("123 Elm Street".to_string()),
88-
id: OrganizationallyIdentifiableInformation::new("12345-52".to_string()),
87+
name: UserName::new("John Doe".to_string()),
88+
address: UserAddress::new("123 Elm Street".to_string()),
89+
id: EmployeeID::new("12345-52".to_string()),
8990
age: 33,
9091
};
9192

crates/data_privacy/src/classified.rs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use crate::DataClass;
1010
/// Although instances are encapsulated, it's possible to extract the instances when
1111
/// classification is no longer needed.
1212
///
13+
/// You rarely implement this trait by hand, instead use the [`classified`](data_privacy_macros::classified) macro.
14+
///
1315
/// # Example
1416
///
1517
/// ```rust
@@ -26,43 +28,45 @@ use crate::DataClass;
2628
/// }
2729
/// }
2830
///
29-
/// struct ClassifiedPerson {
30-
/// person: Person
31-
/// }
31+
/// struct ClassifiedPerson(Person);
3232
///
3333
/// impl ClassifiedPerson {
3434
/// fn new(person: Person) -> Self {
35-
/// Self { person }
35+
/// Self(person)
3636
/// }
3737
/// }
3838
///
39-
/// impl Classified<Person> for ClassifiedPerson {
39+
/// impl Classified for ClassifiedPerson {
40+
/// type Payload = Person;
41+
///
4042
/// fn declassify(self) -> Person {
41-
/// self.person
43+
/// self.0
4244
/// }
4345
///
44-
/// fn as_declassified(&self) -> &Person {
45-
/// &self.person
46+
/// fn as_declassified(&self) -> &Person {
47+
/// &self.0
4648
/// }
4749
///
48-
/// fn as_declassified_mut(&mut self) -> &mut Person {
49-
/// &mut self.person
50+
/// fn as_declassified_mut(&mut self) -> &mut Person {
51+
/// &mut self.0
5052
/// }
5153
///
5254
/// fn data_class(&self) -> DataClass {
5355
/// DataClass::new("example_taxonomy", "classified_person")
5456
/// }
5557
/// }
5658
/// ```
57-
pub trait Classified<T> {
59+
pub trait Classified {
60+
type Payload;
61+
5862
/// Exfiltrates the payload, allowing it to be used outside the classified context.
5963
///
6064
/// Exfiltration should be done with caution, as it may expose sensitive information.
6165
///
6266
/// # Returns
6367
/// The original payload.
6468
#[must_use]
65-
fn declassify(self) -> T;
69+
fn declassify(self) -> Self::Payload;
6670

6771
/// Provides a reference to the declassified payload, allowing read access without ownership transfer.
6872
///
@@ -71,7 +75,7 @@ pub trait Classified<T> {
7175
/// # Returns
7276
/// A reference to the original payload.
7377
#[must_use]
74-
fn as_declassified(&self) -> &T;
78+
fn as_declassified(&self) -> &Self::Payload;
7579

7680
/// Provides a mutable reference to the declassified payload, allowing write access without ownership transfer.
7781
///
@@ -80,15 +84,15 @@ pub trait Classified<T> {
8084
/// # Returns
8185
/// A mutable reference to the original payload.
8286
#[must_use]
83-
fn as_declassified_mut(&mut self) -> &mut T;
87+
fn as_declassified_mut(&mut self) -> &mut Self::Payload;
8488

8589
/// Visits the payload with the provided operation.
86-
fn visit(&self, operation: impl FnOnce(&T)) {
90+
fn visit(&self, operation: impl FnOnce(&Self::Payload)) {
8791
operation(self.as_declassified());
8892
}
8993

9094
/// Visits the payload with the provided operation.
91-
fn visit_mut(&mut self, operation: impl FnOnce(&mut T)) {
95+
fn visit_mut(&mut self, operation: impl FnOnce(&mut Self::Payload)) {
9296
operation(self.as_declassified_mut());
9397
}
9498

@@ -106,7 +110,9 @@ mod tests {
106110
data: u32,
107111
}
108112

109-
impl Classified<u32> for ClassifiedExample {
113+
impl Classified for ClassifiedExample {
114+
type Payload = u32;
115+
110116
fn declassify(self) -> u32 {
111117
self.data
112118
}

crates/data_privacy/src/classified_wrapper.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,17 @@ where
3030
T: Debug,
3131
{
3232
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33-
f.write_fmt(format_args!("<{}/{}:REDACTED>", self.data_class.taxonomy(), self.data_class.name()))
33+
f.write_fmt(format_args!(
34+
"<CLASSIFIED:{}/{}>",
35+
self.data_class.taxonomy(),
36+
self.data_class.name()
37+
))
3438
}
3539
}
3640

37-
impl<T> Classified<T> for ClassifiedWrapper<T> {
41+
impl<T> Classified for ClassifiedWrapper<T> {
42+
type Payload = T;
43+
3844
fn declassify(self) -> T {
3945
self.value
4046
}
@@ -114,7 +120,7 @@ mod tests {
114120
let classified = ClassifiedWrapper::new(42, CommonTaxonomy::Sensitive.data_class());
115121
assert_eq!(classified.as_declassified(), &42);
116122
assert_eq!(classified.data_class(), CommonTaxonomy::Sensitive.data_class());
117-
assert_eq!(format!("{classified:?}"), "<common/sensitive:REDACTED>");
123+
assert_eq!(format!("{classified:?}"), "<CLASSIFIED:common/sensitive>");
118124
}
119125

120126
#[test]
@@ -183,6 +189,6 @@ mod tests {
183189
assert_eq!(classified.as_declassified(), &"hello".to_string());
184190
assert_eq!(classified.data_class(), CommonTaxonomy::UnknownSensitivity.data_class());
185191
// Debug should redact and include the correct class path
186-
assert_eq!(format!("{classified:?}"), "<common/unknown_sensitivity:REDACTED>");
192+
assert_eq!(format!("{classified:?}"), "<CLASSIFIED:common/unknown_sensitivity>");
187193
}
188194
}

crates/data_privacy/src/common_taxonomy/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ mod tests {
5252

5353
#[test]
5454
fn test_debug_trait() {
55-
assert_eq!(format!("{:?}", Sensitive::new(2)), "<common/sensitive:REDACTED>");
56-
assert_eq!(format!("{:?}", Insensitive::new("Hello")), "<common/insensitive:REDACTED>");
55+
assert_eq!(format!("{:?}", Sensitive::new(2)), "<CLASSIFIED:common/sensitive>");
56+
assert_eq!(format!("{:?}", Insensitive::new("Hello")), "<CLASSIFIED:common/insensitive>");
5757
assert_eq!(
5858
format!("{:?}", UnknownSensitivity::new(31.4)),
59-
"<common/unknown_sensitivity:REDACTED>"
59+
"<CLASSIFIED:common/unknown_sensitivity>"
6060
);
6161
}
6262

0 commit comments

Comments
 (0)