Anonymize your custom entities and comply with GDPR the easy way!

Helping my colleague Sergio Sanchez with his GDPR talk in the past Spanish Symposium, I came across a hidden gem in Liferay 7.1. It turns out you can integrate custom Service Builder entities with Liferay’s GDPR framework (UAD), to enable anonymization of personal data in your own entities.

I didn’t know anything about this feature, but it’s really easy to use and works like a charm (after solving a couple “gotchas”!). Some of this gotchas have been identified in LPS's and will be mentioned in the article.

Let’s take a look at how it’s done!

Service Builder to the rescue!

The first step is to create a Service Builder project, using the tool you like most (Blade, Liferay IDE...). This will create two projects, as usual, API and service, and you'll find the first hiccup. Blade doesn’t generate the service.xml file using the 7.1 DTD, it still uses 7.0, so the first thing we need to do is update the DTD to 7.1:

<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.1.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_1_0.dtd">

This issue is being tracked in LPS-86544.

The second “gotcha” is that when you update the DTD to version 7.1 Service Builder will generate code that won't compile or run with Liferay CE 7.1 GA1. To make it compile, you need to add a dependency to Petra and update the kernel to at least version 3.23.0 (that's the kernel version for Liferay CE 7.1 GA2, not released yet), but unfortunately won't run if you deploy it to a Liferay CE 7.1 GA1 instance.

Thanks to Minhchau Dang for pointing this out, Minhchau filed this bug in ticket LPS-86835.

I ended up using these in my build.gradle (of the -service project), I'm using Liferay DXP FP2:


dependencies {
    compileOnly group: "biz.aQute.bnd", name: "biz.aQute.bndlib", version: "3.5.0"
    compileOnly group: "com.liferay", name: "com.liferay.portal.spring.extender", version: "2.0.0"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "3.26.0"
    compileOnly group: "com.liferay", name: "com.liferay.petra.string", version: "2.0.0"
}

Also note that all across the example in Github I've used kernel version 3.26.0, not only in the -service project, but everywhere.

What exactly do you want to anonymize?

Now your service and api projects should be ready to compile, so the next step is to include the necessary information in your service.xml to make your entities anonymizable.

The first two things you need to include are two attributes at the entity level, uad-application-name, and uad-package-path. uad-application-name is the name that Liferay will use to show your application in the anonymization UI, and uad-package-path is the package Service Builder will use to create the UAD classes (Pro tip: don’t include “uad” in the package name as I did, SB will include it for you)

In my example I used this entity:

<entity local-service="true" name="Promotion" remote-service="true" uuid="true" uad-application-name="Citytour" uad-package-path="com.liferay.symposium.citytour.uad">

Once you have specified those, you can start telling Service Builder how your entity’s data will be anonymized. For this, you can use two attributes at a field level: uad-anonymize-field-name and uad-nonanonymizable.

Uad-anonymize-field-name, from the 7.1 service.xml DTD:

“The uad-anonymize-field- name value specifies the anonymous user field that
should be used to replace this column's value during auto anonymization. For
example, if "fullName" is specified, the anonymous user's full name will replace
this column's value during auto anonymization. The uad-anonymize-field-name
value should only be used withuser name columns (e.g. " statusByUserName ").”

For example, if we have a field defined like this:

<column name="userName" type="String" uad-anonymize-field-name="fullName"/>

That means that when the auto-anonymization process runs, it will replace the value of that field with the Anonymous User Full Name.

On the other hand, uad-nonanonymizable, again from the 7.1 DTD:

“The uad- nonanonymizable value specifies whether the column represents data
associated with a specific user that should be reviewed by an administrator in
the event of a GDPR compliance request. This implies the data cannot be
anonymized automatically.”

This means exactly that, the field can’t be auto-anonymized and needs manual revision when deleting user data. That’s partially true because even though the anonymization is not automatic, the admin user doesn’t have to actually do anything, just review the entities and click “anonymize” (providing the anonymization process for the entity is implemented, which we’ll do later on).

