Skip to content

Commit

Permalink
Add replace options (hardcoded, value from dictionary) (#11)
Browse files Browse the repository at this point in the history
* Add new fields to flexipage

* Add replace dictionary for firstname, lastname, fullname

* Fix error on MaskSObjectUtils.executeBatch method

* Remove WIP mention on Launch Batch LWC

* Add demo data to md

* Clarify sobjects description

* fix typo

* Increase coverage and init dictionary only if needed

Co-authored-by: Thomas Prouvot <[email protected]>
  • Loading branch information
tprouvot and tprouvot authored Aug 26, 2022
1 parent 07deafb commit a54575f
Show file tree
Hide file tree
Showing 15 changed files with 1,228 additions and 37 deletions.
32 changes: 27 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@ Mask SObject Framework is not an official Salesforce product, it has not been of

The configuration is based on two objects:

- MaskSObject__c : which define the object to mask with options such as the order sequence and the where clause
- MaskSObjectField___c : which define the fields to mask and the option of masking (erase, randomize ...)
- MaskSObject__c : object to mask with options such as the order sequence and the where clause
- MaskSObjectField___c : field to mask and the option of masking (erase, randomize ...)


[![SObjedt config](./screenshots/2022-08-10_09-42-09.png)](./screenshots/2022-08-10_09-42-09.png)

To assign required pset run following command:
```sh
sfdx force:apex:execute -f scripts/assignPset.apex
```


If you want to insert demo data, please run following command:
```sh
sfdx force:apex:execute -f scripts/importDemo.apex
```
## How To Run Data Masking ?

- With execute anonymous and the following code
Expand All @@ -34,17 +45,28 @@ MaskSObjectUtils.executeBatch('Contact');

**WARNING**: if you choose this option, you need a Partial Copy Sandbox or a Full Copy Sandbox and data configuration on Production.

- (WIP) Manually using [Launch Batch LWC](https://github.com/tprouvot/launch-batch-lwc)
- Manually using [Launch Batch LWC](https://github.com/tprouvot/launch-batch-lwc)

## Actions Types
## Actions
- Randomize:
- Generate a X char String based on `Crypto.generateAesKey(128);` method where X is the number of characters of the input to anonymize.
> 'SALESFORCE.COM FRANCE' => 'iih5e2UT0qGZ8fJaNCbTT'
- Obfuscate:
- Replace and lowercase following chars `{'a', 'e', 'i', 'o', '1', '2', '5', '6'};` by `'x'`
> 'SALESFORCE.COM FRANCE' => 'sxlxsfxrcx.cxm frxncx'
> 'SALESFORCE.COM FRANCE' => 'sxlxsfxrcx.cxm frxncx'
- Erase:
- > 'SALESFORCE.COM FRANCE' => ''
- Replace:
- Actions Types:
- Hardcoded: You must insert an hardocoded value in Value__c field to replace the current field value with hardcoded one.
- Dictionary: You can choose different dictionary fields to replace the current value (Firstname, Lastname, Fullname). A random line from MaskSObjectDictionary.json file will be selected to fill the field.
> Dictionary Firstname : 'Thomas' => 'Corie', Dictionary Fullname : 'John Doe' => 'Corie Joberne' ...

## Data Dictionary
The data dictionary is stored in **MaskSObjectDictionary.json StaticResource**.
You can edit this file and replace the current values with yours if you need more common names for a particular country for example.

Website used to generate the data https://www.mockaroo.com/

## Fields specificity
- Email
Expand Down
18 changes: 16 additions & 2 deletions force-app/main/default/classes/MaskSObjectBatch.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*/
public with sharing class MaskSObjectBatch implements Database.Batchable<sObject>{

private List<MaskSObjectDictionaryModel> dictionary;

private Integer sequence;
private Integer lastSequence;
private Integer batchSize;
Expand Down Expand Up @@ -56,8 +58,10 @@ public with sharing class MaskSObjectBatch implements Database.Batchable<sObject
public void execute(Database.BatchableContext BC, List<SObject> scope){

for(SObject sobj : scope){
Integer random = dictionary != null ? MaskSObjectUtils.getRandomInt(dictionary.size()) : null;
MaskSObjectDictionaryModel data = dictionary != null ? dictionary.get(random) : null;
for(MaskSObjectField__c field : this.fields){
MaskSObjectUtils.maskField(fieldType, field, sobj);
MaskSObjectUtils.maskField(fieldType, field, sobj, data);
}
}

Expand Down Expand Up @@ -116,11 +120,21 @@ public with sharing class MaskSObjectBatch implements Database.Batchable<sObject
//store the fields to mask for the query
this.fields = setting.MaskSObjectFields__r;
queryFields = new List<String>();
Boolean requireDictionary = false;
for(MaskSObjectField__c field : setting.MaskSObjectFields__r){
this.queryFields.add(field.APIName__c);
if(!requireDictionary && field.ActionType__c != null && field.ActionType__c.startsWith(MaskSObjectConstants.ACTION_TYPE_DICT)){
requireDictionary = true;
}
}

if(this.dictionary == null && requireDictionary){
StaticResource sr = [SELECT Body FROM StaticResource WHERE Name = 'MaskSObjectDictionary' LIMIT 1];
this.dictionary = (List<MaskSObjectDictionaryModel>)JSON.deserializeStrict(sr.Body.toString(),
List<MaskSObjectDictionaryModel>.class);
}

//store describe field resultto set it only on new SObject iteration
//store describe field result to set it only on new SObject iteration
setFieldsTypes();
}

Expand Down
36 changes: 24 additions & 12 deletions force-app/main/default/classes/MaskSObjectBatchTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public class MaskSObjectBatchTest {
private static String PHONE = '0606060606';
private static String LASTNAME = 'Doe';
private static String ASSISTANTNAME = 'Jane DOE';
private static String TESTLABEL = 'TEST';
private static String EMAIL = '[email protected]';

/**
Expand All @@ -14,21 +15,31 @@ public class MaskSObjectBatchTest {
public static void createTestData(){
List<MaskSObject__c> sobjMask = new List<MaskSObject__c>{
new MaskSObject__c(Sequence__c = 1, APIName__c = 'Account'),
new MaskSObject__c(Sequence__c = 2, APIName__c = 'Contact', WhereClause__c = 'AssistantName != null')
new MaskSObject__c(Sequence__c = 2, APIName__c = 'Contact', WhereClause__c = 'AssistantName != null', BatchSize__c = 1600)
};
insert sobjMask;

List<MaskSObjectField__c> fieldMask = new List<MaskSObjectField__c>{
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(0).Id, APIName__c = 'Name', Action__c = MaskSObjectConstants.ACTION_OBFUSCATE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(0).Id, APIName__c = 'Phone', Action__c = MaskSObjectConstants.ACTION_OBFUSCATE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'LastName', Action__c = MaskSObjectConstants.ACTION_OBFUSCATE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'AssistantName', Action__c = MaskSObjectConstants.ACTION_ERASE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'Email', Action__c = MaskSObjectConstants.ACTION_RANDOMIZE)
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(0).Id, APIName__c = 'Name',
Action__c = MaskSObjectConstants.ACTION_OBFUSCATE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(0).Id, APIName__c = 'Phone',
Action__c = MaskSObjectConstants.ACTION_OBFUSCATE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'FirstName',
Action__c = MaskSObjectConstants.ACTION_REPLACE, ActionType__c = MaskSObjectConstants.ACTION_TYPE_DICT_FIRST),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'LastName',
Action__c = MaskSObjectConstants.ACTION_REPLACE, ActionType__c = MaskSObjectConstants.ACTION_TYPE_DICT_LAST),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'AssistantName',
Action__c = MaskSObjectConstants.ACTION_REPLACE, ActionType__c = MaskSObjectConstants.ACTION_TYPE_HARDCODED,
Value__c = TESTLABEL),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'HomePhone',
Action__c = MaskSObjectConstants.ACTION_ERASE),
new MaskSObjectField__c(MaskSObject__c = sobjMask.get(1).Id, APIName__c = 'Email',
Action__c = MaskSObjectConstants.ACTION_RANDOMIZE)
};
insert fieldMask;

insert new Account(Name = NAME, Phone = PHONE);
insert new Contact(FirstName = 'John', LastName = LASTNAME, AssistantName = ASSISTANTNAME, Email = EMAIL);
insert new Contact(FirstName = 'John', LastName = LASTNAME, AssistantName = ASSISTANTNAME, Email = EMAIL, HomePhone = '0606060606');
}

@isTest
Expand All @@ -43,28 +54,29 @@ public class MaskSObjectBatchTest {
System.assertNotEquals(NAME, acc.Name, 'The account name should be masked');
System.assertNotEquals(PHONE, acc.Phone, 'The account phone should be masked');

Contact cont = [SELECT Id, LastName, AssistantName, Email FROM Contact LIMIT 1];
Contact cont = [SELECT Id, FirstName, LastName, AssistantName, Email, HomePhone FROM Contact LIMIT 1];
System.assertNotEquals(LASTNAME, cont.FirstName, 'The contact firstname should be masked');
System.assertNotEquals(LASTNAME, cont.LastName, 'The contact lastname should be masked');
System.assertNotEquals(ASSISTANTNAME, cont.AssistantName, 'The contact AssistantName should be masked');
System.assertEquals(TESTLABEL, cont.AssistantName, 'The contact AssistantName should be replaced by ' + TESTLABEL);
System.assertNotEquals(EMAIL, cont.Email, 'The contact Email should be masked');
System.assertEquals(null, cont.HomePhone, 'The contact HomePhone should be erased');
}

@isTest
static void maskSobjectNameTest(){
createTestData();

Test.startTest();
Database.executeBatch(new MaskSObjectBatch('Contact'));
MaskSObjectUtils.executeBatch('Contact');
Test.stopTest();

Contact cont = [SELECT Id, LastName, AssistantName, Email FROM Contact LIMIT 1];
System.assertNotEquals(LASTNAME, cont.LastName, 'The contact lastname should be masked');
System.assertNotEquals(ASSISTANTNAME, cont.AssistantName, 'The contact AssistantName should be masked');
System.assertEquals(TESTLABEL, cont.AssistantName, 'The contact AssistantName should be replaced by ' + TESTLABEL);
System.assertNotEquals(EMAIL, cont.Email, 'The contact Email should be masked');

Account acc = [SELECT Id, Name, Phone FROM Account LIMIT 1];
System.assertEquals(NAME, acc.Name, 'The account name should not be masked');
System.assertEquals(PHONE, acc.Phone, 'The account phone should not be masked');

}
}
9 changes: 9 additions & 0 deletions force-app/main/default/classes/MaskSObjectConstants.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ public with sharing class MaskSObjectConstants {
/**
* MaskSObjectField__c
*/

//Action__c
public static final String ACTION_ERASE = 'Erase';
public static final String ACTION_REPLACE = 'Replace';
public static final String ACTION_OBFUSCATE = 'Obfuscate';
public static final String ACTION_RANDOMIZE = 'Randomize';

//ActionType__c
public static final String ACTION_TYPE_HARDCODED = 'Hardcoded';
public static final String ACTION_TYPE_DICT = 'Dictionary';
public static final String ACTION_TYPE_DICT_FIRST = ACTION_TYPE_DICT + '.firstname';
public static final String ACTION_TYPE_DICT_LAST = ACTION_TYPE_DICT + '.lastname';
public static final String ACTION_TYPE_DICT_FULL = ACTION_TYPE_DICT + '.fullname';
}
11 changes: 11 additions & 0 deletions force-app/main/default/classes/MaskSObjectDictionaryModel.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public with sharing class MaskSObjectDictionaryModel {

public String firstName;
public String lastName;
public String phone;
public String email;
public String streetNumber;
public String street;
public String city;
public String country;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<status>Active</status>
</ApexClass>
33 changes: 28 additions & 5 deletions force-app/main/default/classes/MaskSObjectUtils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ public with sharing class MaskSObjectUtils {
public static void executeBatch(String sobjectName){
List<MaskSObject__c> settings = MaskSObjectUtils.getMaskSObjectList(0, sobjectName);
if(!settings.isEmpty()){
Integer batchSize = settings.get(0).BatchSize__c != null ? Integer.valueOf(settings.get(0).BatchSize__c) : 200;
Database.executeBatch(new MaskSObjectBatch(), batchSize);
MaskSObject__c setting = settings.get(0);
Integer batchSize = setting.BatchSize__c != null ? Integer.valueOf(setting.BatchSize__c) : 200;
Database.executeBatch(new MaskSObjectBatch(sobjectName), batchSize);
}
}

Expand All @@ -20,7 +21,7 @@ public with sharing class MaskSObjectUtils {
*/
public static List<MaskSObject__c> getMaskSObjectList(Integer sequence, String sobjectName){
return [SELECT APIName__c, Sequence__c, WhereClause__c, BatchSize__c
,(SELECT APIName__c, Action__c FROM MaskSObjectFields__r) FROM MaskSObject__c
,(SELECT APIName__c, Action__c, ActionType__c, Value__c FROM MaskSObjectFields__r) FROM MaskSObject__c
WHERE Sequence__c >:sequence AND APIName__c LIKE :sobjectName ORDER BY Sequence__c];
}

Expand Down Expand Up @@ -48,13 +49,19 @@ public with sharing class MaskSObjectUtils {
return val;
}

public static void maskField(Map<String, Schema.DisplayType> fieldType, MaskSObjectField__c field, SObject sobj){
public static void maskField(Map<String, Schema.DisplayType> fieldType, MaskSObjectField__c field, SObject sobj, MaskSObjectDictionaryModel data){
String val = (String)sobj.get(field.APIName__c);
if(val != null){
String retVal;

if(MaskSObjectConstants.ACTION_ERASE.equals(field.Action__c)){
retVal = null;
} else if(MaskSObjectConstants.ACTION_REPLACE.equals(field.Action__c)){
if(MaskSObjectConstants.ACTION_TYPE_HARDCODED.equals(field.ActionType__c)){
retVal = field.Value__c;
} else if(field.ActionType__c.startsWith(MaskSObjectConstants.ACTION_TYPE_DICT)){
retVal = getDictionaryValue(val, field, data);
}
} else {
String prefix = '';
String suffix = '';
Expand Down Expand Up @@ -83,4 +90,20 @@ public with sharing class MaskSObjectUtils {
sobj.put(field.APIName__c, retVal);
}
}
}

public static String getDictionaryValue(String val, MaskSObjectField__c field, MaskSObjectDictionaryModel data){
String retVal;
if(MaskSObjectConstants.ACTION_TYPE_DICT_FIRST.equals(field.ActionType__c)){
retVal = data.firstName;
} else if(MaskSObjectConstants.ACTION_TYPE_DICT_LAST.equals(field.ActionType__c)){
retVal = data.lastName;
} else if(MaskSObjectConstants.ACTION_TYPE_DICT_FULL.equals(field.ActionType__c)){
retVal = data.firstName + ' ' + data.lastName;
}
return retVal;
}

public static Integer getRandomInt(Integer max){
return Integer.valueof((Math.random() * max));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
</componentInstanceProperties>
<componentInstanceProperties>
<name>maxRecordsToDisplay</name>
<value>10</value>
<value>30</value>
</componentInstanceProperties>
<componentInstanceProperties>
<name>parentFieldApiName</name>
Expand All @@ -107,6 +107,12 @@
<valueListItems>
<value>Action__c</value>
</valueListItems>
<valueListItems>
<value>ActionType__c</value>
</valueListItems>
<valueListItems>
<value>Value__c</value>
</valueListItems>
</valueList>
</componentInstanceProperties>
<componentInstanceProperties>
Expand Down Expand Up @@ -138,4 +144,4 @@
<name>flexipage:recordHomeSingleColNoHeaderTemplateDesktop</name>
</template>
<type>RecordPage</type>
</FlexiPage>
</FlexiPage>
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<field>MaskSObject__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>ActionType__c</field>
<behavior>Required</behavior>
<field>Action__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
Expand All @@ -26,8 +26,8 @@
<field>APIName__c</field>
</layoutItems>
<layoutItems>
<behavior>Required</behavior>
<field>Action__c</field>
<behavior>Edit</behavior>
<field>ActionType__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
Expand All @@ -37,9 +37,9 @@
<detailHeading>false</detailHeading>
<editHeading>true</editHeading>
<label>Custom Links</label>
<layoutColumns />
<layoutColumns />
<layoutColumns />
<layoutColumns/>
<layoutColumns/>
<layoutColumns/>
<style>CustomLinks</style>
</layoutSections>
<showEmailCheckbox>false</showEmailCheckbox>
Expand All @@ -48,9 +48,9 @@
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
<summaryLayout>
<masterLabel>00h7Q00000BYmAK</masterLabel>
<masterLabel>00h1X000004kwJl</masterLabel>
<sizeX>4</sizeX>
<sizeY>0</sizeY>
<summaryLayoutStyle>Default</summaryLayoutStyle>
</summaryLayout>
</Layout>
</Layout>
Loading

0 comments on commit a54575f

Please sign in to comment.