Bezpieczeństwo w PASOE cz. IV – LDAP, domeny i CP

W poprzednim artykule wyszliśmy poza technologię Progress Software i dzięki Apache Directory Studio przeprowadziliśmy uwierzytelnienie użytkowników korzystających z restowej aplikacji PASOE.

Pora wykonać następny krok (naprzód!) i uwzględnić w całym procesie token – obiekt CLIENT-PRINCIPAL. Obiekt ten został wprowadzony w OE10, wraz z pojawieniem się technologii Auditing. Przypomnę, że chodziło o rejestrowanie zmian w systemie bazodanowym i niezbędnym było posiadanie informacji, kto te zmiany wprowadza. Domyślnie, auditing korzysta z wartości user-id użytkownika, który jest zdefiniowany w bazie.

Jednakże w kontekście aplikacji bardziej odpowiedni jest identyfikator użytkownika sesji OpenEdge. Dzięki takiemu identyfikatorowi – tokenowi, aplikacja nie musi sprawdzać za każdym razem gdy jest to konieczne, kto jest aktualnie zalogowany. Wszystkie potrzebne informacje są bezpiecznie przechowywane w obiekcie sesji CLIENT-PRINCIPAL. Obiekt ten może zawierać nie tylko dane użytkownika, ale dodatkowe właściwości jak np. role potrzebne w procesie autoryzacji.

CLIENT-PRINCIPAL (CP) jest szczególnie przydatny w aplikacjach rozproszonych (AppServer, PASOE) ponieważ może zarządzać kontrolą dostępu dla wielu procesów aplikacji.

Nie będę omawiał wszystkich szczegółów związanych z CP, gdyż jest to temat zbyt obszerny. Bardzo ważne jest to, że do “zapieczętowania” (seal) obiektu i późniejszego jego użycia w sesji, użytkownik musi być zdefiniowany w tzw. domenie i musi być znany klucz (Access Code) do domeny. Czyli: definiujemy domeny w bazie (nazwa domeny, klucz dostępu), a następnie definiujemy użytkownika w danej domenie. OK, to powinno nam na razie wystarczyć. Na poniższym rysunku widać narzędzie Database Administration i okno do definiowania domen ( opcja: Admin -> Security -> Domain Maintenance -> Domains…).

Teraz uwaga! użytkownicy nie muszą znajdować się w bazie, mogą być zdefiniowani w systemie LDAP, co zaraz pokażę. Definicje domen w bazie będą jednak potrzebne. Później wyjaśnię dlaczego.

Teraz jest dobry moment żeby podłączyć PASOE do serwera bazy danych.

Proszę zwrócić uwagę na dwie domeny: adminusers i webusers (opis: LDAP), które zdefiniowałem na potrzeby tego artykułu. Kodem dostępu do domen jest odpowiednio: adminpa, webpa.
Tworzymy prosty tekstowy plik zawierający pary wartości: nazwa domeny, kod dostępu. Zobaczmy taki przykładowy plik ldapasreg.csv.

Posłuży on jako parametr wejściowy do utworzenia zaszyfrowanego pliku rejestru, który może służyć do zapieczętowania tokena. W oknie znakowym podajemy komendę:
gendomreg ldapasreg.csv ldapasreg.bin.
Wygenerowany plik ldapasreg.bin kopiujemy do podkatalogu PASOE conf.

Teraz zabieramy się za modyfikację struktury w Apache Directory Studio. Po uruchomieniu instancji serwera i otwarciu połączenia dodajemy dwie grupy: OED:adminusers, OED:webusers. Jak łatwo się domyślić, to są nasze domeny. W LDAP nie ma obiektów typu domena, więc definiujemy grupy, a żeby odróżnić je od zwykłych grup dodajmy np. przedrostek. Użyłem przedrostka OED:, ponieważ oznacza OpenEdge Domain, ale można użyć dowolnego jaki nam pasuje.


W grupie OED:adminusers dodałem użytkownika Piotr Adminowy (czyli admin1), a w domenie OED:webusers użytkownika Marek Webowy (user1). Tym samym należą oni do dwóch grup (drugą jest PSCUser).