I used this fields in my “Promotion” entity:

        <!-- PK fields -->
        <column name="promotionId" primary="true" type="long" />
        <!-- Group instance -->
        <column name="groupId" type="long" />
        <!-- Audit fields -->
        <column name="companyId" type="long" />
        <column name="userId" type="long" />
        <column name="userName" type="String" uad-anonymize-field-name="fullName"/>
        <column name="createDate" type="Date" />
        <column name="modifiedDate" type="Date" />
        <!-- Promotion Data -->
        <column name="description" type="String" uad-nonanonymizable="true"/>
        <column name="price" type="double" uad-nonanonymizable="true"/>
        <column name="destinationCity" type="String" uad-nonanonymizable="true"/>
        <!-- Personal User Data -->
        <column name="offereeFirstName" type="String"/>
        <column name="offereeLastName" type="String"/>
        <column name="offereeIdNumber" type="String" />
        <column name="offereeTelephone" type="String" />

In my case, I’m telling the framework that the description, price, and destinationCity fields need
manual review.

So, what does SB do with this two attributes? Actually, if we have an entity with a field marked with uad-anonymize-field-name, when running buildService it will create two new projects to hold the anonymization, display, and data export logic! Isn't Service Builder awesome?

Build Service Builder Services using buildService

Excellent, you’re almost there! Now you’re ready to run the buildService gradle task, and you should see that SB has created two projects for you: <entity>-uad, and <entity>-uad-test. The <entity>-uad project (promotions-uad in my case) contains the custom anonymization and data export logic for your entity, and -uad-test contains well, the test classes.

And now that we have both projects, we must fix another “gotcha”. If you run the build or deploy gradle tasks now on those projects, they will fail spectacularly. Why? Well, if you take a look at your projects, you’ll see that there’s no build.gradle file! That’s Service Builder being a little petty, but don’t worry, you can create a new one and include these dependencies (again, based on my system):


dependencies {	
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "3.26.0"
    compileOnly group: "com.liferay", name: "com.liferay.user.associated.data.api", version: "3.0.1"    
    compileOnly group: "com.liferay", name: "com.liferay.petra.string", version: "2.0.0"
    compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"    
    compileOnly project(":modules:promotions:promotions-api")
}

This bug is being tracked in LPS-86814

Now your project should compile without issue, let’s add our custom anonymization logic!

Customize to anonymize

In your -uad project you’ll find three packages (actually four if you count constants):

  • Anonymizer: Holds the custom anonymization logic for your entity.
  • Display: Holds the custom display logic for Liferay’s UAD UI.
  • Exporter: Holds the custom personal data export logic.

In each one, you’ll find a Base class and another class that extends the Base class. For the anonymizer package in my example, I have a BasePromotionUADAnonymizer class and a PromotionUADAnonymizer class. What we’ll do is use the concrete class (PromotionUADAnonymizer in my case) and override the autoAnonymize method. In this method, you tell the framework how to anonymize each field of your custom entity. In my example I did this:

@Component(immediate = true, service = UADAnonymizer.class)
public class PromotionUADAnonymizer extends BasePromotionUADAnonymizer {
    @Override
    public void autoAnonymize(Promotion promotion, long userId,
        User anonymousUser) throws PortalException {
        if (promotion.getUserId() == userId) {
            promotion.setUserId(anonymousUser.getUserId());
            promotion.setUserName(anonymousUser.getFullName());
            promotion.setOffereeFirstName(anonymousUser.getFirstName());
            promotion.setOffereeLastName(anonymousUser.getLastName());
            promotion.setOffereeIdNumber("XXXXXXXXX");
            promotion.setOffereeTelephone("XXX-XXX-XXX");            
        }

        promotionLocalService.updatePromotion(promotion);
    }
}

I’m using the anonymousUser fields for my entities’ first and last name, and setting the ID and telephone fields to anonymous entries, but you can choose what fields you want to anonymize and how.

Done!

Good job, you’re done! Now your entities are integrated with Liferay’s UAD framework, and when deleting a user, you’ll see that it works like a charm. To see how UAD works from a user perspective, you can check out the Managing User Data chapter in the documentation.

I’ve uploaded this example in my GitHub repo, along with a web portlet to create Promotions. You just need to add the promotions-web portlet to the page, create some promotions with a user different from Test Test and then delete the user, you should see that all entries created by that user have now anonymous fields.

Hope you like it: https://github.com/ibairuiz/sympo-demo