API Platform częśc 2

W pierwszej części artykułu powstała aplikacja, która pozwala w łatwy sposób zarządzać, przedmiotami, lekcjami, oraz fiszkami. Aplikacja jednak wymaga dokończenia. Do zrobienia pozostały następujące elementy: 

  • autentykacja użytkowników,
  • dodawanie własnej logiki do requestów,
  • przypisanie aktualnie zalogowanego użytkownika do tworzonego przedmiotu,
  • ograniczenie dostępu użytkowników tylko do własnych zasobów,
  • rozszerzenie dokumentacji.

Aktualizacja zależności 

Minęło już trochę czasu od powstania pierwszej części artykułu. Sporo elementów zostało poprawionych i dodanych do API platform. Zaktualizujmy więc wykorzystywane biblioteki do nowych wersji:

Composer poinformuje nas o tym że paczka “symfony/lts” została porzucona. Można ją bezpiecznie usunąć poleceniem:

“composer remove symfony/lts”

Autentykacja użytkowników

API Platform umożliwia łatwą autentykację z wykorzystaniem JWT. Aby z niej skorzystać należy zainstalować bibliotekę lexik/jwt-authentication-bundle:


Po instalacji tej biblioteki należy wygenerować klucze, z których będzie korzystała. Robimy to za pomocą polecenia:

Należy zmienić hasło do klucza prywatnego w pliku .env na takie samo jakie zostało podane podczas generowania klucza.
Następnie należy skonfigurować symfony security component tak, aby korzystał z tej biblioteki. Użytkownicy aplikacji powinni być zapisywani w bazie danych. Do tego celu użyjemy z entity user provider. 

Dokładna instrukcja jak skonfigurować entity user provider jest dostępna tutaj: 

https://symfony.com/doc/current/security/entity_provider.html

Konfiguracja może wyglądać następująco:

https://gist.github.com/piotrbrzezina/29ef2ada97052f4da9bf821c2c4b9222

Następnie należy skonfigurować symfony security, aby skorzystać z funkcjonalności, które dostarczyła biblioteka JWT. Konfigurujemy formularz logowania oraz dodajemy główny firewall:

https://gist.github.com/piotrbrzezina/fc272c8c2e7a6e8341ee884124b6a6e4

Należy także stworzyć nowy routing dla logowania, zgodny z tym co jest ustawione w konfiguracji security:

https://gist.github.com/piotrbrzezina/1190633d1e9d1b9d280b53922a7e6e2d


Teoretycznie wszystko powinno działać ale okazuje się, że do poprawnego działania wszystkich elementów niezbędne jest zakodowanie hasła podczas tworzenia nowego użytkownika. Tu z pomocą przychodzi rozbudowany system eventów z API platform.

Event system

Do rozszerzania podstawowej funkcjonalności endpointów można wykorzystać eventy emitowane przez kernel (kernel.request, kernel.view, kernel.response). Api platform korzysta z tych eventów również do obsługi własnych procesów. W celu wykonania dodatkowej operacji przed lub po jakiejś czynności, należy ustawić odpowiedni priorytet dla swojego event listenera. Aby ułatwić poruszanie się po priorytetach API Platform przygotowało kilka stałych, które określają, jaki powinien być priorytet listenera, aby akcja wykonała się w oczekiwanym momencie. Do wyboru są następujące stałe:  PRE_READ, POST_READ, PRE_DESERIALIZE, POST_DESERIALIZE, PRE_VALIDATE, POST_VALIDATE, PRE_WRITE, POST_WRITE, PRE_SERIALIZE, POST_SERIALIZE, PRE_RESPOND, POST_RESPOND. 

Pierwszą czynnością jaką należy wykonać jest ustawienie enkodera, z którego będziemy korzystać przy szyfrowaniu hasła. Aktualnie dokumentacja symfony sugeruje aby korzystać z  enkodera ‘argon2i’. Dodajemy konfigurację enkodera do security.yaml:

https://gist.github.com/piotrbrzezina/8fbb10ac4d384be2e5d7f7f5daf7c610

Należy też poprawić konfigurację samego modelu użytkownika. Pole na zakodowane hasło powinno umożliwiać wpisanie do 255 znaków, a użytkownik powinien mieć przypisaną domyślną grupę. 

https://gist.github.com/piotrbrzezina/ddb12293095aef1b17e16cb0520a6b33

W naszym przypadku przed zapisem nowego użytkownika do bazy danych chcemy zakodować jego hasło. Kodowanie hasła powinno odbyć się również w momencie, gdy użytkownik chce zmienić swoje hasło. W tym celu należy stworzyć własny event subscriber, który nasłuchuje na event ‘kernel.view’ z priorytetem “PRE_WRITE”