Przechodzimy do pliku: [working directory]\[instancja pasoe]\webapps\ROOT\WEB-INF\oeablSecurity.properties i dodajemy ustawienia:

## OEClientPrincipalFilter.registryFile=ABLDomainRegistry.keystore
OEClientPrincipalFilter.registryFile=ldapasreg.bin
...
 ## Used to obtain an OE Domain name from the authenticated user's list of
 ## roles if the user did not supply one (overrides the default domain property)
 OEClientPrincipalFilter.domainRoleFilter=ROLE_OED:(.*)

Pierwszy parametr określa binarny plik, który utworzyliśmy wcześniej, drugi to prefix określający domenę. Z poprzedniego artykułu wiemy, że dla określenie roli, do nazwy grupy użytkownika zostanie dodany prefix ROLE_ (parametr ldap.authpopulator.rolePrefix). Natomiast dla określenia domeny, zostanie zidentyfikowany ciąg znaków ROLE_OED: i wszystko co nastąpi po nim, to nazwa domeny. Czyli dla użytkownika admin1 będzie to adminusers, a dla user1 webusers.
Ponieważ w pliku binarnym istnieje taka domena i kod dostępu, uwierzytelnienie powinno się udać.

Zanim przejdę dalej trzeba odpowiedzieć na jedno ważne pytanie: dlaczego informacje o domenach znajdują się w trzech miejscach: bazie danych, pliku bin, serwerze LDAP? Aby to zrozumieć musimy przypomniec sobie, że PASOE to połączenie dwóch technologii: ABL i Java (Tomcat). W Spring Security (Java) tworzony jest token, który Tomcat może zapieczętować dzięki informacjom w pliku bin. Token ten jest transformowany do CLIENT-PRINCIPAL ABL, dzięki czemu możemy korzystać z metod ABL aby np. modyfikować role, zarządzać stanem CP itd. CP nie korzysta z pliku bin, dane o domenach pobiera z bazy. Tutaj w bazie nie trzeba definiować użytkowników w domenach, są oni zdefiniowani tylko na serwerze LDAP.

Ponieważ możemy teraz korzystać z obiektu CP, dodajmy dwie procedury w konfiguracji naszej instancji PASOE, dla otwarcia sesji i rozpoczęciu żądania. Całe procedury można pobrać tutaj.


Pierwsza procedura wczytuje parametry domen z bazy.
cpStartup.p

// Loading domain registry for the session
m_lOK = SECURITY-POLICY:LOAD-DOMAINS(1).

pcActivate.p weryfikuje poprawność zapieczętowania CP i wydruk jego atrybutów do logu.
cpActivate.p

// Verify if the Client-Principal is sealed successfully or not. If not, return error.
IF lOK THEN 
DO:
    // Dump the Client-Principal attributes
   
    MESSAGE "    ID:         '" + hCP:QUALIFIED-USER-ID + "'".
    MESSAGE "    session-id:" hCP:SESSION-ID.
    MESSAGE "    state:" hCP:LOGIN-STATE.
    MESSAGE "    roles: " + hCP:ROLES.
    MESSAGE "     domain: " + hCP:DOMAIN-NAME.
    
    cList = hCP:LIST-PROPERTY-NAMES.
    iListSize = NUM-ENTRIES(cList, ",").
    IF ( 0 < iListSize ) THEN 
    DO iListPos = 1 TO iListSize:
        DEFINE VARIABLE cProp AS CHARACTER NO-UNDO.
        DEFINE VARIABLE cVal  AS CHARACTER NO-UNDO.

        MESSAGE "    properties:".
        cProp = ENTRY(iListPos, cList, ",").
        cVal = hCP:GET-PROPERTY(cProp).
        MESSAGE "          property:" cProp ", value:" cVal.
    END.
    
    // Set the Client-Principal as client to connect with DB.
    SET-DB-CLIENT(hCP).
    
END.

Restartujemy teraz PASOE i wywołujemy ekran logowania: http://localhost:[port]/rest/_oepingService/_oeping
Podajemy dane: user1, user1. Dane są uwierzytelnione, ale najciekawsze informacje znajdujemy w logu agenta ldapas.agent.[data].log
Po pierwsze mamy wpis o prawidłowym wczytaniu domen z pliku cpStartup.p

