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.

AI w Developers Studio

Nie byłem do końca pewien czy powinienem napisać ten artykuł, bo nie mogę pokazać efektu końcowego, ale ze względu na zainteresowanie AI w OpenEdge postanowiłem pokazać jak rozpocząć konfigurację ChatGPT w Developers Studio. Całość jest dość dobrze udokumentowana na stronach progressowych choć nie do końca.
Po pierwsze musimy mieć minimum wersję OE 12.8.4. Wersja ta umożliwia instalację wtyczki ChatGPT o nazwie AI Coding Assistant.
Wchodzimy na stronę Progress Electronic Software Download (ESD) (musimy oczywiście mieć konto), skąd pobieramy plugin AssistAI-ChatGPT-PDSOE-Plugin….zip, który rozpakowujemy na lokalnym dysku.

Teraz wchodzimy do Developers Studio i wybieramy Help -> Install New Software. Wypełniamy pola jak poniżej i przyciskamy Add .

Plugin AssistAI jest już na liście.

Zaznaczamy plugin, klikamy Next, zaznaczmy zgodę license agreement i klikając Finish rozpoczynamy instalację.
Na stronie Trust zaznaczamy Unsigned i Always trust all content.

Akceptujemy ryzyko i kończymy instalację. Developers Studio trzeba teraz zrestartować.

Widok ChatGPT mamy już na liście, ale to niestety nie koniec.

Jeśli otworzymy widok i wpiszemy jakieś zapytanie nie dostaniemy odpowiedzi.

Żeby podejrzeć błędy jakie generują się podczas naszych prób użycia ChatGPT warto otworzyć mało znany widok: Error Log ponieważ w widoku Problems nie znajdziemy błędów dla tego działania.


W logu znajdziemy informację, że potrzebny jest klucz, jaki możemy wygenerować rejestrując się na stronie OpenAI.

Rejestracja i wygenerowanie klucza dla darmowego konta jest bardzo proste. Następnym krokiem jest aktywacja pluginu. Wchodzimy w Windows -> Preferences -> AssistAI wypełniając pola, podając wygenerowany klucz.

Niestety, ChatGPT wciąż nie odpowiada. Sprawdzamy Error Log.

Aby móc korzystać z ChatGPT trzeba założyć konto z płatną subskrypcją.

Klient HTTP ABL 2

Kontynuując temat związany z budowaniem klienta HTTP w aplikacji ABL, napiszę krótko o najprostszym uwierzytelnianiu. Zabezpieczenia w serwerze PASOE opisałem w serii artykułów, a teraz przyda się odświeżenie podstawowych wiadomości zamieszczonych tutaj.
W lokalizacji dla web aplikacji …\webapps\myrest\WEB-INF znajduje się plik oeablSecurity.properties. Znajdujemy w nim wpis: client.login.model=anonymous i zmieniamy na basic. Wymusi to logowanie się do serwisu.

Sposób uwierzytelniania widoczny jest poprzez parametr http.all.authmanager=local oznaczający, że uwierzytelnienie jest lokalne w oparciu o użytkowników zdefiniowanych w pliku:
C:\WrkOpenEdge121\mysec\webapps\ROOT\WEB-INFF\users.properites.
Na pierwszej pozycji znajduje się wpis: restuser=password,ROLE_PSCUser,enabled. Zapamiętujemy nazwę użytkownika i hasło (możemy także zdefiniować własnych użytkowników).

USING OpenEdge.Core.Collections.IStringStringMap FROM PROPATH.
USING OpenEdge.Net.HTTP.ClientBuilder FROM PROPATH.
USING OpenEdge.Net.HTTP.Credentials FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpClient FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpRequest FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpResponse FROM PROPATH.
USING OpenEdge.Net.HTTP.RequestBuilder FROM PROPATH.
USING OpenEdge.Net.URI FROM PROPATH.
USING Progress.Json.ObjectModel.JsonObject FROM PROPATH.
USING Progress.Json.ObjectModel.ObjectModelParser FROM PROPATH.

/* ***************************  Main Block  *************************** */
DEFINE VARIABLE oClient AS IHttpClient NO-UNDO.
DEFINE VARIABLE oURI AS URI NO-UNDO.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oForm AS IStringStringMap NO-UNDO.
DEFINE VARIABLE OResponse AS IHttpResponse NO-UNDO.
DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE JsonString AS LONGCHAR NO-UNDO.
DEFINE VARIABLE iCount AS INTEGER NO-UNDO.
DEFINE VARIABLE oCreds AS Credentials NO-UNDO.


// Build the client
oClient = ClientBuilder:Build():Client.
oURI = URI:Parse("http://localhost:8810/myrest/rest/myrestService/customer?filter=CustNum=4000").

// Create credentials
oCreds = NEW Credentials().
oCreds:UserName = "restuser".
oCreds:Password = "password".

// Build the request
oRequest = RequestBuilder:GET(oURI):UsingBasicAuthentication(oCreds):Request.

// Execute the request
oResponse = oClient:Execute(oRequest).
 
//Process the response
IF oResponse:StatusCode <> 200 THEN DO:
    MESSAGE "Request Error " + STRING(OResponse:StatusCode).
    RETURN ERROR "Request Error: " + STRING(oResponse:StatusCode).
    END.
ELSE DO:
    oJsonObject = CAST(oResponse:Entity, JsonObject).
    oJsonObject:Write(JsonString, TRUE).
    MESSAGE STRING(JsonString) VIEW-AS ALERT-BOX.
    END.

