Spending some quality time with the Kentico Data Protection module
By now, I’m sure you’re an expert on GDPR. You‘ve hung out with your favorite legal team to talk about it, read all the riveting blogs about the rules, and started dreaming in GDPR-eze. You have all the info on what you need to do, but may not be sure how to get things going within your Kentico sites. In this article, I’m going to walk you through the new Data Protection module included with your Kentico 11 EMS installation to help you get compliant.
It’s tough to make a topic around regulations sound exciting. I mean, if you’re totally into rules and legal jargon, then I suppose that any T&C is your go-to reading material. If not, then you know that every new standard brings along a lot of changes and work for developers. With GDPR, this means understanding your data a lot more, and preparing your company for the future. While not as exciting as the latest blockbuster, it’s still very important information you need to know.
With Kentico 11, we introduced a new suite of tools to help you on your journey. The Data Protection module provides the foundation you’ll use to collect user data, fulfill data requests, and remove sensitive information, if needed. In this blog, I’ll walk you through each new tool to help you understand how you can leverage this new module to get your applications GDPR-ready.
Kentico GDPR Documentation
The basics
Before we dive into the Data Protection module, it’s important to understand the underlying components you’ll be working with. There are several classes and systems, and you will end up making custom versions of each. To help you get started, I want to describe each component.
Identity collector
Before you can start working with user information, you need to define how the user exists in your system. You could have contacts, customers, users, and even a custom class where you keep people’s data. The identity collector is where you will define the identifies you are going to work with. These identities are passed onto the data collector and eraser classes when a user makes a GDPR-related request.
Data Collector
GDPR is about providing users with information. The data collector class is responsible for finding all the GDPR data within your system and preparing it for the user. This can include contact information, activity logs, and any number of other types of data, depending on what you are tracking and storing.
Data Writers
After you collect your data, you’re going to need to prepare it for the user in the right format. Because GDPR compliance means providing information in human and machine-readable formats, your data writers will be responsible for structuring the output properly.
Data Erasers
The last big area for your data is the ability to remove or anonymize it. Because GDPR requires users have the “right to be forgotten”, you will need to code your data erasers to completely remove the user data you’re tracking. If your situation requires you to keep some data in an anonymous state, your data eraser will be where you update the information. Again, the data collector is key here, as it will define the data you will potentially need to remove and/or update.
Consents
GDPR really starts with getting the user’s permission to collect and use information. This means explicitly stating the data you’re going to collect, how you are going to use it, and how they can manage their information in the future. The Data Protection module provides a simple interface for creating and updating consents. It supports multiple languages, and archives previous versions as they are updated.
Behind the scenes, Kentico stores consents at the contact level, allowing you to quickly identify any users that have agreed to your site conditions.The actual text of your consent will depend on the data you are collecting and how it will be used.
Let’s see it in action
For demonstration purposes, I updated my site to sotre some custom personal data. In my case, I have a custom table with “bios” for a few great names in the history of the universe. For each record, I have their name, email, birthday, and biography. You may have similar data in page types, contact records, or some other repository within your site.
Note
The Data Protection module is a built-in feature that is enabled if you have an EMS license. If you go to the module without creating your collectors, writers, and erasers, you'll see a warning message in the UI. Once you implement your custom code, this message will dissappear.
Custom code
For my demo, I needed to create my custom data protection classes. You can view this code below to see how I define the identities, collect the data, format it for delivery, and determine how it is removed. Note how I leveraged the Kentico APis to retrieve and update the data. These custom classes were implemented using a custom module within my project.
Identify Collector Code
public class CustomIdentityColector : IIdentityCollector
{
public void Collect(IDictionary<string, object> dataSubjectFilter, List<BaseInfo> identities)
{
// Does nothing if the identifier inputs do not contain the "email" key or if its value is empty
if (!dataSubjectFilter.ContainsKey("email"))
{
return;
}
string email = dataSubjectFilter["email"] as string;
if (String.IsNullOrWhiteSpace(email))
{
return;
}
// Prepares the code name (class name) of the custom table
string customTableClassName = "customtable.bio";
// Gets the custom table
DataClassInfo customTable = DataClassInfoProvider.GetDataClassInfo(customTableClassName);
if (customTable != null)
{
// Gets all data records from the custom table whose 'ItemText' field value starts with 'New text'
List<CustomTableItem> records = CustomTableItemProvider.GetItems(customTableClassName)
.WhereEquals("Email", email)
.ToList();
// Adds the matching objects to the list of collected identities
identities.AddRange(records);
}
}
}
Data Collector Code
class CustomDataCollector : IPersonalDataCollector
{
// Prepares a list of contact columns to be included in the personal data
// Every Tuple contains a column name, and user-friendly description of its content
private readonly List<Tuple<string, string>> recordColumns = new List<Tuple<string, string>> {
Tuple.Create("Name", "Name"),
Tuple.Create("Email", "Email"),
Tuple.Create("Bio", "Bio"),
Tuple.Create("Birthday", "Birthday")
};
public PersonalDataCollectorResult Collect(IEnumerable<BaseInfo> identities, string outputFormat)
{
// Gets a list of all objects added by registered IIdentityCollector implementations
List<CustomTableItem> records = identities.OfType<CustomTableItem>().ToList();
// Uses a writer class to create the personal data, in either XML format or as human-readable text
string recordData = null;
if (records.Any())
{
switch (outputFormat.ToLowerInvariant())
{
case PersonalDataFormat.MACHINE_READABLE:
recordData = GetXmlRecordData(records);
break;
case PersonalDataFormat.HUMAN_READABLE:
default:
recordData = GetTextRecordData(records);
break;
}
}
return new PersonalDataCollectorResult
{
Text = recordData
};
}
private string GetXmlRecordData(List<CustomTableItem> records)
{
using (var writer = new CustomXmlPersonalDataWriter())
{
// Wraps the contact data into a <OnlineMarketingData> tag
writer.WriteStartSection("CustomTableData");
foreach (CustomTableItem record in records)
{
// Writes a tag representing an object
writer.WriteStartSection(record.ClassName);
// Writes tags for the record's personal data columns and their values
writer.WriteObject(record, recordColumns.Select(t => t.Item1).ToList());
// Closes the object tag
writer.WriteEndSection();
}
// Closes the <OnlineMarketingData> tag
writer.WriteEndSection();
return writer.GetResult();
}
}
private string GetTextRecordData(List<CustomTableItem> records)
{
var writer = new CustomTextPersonalDataWriter();
writer.WriteStartSection("Custom table data");
foreach (CustomTableItem record in records)
{
writer.WriteStartSection("Record");
// Writes user-friendly descriptions of the contact's personal data columns and their values
writer.WriteObject(record, recordColumns);
writer.WriteEndSection();
}
return writer.GetResult();
}
}
Data Writers Code
class CustomXmlPersonalDataWriter : IDisposable
{
private readonly StringBuilder stringBuilder;
private readonly XmlWriter xmlWriter;
public CustomXmlPersonalDataWriter()
{
stringBuilder = new StringBuilder();
xmlWriter = XmlWriter.Create(stringBuilder, new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true });
}
// Writes an opening XML tag with a specified name
public void WriteStartSection(string sectionName)
{
// Replaces period characters in object names with underscores
sectionName = sectionName.Replace('.', '_');
xmlWriter.WriteStartElement(sectionName);
}
// Writes XML tags representing the specified columns of a Kentico object (BaseInfo) and their values
public void WriteObject(BaseInfo baseInfo, List<string> columns)
{
foreach (string column in columns)
{
object value = baseInfo.GetValue(column);
if (value != null)
{
xmlWriter.WriteStartElement(column);
xmlWriter.WriteValue(XmlHelper.ConvertToString(value));
xmlWriter.WriteEndElement();
}
}
}
// Writes a closing XML tag for the most recent open tag
public void WriteEndSection()
{
xmlWriter.WriteEndElement();
}
// Gets a string containing the writer's overall XML data
public string GetResult()
{
xmlWriter.Flush();
return stringBuilder.ToString();
}
// Releases all resources used by the current XmlPersonalDataWriter instance.
public void Dispose()
{
xmlWriter.Dispose();
}
}
class CustomTextPersonalDataWriter
{
private readonly StringBuilder stringBuilder;
private int indentationLevel;
public CustomTextPersonalDataWriter()
{
stringBuilder = new StringBuilder();
indentationLevel = 0;
}
// Writes horizontal tabs based on the current indentation level
private void Indent()
{
stringBuilder.Append('\t', indentationLevel);
}
// Writes text representing a new section of data, and increases the indentation level
public void WriteStartSection(string sectionName)
{
Indent();
stringBuilder.AppendLine(sectionName + ": ");
indentationLevel++;
}
// Writes the specified columns of a Kentico object (BaseInfo) and their values
public void WriteObject(BaseInfo baseInfo, List<Tuple<string, string>> columns)
{
foreach (var column in columns)
{
// Gets the name of the current column
string columnName = column.Item1;
// Gets a user-friendly name for the current column
string columnDisplayName = column.Item2;
// Filters out identifier columns from the human-readable text data
if (columnName.Equals(baseInfo.TypeInfo.IDColumn, StringComparison.Ordinal) ||
columnName.Equals(baseInfo.TypeInfo.GUIDColumn, StringComparison.Ordinal))
{
continue;
}
// Gets the value of the current column for the given object
object value = baseInfo.GetValue(columnName);
if (value != null)
{
Indent();
stringBuilder.AppendFormat("{0}: ", columnDisplayName);
stringBuilder.Append(value);
stringBuilder.AppendLine();
}
}
}
// "Closes" a text section by reducing the indentation level
public void WriteEndSection()
{
indentationLevel--;
}
// Gets a string containing the writer's overall text
public string GetResult()
{
return stringBuilder.ToString();
}
}
Data Eraser code
public class CustomDataEraser : IPersonalDataEraser
{
public void Erase(IEnumerable<BaseInfo> identities, IDictionary<string, object> configuration)
{
// Gets all objects added by registered IIdentityCollector implementations
var records = identities.OfType<CustomTableItem>();
// Does nothing if no records were collected
if (!records.Any())
{
return;
}
// The context ensures that objects are permanently deleted with all versions, without creating recycle bin records
using (new CMSActionContext() { CreateVersion = false })
{
// Deletes the given records
foreach (CustomTableItem record in records)
{
CustomTableItemProvider.DeleteItem(record);
}
}
}
}
Data Portability
Depending on the purpose, the user data you deliver will need to be in either machine or human-readable format. While the exact structure depends on your implementation, the Data Protection module is already set up to help you quickly scour your system for information and deliver it. Your identity and data collector(s) power this functionality by defining the information you need to return to the user.
In the Data Protection module, select Data Portability. In the field, enter the user’s email. The module will collect the information using your custom classes, then format it with the XmlPersonalDataWriter into as machine-readable format.
For my demo, I selected a user and confirmed it displayed the data correctly.
This data can then be delivered to the user to comply with the new regulations. Depending on your identity and data collector implementations, this information will be unique for your scenario.
Right to Access
Much like the Data Portability utility, the Right to Access tool uses the identity and data collector(s) to retrieve the specified user’s data. This page uses the TextPersonalDataWriter class to format the information into a human-readable structure.
For my demo, I selected a user and confirmed it displayed the data correctly.
Your site administrators can copy the returned data to return the users.
Right to be Forgotten
If a user requests their information be “forgotten”, this utility will be used to process their data. Based on your data eraser definition, the options presented to your site administrators will allow them to effectively remove the identifiable information for the user.
In my demo, I selected one of my users and “removed” them from the site.
The Dancing Goat demo
Let’s talk about the Dancing Goat demo site. If you’ve installed the site, you probably have come across the Generator page under /Special-Pages. This page is designed to simulate various types of data and functionality, for demo purposes. This includes sample EMS data, Azure Search integration, and GDPR information.
It’s important to know that the GDPR implementation enabled with this page is only a sample of what a company would need to do to bring their site up to compliance. GDPR is a very complex concept, and every site will be different as to what data they collect, how they collect it, and what functionality they need to provide to their users.
The important thing to take away from this is that the Dancing Goat GDPR demo is only an example of using a data collector and eraser. You will need to create your own collectors and erasers to match your data and requirements!
Give it a try
GDPR is a pretty big deal, and will require some work on your side to implement correctly. By using the new Data Protection module in Kentico, you can define your data collection and erasure processes, allowing you to respond to user requests quickly and easily. By maintaining your consents within the site, you can ensure each contact is agreeing to your policies. As you start to get ready for GDPR, know that Kentico has your back to help you every step of the way!