Po drugie możemy odczytać parametry tokena CLIENT-PRINCIPAL wygenerowane przez plik cpActivate.p.

Widać, że pełne id użytkownika zostało uzupełnione o nazwę domeny a także, że użytkownik dostał nową rolę.
To chyba jest najbardziej zaawansowany z artykułów, które do tej pory Wam przedstawiłem. Jeśli ktoś przećwiczy cały proces – gratuluję!
W razie pytań piszcie, jak zwykle, na PUG Poland.

Bezpieczeństwo w PASOE cz. III – LDAP

W tym artykule powracamy do tematu związanego z bezpieczeństwem w PASOE, które było omawiane ok. pół roku temu. Dla przypomnienia: część I oraz część II. Warto przypomnieć sobie te wiadomości bo będziemy z nich zaraz korzystać.

Spring Security to elastyczny framework, w którym administrator może wybrać poziom  zabezpieczeń w zależności od wymagań związanych z aplikacją. Pokażę jak przeprowadzić uwierzytelnienie użytkowników, którzy nie znajdują się w bazie, nie są zdefiniowani w plikach tekstowych, ale w zewnętrznym systemie LDAP, na przykładzie Apache Directory.

Instalujemy Apache Directory Studio, które zawiera serwer LDAP oraz środowisko oparte na Eclipse, do stworzenia hierarchicznej struktury, zawierającej m.in. konta użytkowników.

Na temat tworzenia takiej struktury w ADS czy innym systemie LDAP (Active Directory, OpenLDAP, itp) można znaleźć wiele informacji w sieci. Bardzo przydatne na początku są filmiki instruktażowe na youtube. Ja omówię ten proces bardzo ogólnie, na potrzebę niniejszego artykułu. Po pierwsze dodajemy nowy serwer, nazwę go localhost.


W widoku Connections definiuję połączenie dla tego serwera o nazwie local: Hostname: localhost (lub IP), domyślny port 10389.

Następnie w zakładce Authentication, wybieram Bind DN or user: uid=admin,ou=system. Domyślne hasło: secret.

Uruchamiamy zdefiniowany serwer oraz połączenie i możemy przystąpić do tworzenia schematu. Trzeba pamiętać, że będzie on dostępny tylko przy aktywnym połączeniu.
Ten schemat można utworzyć na różne sposoby. Ja zacznę od stworzenia obiektu organizacji.
Pod głównym węzłem dc=example,dc=com dodajemy obiekt typu organization o nazwie o=progress
Pod tym elementem tworzymy dwa obiekty typu organizationalUnit ou=groups oraz ou=users.

Pod users dodajemy przykładowych użytkowników typu inetOrgPerson. Dodałem dwóch użytkowników – nasuwa się pytanie jak ich identyfikować, mamy pola cn i sn… – otóż dla identyfikacji z zewnątrz ważny jest atrybut uid (admin1 i user1) oraz userPassword, taki sam jak nazwa użytkownika. Czyli logując się jako zwykły użytkownik (Marek Webowy) podamy dane: user1, user1.

Po uwierzytelnieniu przychodzi pora na autoryzację. Ta jest realizowana za pomocą ról. Zaglądając do poprzednich artykułów możemy zobaczyć, że w pliku pasoe oeablSecurity.csv są zdefiniowane role ROLE_PSCUser, ROLE_PSCAdmin, ROLE_PSCDebug. Dla demonstracji wykorzystamy tylko jedna rolę i stworzymy jedną grupę PSCUser typu groupOfUniqueNames.

W grupie tej umieszczamy użytkowników, tzn. ich atrybuty cn (Common Name). Pamiętajmy, że pełna nazwa zawiera elementy aż do węzła głównego, czyli np. dla użytkownika user1:
cn=Marek Webowy,ou=users,o=progress,dc=example,dc=com.

OK, zrestartujmy połączenie. Możemy teraz przystąpić do konfiguracji PASOE. Ja tworzę nową instancję o nazwie ldapas bez dostępu do bazy.
Konfigurację tę przeprowadzimy w, omawianym już w poprzednich częściach, pliku [working directory]\[instancja pasoe]\webapps\ROOT\WEB-INF\oeablSecurity.properties.
Wybieramy proces zarządzający uwierzytelnieniem ldap oraz model logowania form.

