Introduction
I've just released a brand new package: Redactable Models! ๐
It's a Laravel package that you can use to redact, obfuscate, or mask fields from your models in a consistent and easy way.
When building web applications, you'll often need to keep hold of old data for auditing or reporting purposes. But for data privacy and security reasons, you may want to redact the sensitive information from the data that you store. This way, you can keep the rows in the database, but without the sensitive information.
This package allows you to define which models and fields should be redacted, and how they should be redacted.
In this article, we're going to cover what the package does and how you can use it in your Laravel apps.
Or, if you'd prefer, you can check out the video I recorded about the package:
Install the Package
To get started with using the Redactable Models package, you'll need to install it via Composer by running the following command:
1composer require ashallendesign/redactable-models
The ashallendesign/redactable-models
package should now be installed.
Defining Redactable Models
In order to make a model redactable, you need to add the AshAllenDesign\RedactableModels\Interfaces\Redactable
interface to the model. This will enforce two new methods (redactable
and redactionStrategy
) that you need to implement.
Let's imagine we wanted to make our App\Models\User
model redactable. The model may look something like so:
1use AshAllenDesign\RedactableModels\Interfaces\Redactable; 2use Illuminate\Contracts\Database\Eloquent\Builder; 3 4class User extends Model implements Redactable 5{ 6 // ... 7 8 public function redactable(): Builder 9 {10 // ...11 }12 13 public function redactionStrategy(): RedactionStrategy14 {15 // ...16 }17}
The redactable
method allows you to return an instance of Illuminate\Contracts\Database\Eloquent\Builder
which defines the models that are redactable.
The redactionStrategy
method allows you to return an instance of AshAllenDesign\RedactableModels\Interfaces\RedactionStrategy
which defines how the fields should be redacted. We'll cover the available strategies further down.
As an example, if we wanted to redact the email
and name
fields from all App\Models\User
models older than 30 days, we could do the following:
1use AshAllenDesign\RedactableModels\Support\Strategies\ReplaceContents; 2use AshAllenDesign\RedactableModels\Interfaces\Redactable; 3use Illuminate\Contracts\Database\Eloquent\Builder; 4 5class User extends Model implements Redactable 6{ 7 // ... 8 9 public function redactable(): Builder10 {11 return static::query()->where('created_at', '<', now()->subDays(30));12 }13 14 public function redactionStrategy(): RedactionStrategy15 {16 return app(ReplaceContents::class)->replaceWith([17 'name' => 'REDACTED',18 'email' => 'redacted@redacted.com',19 ]);20 }21}
The model:redact
Command
In order to automatically redact the fields on the models, you can use the package's model:redact
command like so:
1php artisan model:redact
This will find all the models within your app's app/Models
directory that implement the AshAllenDesign\RedactableModels\Interfaces\Redactable
interface and redact the fields based on the defined redaction strategy and query.
You may want to set this to run on a schedule (such as on a daily basis) in your Laravel app's scheduler.
Redaction Strategies
The package ships with several strategies that you can use for redacting fields:
ReplaceContents
The ReplaceContents
strategy allows you to replace the contents of the fields with a specified value.
For example, if we wanted to replace the name
and email
fields, we could do the following:
1use AshAllenDesign\RedactableModels\Support\Strategies\ReplaceContents; 2use AshAllenDesign\RedactableModels\Interfaces\Redactable; 3use Illuminate\Foundation\Auth\User as Authenticatable; 4 5class User extends Authenticatable implements Redactable 6{ 7 // ... 8 9 public function redactionStrategy(): RedactionStrategy10 {11 return app(ReplaceContents::class)->replaceWith([12 'name' => 'REDACTED',13 'email' => 'redacted@redacted.com',14 ]);15 }16}
Running this against a model would replace the name
field with REDACTED
and the email
field with redacted@redacted.com
in the database.
The ReplaceContents
strategy also allows you to use a closure to define the replacement value. This can be useful if you want a bit more control over the redaction process. The closure should accept the model as an argument and have a void
return type.
For example, say you want to replace the name
field with name_
followed by their ID. You could do the following:
1use AshAllenDesign\RedactableModels\Support\Strategies\ReplaceContents; 2use AshAllenDesign\RedactableModels\Interfaces\Redactable; 3use Illuminate\Foundation\Auth\User as Authenticatable; 4 5class User extends Authenticatable implements Redactable 6{ 7 // ... 8 9 public function redactionStrategy(): RedactionStrategy10 {11 return app(ReplaceContents::class)->replaceWith(function (User $user): void {12 $user->name = 'name_'.$user->id;13 });14 }15}
Imagine we have a user with ID 123
and a name
of John Doe
. Running the above code would replace the name
field with name_123
.
HashContents
The HashContents
strategy allows you to MD5 hash the contents of the field.
This can be useful when you still want to be able to compare the redacted fields, but don't want to expose the original data.
For example, imagine you have an invitations
table that contains an email
field. You may want to find out how many unique email addresses have been invited, but you don't want to expose the email addresses themselves. You could do the following:
1use AshAllenDesign\RedactableModels\Support\Strategies\HashContents; 2use AshAllenDesign\RedactableModels\Interfaces\Redactable; 3use Illuminate\Database\Eloquent\Model; 4 5class Invitation extends Model implements Redactable 6{ 7 // ... 8 9 public function redactionStrategy(): RedactionStrategy10 {11 return app(HashContents::class))->fields([12 'email',13 ]);14 }15}
If the above strategy was run against a user with an email of ash@example.com
, their email would now be c223f6df7371b5b21b750ff3e7c9e71f
.
MaskContents
The MaskContents
strategy allows you to mask the contents of the field with a specified character.
You can define the character to use for the mask, and how many characters from the start and end of the field to leave unmasked like so:
1use AshAllenDesign\RedactableModels\Support\Strategies\MaskContents; 2use AshAllenDesign\RedactableModels\Interfaces\Redactable; 3use Illuminate\Foundation\Auth\User as Authenticatable; 4 5class User extends Authenticatable implements Redactable 6{ 7 // ... 8 9 public function redactionStrategy(): RedactionStrategy10 {11 return app(MaskContents::class)12 ->mask(field: 'name', character: '*', index: 0, length: 4)13 ->mask(field: 'email', character: '-', index: 2, length: 3);14 }15}
In the above example, the name
field would be masked with *
and the first 4 characters would be left unmasked. The email
field would be masked with -
and the first 2 and last 3 characters would be left unmasked.
This means if a user's name was Ash Allen
and their email was ash@example.com
, after redaction their name would be ****Allen
and their email would be as---xample.com
.
Custom Redaction Strategies
Although the package ships with several redaction strategies out of the box, you can create your own custom redaction strategies.
You just need to create a class that implements the AshAllenDesign\RedactableModels\Interfaces\RedactionStrategy
interface. This method enforces an apply
method which accepts the model and defines the redaction logic.
An example of a custom redaction strategy might look like so:
1use AshAllenDesign\RedactableModels\Interfaces\Redactable; 2use AshAllenDesign\RedactableModels\Interfaces\RedactionStrategy; 3use Illuminate\Database\Eloquent\Model; 4 5class CustomStrategy implements RedactionStrategy 6{ 7 public function apply(Redactable&Model $model): void 8 { 9 // Redaction logic goes here10 }11}
Manually Redacting Models
There may be times when you want to manually redact a model rather than using the model:redact
command. To do this you can use the redactFields
method that is available via the AshAllenDesign\RedactableModels\Traits\HasRedactableFields
trait.
You can apply the trait to your model like so:
1use AshAllenDesign\RedactableModels\Interfaces\Redactable; 2use AshAllenDesign\RedactableModels\Traits\HasRedactableFields; 3use Illuminate\Foundation\Auth\User as Authenticatable; 4 5class User extends Authenticatable implements Redactable 6{ 7 use HasRedactableFields; 8 9 // ...10}
You can now use the redactFields
method to redact the fields on the model like so:
1$user = User::find(1);2 3$user->redactFields();
By default, this will redact the fields using the strategy defined in the model's redactionStrategy
method.
You can override this by passing a custom redaction strategy to the redactFields
method like so:
1use App\Models\User;2use AshAllenDesign\RedactableModels\Support\Strategies\ReplaceContents;3 4$user = User::find(1);5 6$user->redactFields(7 strategy: app(ReplaceContents::class)->replaceWith(['name' => 'REDACTED'])8);
Events
When a model is redacted, an AshAllenDesign\RedactableModels\Events\ModelRedacted
event is fired that can be listened on.
That might be useful if you want to log an audit event to record that a model was redacted on a given date.
Want to Contribute?
If you'd like to contribute to the ashallendesign/redactable-models
package, you can check out the repository on GitHub at: https://github.com/ash-jc-allen/redactable-models.
I'm always open to accepting new PRs that add cool new features, or fix any bugs that might have slipped through!
If this package comes in handy for you, I'd also love to know!
Keep on building awesome stuff! ๐