Ostatnimi czasy postanowiłem sobie, że stworzę aplikację w oparciu o Quarkusa oraz MongoDB. Tak się zdarzyło, że przy okazji napotkałem ogłoszenie (było już nieaktualne) dotyczące konkursu “Monitorowanie Jednostek Morskich” na blogu Przemka Bykowskiego. Polegał on na pobieraniu danych z AIS i wyświetlaniu na mapie zdobytych informacji na temat jednostek morskich. Uznałem, że jest to dosyć ciekawe zadanie, ale i na tyle proste, aby nauczyć się czegoś nowego.

Rozpoczęcie projektu w nowym frameworku nie jest łatwe, ponieważ nie znamy szczegółowych zasad jego gry. Musiałem na początku poświęcić trochę czasu na zapoznanie się z dokumentacją, aby wiedzieć od czego w ogóle zacząć. Przy okazji muszę przyznać, że łączenie się z zewnętrznym API zostało w Quarkusie naprawdę ciekawie rozwiązane. Może w następnym wpisie przedstawię jak to wygląda na przykładzie tej aplikacji. Jednak przechodząc do głównego wątku, jak w każdym programie musi być jakiś system weryfikowania kto może korzystać z odpowiednich funkcjonalności. Z tego powodu musiałem zagłębić się w rozszerzenie Quarkusa o nazwie quarkus-security-jpa.

Przykład z dokumentacji

Wpadłem na pomysł, że skopiuję dostępne rozwiązanie znajdujące się w samouczku Quarkusa i dostosuję go do swoich potrzeb. Wchodząc na stronę https://quarkus.io/guides/security-jpa otrzymujemy przepis jak krok po kroku zablokować dostęp dla niezautoryzowanych użytkowników do odpowiednich endpointów naszej aplikacji. Oczywiście jak to bywa w takich sytuacjach, przykład nie mógł odwzorowywać jeden do jednego moich wymagań. Opiera się on bowiem na Postgresie. Z tego powodu w pliku POM znajdziemy rozszerzenia quarkus-security-jpa oraz quarkus-jdbc-postgresql.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.acme.elytron.security.jpa;

import javax.persistence.Entity;
import javax.persistence.Table;

import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;

@Entity
@Table(name = "test_user")
@UserDefinition
public class User extends PanacheEntity {
  @Username
  public String username;
  @Password
  public String password;
  @Roles
  public String role;

  /**
   * Adds a new user in the database
   * @param username the user name
   * @param password the unencrypted password (it will be encrypted with bcrypt)
   * @param role the comma-separated roles
   */
  public static void add(String username, String password, String role) {
    User user = new User();
    user.username = username;
    user.password = BcryptUtil.bcryptHash(password);
    user.role = role;
    user.persist();
  }
}

Tak prezentuje się klasa użytkownika dostępna w samouczku. Posiada ona pola odpowiedzialne za przechowywanie informacji o nazwie użytkownika, haśle oraz roli. Mamy też oznaczenie w postaci adnotacji, że jest to encja bazodanowa oraz na jaką tabelę mapuje się ta klasa. Najważniejsza z punktu widzenia tego artykułu jest właśnie adnotacja @UserDefinition. Nigdzie nie znajdziemy informacji do czego ona służy poza Javadoc na Github oraz wspomnianym wcześniej tutorialu.

Indicates that this entity class should be used as a source of identity information. At most one entity can have that annotation in an application. The entity must contain fields or properties annotated with the {@link Username}, {@link Password} and {@link Roles} annotations.

Wychodzi na to, że co najwyżej jedna klasa może być oznaczona taką adnotacją. Musi posiadać ona również wszelkie informacje o użytkowniku, czyli wcześniej wspomnianą nazwę, hasło oraz role. Dodatkowo z oficjalnej strony Quarkusa dowiadujemy się, że ta klasa może być encją Hibernate ORM albo Hibernate ORM oparta o bibliotekę Panache.

Czas na dostosowanie do własnego rozwiązania

W końcu, gdy przykład z tutorialu zadziałał na moim lokalnym środowisku przyszło mi go przenieść na własne rozwiązanie. Stworzyłem, więc identyczną klasę User tylko w oparciu o adnotację odpowiednią dla MongoDB.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.acme.elytron.security.jpa;

import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.mongodb.panache.PanacheMongoEntity;
import io.quarkus.mongodb.panache.common.MongoEntity;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;

@MongoEntity(collection = "user")
@UserDefinition
public class User extends PanacheMongoEntity {
  @Username
  public String username;
  @Password
  public String password;
  @Roles
  public String role;

  /**
   * Adds a new user in the database
   * @param username the user name
   * @param password the unencrypted password (it will be encrypted with bcrypt)
   * @param role the comma-separated roles
   */
  public static void add(String username, String password, String role) {
    User user = new User();
    user.username = username;
    user.password = BcryptUtil.bcryptHash(password);
    user.role = role;
    user.persist();
  }
}

Wydawało mi się, że wszystko wygląda jak należy aż do momentu uruchomienia aplikacji. W konsoli zrobiło się po prostu czerwono!

Problem z uruchomieniem aplikacji opartej o Quarkus

W oczy rzucają się dwa błędy:

  • Unsatisfied dependency for type javax.persistence.EntityManagerFactory and qualifiers [@Default] - java member: io.quarkus.security.jpa.runtime.JpaTrustedIdentityProvider#entityManagerFactory
  • Unsatisfied dependency for type javax.persistence.EntityManagerFactory and qualifiers [@Default] - java member: io.quarkus.security.jpa.runtime.JpaIdentityProvider#entityManagerFactory

Wystarczy tylko zakomentować adnotację @UserDefinition i aplikacja natychmiast wstaje bez żadnych problemów. Nie działa natomiast wtedy autoryzacja, czyli usunięcie tytułowego oznaczenia niekoniecznie jest najlepszym rozwiązaniem. Wpisując napotkany problem w wyszukiwarkę internetową nie znalazłem nic! Dosłownie nic! Żadnego wpisu na StackOverflow.

Nie wiem czy jest to spowodowane tym, że nikt jeszcze takiego problemu nie napotkał czy może jest to wina niepoprawnie użytego narzędzia. Przecież @UserDefinition znajduje się w pakiecie io.quarkus.security.jpa, czyli służy do stosowania dla ORM (Object Rational Mapping). Po długim zastanawianiu się jak obejść ten problem uznałem, że porzucam to rozwiązanie i zastosuję zabezpieczenie wykorzystujące JWT.

Podsumowanie

Daj znać czy podobał Ci się wpis w takiej tematyce, gdzie pokazuję, że droga dewelopera nie zawsze jest usłana różami. Często popełniamy błędy i szukamy rozwiązań pasujących do naszego problemu. Na szczęście najczęściej nikt na tym nie cierpi tylko zostaniemy wyprowadzeni z równowagi oraz stracimy trochę czasu. W tym przypadku mój błąd był ewidentny, bo nie zauważyłem z jakiego pakietu jest zaciągana dana adnotacja. Na swoją obronę powiem, że Quarkus jest dla mnie po prostu czymś nowym. Mam z nim styczność dopiero od kilku dni i każdy dał mi owocną lekcję pokory.