GDPR-Compliant Development in ABAP Cloud

Category
Security
Published
Author
Johannes

The General Data Protection Regulation (GDPR) places specific requirements on software development. As an ABAP Cloud developer, you need to implement these requirements technically. This article shows how to develop GDPR-compliant applications in ABAP Cloud.

GDPR Requirements for Developers

The GDPR defines rights for data subjects that directly affect development:

RightArticleTechnical Implementation
Right of AccessArt. 15Data export function
Right to RectificationArt. 16Edit functions
Right to ErasureArt. 17Deletion concept
Right to RestrictionArt. 18Blocking function
Data PortabilityArt. 20Export in standard format
Right to ObjectArt. 21Consent management

Technical Principles

GDPR requires Privacy by Design and Privacy by Default:

Privacy by Design:
┌─────────────────────────────────────────────────────┐
│ Integrate data protection in all development phases │
├─────────────────────────────────────────────────────┤
│ • Minimal data collection │
│ • Check purpose limitation │
│ • Define retention periods │
│ • Implement encryption │
│ • Activate access logging │
└─────────────────────────────────────────────────────┘

Implementing Deletion Concepts (Data Retention)

A deletion concept defines when and how personal data is deleted. In ABAP Cloud, you implement this through Retention Rules and corresponding deletion jobs.

Define Retention Table

@EndUserText.label: 'Data Category Retention Rules'
@AbapCatalog.enhancement.category: #NOT_EXTENSIBLE
define table zretention_rules {
key client : abap.clnt not null;
key data_category : abap.char(30) not null;
retention_period_days : abap.int4;
deletion_method : abap.char(10); -- HARD, SOFT, ANON
legal_basis : abap.char(100);
last_review_date : abap.dats;
responsible_role : abap.char(30);
}

Implement Deletion Service

