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.

Consents

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.

Custom Table Data

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.

Empty Data Collector

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.

Data Portability

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.

Right to Access

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.

Right to be Forgotten 1

Right to be Forgotten 2

Right to be Forgotten 3

Right to be Forgotten 4

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.

Generator

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!

Share this article on   LinkedIn

Bryan Soltis

Hello. I am a Technical Evangelist here at Kentico and will be helping the technical community by providing guidance and best practices for all areas of the product. I might also do some karaoke. We'll see how the night goes...