DDM – rozszerzenie tematu

O technologii Dynamic Data Masking pisałem dwukrotnie w 2025 roku, aby przybliżyć jej działanie użytkownikom baz danych Progress OpenEdge od wersji 12.8. W poprzednich artykułach skupiłem się głównie na podstawach: omówiłem sposób aktywacji mechanizmu DDM w bazie danych oraz konfigurację czterech dostępnych typów masek dla wybranych pól i tabel.

Dynamic Data Masking jest jednak rozwiązaniem, które poza samą konfiguracją wymaga również dobrego zrozumienia sposobu działania mechanizmu maskowania, jego wpływu na aplikacje oraz pewnych ograniczeń. W praktyce oznacza to, że wdrożenie DDM nie sprowadza się wyłącznie do zdefiniowania reguł maskowania, ale powinno być poprzedzone analizą wymagań bezpieczeństwa, zgodności oraz wpływu na istniejące reguły biznesowe.

W tym artykule chciałbym więc spojrzeć na tę technologię z bardziej praktycznej i technicznej perspektywy. Pokażę nie tylko zalety wynikające z zastosowania Dynamic Data Masking, ale również ograniczenia i potencjalne problemy, które mogą pojawić się podczas wdrożenia. Odniosę się także do kilku nieporozumień dotyczących działania DDM, które pojawiały się w pytaniach i dyskusjach po publikacji wcześniejszych materiałów.

Jednym z pytań było: co się stanie jeśli pole typu INTEGER będzie przesłane do obiektu, jako wartość wymagana, a administrator nałoży na nie maskę PARTIAL. Otóż należy pamiętać, że dla każdego rodzaju maski obowiązuje zasada zgodności typów. Spróbujmy zastosować taką maskę na pole Balance (DECIMAL). Postępujemy dokładnie tak jak w poprzednich artykułach, a na koniec nakładamy maskę:

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Balance","P:2,*","#DDM_SEE_ContactInfo").

Dostajemy komunikat o błędzie wynikający z konfliktu pola liczbowego z maską znakową.


Przejdę teraz do ważniejszej kwestii związanej z DDM, ale najpierw zdefiniuję maskę domyślną dla pola Balance.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Balance","D:","#DDM_SEE_ContactInfo").

Startuję dwie sesje w edytorze procedur, logując się do bazy jako Admin i jako User. Uruchamiam prosty program wyświetlający wartość pola Balance dla pierwszego rekordu Customer. Zgodnie z przewidywaniem dla Admina jest to prawdziwa wartość (z lewej), a dla Usera wartość zamaskowana, czyli zero (z prawej).

Podstawowe pytanie brzmi: na jakim poziomie odbywa się maskowanie danych? Czy na warstwie prezentacji czy dostępu do danych? Jest to kwestia zasadnicza, ale zanim to wyjaśnimy uruchomię jeszcze jeden prosty przykład. Dodałem do tablicy Customer pole znakowe cStatus. W zależności od wartości pola Balance ustawiany jest status klienta. Jeśli jego Balance, czyli “saldo” wynosi zero to status zmienia się na “Blocked”.
Pole Balance nie jest tutaj wyświetlane. Uruchamiam program ponownie jako Admin

… i jako User.

Widzimy, że chociaż wartość pola Balance jest niezerowa, to logika aplikacji w drugim oknie widzi ją jako właśnie zerową. Ten przykład pokazuje najważniejszy problem związany z Dynamic Data Masking — mechanizm maskowania może wpływać nie tylko na prezentację danych, ale również na działanie logiki biznesowej. Zresztą, nie ma co się dziwić. Gdyby maskowanie dotyczyło tylko wyświetlania danych wystarczyłoby przypisać pole do zmiennej i je wyświetlić.

A zatem w wielu przypadkach aplikacja otrzymuje już zamaskowaną wartość podczas odczytu danych z bazy. Oznacza to, że warunki biznesowe, walidacje czy obliczenia mogą działać na danych zmodyfikowanych przez mechanizm DDM. Dlatego maskowanie danych nie powinno być traktowane jako element logiki aplikacyjnej.

Najbezpieczniejszym podejściem jest rozdzielenie warstw odpowiedzialności:
– logika biznesowa powinna operować na rzeczywistych danych,
– maskowanie powinno być stosowane na etapie prezentacji danych użytkownikowi,
– mechanizm DDM warto wykorzystywać głównie dla raportowania, odczytu danych oraz ograniczania dostępu do danych poufnych dla użytkowników biznesowych, audytorów czy pracowników wsparcia technicznego.


Rozważmy kolejny przypadek.