CLASS zcl_gdpr_deletion_service DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
TYPES:
BEGIN OF ty_deletion_result,
data_category TYPE string,
records_deleted TYPE i,
records_failed TYPE i,
deletion_date TYPE timestampl,
END OF ty_deletion_result,
tt_deletion_results TYPE STANDARD TABLE OF ty_deletion_result WITH EMPTY KEY.
METHODS:
execute_retention_deletion
RETURNING VALUE(rt_results) TYPE tt_deletion_results,
delete_customer_data
IMPORTING iv_customer_id TYPE zcustomer_id
iv_deletion_type TYPE string DEFAULT 'HARD'
RETURNING VALUE(rv_success) TYPE abap_bool,
get_retention_period
IMPORTING iv_data_category TYPE string
RETURNING VALUE(rv_days) TYPE i.
PRIVATE SECTION.
METHODS:
get_deletion_candidates
IMPORTING iv_data_category TYPE string
iv_cutoff_date TYPE d
RETURNING VALUE(rt_keys) TYPE ztt_entity_keys,
perform_hard_delete
IMPORTING it_keys TYPE ztt_entity_keys
RETURNING VALUE(rv_count) TYPE i,
perform_soft_delete
IMPORTING it_keys TYPE ztt_entity_keys
RETURNING VALUE(rv_count) TYPE i,
log_deletion
IMPORTING iv_category TYPE string
iv_count TYPE i
iv_method TYPE string.
ENDCLASS.
CLASS zcl_gdpr_deletion_service IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA(lt_results) = execute_retention_deletion( ).
LOOP AT lt_results INTO DATA(ls_result).
out->write( |Category: { ls_result-data_category }| ).
out->write( |Deleted: { ls_result-records_deleted }| ).
out->write( |Failed: { ls_result-records_failed }| ).
out->write( |---| ).
ENDLOOP.
ENDMETHOD.
METHOD execute_retention_deletion.
" Get all active retention rules
SELECT * FROM zretention_rules
INTO TABLE @DATA(lt_rules).
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
LOOP AT lt_rules INTO DATA(ls_rule).
" Calculate cutoff date
DATA(lv_cutoff_date) = lv_today - ls_rule-retention_period_days.
" Determine deletion candidates
DATA(lt_candidates) = get_deletion_candidates(
iv_data_category = ls_rule-data_category
iv_cutoff_date = lv_cutoff_date ).
" Perform deletion
DATA(lv_deleted) = COND #(
WHEN ls_rule-deletion_method = 'HARD'
THEN perform_hard_delete( lt_candidates )
WHEN ls_rule-deletion_method = 'SOFT'
THEN perform_soft_delete( lt_candidates )
WHEN ls_rule-deletion_method = 'ANON'
THEN anonymize_records( lt_candidates )
ELSE 0 ).
" Log result
log_deletion(
iv_category = ls_rule-data_category
iv_count = lv_deleted
iv_method = ls_rule-deletion_method ).
APPEND VALUE #(
data_category = ls_rule-data_category
records_deleted = lv_deleted
records_failed = lines( lt_candidates ) - lv_deleted
deletion_date = utclong_current( )
) TO rt_results.
ENDLOOP.
ENDMETHOD.
METHOD delete_customer_data.
" Individual deletion for data subject request
TRY.
CASE iv_deletion_type.
WHEN 'HARD'.
" Irreversible deletion
DELETE FROM zcustomer WHERE customer_id = @iv_customer_id.
DELETE FROM zcustomer_contact WHERE customer_id = @iv_customer_id.
DELETE FROM zcustomer_address WHERE customer_id = @iv_customer_id.
WHEN 'SOFT'.
" Mark as deleted
UPDATE zcustomer
SET is_deleted = @abap_true,
deletion_date = @( cl_abap_context_info=>get_system_date( ) )
WHERE customer_id = @iv_customer_id.
WHEN 'ANONYMIZE'.
" Anonymization instead of deletion
UPDATE zcustomer
SET first_name = 'ANONYMIZED',
last_name = 'ANONYMIZED',
email = '[email protected]',
phone = '000000000',
is_anonymized = @abap_true
WHERE customer_id = @iv_customer_id.
ENDCASE.
rv_success = abap_true.
CATCH cx_sy_open_sql_db.
rv_success = abap_false.
ENDTRY.
ENDMETHOD.
METHOD get_retention_period.
SELECT SINGLE retention_period_days
FROM zretention_rules
WHERE data_category = @iv_data_category
INTO @rv_days.
IF sy-subrc <> 0.
rv_days = 365 * 10. " Default: 10 years
ENDIF.
ENDMETHOD.
" ... additional methods
ENDCLASS.

RAP Action for Deletion

" Behavior Definition
define behavior for ZI_Customer alias Customer
implementation in class zbp_i_customer unique
{
// GDPR deletion as Action
action deletePersonalData result [1] $self;
// Soft Delete instead of physical deletion
delete ( features : instance );
}
" Behavior Implementation
METHOD deletePersonalData.
DATA(lo_deletion_service) = NEW zcl_gdpr_deletion_service( ).
LOOP AT keys INTO DATA(ls_key).
DATA(lv_success) = lo_deletion_service->delete_customer_data(
iv_customer_id = ls_key-CustomerId
iv_deletion_type = 'ANONYMIZE' ).
IF lv_success = abap_true.
" Return result
READ ENTITIES OF zi_customer IN LOCAL MODE
ENTITY Customer
ALL FIELDS WITH VALUE #( ( %key = ls_key-%key ) )
RESULT DATA(lt_customers).
result = VALUE #( FOR customer IN lt_customers
( %tky = customer-%tky
%param = customer ) ).
ELSE.
APPEND VALUE #(
%tky = ls_key-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Deletion failed' )
) TO reported-customer.
ENDIF.
ENDLOOP.
ENDMETHOD.

Data Anonymization

Anonymization makes data irreversibly non-attributable to a person. It’s an alternative to deletion when data is needed for statistics.

Anonymization Service

