Saturday, May 2, 2020

Execute GDPR "Right to be forgotten" on your custom data in Sitecore 9.3

This article is inspired by Rob Habraken presentation during Global Virtual SUGCON 2020. Rob described how to automate anonymizing based on Sitecore Marketing Automation actions. Also, he described how it could be applied to data in Sitecore Forms.

From my side, I recently worked on cleaning up custom data in Sitecore to execute "right to be forgotten" and be fully GDPR compliant. I want to present general way, how to implement it in Sitecore for any custom data.

Sitecore 9.3 is GDPR compliant. And you should not worry, how it saves contact data. But you should know that all contact data could be anonymized by next action:

  1. Open Experience Profile
  2. Find contact that you are interested in
  3. Click on actions menu
  4. Select Anonymize action

This sequence of action, will execute right to be forgotten.

It works until you need to save some additional data in some separate locations(CRM, data storage, etc.). Once you need to have right to be forgotten for your custom data, you need to implement it by yourself. You are not able to rely on Sitecore, because Sitecore has no idea how you implemented custom saving of data. That is why you need to subscribe to Sitecore xConnect events to clean up your data in a same time, when it is done by Sitecore.

Sitecore xConnect has plugin architecture and you are able to register your plugin. It is very convenient, your configuration and your code lives separately from xConnect, but interacts via events. It makes your code very maintainable and you can easily upgrade it to next version of Sitecore.

To join your custom data with Sitecore - you have one requirement, you need to save your custom data with contact identifier. Having contact identifier provides you ability to find records related to contact, for which you need to execute right to be forgotten.

To run your code during right to be forgotten, you need to register your plugin in Sitecore xConnect:
<Settings>
  <Sitecore>
    <XConnect>
      <Collection>
        <Services>
          <EraseFormSubmissionWhenExecutingRightToBeForgotten>
            <Type>Sitecore.Weareyou.Feature.FormsExtension.XConnectServicePlugin.EraseFormSubmissionWhenExecutingRightToBeForgotten, Sitecore.Weareyou.Feature.FormsExtension</Type>
            <As>Sitecore.XConnect.Service.Plugins.IXConnectServicePlugin, Sitecore.XConnect.Service.Plugins</As>
            <LifeTime>Singleton</LifeTime>
          </EraseFormSubmissionWhenExecutingRightToBeForgotten>
        </Services>
      </Collection>
    </XConnect>
  </Sitecore>
</Settings>

Place this file under <xConnect root folder>\\App_Data\Config\Sitecore\Collection\.

Then you need to implement your plugin:


using System;
using Microsoft.Extensions.Logging;
using Sitecore.Framework.Conditions;
using Sitecore.XConnect;
using Sitecore.XConnect.Operations;
using Sitecore.XConnect.Service.Plugins;

namespace Sitecore.Weareyou.Feature.FormsExtension.XConnectServicePlugin
{
    /// <summary>
    /// Implementation was inspired by Sitecore.EmailCampaign.XConnect.Operations.ClearSupressionListWhenExecutingRightToBeForgotten
    /// XConnect service plugin that clears forms data on executing right to be forgotten
    /// Makes Sitecore Forms to be GDPR compliant
    /// e.g. Sitecore > xProfile > Any contact > Action > Anonymize
    ///
    /// To enable it you need to copy XML configuration to sc93xconnect.dev.local\App_Data\Config\Sitecore\Collection
    /// Configuration is present in this repository Project\xConnect\code\App_data\config\sitecore\Collection\sc.XConnect.Service.Plugins.FormsAnonymize.xml
    /// </summary>
    public class EraseFormSubmissionWhenExecutingRightToBeForgotten : IXConnectServicePlugin, IDisposable
    {
        private const string PluginName = "EraseFormSubmissionWhenExecutingRightToBeForgotten";
        private XdbContextConfiguration _configuration;
        private readonly ILogger _logger;
        
        public EraseFormSubmissionWhenExecutingRightToBeForgotten(ILogger<EraseFormSubmissionWhenExecutingRightToBeForgotten> logger)
        {
        _logger = logger;
        }
        public void Dispose()
        {
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Subscribes  to events
        /// </summary>
        /// <param name="config">XdbContextConfiguration</param>
        public void Register(XdbContextConfiguration config)
        {
            _configuration = Condition.Requires(config, "config").IsNotNull().Value;
            _configuration.OperationAdded += OnOperationAdded;
        }

        private void OnOperationAdded(object sender, XdbOperationEventArgs xdbOperationEventArgs)
        {
            Condition.Requires(xdbOperationEventArgs, "xdbOperationEventArgs").IsNotNull();
            RightToBeForgottenOperation rightToBeForgottenOperation;
            if ((rightToBeForgottenOperation = (xdbOperationEventArgs.Operation as RightToBeForgottenOperation)) != null)
            {
                if (rightToBeForgottenOperation.Status == XdbOperationStatus.Canceled)
                {
                    _logger.LogDebug($"[{PluginName}] Skipping operation as it has been cancelled");
                }
                else
                {
                    rightToBeForgottenOperation.DependencyAdded += OnDependencyAddedToRightToBeForgottenOperation;
                }
            }
        }

        private void OnDependencyAddedToRightToBeForgottenOperation(object sender, DependencyAddedEventArgs args)
        {
            Condition.Requires(args, "args").IsNotNull();
            GetEntityOperation<Contact> getEntityOperation;
            if ((getEntityOperation = (args.Predecessor as GetEntityOperation<Contact>)) != null)
            {
                getEntityOperation.StatusChanged += OnGetEntityOperationStatusChanged;
            }
        }

        private void OnGetEntityOperationStatusChanged(object sender, StatusChangedEventArgs args)
        {
            Condition.Requires(sender, "sender").IsNotNull();
            Condition.Requires(args, "args").IsNotNull();
            GetEntityOperation<Contact> getEntityOperation;
            if ((getEntityOperation = (sender as GetEntityOperation<Contact>)) != null && args.NewStatus == XdbOperationStatus.Succeeded)
            {
                var entity = getEntityOperation.Entity;
                var contactId = entity.Id;
                if (contactId.HasValue)
                {
                    var identifier = entity.Identifiers
                        .FirstOrDefault(i => i.Source.Equals("xDB.Tracker"));
                    if(identifier != null) { 
                        if (Guid.TryParse(identifier?.Identifier, out var id))
                        {
                           //-------------------
                           //HERE SHOULD BE CODE THAT CLEAN UP CONTACT DATA IN YOUR CUSTOM LOCATION
                           //-------------------
                        }
                    }
                }
            }
        }

        public void Unregister()
        {
            _configuration.OperationAdded -= OnOperationAdded;
        }
    }
}

What this plugin does: it subscribes on xConnect events and run your code once right to be forgotten event was triggered.


Let's sum up, how to continue to be GDPR complaint if you need to save some custom user data:
  1. If it is possible, you need to save user contact data in Sitecore to have anything in one place (e.g. contact facets)
  2. If for some reason, you need separate storage for contacts data then you need to save these data with contact identifier. It allows you to find these data later.
  3. You need to implement and register xConnect plugin. It should clear your data, when right to be forgotten was triggered in Sitecore

You can see this approach in an action in my GitHub repository forked from Rob Haraken repository that he used for demo on Sitecore Virtual SUGCON 2020.

No comments:

Post a Comment