W sekcji // Create credentials budowany jest obiekt zawierający nazwę użytkownika i hasło:
oCreds = NEW Credentials(). W tym bardzo prostym przypadku dane są podane w sposób jawny.
Najciekawsze instrukcja to budowa żądania z wymuszeniem uwierzytelnienia.
oRequest = RequestBuilder:GET(oURI):UsingBasicAuthentication(oCreds):Request.
Możecie przetestować ten przykład podając dane poprawne, a potem zmienić nazwę klienta lub hasło.
Efekty widać poniżej:


Dodam tylko, że metod i możliwości obsługi klienta HTTP z poziomu języka ABL jest znacznie więcej. Są np. metody do obsługi nagłówka czy ciasteczek. Może jeszcze o tym napiszę.

Klient HTTP ABL

W całym cyklu artykułów kilkakrotnie pisałem o tworzeniu serwisów generujących dane do tzw. endpoint a także o korzystaniu z tych danych do generowania prostego interfejsu użytkownika.

Żeby nie być gołosłownym przypomnę, że dane takie można wygenerować np. do formatu JSON za pomocą wizardów w OE Develeopr Studio dla serwisów rest
1) poprzez warstwę transportową REST
2) lub warstwę transportową WEB.

Wybierając jedną z powyższych metod tworzymy nowy projekt dla wybranej warstwy transportowej, generujący dane do pliku .cls (tutaj plik customer.cls).

Klasa ta ma metody do manipulacji dla danych, które odpowiadają czasownikom HTTP (Read -> GET, Update -> PUT, Create -> POST, Delete -> DELETE).

Dane dostarczane przez serwis możemy testować przy pomocy np. darmowych rozwiązań jak Chrome Advanced REST client czy Postman. Z pierwszego rozwiązania korzystałem już wcześniej tutaj, a z Postmana w niniejszym artykule. Jest to bardzo intuicyjne, czytelne narzędzie zawierające wszystko co możemy potrzebować przy testowaniu żądań HTTP (patrz poniżej).

Jeśli chodzi o drugą stronę, czyli o klienta czytającego wystawione dane, możemy skorzystać z różnych technologii dostępnych na rynku. Ja pokazałem przykład ze zwykłym JavaScriptem tutaj.

W OE 12 możemy zbudować w programie własnego klienta HTTP, czym zajmiemy się teraz.
W tym celu będziemy korzystać z biblioteki OpenEdge.Net, która udostępnia klasy i interfejsy umożliwiające tworzenie żądań HTTP oraz przetwarzanie odpowiedzi HTTP w języku ABL.

USING OpenEdge.Core.Collections.IStringStringMap FROM PROPATH.
USING OpenEdge.Net.HTTP.ClientBuilder FROM PROPATH.
USING OpenEdge.Net.HTTP.Credentials FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpClient FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpRequest FROM PROPATH.
USING OpenEdge.Net.HTTP.IHttpResponse FROM PROPATH.
USING OpenEdge.Net.HTTP.RequestBuilder FROM PROPATH.
USING OpenEdge.Net.URI FROM PROPATH.
USING Progress.Json.ObjectModel.JsonObject FROM PROPATH.

/* ***************************  Main Block  *************************** */
DEFINE VARIABLE oClient AS IHttpClient NO-UNDO.
DEFINE VARIABLE oURI AS URI NO-UNDO.
DEFINE VARIABLE oRequest AS IHttpRequest NO-UNDO.
DEFINE VARIABLE oForm AS IStringStringMap NO-UNDO.
DEFINE VARIABLE OResponse AS IHttpResponse NO-UNDO.
DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
DEFINE VARIABLE JsonString AS LONGCHAR NO-UNDO.
DEFINE VARIABLE oCredentials AS Credentials NO-UNDO.

// Build the client
oClient = ClientBuilder:Build():Client.
oURI = URI:Parse("http://localhost:8810/myrest/rest/myrestService/customer?filter=CustNum=3000").

// Build the request
oRequest = RequestBuilder:GET(oURI):Request.

// Execute the request
oResponse = oClient:Execute(oRequest).
 
//Process the response
IF oResponse:StatusCode <> 200 THEN
    MESSAGE "Request Error " + STRING(OResponse:StatusCode) VIEW-AS ALERT-BOX.
ELSE DO:
    oJsonObject = CAST(oResponse:Entity, JsonObject).
    oJsonObject:Write(JsonString, true).
    MESSAGE STRING(JsonString) VIEW-AS ALERT-BOX.
    END.

Kod procedury jest dość prosty. Najpierw budowany jest klient HTTP, potem żądanie, a na koniec żądanie jest wykonywane. Musimy podać tu tylko URI do zdefiniowanego wcześniej serwisu. Instrukcja MESSAGE zwraca nam pobrany JSON, zapamiętajmy nazwy obiektów.

Jeśli chcemy korzystać z poszczególnych danych w programie (no bo po co czytaliśmy JSON?), to wygodnie wczytać je do tabeli tymczasowej w ProDataSecie. Nazwy obiektów muszą być takie jak w JSON, czyli: ttCustomer, dsCustomer. Dodajemy poniższy kod i testujemy.

    DEFINE VARIABLE lRetOK      AS LOGICAL   NO-UNDO.
    DEFINE VARIABLE hdsCust     AS HANDLE    NO-UNDO.
    
    DEFINE TEMP-TABLE ttCustomer LIKE Customer.
    DEFINE DATASET dsCustomer FOR ttCustomer.
    
    hdsCust = DATASET dsCustomer:HANDLE.
    lRetOK = hdsCust:READ-JSON("LONGCHAR", JsonString, "EMPTY").
    FIND FIRST ttCustomer.
    DISPLAY custnum NAME.


