Kontrola dostępu do zasobu - ID właściciela jako klucz obcy w encjach

1

Używam Spring Security, JPA i CrudRepository. Chciałbym, żeby każdy użytkownik miał dostęp tylko do swoich obiektów.
Tak wygląda użytkownik:

public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}

Mój plan: do każdej encji dodać informację, kto jest jej właścicielem: userId, który odnosi się do UserEntity.id, a potem w repozytoriach tylko:

@PostFilter("filterObject.userId == principal.userId")
  • chcę mieć klucz obcy userId w każdej encji, żeby zapobiegać sytuacjom typu encja bez właściciela
  • nie chcę mapować obiektu, bo to by wymagało operowania na obiekcie UserEntity, a potrzebuję tylko UserEntity.id. Przeszukałem internety i wygląda na to, że mapowanie samego id nie jest możliwe. Jest na to jakiś sposób?
2

Ja bym się raczej wpinał w JPA: https://www.baeldung.com/database-auditing-jpa

Natomiast pierwszy check robiłbym na poziomie serwisu i agregatu, być może nie musisz dodawać userId do każdej encji.

Znalazłem coś takiego: https://www.baeldung.com/hibernate-5-multitenancy

0
Charles_Ray napisał(a):

Ja bym się raczej wpinał w JPA: https://www.baeldung.com/database-auditing-jpa

@CreatedBy bardzo fajne ;)

Natomiast pierwszy check robiłbym na poziomie serwisu i agregatu, być może nie musisz dodawać userId do każdej encji.

Zastanawiałem się jak to zrobić i pomyślałem, że fajny by było, gdybym nie musiał pakować do serwisów rzeczy związanych z security. Jeżeli chodzi o agregaty - co masz na myśli?
Do wszystkich rzeczywiście nie muszę, jak np. mam A one to many B i w A trzymam userId. O to chodziło? Chociaż teraz, jak wyciągam B, to muszę sprawdzić w A, czy userId się zgadza. Prościej będzie dodać do wszystkich, bo encji jest mało.

Znalazłem coś takiego: https://www.baeldung.com/hibernate-5-multitenancy

Tu bym potrzebował trzeciego sposobu, który nie jest jeszcze wspierany.

Czyli aktualnie:

  • dodawanie userId do encji - za pomocą @CreatedBy
  • filtrowanie danych, do których użytkownik ma dostęp - za pomocą @PostFilter
    Został problem założenia klucza obcego (constraint).

PS: schemat bazy jest generowany automatycznie na podstawie klas encji (spring.jpa.generate-ddl, spring.jpa.hibernate.ddl-auto).

0

Weź pod uwagę, że @CreatedBy służy do audytu, a nie do ochrony dostępu, tzn. zawsze niepowołany user może taką encję zapisać. Ja bym wywalał transakcję przy próbie zapisu lub usunięcia encji (rzucenie wyjątku w @PrePersist, @PreUpdate), natomiast nigdy produkcyjnie tego nie implementowałem - korzystaliśmy z podejścia database per tenant (własna implementacja).

0

@Potat0x: trochę nie rozumiem Twojego problemu
Przecież jak w masz bazie danych powiązanie to możesz łatwo to zrobić w logice biznesowej


public final class CancelOrderUseCase{

//whatever

    ChangeOrderResult cancelOrder(OrderId orderId, UserId userId) {
      Order order =  orderRepository.findById(orderId);
      if (!order.getUserId().equals(userId)) {
         return ChangeOrderResult.NOT_ALLOWED;
      }
   //blablabka
}; 

PS wiem że tak nie powinno się robić anulowania dokładnie, ale chodziło o pokazanie walidacji ;)

0

Przykład który podałeś, to najmniejszy problem. Powiedzmy, że użytkownik chce dodać nowe zamówienie. Zamówienie powinno zawierać informacje, do jakiego użytkownika należy (informacja potrzebna, aby użytkownicy mogli wyświetlać tylko swoje zamówienia).

public final class AddOrderUseCase{
    AddOrderResult addOrder(UserId userId) {
       OrderEntity order = new OrderEntity("bla", "bla", userId); //userId siedzi u mnie w security context
       orderRepository.save(order);
  }
}

Jaki problem jest z tym kodem? userId wewnątrz OrderEntity jest na dziko - nie ma żadnego zabezpieczenia (klucza obcego) w bazie. Teraz, gdy użytkownik zostanie usunięty, to zamówienie będzie "bezpańskie".
Można to rozwiązać, tworząc relację w JPA. Ale wtedy userId użytkownika dodającego zamówienie nie wystarczy. Będę musiał podać do konstruktora zamówienia całą encję użytkownika, którą wcześniej jeszcze trzeba zaciągnąć z bazy:

public final class AddOrderUseCase{
    AddOrderResult addOrder(UserId userId) {
       UserEntity user = userRepository.findById(userId);
       OrderEntity order = new OrderEntity("bla", "bla", user);
       orderRepository.save(order);
  }
}

I to mnie boli.

0
  1. To że jest metoda getUserId nie znaczy że encja JPA nie ma mapowania. Po prostu może to być metoda która pod spodem wywowała getUser :)
    2.No tak będziesz musiał stworzyć powiązanie. Dla mnie byłoby dziwne nie tworzyć... Ale włascwie pod spodem to zadziała tak że jak będzie zwykły insert idka do wiersza :)
0

No tak, po prostu nie podoba mi się konieczność wyciągania całej encji UserEntity, gdy potrzebuję tylko jego ID. Co więcej, to ID mam już "za darmo" z security contextu.

1
  1. Tak jak napisałem w komentarzu, FK jest na bazie danych, a nie w JPA.
  2. Nawet jak zrobisz encje (chociaż nie musisz), to Hibernate pobiera id, żeby moc potem dofetchować resztę obiektu (lazy fetching)

1 użytkowników online, w tym zalogowanych: 0, gości: 1