http.all.authmanager=ldap
client.login.model=form

Następnie dodajemy parametry logowania do serwera ldap oraz filtry wyszukiwania informacji o użytkowniku oraz grupie, do której należy.

ldap.root.dn to definicja węzła, od którego zaczyna się wyszukiwanie informacji w strukturze, ldap.usersearch.base to z kolei węzeł, pod którym znajdują się definicje użytkowników, ldap.usersearch.filter określa zakończenie wyszukiwania po znalezieniu elementu uid.

Podobne parametry definiujemy dla grup. Całość wygląda mniej więcej tak:

 ## Required LDAP Server Authentication Manager configuration properties 
 ldap.url=ldap://localhost:10389
 ldap.manager-dn=uid=admin,ou=system
 ldap.manager-password=secret
 
 ldap.root.dn=o=progress,dc=example,dc=com

 ldap.usersearch.base=ou=users  
 ldap.usersearch.searchSubtree=true
 ldap.usersearch.filter=(uid={0})
 
 ldap.grouprole.attribute=cn
 ldap.groupsearch.base=ou=groups
 ldap.groupsearch.searchSubtree=true
 ldap.groupsearch.filter=(uniqueMember={0})

 ldap.authpopulator.rolePrefix=ROLE_

Podawanie hasła w postaci jawnej (secret), to niezbyt dobry pomysł. Można to wyeliminować kodując hasło przy pomocy komendy: stspwdutil encrypt [hasło].

Ostatni parametr (wartość domyślna ROLE_) określa prefix dla roli jaki będzie dodany do nazwy grupy, czyli dla PSCUser pełna nazwa będzie ROLE_PSCUser.

Według konwencji nazewnictwa w LDAP, nazwy ról mają być pisane dużymi literami. W LDAP są one zamieniane z automatu, my musimy zmienić je w pliku oeablSecurity.csv, np. z ROLE_PSCUser na ROLE_PSCUSER itd.

Restartujemy PASOE. Wchodzimy do linku testującego:
http://localhost:[port]/rest/_oepingService/_oeping
Podajemy dane: user1, user1.

Mamy dostęp do serwisu.

OK, ale zazwyczaj nie ma tak dobrze. Często zanim wszystko skonfigurujemy pojawia się komunikat o braku dostępu. W logach nic nie ma, gdzie szukać informacji? Musimy odblokować ilość informacji zapisywanych do logów. W pliku [working directory]\[instancja pasoe]\conf\logging-pasoe.properties zmieniamy poziom zapisów z WARN na DEBUG, a ERROR na INFO, jak poniżej. Konfigurację zapisów do logów możemy zrobić także z poziomu danej aplikacji, ale tutaj poniższa zmiana wystarczy. Uwaga! ta zmiana powoduje znaczne zwiększenie tekstu w logach.

W pliku ldapas_authn.[data].log (przypominam, że moja instancja nazywa się ldapas) znajduje się informacja o udanym uwierzytelnieniu. Informacje o nieudanych logowaniach znajdują się w pliku ldapas_authz.[data].log

[data i czas] ldapas ROOT:f:0000000a success user1

W logu ldapas.[data].log znajduja się m.in. wpisy:

...LdapAuthenticationProvider - Processing authentication request for user: user1...
...FilterBasedLdapUserSearch - Searching for user 'user1', with user search [ searchFilter: '(uid={0})', searchBase: 'ou=users  '...
...Searching for entry under DN 'o=progress,dc=example,dc=com', base = 'ou=users', filter = '(uid={0})'... 
...Found DN: cn=Marek Webowy,ou=users...
......
...Searching for roles for user 'user1', DN = 'cn=Marek Webowy,ou=users,o=progress,dc=example,dc=com', with filter (uniqueMember={0}) in search base 'ou=groups'...
......
...Granted Authorities: ROLE_PSCUSER;...

Wpisów jest wiele, ale można prześledzić proces wyszukiwania oraz uwierzytelniania i autoryzacji.

Następnym razem chciałbym pokazać jak PASOE korzysta z obiektu CLIENT-PRINCIPAL przy autoryzacji LDAP.