Powrócę wkrótce do klienta HTTP z tematem związanym z uwierzytelnianiem. Zachęcam do testów!

Śledzenie aplikacji ABL

Śledzenie aplikacji ABL to nie jest temat nowy – po raz pierwszy zostało zaimplementowane w wersji 9, a więc jeszcze w starym 4GL.
Śledzenie to nie zostało niestety uwzględnione ani w szkoleniu z podstaw języka, ani ze strojenia aplikacji i może dlatego nie wszystkim deweloperom aplikacji OpenEdge jest znane.
W ostatnich tygodniach klienci zgłaszali mi dwa problemy, przy rozwiązaniu których ta technika okazała się pomocna i dlatego postanowiłem o niej napisać.
Chodzi w niej o zrzut określonych danych do własnego pliku log. Dane te mogą dotyczyć np. szczegółów transakcji, łączności z bazą danych, uruchamiania obiektów dynamicznych itd. Można określić także ilość zrzucanych informacji.

Śledzenie można włączyć na różne sposoby. Jeśli chcemy mieć kompletny zapis działania aplikacji (gdy np. nie wiemy gdzie jest wadliwy fragment kodu) możemy uruchomić sesję z określonymi parametrami np.
prowin -p plik.p -db baza -debugalert -clientlog mylog.txt -logginglevel 3 -logentrytypes 4GLTrace,DynObjects.*
Na uwagę zasługują tutaj następujące parametry:

-clientlog – nazwa naszego pliku log

-logginglevel – poziom zapisu, 3 oznacza Verbose i taką wartość będę tutaj stosował

-logentrytypes – typ zrzucanej informacji.

Przyjrzyjmy się wybranym wartościom trzeciego parametru.

  • 4GLMessages – zapisuje komunikaty VIEW-AS ALERT-BOX – jeśli włączony jest Debug Alert (parametr -debugalert), to zapisywane są także informacje stosu ABL
  • 4GLTrace – chyba najczęściej stosowany typ informacji, rejestruje wywołanie procedur, funkcji, trygerów, metod, konstruktorów, destruktorów, zdarzeń PUBLISH/SUBSTRIBE itp.
  • 4GLTrans – rejestrowanie przetwarzanych transakcji i podtransakcji w procedurach ABL
  • DB.Connects – rejestrowanie przyłączania/odłączania baz danych
  • DynObjects.* – rejestrowanie tworzenia i usuwania dynamicznych obiektów różnego typu (* dla wszystkich)
  • QryInfo – rejestrowanie zapytań (OPEN QUERY oraz FOR EACH)
  • Temp-tables – rejestrowanie informacji o tabelach tymczasowych wykorzystywanych w aplikacji
  • TTStats – rejestrowanie statystyk tabel tymczasowych

Niektóre z tych wybranych wartości można stosować jedynie w sesji klienta inne także w sesji serwera aplikacji. Po kompletny wykaz zapraszam do dokumentacji dla konkretnej wersji.

Jest jeszcze inny sposób na uruchomienie tego procesu, szczególnie przydatny jeśli chcemy badać tylko określony fragment aplikacji, co zdarza się chyba o wiele częściej.

Wykorzystuje się do tego obiekt LOG-MANAGER, co pokażę na poniższych przykładach.

/* trace1.p */
DEFINE BUTTON btnCust LABEL "Show Customer".
DEFINE BUTTON btnExit LABEL "Exit".

LOG-MANAGER:LOGFILE-NAME = 'mylogmanager.txt'.
LOG-MANAGER:LOGGING-LEVEL = 3.
LOG-MANAGER:LOG-ENTRY-TYPES = '4GLTrace,4GLMessages'.

PROCEDURE ShowCust:
  FIND FIRST customer.
  MESSAGE name
  VIEW-AS ALERT-BOX.
END PROCEDURE.

SUBSCRIBE TO "FindCustomer" IN THIS-PROCEDURE RUN-PROCEDURE "ShowCust".

FORM btnCust btnExit WITH FRAME x.
ENABLE ALL WITH FRAME x.

ON CHOOSE OF btnCust PUBLISH "FindCustomer".

WAIT-FOR CHOOSE OF btnExit.

LOG-MANAGER:CLOSE-LOG.

Pierwszy przykład. Dane są zrzucane do pliku mylogmanager.txt, poziom = 3, a typ informacji to 4GLTrace oraz 4GLMessages. Przykładowa aplikacja GUI składa się z okna, dwóch przycisków i wewnętrznej procedury. Jest też prosta implementacja PUBLISH/SUBSCRIBE. Uruchamiam procedurę, naciskam przycisk btnCust (pojawia się nazwa pierwszego klienta) i następnie btnExit.
Zobaczmy zawartość pliku mylogmanager.txt (dla większej czytelności usunąłem z każdej linii dane czasowe).