CLASS zcl_gdpr_anonymization DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_field_rule,
field_name TYPE string,
anon_method TYPE string, -- MASK, HASH, RANDOM, CONSTANT, NULLIFY
constant_value TYPE string,
END OF ty_field_rule,
tt_field_rules TYPE STANDARD TABLE OF ty_field_rule WITH EMPTY KEY.
METHODS:
anonymize_customer
IMPORTING iv_customer_id TYPE zcustomer_id
RETURNING VALUE(rv_success) TYPE abap_bool,
anonymize_field
IMPORTING iv_value TYPE any
iv_method TYPE string
iv_constant TYPE string OPTIONAL
RETURNING VALUE(rv_result) TYPE string,
generate_pseudonym
IMPORTING iv_original TYPE string
RETURNING VALUE(rv_pseudonym) TYPE string.
ENDCLASS.
CLASS zcl_gdpr_anonymization IMPLEMENTATION.
METHOD anonymize_customer.
" Define anonymization rules
DATA(lt_rules) = VALUE tt_field_rules(
( field_name = 'FIRST_NAME' anon_method = 'CONSTANT' constant_value = 'ANON' )
( field_name = 'LAST_NAME' anon_method = 'CONSTANT' constant_value = 'ANON' )
( field_name = 'EMAIL' anon_method = 'HASH' )
( field_name = 'PHONE' anon_method = 'MASK' )
( field_name = 'BIRTH_DATE' anon_method = 'NULLIFY' )
( field_name = 'ADDRESS' anon_method = 'CONSTANT' constant_value = 'Anonymized' )
).
" Load customer data
SELECT SINGLE *
FROM zcustomer
WHERE customer_id = @iv_customer_id
INTO @DATA(ls_customer).
IF sy-subrc <> 0.
rv_success = abap_false.
RETURN.
ENDIF.
" Anonymize fields
ls_customer-first_name = anonymize_field(
iv_value = ls_customer-first_name
iv_method = 'CONSTANT'
iv_constant = 'ANON' ).
ls_customer-last_name = anonymize_field(
iv_value = ls_customer-last_name
iv_method = 'CONSTANT'
iv_constant = 'ANON' ).
ls_customer-email = anonymize_field(
iv_value = ls_customer-email
iv_method = 'HASH' ).
ls_customer-phone = anonymize_field(
iv_value = ls_customer-phone
iv_method = 'MASK' ).
ls_customer-is_anonymized = abap_true.
ls_customer-anonymization_date = cl_abap_context_info=>get_system_date( ).
" Save anonymized data
UPDATE zcustomer FROM @ls_customer.
rv_success = xsdbool( sy-subrc = 0 ).
ENDMETHOD.
METHOD anonymize_field.
CASE iv_method.
WHEN 'MASK'.
" Partially mask (e.g., ***1234)
DATA(lv_length) = strlen( iv_value ).
IF lv_length > 4.
rv_result = repeat( val = '*' occ = lv_length - 4 ) &&
substring( val = iv_value off = lv_length - 4 len = 4 ).
ELSE.
rv_result = repeat( val = '*' occ = lv_length ).
ENDIF.
WHEN 'HASH'.
" One-way hash (not reversible)
TRY.
cl_abap_message_digest=>create_md5(
EXPORTING
if_data = cl_abap_codepage=>convert_to( iv_value )
IMPORTING
ef_hashstring = rv_result ).
CATCH cx_abap_message_digest.
rv_result = 'HASH_ERROR'.
ENDTRY.
WHEN 'RANDOM'.
" Generate random value
rv_result = cl_system_uuid=>create_uuid_x16_static( ).
WHEN 'CONSTANT'.
" Fixed replacement value
rv_result = iv_constant.
WHEN 'NULLIFY'.
" Empty value
rv_result = ''.
WHEN OTHERS.
rv_result = iv_value.
ENDCASE.
ENDMETHOD.
METHOD generate_pseudonym.
" Deterministic pseudonym (same original = same pseudonym)
DATA(lv_salt) = 'GDPR_PSEUDONYM_SALT_2024'.
DATA(lv_input) = lv_salt && iv_original.
TRY.
cl_abap_message_digest=>create_sha256(
EXPORTING
if_data = cl_abap_codepage=>convert_to( lv_input )
IMPORTING
ef_hashstring = DATA(lv_hash) ).
" Create shorter, readable pseudonym
rv_pseudonym = 'PSN_' && substring( val = lv_hash len = 12 ).
CATCH cx_abap_message_digest.
rv_pseudonym = 'PSN_ERROR'.
ENDTRY.
ENDMETHOD.
ENDCLASS.