Admin uruchamia zapytanie dla 5 pierwszych rekordów Customer. Widać, że dwa z nich mają wartość pola Balance powyżej 1000.

A teraz uruchamiam poniższy program jako User.

Nie powinno nas dziwić, że licznik cnt wynosi zero ponieważ wartości pola Blanace są dla Usera zamaskowane i program widzi same zera.
Ale co się dzieje w poniższym przykładzie?

Zmieniliśmy nieco logikę przesuwając warunek na pole Balance do otwarcia pętli (fraza WHERE) i licznik wychwytuje teraz te dwie wartości > 1000.

Możecie sprawdzić sami, że jeśli wyświetlilibyśmy teraz to pole w pętli to nadal byłoby zamaskowane.
Dlaczego tak się dzieje? Otóż tutaj filtrowanie jest wykonywane bezpośrednio przez silnik bazy danych przed zastosowaniem maskowania. Oznacza to, że rekord może zostać wybrany na podstawie rzeczywistej wartości Balance, mimo że użytkownik po odczycie nadal zobaczy wartość 0. I choć maskowanie nie pozwoli nam na poejrzenie wartości możemy dostać pewną informację na temat ilości klientów których saldo jest większe od określonej wartości. Jeśli zbiór danych nie jest duży to dość łatwo możemy zawęzić zakres wartości pola Balance dla poszczególnych klientów.

Samo maskowanie wartości często nie wystarcza, ponieważ liczba rekordów, wyniki agregacji czy czas wykonania zapytania również mogą ujawniać informacje o danych.
W praktyce oznacza to, że mechanizm DDM musi być projektowany razem z logiką aplikacji, a nie traktowany jako niezależna warstwa bezpieczeństwa.

Na zakończenie: nie tylko OpenEdge

Warto podkreślić, że opisane ograniczenia nie są specyficzne wyłącznie dla Dynamic Data Masking w Progress OpenEdge. Podobne ograniczenia mają również inne technologie tego typu dostępne na rynku.

To nie jest więc „bug” konkretnego rozwiązania, ale raczej naturalna cecha samego podejścia do maskowania danych. DDM ukrywa wartości pól, ale nie zawsze ukrywa informacje wynikające z działania aplikacji, zapytań czy agregacji.

Dlatego nawet jeśli użytkownik nie widzi danych bezpośrednio, nadal może czasem wyciągać pewne wnioski na podstawie:

– liczby rekordów,
– działania warunków,
– agregacji,
– sortowania,
itp.

W praktyce oznacza to, że Dynamic Data Masking warto traktować jako dodatkową warstwę ochrony danych, a nie jedyne zabezpieczenie systemu.

Dynamic Data Masking II

Kolejnym krokiem jest zdefiniowanie masek dla wybranych pól. Maski te zasłaniają nieautoryzowanym użytkownikom część lub całą zawartość pola. Mamy cztery rodzaje takich masek: domyślna, częściowa (partial), literal, null.

Wszystkie ustawienia DDM w ABL (tak jak w poprzednim artykule) realizujemy poprzez metody DataAdminService. Zaczynamy od maski domyślnej, w ktorej mamy małe pole do menewru. Pola znakowe są oznaczane jako XXX (liczba X zależy od maskowanych danych), pola decimal 0.00 itd. Maska jest ustawiana poprzez prefiks “D:”. Zaczniemy od pola Balance.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Balance","D:","#DDM_SEE_ContactInfo").

Ustawmy taką samą maskę także na pola znakowe SalesRep i State.

Żeby ustawienia maskowania były widoczne trzeba aktywować usługę DDM w bazie poleceniem:
proutil sports2020 -C activateddm.

Do testowania użyjemy prostej procedury findCust.p.

// findCust.p
FIND FIRST customer.
DISPLAY customer EXCEPT comments WITH 1 COLUMN.
PAUSE.

Uruchamiamy dwie sesje klienta z bazą sports2020 logując się raz jako Admin i raz jako User (patrz poprzedni artykuł). Poniżej widać porównanie danych dla obu sesji – różnice zaznaczyłem na czerwono.

Maska literal służy do maskowania danych gdy chcemy zamiast originalnej wartości podać własną. Należy pamiętać, że dla każdego rodzaju maski obowiązuje zasada zgodności typów, i tak tutaj dla pola znakowego City możemy podać własną maskę np. “UKRYTE“, dla pola PostalCode 00000 itd.
Maska literal jest ustawiana poprzez prefiks “L:”.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","City","L:UKRYTE","#DDM_SEE_ContactInfo").
lResult = service:setDDMConfig("Customer","PostalCode","L:00000","#DDM_SEE_ContactInfo").