1 4GL -- Logging level set to = 2
1 4GL -- No entry types are activated
1 4GL -- Logging level set to = 3
1 4GL -- Log entry types activated: 4GLTrace,4GLMessages
2 4GL 4GLTRACE       SUBSCRIBE FindCustomer "C:\WrkOpenEdge128\workspace\ABLProj\trace1.p" C:\WrkOpenEdge128\workspace\ABLProj\trace1.p [Main Block - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 14]
2 4GL 4GLTRACE       PUBLISH FindCustomer [USER-INTERFACE-TRIGGER - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 19]
2 4GL 4GLTRACE       RUN ShowCust in C:\WrkOpenEdge128\workspace\ABLProj\trace1.p [USER-INTERFACE-TRIGGER - C:\WrkOpenEdge128\workspace\ABLProj\trace1.p @ 19]
2 4GL 4GLMESSAGE     Lift Tours
2 4GL 4GLMESSAGE     ** ABL Debug-Alert Stack Trace **
2 4GL 4GLMESSAGE     --> ShowCust C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 10  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
2 4GL 4GLMESSAGE         USER-INTERFACE-TRIGGER C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 19  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
2 4GL 4GLMESSAGE         C:\WrkOpenEdge128\workspace\ABLProj\trace1.p at line 21  (C:\WrkOpenEdge128\workspace\ABLProj\trace1.r)
3 4GL 4GLTRACE       Return from ShowCust [C:\WrkOpenEdge128\workspace\ABLProj\trace1.p]
3 4GL 4GLTRACE       Return from PUBLISH FindCustomer [C:\WrkOpenEdge128\workspace\ABLProj\trace1.p]
1 4GL ----------     Log file closed at user's request

Z lewej strony widzimy którego typu wpis w danej linii dotyczy. Można np. prześledzić mechanizm PUBLISH/SUBSCRIBE. Widać też jaka wartość została wyświetlona w komunikacie (“Lift Tours”). Ponieważ parametr sesji DEBUG-ALERT jest ustawiony na ‘yes’ to zostaje wyświetlona również informacja stosu ABL (szczególnie przydatna w przypadku wystąpienia błędów). Mamy także informacje o wywołaniu procedury i trygera.

Weźmy teraz inny przykład. Tym razem będą interesować nas transakcje i sub-transakcje, dlatego jako typ informacji dodajemy 4GLTrans.

/* trace2.p */
LOG-MANAGER:LOGFILE-NAME = 'mylogmanager.txt'.
LOG-MANAGER:LOGGING-LEVEL = 3.
LOG-MANAGER:LOG-ENTRY-TYPES = '4GLTrace,4GLTrans'.

CustLab:
REPEAT:
   FIND LAST customer.
   DO TRANSACTION:
      country = "USA". 
   END.    
   UPDATE NAME.
   IF NAME BEGINS "x" THEN UNDO, RETRY.
   ELSE LEAVE.
END.   

LOG-MANAGER:CLOSE-LOG.
QUIT.

Przykład składa się tylko z bloku REPEAT wewnątrz którego jest zdefiniowana transakcja, polegająca na edycji pola Name. Jeśli nazwa będzie zaczynać się na literę “x” transakcja ma być wycofana, w przeciwnym razie zatwierdzona. Wewnątrz bloku transakcyjnego zdefiniowano sub-transakcję, w której ustawiany jest kraj “USA”.
Przetestuję procedurę wstawiając najpierw dwa razy nazwę zaczynającą się od “x”, a następnie od innej, poprawnej litery. Pierwsza i druga transakcja powinna zatem zostać wycofana, a trzecia zatwierdzona.

1 4GL -- Logging level set to = 2
1 4GL -- No entry types are activated
1 4GL -- Logging level set to = 3
1 4GL -- Log entry types activated: 4GLTrace,4GLTrans
3 4GL 4GLTRANS       BEGIN TRANS 28 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 7]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 29 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 29 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       UNDO TRANS 28 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN TRANS 31 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 32 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 32 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       UNDO TRANS 31 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN TRANS 34 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
3 4GL 4GLTRANS       BEGIN SUB-TRANS 35 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 9]
3 4GL 4GLTRANS       END SUB-TRANS 35 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 11]
3 4GL 4GLTRANS       END TRANS 34 [C:\WrkOpenEdge128\workspace\ABLProj\trace2.p @ 15]
1 4GL ----------     Log file closed at user's request

Sprawdzamy zapisy w pliku mylogmanager.txt. Widać, że pierwsze dwie transakcje (o numerach 28 i 31) zostały wycofane razem z sub-transakcjami (odpowiednio 29 i 32). Trzecia transakcja (nr 34) i sub-transakcja (nr 35) nie zostały wycofane, a więc są zatwierdzone.
Jak widać jest to bardzo użyteczne narzędzie do badania transakcji podczas runtime’u.
Dodam jeszcze, że numery transakcji maja tutaj wartości nadawane lokalnie dla odczytu pliku log i nie mają nic wspólnego z rzeczywistym id transakcji, które można podejrzeć np. w promonie: R&D ->Status Displays -> Processes/Clients… -> 3. Active Transactions

Kafka i OpenEdge cz. II

W poprzednim artykule pokazałem jak zainstalować i skonfigurować lokalne środowisko Kafki a teraz pokażę jak zintegrować je z OpenEdgem.
Niektórzy z Was, którzy będą potrzebować zintegrować się z istniejącą architekturą Kafki, będą zainteresowani od tego właśnie miejsca.

Najlepiej w tym celu sprawdzić dokumentację OE dla danej wersji np. online. Ważne, że trzeba posiadać wersję OE 12.5 lub wyższą.

Pierwszym krokiem jest pobranie biblioteki Apache Kafka C/C++ dla Windows lub Unixa (link Download package po prawej stronie). Minimalna wymagana wersja to 1.7, ale zaleca się pobranie ostatniej wersji.

Gdy plik mamy już pobrany zmieniamy jego rozszerzenie z nupkg na zip i rozpakowujemy. W przypadku Windows będą nas interesowały pliki z podkatalogu runtimes\win-x64\native\, które umieszczamy w katalogu roboczym naszej aplikacji ABL.