Pseudonymization vs. Anonymization

AspectPseudonymizationAnonymization
ReversibleYes, with keyNo
GDPR StatusPersonal dataNot personal data
ApplicationProcessingStatistics, archive
ExamplePSN_A3F82BANON

Implementing Right of Access (Data Export)

Data subjects have the right to receive all stored data about them in a machine-readable format.

Data Export Service

CLASS zcl_gdpr_data_export DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_export_section,
category TYPE string,
data TYPE REF TO data,
END OF ty_export_section,
tt_export_sections TYPE STANDARD TABLE OF ty_export_section WITH EMPTY KEY.
METHODS:
export_customer_data
IMPORTING iv_customer_id TYPE zcustomer_id
RETURNING VALUE(rv_json) TYPE string,
export_as_json
IMPORTING it_sections TYPE tt_export_sections
RETURNING VALUE(rv_json) TYPE string,
create_export_request
IMPORTING iv_customer_id TYPE zcustomer_id
iv_request_type TYPE string
RETURNING VALUE(rv_request_id) TYPE sysuuid_x16.
ENDCLASS.
CLASS zcl_gdpr_data_export IMPLEMENTATION.
METHOD export_customer_data.
DATA: lt_sections TYPE tt_export_sections.
" 1. Master data
SELECT SINGLE customer_id, first_name, last_name, email,
phone, birth_date, created_at
FROM zcustomer
WHERE customer_id = @iv_customer_id
INTO @DATA(ls_customer).
IF sy-subrc = 0.
GET REFERENCE OF ls_customer INTO DATA(lr_customer).
APPEND VALUE #( category = 'Master Data' data = lr_customer ) TO lt_sections.
ENDIF.
" 2. Addresses
SELECT address_type, street, city, postal_code, country
FROM zcustomer_address
WHERE customer_id = @iv_customer_id
INTO TABLE @DATA(lt_addresses).
IF sy-subrc = 0.
GET REFERENCE OF lt_addresses INTO DATA(lr_addresses).
APPEND VALUE #( category = 'Addresses' data = lr_addresses ) TO lt_sections.
ENDIF.
" 3. Orders
SELECT order_id, order_date, status, total_amount, currency
FROM zorder
WHERE customer_id = @iv_customer_id
INTO TABLE @DATA(lt_orders).
IF sy-subrc = 0.
GET REFERENCE OF lt_orders INTO DATA(lr_orders).
APPEND VALUE #( category = 'Orders' data = lr_orders ) TO lt_sections.
ENDIF.
" 4. Communications
SELECT communication_date, channel, subject
FROM zcommunication_log
WHERE customer_id = @iv_customer_id
INTO TABLE @DATA(lt_communications).
IF sy-subrc = 0.
GET REFERENCE OF lt_communications INTO DATA(lr_comms).
APPEND VALUE #( category = 'Communications' data = lr_comms ) TO lt_sections.
ENDIF.
" 5. Consent history
SELECT consent_type, consent_date, consent_status, ip_address
FROM zconsent_log
WHERE customer_id = @iv_customer_id
INTO TABLE @DATA(lt_consents).
IF sy-subrc = 0.
GET REFERENCE OF lt_consents INTO DATA(lr_consents).
APPEND VALUE #( category = 'Consents' data = lr_consents ) TO lt_sections.
ENDIF.
" Export as JSON
rv_json = export_as_json( lt_sections ).
ENDMETHOD.
METHOD create_export_request.
" Log export request
rv_request_id = cl_system_uuid=>create_uuid_x16_static( ).
INSERT INTO zgdpr_requests VALUES @( VALUE #(
request_id = rv_request_id
customer_id = iv_customer_id
request_type = iv_request_type " EXPORT, DELETE, RECTIFY
request_date = cl_abap_context_info=>get_system_date( )
request_time = cl_abap_context_info=>get_system_time( )
status = 'NEW'
requestor = cl_abap_context_info=>get_user_technical_name( )
) ).
ENDMETHOD.
ENDCLASS.