Pora przejść do najciekawszej maski, czyli częściowej, dającej najwięcej możliwości, ale stosowanej tylko dla pól znakowych.
Składnia maski wygląda tak: P:prefix,char[:maxchars][,suffix]

prefix – określa liczbę niezamaskowanych znaków, które mogą być wyświetlane na początku pola.
char[:maxchars] – określa pojedynczy znak ASCII, który ma być użyty do ukrycia wartości kolumny, maxchars (wartość opcjonalna) określa maksymalną liczbę znaków do zamaskowania przy użyciu podanego znaku ASCII, reszta ciągu jest przycinana.
[,suffix] – (wartość opcjonalna) określa liczbę znaków na końcu pola, które mogą być niezamaskowane.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","Phone","P:5,*:2,4","#DDM_SEE_ContactInfo").

Testujemy pole Phone. Działa to w następujący sposób: pierwsze 5 znaków jest niezamaskowana, podobnie jak ostatnie 4 znaki, to co w środku jest zamaskowane maksymalnie 2 znakami “*“. Jeśli długość pola będzie miała tylko 10 znaków to będzie wyświetlana tylko jedna gwiazdka itd.

Ostatnim rodzajem maski jest null, którą można stosować dla każego typu danych. Zamaskowane wartości są wyświetlane jako “?“. Maskę null ustawiamy za pomocą prefiksu N:.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")).
lResult = service:setDDMConfig("Customer","CreditLimit","N:","#DDM_SEE_ContactInfo").
lResult = service:setDDMConfig("Customer","Terms","N:","#DDM_SEE_ContactInfo").


Jeśli trzeba usunąć maskowanie z pola musimy posłużyć się metodą unsetDDMMask. Usuńmy maskę np. z pola State.

USING OpenEdge.DataAdmin.DataAdminService FROM PROPATH.

DEFINE VARIABLE service AS DataAdminService NO-UNDO. 
DEFINE VARIABLE lResult AS LOGICAL NO-UNDO.

service = NEW DataAdminService(LDBNAME("DICTDB")). 

lResult = service:unsetDDMMask("Customer","State").


Przy pomocy metod DataAdminService można manipulować rolami, tagami autoryzacji itp. Możemy podejrzeć np. tag i maskę dla danego pola.

USING OpenEdge.DataAdmin.*. 
VAR DataAdminService oDAS. 
VAR CHARACTER cMask. 
VAR CHARACTER cAuthTag. 

oDAS = new DataAdminService (ldbname("DICTDB")). 
oDAS:GetFieldDDMConfig ("Customer","Phone", OUTPUT cMask, OUTPUT cAuthTag).
MESSAGE cMask SKIP cAuthTag
VIEW-AS ALERT-BOX. 

Poniżej widać dane dla pola Phone.

OK, na tym na razie kończę. Do tematu powrócę jeśli będą od Was jakieś pytania.

Dynamic Data Masking I

Na początku chciałbym podać nieco wiadomości wprowadzających do Dynamic Data Masking (DDM). Najprościej mówiąc jest to zabezpieczenie danych w bazie na poziomie logicznym, ale definiowane przez administratora.
Wyobrażmy sobie sytuację gdy deweloper tworzy aplikację, w której wyświetlane są dane wraźliwe jak np. numer telefonu, adres lub nawet pesel czy miesięczne zarobki. Z drugiej strony są osoby w firmie, które powinny te dane widzieć. Możemy to rozwiązać na poziomie pisania aplikacji lub zrobić to globalnie na poziomie bazy. Takie rozwiązanie zmusza każdego użytkownika do zalogowania się w bazie i, w zależności od uprawnień, daje wgląd do wybranych pól w tabelach.
Administrator DDM (lub administrator zabezpieczeń) może skonfigurować maskę dla pól tabeli, która ukrywa poufne dane w zestawie wyników zapytania. Kontroluje on również uprawnienia dostępu użytkowników do przeglądania niezamaskowanych wartości określonych pól.
Żeby korzystać z DDM trzeba mieć licencję na dodatek The OpenEdge Advanced Security (Progress OEAS) i serwer bazy danych Enterprise. Minimalna wersja to 12.8.4.

OK, tworzymy testową bazę, kopię bazy sports2020 i włączamy funkcję DDM.
proutil sports2020 -C enableddm
Sprawdzamy czy funkcja ta jest dodana (na razie jeszcze nieaktywna).
proutil sports2020 -C describe

Pierwszym krokiem jest dodanie użytkowników do bazy. W dokumentacji online jest to zrealizowane przez program w obiektowym ABL. Ja zrobię to standardowo w narzędziu Data Administration -> Admin -> Security -> Edit User List.
Dodaję użytkowników: Admin, User. Oczywiście można wykorzystać tutaj konta użytkowników zdefiniowanych nie w bazie ale w zewnętrznych systemach uwierzytelniania.