Dodajemy do PROPATH następujace biblioteki:

  • $DLC/tty/netlib/OpenEdge.Net.pl
  • $DLC/tty/messaging/OpenEdge.Messaging.pl

Teraz możemy wziąć się za napisanie procedury w ABL, a raczej za pobranie jej z dokumentacji i małą kastomizację.

/* producer.p */

using OpenEdge.Messaging.*.
using OpenEdge.Messaging.Kafka.*.

block-level on error undo, throw.

var RecordBuilder recordBuilder.
var KafkaProducerBuilder pb.
var IProducer producer.
var IProducerRecord record.
var ISendResponse response.

pb = cast(ProducerBuilder:Create("progress-kafka"), KafkaProducerBuilder).

pb:SetBootstrapServers("172.22.13.101:9092").
pb:SetClientId("test client").
pb:SetBodySerializer(new StringSerializer()).
pb:SetKeySerializer(new StringSerializer()).

pb:SetProducerOption("message.timeout.ms", "10000").

producer = pb:Build().

recordBuilder = producer:RecordBuilder.
recordBuilder:SetTopicName("mytopic").   

Find first customer.
recordBuilder:SetBody(name).

record = recordBuilder:Build().

response = producer:Send(record).

producer:Flush(1000).
    
repeat while not response:Completed:
    pause .1 no-message.
end.

if response:Success then do:
   message "Send successful" view-as alert-box.
end.
else do:
    undo, throw new Progress.Lang.AppError("Failed to send the record: " +
        response:ErrorMessage, 0).
end.
 

catch err as Progress.Lang.Error :
    message err:GetMessage(1) view-as alert-box.
end catch.

finally:
    delete object producer no-error.
end.

Po pierwsze wstawiam własny adres IP w metodzie SetBootstrapServers i nazwę topicu mytopic.
Potem ustawiam SetBodySerializer() (metoda ustawia proces serializacji dla treści wiadomości) na StringSerializer. Serializator zajmuje się konwersją danych na bajty w celu przesłania do topicu. OpenEdge udostępnia również MemptrSerializer i JsonSerializer, jak również tworzenie własnych niestandardowych serializatorów.
I wreszcie znajduję pierwszy rekord customer i ustawiam jego nazwę (name) jako wartość do przesłania.
Po uruchomieniu procedury sprawdzam co pojawiło się w topicu Kafki. Na czerwono zaznaczyłem wartość przesłanej wartości (Lift Tours).

Teraz pora na odczyt wartości z topicu Kafki w procedurze ABL.

/* consumer.p */

block-level on error undo, throw.
 
using OpenEdge.Messaging.ConsumerBuilder from propath.
using OpenEdge.Messaging.IConsumer from propath.
using OpenEdge.Messaging.IConsumerRecord from propath.
using Progress.Json.ObjectModel.JsonConstruct from propath.
using Progress.Json.ObjectModel.JsonObject from propath.
 
var ConsumerBuilder cb.
var IConsumer consumer.
var IConsumerRecord record.
//var Progress.Lang.Object messageBody.
var char messageBody.
 
cb = ConsumerBuilder:Create("progress-kafka").
 
// Kafka requires at least one bootstrap server host and port.
cb:SetConsumerOption("bootstrap.servers", "172.22.13.101:9092").

// Explicitly disable auto commit so it can be controlled within the application.
cb:SetConsumerOption("enable.auto.commit", "false").

// Identify the consumer group. The consumer group allows multiple clients to
//  coordinate the consumption of multiple topics and partitions
//cb:SetConsumerOption("group.id", "my.consumer.group").
cb:SetConsumerOption("group.id", "test-consumer-group").

// Specify whether the consumer group should automatically be deleted when
//  the consumer is garbage collected.
cb:SetConsumerOption("auto.delete.group", "true").

// Configure the consumer's deserializer in order to convert values from
//  the network messages to string objects.
cb:SetConsumerOption("value.deserializer", "OpenEdge.Messaging.StringDeserializer").

// Set the consumer starting position to the most recent message
cb:SetConsumerOption("auto.offset.reset", "latest").
     
// identify one or more topics to consume
cb:AddSubscription("mytopic").
     
// build the consumer
consumer = cb:Build().
          
// loop forever receiving and processing records.
repeat while true:
    // request a record, waiting up to 1 second for some records to be available
    record = consumer:Poll(1000).
         
    if valid-object(record) then do:
           
        // acknowledge the message so the client can resume where it leaves off
        // the next time it is started
        consumer:CommitOffset(record).

        MESSAGE record:TopicName VIEW-AS ALERT-BOX.   

       messageBody = record:Body:ToString().      
       message messageBody view-as alert-box.
      
    end.
         
end.
     
catch err as Progress.Lang.Error :
    message err:GetMessage(1) view-as alert-box.
end catch.

finally:
    delete object consumer no-error.
end.

Uruchamiamy procedurę producer.p i consumer.p. Widać, że pierwsza wyświetla komunikat o poprawnym wysłaniu, a druga prawidłową wartość pobraną z kolejki.

Na koniec jeszcze jedna informacja związana z ustawianiem wartości parametrów. Otóż można to zrobić na dwa sposoby: poprzez użycie metod akceptujących wartości silnie typizowane (tzw. strongly-typed) lub używając par nazwa-wartość.
Obie poniższe linie kodu są równoważne:

pb:SetProducerOption("bootstrap.servers", "172.22.13.101:9092").
pb:SetBootstrapServers("172.22.13.101:9092").

Kafka i OpenEdge cz. I