Consents must be documented, revocable, and verifiable.

@EndUserText.label: 'Consent Types'
define table zconsent_types {
key client : abap.clnt not null;
key consent_type : abap.char(30) not null;
description : abap.char(200);
legal_basis : abap.char(100);
is_mandatory : abap_boolean;
default_duration : abap.int4; -- Days
}
@EndUserText.label: 'Consent Log'
define table zconsent_log {
key client : abap.clnt not null;
key consent_id : sysuuid_x16 not null;
customer_id : zcustomer_id;
consent_type : abap.char(30);
consent_status : abap.char(10); -- GRANTED, WITHDRAWN, EXPIRED
consent_date : abap.dats;
consent_time : abap.tims;
expiry_date : abap.dats;
ip_address : abap.char(45);
user_agent : abap.char(500);
consent_text : abap.string;
withdrawal_date : abap.dats;
withdrawal_reason : abap.char(200);
}
CLASS zcl_consent_manager DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
CONSTANTS:
BEGIN OF gc_consent_status,
granted TYPE string VALUE 'GRANTED',
withdrawn TYPE string VALUE 'WITHDRAWN',
expired TYPE string VALUE 'EXPIRED',
END OF gc_consent_status.
METHODS:
grant_consent
IMPORTING iv_customer_id TYPE zcustomer_id
iv_consent_type TYPE string
iv_ip_address TYPE string OPTIONAL
iv_user_agent TYPE string OPTIONAL
RETURNING VALUE(rv_consent_id) TYPE sysuuid_x16,
withdraw_consent
IMPORTING iv_customer_id TYPE zcustomer_id
iv_consent_type TYPE string
iv_reason TYPE string OPTIONAL
RETURNING VALUE(rv_success) TYPE abap_bool,
check_consent
IMPORTING iv_customer_id TYPE zcustomer_id
iv_consent_type TYPE string
RETURNING VALUE(rv_granted) TYPE abap_bool,
expire_old_consents.
ENDCLASS.
CLASS zcl_consent_manager IMPLEMENTATION.
METHOD grant_consent.
" Load consent type details
SELECT SINGLE * FROM zconsent_types
WHERE consent_type = @iv_consent_type
INTO @DATA(ls_type).
IF sy-subrc <> 0.
RETURN.
ENDIF.
" Calculate expiry date
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
DATA(lv_expiry) = COND #(
WHEN ls_type-default_duration > 0
THEN lv_today + ls_type-default_duration
ELSE '99991231' ).
" Create new consent
rv_consent_id = cl_system_uuid=>create_uuid_x16_static( ).
INSERT INTO zconsent_log VALUES @( VALUE #(
consent_id = rv_consent_id
customer_id = iv_customer_id
consent_type = iv_consent_type
consent_status = gc_consent_status-granted
consent_date = lv_today
consent_time = cl_abap_context_info=>get_system_time( )
expiry_date = lv_expiry
ip_address = iv_ip_address
user_agent = iv_user_agent
consent_text = ls_type-description
) ).
" Invalidate old consent
UPDATE zconsent_log
SET consent_status = @gc_consent_status-withdrawn
WHERE customer_id = @iv_customer_id
AND consent_type = @iv_consent_type
AND consent_id <> @rv_consent_id
AND consent_status = @gc_consent_status-granted.
ENDMETHOD.
METHOD withdraw_consent.
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
UPDATE zconsent_log
SET consent_status = @gc_consent_status-withdrawn,
withdrawal_date = @lv_today,
withdrawal_reason = @iv_reason
WHERE customer_id = @iv_customer_id
AND consent_type = @iv_consent_type
AND consent_status = @gc_consent_status-granted.
rv_success = xsdbool( sy-subrc = 0 ).
ENDMETHOD.
METHOD check_consent.
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
SELECT SINGLE @abap_true
FROM zconsent_log
WHERE customer_id = @iv_customer_id
AND consent_type = @iv_consent_type
AND consent_status = @gc_consent_status-granted
AND expiry_date >= @lv_today
INTO @rv_granted.
ENDMETHOD.
METHOD expire_old_consents.
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
UPDATE zconsent_log
SET consent_status = @gc_consent_status-expired
WHERE expiry_date < @lv_today
AND consent_status = @gc_consent_status-granted.
ENDMETHOD.
ENDCLASS.
METHOD send_marketing_email.
DATA(lo_consent) = NEW zcl_consent_manager( ).
" Check if marketing consent exists
IF lo_consent->check_consent(
iv_customer_id = iv_customer_id
iv_consent_type = 'MARKETING_EMAIL' ) = abap_false.
" No consent - do not send email
RAISE EXCEPTION TYPE zcx_no_consent
EXPORTING
textid = zcx_no_consent=>no_marketing_consent.
ENDIF.
" Send marketing email
" ...
ENDMETHOD.