Loguję się jako Admin i ustawiam go jako security adminisrtator Admin -> Security -> Security Administrators.

Warto zablokować dostęp podczas runtime’u dla niezalogowanych użytkowników oraz ustawić sprawdzanie uprawnień kont także podczas runtime’u.Admin -> Database Options

Aby określić uprawnienia użytkownika do dostępu do zamaskowanych danych, trzeba utworzyć w kodzie ABL rolę dla użytkowników zdefiniowanych w bazie (operacja mapowania użytkownika do roli).
Poniższy przykładowy kod tworzy rolę myRole, którą wykorzystamy do zdefiniowania uprawnień przyznawanych użytkownikom do demaskowania danych:

USING OpenEdge.DataAdmin.*.

VAR DataAdminService service. 
VAR IRole oRole.
VAR LOGICAL lResult.

service = NEW DataAdminService(LDBNAME("DICTDB")).

oRole = service:NewRole("myRole"). 
oRole:Description = "Role for DDM Admin".
oRole:IsDDM = true.
lResult = service:CreateRole(oRole). 

DELETE OBJECT service.

Następnym krokiem jest utwórzenie tagu autoryzacji i powiązania go z rolą utworzoną w poprzednim kroku. Tag autoryzacji ustanawia połączenie między zdefiniowanymi przez użytkownika rolami DDM a polami tabeli, do których ma zostać zastosowana maska. Nazwa tagu musi zaczynać się od #DDM_SEE_.

Poniższy kod kojarzy tag autoryzacji #DDM_SEE_ContactInfo z rolą myRole:

USING OpenEdge.DataAdmin.*.

VAR DataAdminService service.
VAR IAuthTag oTag.
VAR LOGICAL lReturn.

service = NEW DataAdminService (LDBNAME("DICTDB")).

     oTag = service:NewAuthTag("#DDM_SEE_ContactInfo"). 
     oTag:RoleName = service:GetRole("myRole"):Name.
     oTag:description = "To see contact info".
     lRETURN = service:CreateAuthTag(oTag).

No i wreszcie przydzielamy zdefiniowaną rolę użytkownikowi Admin.

USING OpenEdge.DataAdmin.*.
 
VAR DataAdminService service.
VAR IGrantedRole oRole.
VAR IUser oUser.
VAR LOGICAL lResult = FALSE.
 
service = NEW DataAdminService(LDBNAME("DICTDB")).
oRole = service:NewGrantedRole().
oRole:Role = service:GetRole("myRole").
oUser = service:GetUser("Admin").
 
IF VALID-OBJECT(oRole:Role) AND VALID-OBJECT(oUser) THEN DO:
  oRole:Grantee = oUser:Name.
 
   IF oUser:Domain:Name NE "" THEN
      oRole:Grantee = oRole:Grantee  + "@" + oUser:Domain:Name.
 
    oRole:CanGrant = false. // Cannot grant to others.
    // Granting Role to Admin
   lResult = service:CreateGrantedRole(oRole).
   
END.

Następnym krokiem jest zdefiniowanie maski dla wybranych pól i przetestowanie przykładów, ale tym zajmiemy się w następnym artykule. Teraz chciałbym pokazać gdzie można szukać informacji o zefiniowanych rolach i tagach.
Elementy te zostały dodane do metaschematu bazy, a jeśli tak, to są w określonych nowych tablicach VST.
Po pierwsze możemy zrzucić dane do plików poprzez opcję w Data Administration Admin -> Dump Data and Definitions -> Security Permissions.
Poniżej widać pierwsze linie z dwóch plików, w których są zdefiniowane przez nas dane.

_sec-auth-tag.d

"myRole" "#DDM_SEE_ContactInfo" "Can see contact info"
...

_sec-granted-role.d

"f82629bb-b26d-6b83-d314-c5d870ea3fee" "Admin" "myRole" no "Admin" ""
...

Informacja w plikach pochodzi z poniższych ukrytych tablic VST.

Wykorzystując je możmy sami wygenerować potrzebne informacje jak w poniższym przykładzie.

FIND FIRST _sec-granted-role NO-LOCK.
   DISPLAY  _grantee _role-name  WITH SIDE-LABELS.

FIND FIRST _sec-auth-tag NO-LOCK.
   DISPLAY  _sec-auth-tag._role-name _auth-tag SKIP 
            _description WITH SIDE-LABELS.


OK, następnym razem weźmiemy na warsztat tworzenie różnych masek i testowanie programów.