O integracji OpenEdge z Apache Kafka chciałem napisać od dawna. Najpierw musiałem pokonać pewne kłopoty techniczne, tak  aby pokazać pełna ścieżkę od instalacji tejże Kafki do napisania programów w ABL do wysyłania i pobierania danych, ale po kolei. Co to jest Kafka.

Na głównej stronie Apache Kafki możemy przeczytać w tłumaczeniu:

Apache Kafka to rozproszona platforma do strumieniowego przesyłania komunikatów typu open-source, z której korzystają tysiące firm w celu zapewnienia wydajnego przetwarzania danych, analizy przesyłania strumieniowego, integracji danych i aplikacji o znaczeniu krytycznym.

Komunikaty w Kafce pogrupowane są w tzw. topiki (tematy, ang. topic). Nadawca (producer), jak i odbiorca (consumer) powiązani są z jednym lub wieloma topikami.
Rzućmy okiem na uproszczony schemat systemu Kafki.

Kafka zapewnia przesyłanie danych w czasie zbliżonym do rzeczywistego ze źródeł, takich jak bazy danych, aplikacje, czujniki, urządzenia mobilne, usługi w chmurze itd.

Nadawcy i odbiorcy mogą być procesami napisanymi w różnych językach programowania, na różnych systemach operacyjnych; musi istnieć tylko mechanizm aby wpięli się do systemu. Dla klientów ABL również jest taki mechanizm ale o tym napiszę później.

Klientom Progressa ta architektura może przypominać produkty z grupy Sonic, jednakże Kafka jest technologią nieporównywalnie popularniejszą, stosowaną przez wiele wielkich i rozpoznawalnych firm jak Netflix, Twitter, Spotify, Cisco, LinkedIn i dziesiątki innych. Zresztą różnic jest więcej. Kafka jest platformą o wysokiej wydajności i skalowalności, przesyłane komunikaty nie są automatycznie kasowane z kolejki po odczytaniu (mogą być przechowywane bez ograniczenia czasowego) itd…

Komunikaty (nazywane także zdarzeniami lub rekordami) z danego topiku dopisywane są na końcu tzw. partycji. Partycja to uporządkowany rejestr komunikatów, mówiąc niskopoziomowo, jest to plik na dysku brokera, do którego zapisywane są komunikaty. Aby konsument był w stanie odebrać określony komunikat / sekwencję komunikatów – musi znać pozycję ostatnio przeczytanego komunikatu.

OpenEdge udostępnia nowe OpenEdge.Messaging API do wysyłania wiadomości do klastra Kafki poprzez Kafka producer i odbierania wiadomości z tego klastra przez Kafka consumer.

Tyle wstępnych informacji. Chcę Wam pokazać cały proces od zainstalowania Kafki na lokalnej maszynie aby każdy mógł przetestować ten system.

Kafka jest przeznaczona do uruchamiania w systemie Linux i Mac i nie jest przeznaczona do natywnego uruchamiania w systemie Windows. Dlatego w przypadku tej platformy zaleca się stosować technologię Docker lub WSL2.

WSL2 (Windows Subsystem for Linux 2) zapewnia środowisko Linux dla komputera z systemem Windows 10+, które nie wymaga maszyny wirtualnej.

Moja konfiguracja wygląda następująco: Windows 10, zainstalowany WSL2 oraz Windows Terminal, w którym mam dostęp do Linuxa Ubuntu.

Instaluję Apache Kafka korzystając z oryginalnej strony. Polega to (ach ten Linux!) jedynie na pobraniu i rozpakowaniu plików. Najpierw niezbędne jest zainstalowanie infrastruktury do utrzymywania i koordynowania brokerów. Może to być KRaft lub ZooKeeper. Ja wybieram to drugie rozwiązanie.

W Windows Terminal otwieram kilka zakładek dla Ubuntu.

W pierwszej uruchamiam ZooKeepera:

sudo bin/zookeeper-server-start.sh config/zookeeper.properties

W drugiej Kafkę:

sudo bin/kafka-server-start.sh config/server.properties

Jak widać cała operacja jest bardzo prosta. Mogę dalej podążać za instrukcją: dodać topik, okno consumera, nadawać i odbierać komunikaty. Ta testowa konfiguracja odwołuje się do parametru bootstrap-server, który zawiera listę par „hostname:port”, które adresują jednego lub więcej brokerów. W tym prostym przypadku dodanie topicu wygląda następująco:

kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092

Zastopujmy teraz wszystkie procesy Kafki poprzez zamknięcie zakładek.

Ponieważ będę potrzebował mieć dostęp z zewnętrznej aplikacji ABL, więc zamiast wartości localhost muszę wstawić wartość ip “maszyny” z Linuxem. Jeśli ip wynosi np. 172.22.13.101, to tę wartość wstawiam w następujących miejscach.
plik: config/server.properties
listeners=PLAINTEXT://172.22.13.101:9092
jak poniżej:

plik: config/producer.properties
bootstrap-servers=172.22.13.101:9092

plik: config/consumer.properties
bootstrap-servers=172.22.13.101:9092

OK, uruchamiam ponownie procesy ZooKeepera i Kafki. W trzeciej zakładce dodaję topic mytopic,

bin/kafka-topics.sh --create --topic mytopic --bootstrap-server 172.22.13.101:9092

a następnie uruchamiam proces producera:

bin/kafka-console-producer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

a w oknie czwartym consumera:

bin/kafka-console-consumer.sh --topic mytopic --bootstrap-server 172.22.13.101:9092

Wysyłam przykładowe wiadomości w oknie producera.