GDPR Compliance Checklist

Development Checklist

CheckpointImplementedNotes
Data Minimization
Only collect necessary fields
Required fields minimized
No “just in case” data collection
Purpose Limitation
Usage purpose documented
Consent obtained for each purpose
No misuse of data
Storage Limitation
Retention periods defined
Deletion jobs implemented
Archiving configured
Data Subject Rights
Access function available
Rectification function available
Deletion function available
Blocking function available
Export function available
Security
Access control implemented
Encryption during transfer
Logging activated
Documentation
Processing records up to date
Technical documentation available
Data protection impact assessment conducted

Code Review Checklist for GDPR

" ❌ NOT GDPR compliant
DATA: gv_customer_ssn TYPE string. " Sensitive data global
" ✅ GDPR compliant
DATA(lo_customer) = zcl_customer_handler=>get_instance( iv_customer_id ).
DATA(lv_masked_ssn) = lo_customer->get_masked_ssn( ).
" ❌ NOT GDPR compliant - Logging with personal data
MESSAGE |Customer { ls_customer-name } placed order { lv_order_id }| TYPE 'I'.
" ✅ GDPR compliant - No personal data in log
MESSAGE |Order { lv_order_id } created| TYPE 'I'.
" ❌ NOT GDPR compliant - Unrestricted export
SELECT * FROM zcustomer INTO TABLE @DATA(lt_all_customers).
" ✅ GDPR compliant - Authorization check and logging
IF lo_auth->check_export_authorization( ) = abap_true.
lo_audit->log_data_export( iv_reason = 'CUSTOMER_REQUEST' ).
SELECT * FROM zcustomer WHERE customer_id = @iv_customer_id
INTO TABLE @DATA(lt_customer_data).
ENDIF.

Best Practices

1. Privacy by Design

" Data minimization already in data model
define table zcustomer_minimal {
key customer_id : sysuuid_x16; -- Pseudonym instead of real name
display_name : abap.char(100); -- Optional
email_hash : abap.char(64); -- Hashed email
created_at : timestampl;
" NO: Birth date, address, phone - if not needed
}

2. Privacy by Default

" Default: Minimal data processing
METHOD constructor.
" Consent is NOT automatically assumed
me->consent_status = gc_consent_status-not_given.
" Marketing is NOT enabled by default
me->marketing_enabled = abap_false.
" Data is NOT stored indefinitely
me->retention_date = cl_abap_context_info=>get_system_date( ) + 365.
ENDMETHOD.

Summary

GDPR RequirementABAP Cloud Solution
Right to ErasureRAP Action + Deletion Service
AnonymizationAnonymization Service
Right of AccessData Export as JSON
Consent ManagementConsent Service + CDS Views
RetentionILM Integration
LoggingApplication Logging

GDPR compliance requires consistent consideration of data protection requirements throughout the entire application - from data model through business logic to user interface.