https://gist.github.com/piotrbrzezina/c09368f044c9e376a411e31ecf8dad06

Metoda getControllerResult() zwraca aktualnie przetwarzany obiekt. Interesuje nas tylko przypadek, gdy request jest związany z obiektem typu User i gdy ten obiekt ma ustawioną właściwość plainPassword. Gdy obydwa te warunki są spełnione należy zakodować hasło z wykorzystaniem wcześniej stworzonego enkodera. 

Zabezpieczanie endpointów.

Mamy już skonfigurowane security dla naszej aplikacji ale wszystkie endpointy nadal są dostępne dla niezalogowanych użytkowników. Istnieją co najmniej 2 sposoby na rozwiązanie tego problemu. 

Pierwszy sposób to dodanie wpisu do sekcji ‘access_control’ w pliku security.yaml tak aby chronić endpointy przed nieautoryzowanym dostępem. Więcej o tym jak to zrobić można znaleźć na stronie dokumentacji .

Drugim mechanizmem, z którego można skorzystać jest dodanie atrybutu ‘access_control’ do konfiguracji resource-ów. 

Dostęp do Resource Subject powinni mieć tylko użytkownicy, którzy mają rolę “ROLE_USER”

https://gist.github.com/piotrbrzezina/46849d941560ac22a4f56169918d995e/edit

Można też ustawić bardziej skomplikowane zasady udzielania dostępu do zasobów.

Chcemy umożliwić użytkownikowi dostęp tylko do powiązanych z nim zasobów. W tym celu możemy posłużyć się następującym zapisem:

https://gist.github.com/piotrbrzezina/0efc33a736bce5b0470b4fdf458d0cac

Od tej pory metodę PUT na Resource Subject może wykonać tylko użytkownik, który stworzył ten resource.

Extensions

Mamy już prawie rozwiązaną kwestię dostępu do danych, ale pozostał problem wyświetlania na listingach wszystkich danych niezależnie od zalogowane użytkownika.

API Platform dostarcza mechanizm Extension pozwalający na modyfikowanie zapytań do bazy danych. Dzięki tej funkcjonalności jesteśmy w stanie ograniczyć wyniki kolekcji tak aby zawierały zasoby, które są powiązane z zalogowanym użytkownikiem.

Wystarczy zaimplementować interfejs QueryCollectionExtensionInterface

Po dodaniu powyższego kodu przy pobieraniu przedmiotów do zapytania zawsze dodawany będzie warunek ograniczający wyniki wyszukiwania. Powyższą klasę należy rozszerzyć o zabezpieczenie dla klas lekcji i fiszek. Oprócz tego możliwe jest również ograniczanie zapytań dla pojedynczych elementów. Wtedy należy zaimplementować interfejs QueryItemExtensionInterface

Rozszerzanie dokumentacji

Niestey API platform nie stworzy dokumentacji do endpointu umożliwiającego logowanie. Na szczęście istnieje możliwość dodania własnych danych do dokumentacji. Wystarczy udekorować serwis api_platform.swagger.normalizer.documentation. Wewnątrz dekoratora mamy dostęp do tablicy, która zawiera wszystkie elementy dokumentacji. Tablicę możemy dowolnie modyfikować. Dodam do tablicy informację o endpoincie do logowania:  

Od teraz w dokumentacji jest również informacja jak można zalogować się do aplikacji 

Podsumowanie

Nasz aplikacja jest już w pełni funkcjonalna. Można tworzyć nowe przedmioty, lekcje oraz fiszki, a użytkownicy mają dostęp tylko do swoich danych. Nasza praca jako backendowców jest zakończona. Teraz pozostało stworzyć aplikację korzystającą z naszego API. API platform też potrafi pomóc również w tym. Twórcy tej biblioteki przygotowali generatory klientów. Po szczegóły odsyłam do dokumentacji: https://api-platform.com/docs/client-generator/

Poza opisanymi przeze mnie funkcjonalnościami API platform posiada wiele innych ciekawych rozwiązań takich jak CQRS, DTO, integracja z ElasticSearch, MongoDb, GraphQL, panel admina generowany na podstawie JSON-LD itp. Stosując tą bibliotekę można znacząco zmniejszyć czas poświęcany na pracę z powtarzalnymi zadaniami. Można skupić się na bardziej wymagających i ciekawszych zadaniach. 
Aplikacja, która była tworzona podczas pisania tego artykuł dostępna jest tutaj: https://gitlab.com/flashcards_uszanowanko/api_article/commits/part2