Pojawiają się one natychmiast w oknie consumera.

OK, mamy zatem skonfigurowany prosty system Kafki i w następnym wpisie pokażę jak przyłączyć się do niego z poziomu aplikacji ABL.

Podpisane biblioteki procedur ABL

Dziś napiszę wstęp o zabezpieczeniach r-kodów w tzw. podpisanych bibliotekach archiwum procedur. Temat ten wypłynął podczas kwietniowego spotkania World Tour i byłem poproszone o napisanie trochę więcej informacji niż było to w prezentacji.

Biblioteki archiwum ABL to zbiór r-kodów (rozszerzenie .apl), podobnie jak dobrze znane biblioteki .pl ale obsługują także podpisywanie i weryfikację r-kodów.
Podpisany plik archiwum gwarantuje, że żaden r-kod nie został uszkodzony ani naruszony. Jest to bardzo istotne np. podczas przeprowadzanych zewnętrznych audytów aby mieć pewność, że aplikacje przejdą kontrolę bezpieczeństwa.

Deweloperzy dostają dwa narzędzia uruchamiane z wiersza poleceń PROPACK i PROSIGN. Tworzą one cyfrowo podpisane archiwum oparte na standardzie Java JAR.

Zacznijmy od spakowania dwóch plików procedur findcustomer.r i updatecustomer.r wykorzystując komendę PROPACK. Nasz plik wynikowy bedzie miał nazwę cust.apl. Pozostaje do określenia ważny atrybut sygnatury. Może on mieć wartość open (pliki archiwum nie muszą być podpisane) lub required (podpis jest wymagany). Użyję tutaj domyślnej wartości open. Przykład komendy będzie wyglądał następująco:

propack --create --file=cust.apl --vendor "PSC" --signature=open .

Żeby sprawdzić jakie pliki znajdują się w archiwum uruchamiamy komendę z opcją –list:

propack --list --file=cust.apl

Widać, że w bibliotece znajduje się plik MANIFEST.MF
Zawiera on informacje na temat archiwum, a w przypadku podpisanej biblioteki także sygnatury r-kodów (o czym za chwilę).


Zobaczmy teraz co się stanie jeśli utworzę taką bibliotekę z parametrem signature=required.

Dodaję nazwę nowej biblioteki w Protools do PROPATH i uruchamiam prostą komendę.

Dostajemy błąd ponieważ plik musi być podpisany.

Wróćmy do pliku z sygnaturą typu open i zajmijmy się podpisaniem biblioteki. Przed uzyskaniem certyfikatu cyfrowego należy utworzyć magazyn kluczy do przechowywania certyfikatów własnych i z urzędu certyfikacji (CA). Utworzenie magazynu kluczy powoduje również umieszczenie w magazynie certyfikatu z podpisem własnym (self-signed) i pary kluczy (publiczny-prywatny). Nam dla testów będzie wystarczył certyfikat self-signed.
Użyjemy poniższej komendy pamiętając, że w oknie komend musi to być ciąg znaków bez znaków końca linii.

keytool -genkey 
-dname "CN=Host, OU=Education O=PSC, L=Warsaw, S=Mazovia, C=Poland"  
-alias server 
-keystore myKeystore.jks 
-storepass mykeystorepass1 
-validity 90 
-keyalg RSA 
-keysize 4096

Magazyn klucza jest już wygenerowany (.jks). Zapisujemy wartość aliasu i hasła do magazynu.
Teraz możemy użyć komendy PROSIGN.

prosign --archive cust.apl --signedarchive signedcust.apl 
--alias server --keystore myKeystore.jks --storepass mykeystorepass1


Biblioteka jest już podpisana ale dostaliśmy ostrzeżenie, że certyfikat jest self-signed i w środowisku produkcyjnym musimy zadbać o taki, który jest wydany przez oficjalny urząd (CA).

Zobaczmy teraz zawartość pliku MANIFEST.MF. Zawiera on na końcu funkcje skrótu SHA-256 dla obu r-kodów, gwarantując ich oryginalną zawartość.

Manifest-Version: 1.0
Implementation-Title: cust.apl
Implementation-Vendor: PSC
Implementation-Version: 0.0.0.0
Component-Name: cust.apl
Package-Type: apl
Signature-Policy: open
Validation-Policy: warn
Build-OS: all
Build-Date: 2024-05-10T14:57:39.157+01:00
OpenEdge-Tool: propack v1.00 (MSWin32)
OpenEdge-Version: 12.8
Created-By: 17.0.10 (Oracle Corporation)

Name: findCustomer.r
SHA-256-Digest: 1wpcgS+b6ya5OvqTjUek29hJdnqWNsjdRBJTPzYDCyo=

Name: updateCustomer.r
SHA-256-Digest: fKk6cX6dlJsN8VxgKzdbjM1XaXQRL/N3sdlHySHb0vE=

OpenEdge 12.8

Nareszcie mamy na rynku następną wersję LTS (Long-Term Support) OE 12.8. Poprzednia OE 12.2 pojawiła się 4 lata temu. 12.8 zawiera elementy wprowadzane w kolejnych wersjach Innovation 12.3-12.7 włącznie, co z grubsza opisałem na tym blogu. Teraz zobaczymy co ciekawego jest w samej wersji 12.8.

Zacznijmy od administracji PASOE. W poprzednim artykule opisałem nową komendę OEMANAGER, stosowaną do przeglądu statystyk i podstawowych operacji. Osoby zainteresowane odsyłam bezpośrednio do tego wpisu.

