
JasperFx Software helps our customers be more successful with their usage of the “Critter Stack” tools (or any other server side .NET tooling you might be using). The work in this post was delivered for a JasperFx customer to help protect their customer’s private information. If you need or want any help with event sourcing, Event Driven Architecture, or automated testing, drop us a note and we’d be happy to talk with you about what JasperFx can do for you.
I defy you to say the title of this post out loud in rapid succession without stumbling over it.
According to the U.S. Department of Labor, “Personal Identifiable Information” (PII) is defined as:
Any representation of information that permits the identity of an individual to whom the information applies to be reasonably inferred by either direct or indirect means.
Increasingly, Marten users are running into requirements to be able to “forget” PII that is persisted within a Marten database. For the document storage, I think this is relatively easy to do with a host of existing functionality including the partial update functionality that Marten got (back) in V7. For the event store though, there wasn’t anything built in that would have made it easy to erase or “mask” protected information within the persisted event data — until now!
The Marten 7.31 adds a new capability to erase or mask PII data within the event store.
For a variety of reasons, you may wish to remove or mask sensitive data elements in a Marten database without necessarily deleting the information as a whole. Documents can be amended with Marten’s Patching API. With event data, you now have options to reach into the event data and rewrite selected members as well as to add custom headers. First, start by defining data masking rules by event type like so:
var builder = Host.CreateApplicationBuilder();
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("marten"));
// By a single, concrete type
opts.Events.AddMaskingRuleForProtectedInformation<AccountChanged>(x =>
{
// I'm only masking a single property here, but you could do as much as you want
x.Name = "****";
});
// Maybe you have an interface that multiple event types implement that would help
// make these rules easier by applying to any event type that implements this interface
opts.Events.AddMaskingRuleForProtectedInformation<IAccountEvent>(x => x.Name = "****");
// Little fancier
opts.Events.AddMaskingRuleForProtectedInformation<MembersJoined>(x =>
{
for (int i = 0; i < x.Members.Length; i++)
{
x.Members[i] = "*****";
}
});
});
That’s strictly a configuration time effort. Next, you can apply the masking on demand to any subset of events with the IDocumentStore.Advanced.ApplyEventDataMasking() API. First, you can apply the masking for a single stream:
public static Task apply_masking_to_streams(IDocumentStore store, Guid streamId, CancellationToken token)
{
return store
.Advanced
.ApplyEventDataMasking(x =>
{
x.IncludeStream(streamId);
// You can add or modify event metadata headers as well
// BUT, you'll of course need event header tracking to be enabled
x.AddHeader("masked", DateTimeOffset.UtcNow);
}, token);
}
As a finer grained operation, you can specify an event filter (Func<IEvent, bool>) within an event stream to be masked with this overload:
public static Task apply_masking_to_streams_and_filter(IDocumentStore store, Guid streamId, CancellationToken token)
{
return store
.Advanced
.ApplyEventDataMasking(x =>
{
// Mask selected events within a single stream by a user defined criteria
x.IncludeStream(streamId, e => e.EventTypesAre(typeof(MembersJoined), typeof(MembersDeparted)));
// You can add or modify event metadata headers as well
// BUT, you'll of course need event header tracking to be enabled
x.AddHeader("masked", DateTimeOffset.UtcNow);
}, token);
}
Note that regardless of what events you specify, only events that match a pre-registered masking rule will have the header changes applied.
To apply the event data masking across streams on an arbitrary grouping, you can use a LINQ expression as well:
public static Task apply_masking_by_filter(IDocumentStore store, Guid[] streamIds)
{
return store.Advanced.ApplyEventDataMasking(x =>
{
x.IncludeEvents(e => e.EventTypesAre(typeof(QuestStarted)) && e.StreamId.IsOneOf(streamIds));
});
}
Finally, if you are using multi-tenancy, you can specify the tenant id as part of the same fluent interface:
public static Task apply_masking_by_tenant(IDocumentStore store, string tenantId, Guid streamId)
{
return store
.Advanced
.ApplyEventDataMasking(x =>
{
x.IncludeStream(streamId);
// Specify the tenant id, and it doesn't matter
// in what order this appears in
x.ForTenant(tenantId);
});
}
Here’s a couple more facts you might need to know:
- The masking rules can only be done at configuration time (as of right now)
- You can apply multiple masking rules for certain event types, and all will be applied when you use the masking API
- The masking has absolutely no impact on event archiving or projected data — unless you rebuild the projection data after applying the data masking of course
Summary
The Marten team is at least considering support for crypto-shredding in Marten 8.0, but no definite plans have been made yet. It might fit into the “Critter Stack 2025” release cycle that we’re just barely starting.
besides crypto shedding, is there an option for encryption at the moment? Or a recommended method?
PostgreSQL native encryption