Inną ciekawą i bardzo ważną funkcją jest zapowiadana technologia Dynamic Data Masking (DDM) służąca do dynamicznego zaciemniania lub maskowania wrażliwych danych w bazie przed nieautoryzowanymi użytkownikami. Przykładem może być maskowanie w danych osobowych pola “pesel” lub w danych finansowych pola “wynagrodzenie”.

Technologia ta składa się niejako z dwóch etapów. Pierwszy należy do administratora bazy i polega na włączeniu funkcji poleceniem PROUTIL [baza] -C ENABLEDDM. Komenda ta sprawdza czy baza jest licencjonowana do użycia DDM. Następne polecenie PROUTIL [baza] -C ACTIVATEDDM uaktywnia DDM w bazie (dostępne są także opcje DEACTIVATEDDM i DISABLEDDM).

DDM do kontrolowania uprawnień przyznawanych użytkownikom w zakresie maskowania danych wykorzystuje konfigurację opartą na rolach. Administrator DDM może skonfigurować maskę nad polami tabeli, która ukrywa wrażliwe dane w zestawie wynikowym zapytania, a także utworzyć i przypisać nowe tagi autoryzacyjne do ról zdefiniowanych przez użytkownika.

Drugim etapem jest zatem zdefiniowanie własnych ról oraz znaczników autoryzacji i przypisanie ich do zdefiniowanych ról, przydzieleniu ról uwierzytelnionym użytkownikom, konfiguracja masek i znaczników autoryzacji dla pól tabeli. Etap ten realizujemy wykorzystując interfejs IDataAdminService, który udostępnia zestaw właściwości i metod obsługujących operacje CRUD związane z DDM. Poniżej zamieszczam wzięty z dokumentacji przykład utworzenia własnej roli TestUser.

USING OpenEdge.DataAdmin.*.

VAR DataAdminService oDAS. VAR IRole oRole.
VAR LOGICAL lResult.

ASSIGN oDAS = NEW DataAdminService(LDBNAME("DICTDB")).

oRole = oDAS:NewRole("TestUser"). 
oRole:Description = "A Test User".
// This role will be used for DDM
oRole:IsDDM = true.
lResult = oDAS:CreateRole(oRole). 

DELETE OBJECT oDAS.

W OpenEdge jest systematycznie rozwijana technologia wizualizacji danych poprzez standard OpenTelemetry i wykorzystanie zewnętrznych aplikacji typu APM (Application Performance Monitoring).
W 12.8 dodano możliwość monitorowania procesów agentów PASOE oraz klientów ABL. W tym drugim przypadku należy uruchomić sesję ABL z parametrem -otelconfig myotelConfig.json, wskazującym na plik konfiguracyjny w standardzie JSON. Przykładowy taki plik zamieszczam poniżej:

{
"OpenTelemetryConfiguration": {
        "exporters": {
            "otlp": {
                "grpc": [
                    {
                        "endpoint": "http://localhost:4317",
                        "span_processor": "batch",              
                     }
            ]
        }
      }
    },
    "OpenEdgeTelemetryConfiguration": {
        "trace_procedures": "Customer*.p",
		"trace_classes": "*",
        "trace_abl_transactions": true,
        "trace_requires_parent": true,
        "trace_request_start": true
    }
}

Plik ten określa m.in. proces OTel Collectora (port:4317), a także maskę monitorowanych plików np. każda procedura zawierająca w nazwie słowo Customer i wszystkie klasy (*). O pełnej konfiguracji monitorowania przy pomocy Open Telemetry napiszę innym razem.

A teraz kilka słów na temat administrowania bazą. Pojawiła się komenda PROUTIL [baza] -C PROBE [state] która sprawdza aktualny operacyjny stan bazy. Sprawdzane są trzy stany (parametr state):

  • startup – czy baza danych zakończyła uruchamianie
  • liveness – czy broker bazy danych działa i odpowiada
  • readiness – czy baza danych może wykonywać operacje (np. CRUD)

Jeśli wykonanie komendy z parametrem –verbose nie wyświetli żadnych informacji, będzie to oznaczać sukces.

Administratorzy wykorzystujący możliwość zwiększania wartości niektórych parametrów poleceniem PROUTIL… INCREASETO mogą teraz wygenerować plik .pf z aktualnymi ustawieniami podczas runtime’u. Daje to możliwość zapamiętywania i porównywania różnych konfiguracji. Plik możemy wygenerować wykorzystując tablice VST lub z poziomu narzędzia PROMON -> R&D Advanced Options -> 4. Administrative Functions -> 17. Generate parameter file

Dużym zainteresowaniem cieszyła się nowa komenda PROUTIL… TABLEREORG wprowadzona w OE 12.7, która pozwala na przeładowanie tablicy i polepszenie stopnia fragmentacji i rozrzucenia rekordów przy pracującej bazie. Podczas ładowania wykorzystuje się sortowanie rekordów wg indeksu. W OE 12.7 musiał to być indeks unikalny, jednakże czasami lepszą wydajność można uzyskać dla indeksu nieunikalnego ale często używanego w aplikacji. W OE 12.8 indeksy mogą już być nieunikalne.

Inne ciekawe i ważne nowości to np. podniesie wersji Spring Security 6.1.4 oraz Tomcata 10.1.15. Ma to swoje implikacje podczas migracji z wcześniejszych wersji PASOE.
W Developer’s Studio dodano typ projektu .NET z narzędziami: Class Browser, Content Assist, Outline View, co ma pomóc w programowaniu przy użyciu .NET 6.

Jest jeszcze sporo innych nowinek po które zapraszam do dokumentacji.

1 2 3 6