Jak mogę zaimplementować listę kontroli dostępu w mojej aplikacji Web MVC?

96

Pierwsze pytanie

Proszę, czy możesz mi wyjaśnić, jak najprostszą listę ACL można zaimplementować w MVC.

Oto pierwsze podejście do korzystania z listy ACL w kontrolerze ...

<?php
class MyController extends Controller {

  public function myMethod() {        
    //It is just abstract code
    $acl = new Acl();
    $acl->setController('MyController');
    $acl->setMethod('myMethod');
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...    
  }

}
?>

Jest to bardzo złe podejście, a minusem jest to, że musimy dodać fragment kodu ACL do metody każdego kontrolera, ale nie potrzebujemy żadnych dodatkowych zależności!

Następnym podejściem jest utworzenie wszystkich metod kontrolera privatei dodanie kodu ACL do __callmetody kontrolera .

<?php
class MyController extends Controller {

  private function myMethod() {
    ...
  }

  public function __call($name, $params) {
    //It is just abstract code
    $acl = new Acl();
    $acl->setController(__CLASS__);
    $acl->setMethod($name);
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...   
  }

}
?>

Jest lepszy niż poprzedni kod, ale główne minusy to ...

  • Wszystkie metody kontrolera powinny być prywatne
  • Musimy dodać kod ACL do metody __call każdego kontrolera.

Następnym podejściem jest umieszczenie kodu ACL w kontrolerze nadrzędnym, ale nadal musimy zachować prywatność wszystkich metod kontrolera podrzędnego.

Jakie jest rozwiązanie? A jaka jest najlepsza praktyka? Gdzie powinienem wywołać funkcje ACL, aby zdecydować o zezwoleniu lub zakazie wykonania metody.

Drugie Pytanie

Drugie pytanie dotyczy zdobycia roli za pomocą ACL. Wyobraźmy sobie, że mamy gości, użytkowników i znajomych użytkownika. Użytkownik ma ograniczony dostęp do przeglądania swojego profilu, który mogą wyświetlać tylko znajomi. Wszyscy goście nie mogą wyświetlić profilu tego użytkownika. Oto logika ...

  • musimy upewnić się, że wywoływana metoda to profile
  • musimy wykryć właściciela tego profilu
  • musimy wykryć, czy przeglądający jest właścicielem tego profilu, czy nie
  • musimy przeczytać zasady ograniczeń dotyczące tego profilu
  • musimy zdecydować, czy wykonać, czy nie wykonać metody profilu

Główne pytanie dotyczy wykrycia właściciela profilu. Możemy wykryć, kto jest właścicielem profilu, wykonując tylko metodę modelu $ model-> getOwner (), ale Acl nie ma dostępu do modelu. Jak możemy to zaimplementować?

Mam nadzieję, że moje myśli są jasne. Przepraszam za mój angielski.

Dziękuję Ci.

Kirzilla
źródło
1
Nie rozumiem nawet, dlaczego potrzebujesz „list kontroli dostępu” do interakcji użytkownika. Nie wystarczy powiedzieć coś takiego if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(innego, wyświetlacz „Nie masz dostępu do profilu tego użytkownika” lub coś podobnego, że nie rozumiem?.
Buttle Butkus
2
Zapewne dlatego, że Kirzilla chce zarządzać wszystkimi warunkami dostępu w jednym miejscu - głównie w konfiguracji. Tak więc każdą zmianę uprawnień można wprowadzić w Administratorze zamiast zmieniać kod.
Mariyo

Odpowiedzi:

185

Pierwsza część / odpowiedź (implementacja ACL)

Moim skromnym zdaniem najlepszym sposobem podejścia do tego byłoby użycie wzoru dekoratora. Zasadniczo oznacza to, że bierzesz swój przedmiot i umieszczasz go w innym obiekcie, który będzie działał jak ochronna powłoka. To NIE wymagałoby rozszerzenia oryginalnej klasy. Oto przykład:

class SecureContainer
{

    protected $target = null;
    protected $acl = null;

    public function __construct( $target, $acl )
    {
        $this->target = $target;
        $this->acl = $acl;
    }

    public function __call( $method, $arguments )
    {
        if ( 
             method_exists( $this->target, $method )
          && $this->acl->isAllowed( get_class($this->target), $method )
        ){
            return call_user_func_array( 
                array( $this->target, $method ),
                $arguments
            );
        }
    }

}

A tak wyglądałoby użycie tego rodzaju struktury:

// assuming that you have two objects already: $currentUser and $controller
$acl = new AccessControlList( $currentUser );

$controller = new SecureContainer( $controller, $acl );
// you can execute all the methods you had in previous controller 
// only now they will be checked against ACL
$controller->actionIndex();

Jak możesz zauważyć, to rozwiązanie ma kilka zalet:

  1. zawieranie może być używane na dowolnym obiekcie, a nie tylko na wystąpieniach Controller
  2. sprawdzenie autoryzacji odbywa się poza obiektem docelowym, co oznacza, że:
    • oryginalny obiekt nie odpowiada za kontrolę dostępu, jest zgodny z SRP
    • kiedy otrzymujesz „odmowę uprawnień”, nie jesteś zamknięty w kontrolerze, więcej opcji
  3. możesz to wstrzyknąć zabezpieczoną instancję do dowolnego innego obiektu, zachowa ochronę
  4. zawiń i zapomnij ... możesz udawać , że to oryginalny obiekt, zareaguje tak samo

Ale jest też jeden poważny problem z tą metodą - nie można natywnie sprawdzić, czy zabezpieczony obiekt implementuje i interfejs (co dotyczy również wyszukiwania istniejących metod) lub jest częścią jakiegoś łańcucha dziedziczenia.

Część druga / odpowiedź (RBAC dla obiektów)

W tym przypadku główną różnicą, którą powinieneś zauważyć, jest to, że same obiekty domeny (na przykład Profile:) zawierają szczegółowe informacje o właścicielu. Oznacza to, że abyś mógł sprawdzić, czy (i na jakim poziomie) użytkownik ma do niego dostęp, będzie wymagał zmiany tej linii:

$this->acl->isAllowed( get_class($this->target), $method )

Zasadniczo masz dwie opcje:

  • Podaj listę ACL z odpowiednim obiektem. Ale musisz uważać, aby nie naruszyć prawa Demeter :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Poproś o wszystkie istotne szczegóły i podaj ACL tylko to, czego potrzebuje, co sprawi, że będzie nieco bardziej przyjazny dla testów jednostkowych:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )

Kilka filmów, które mogą pomóc w wymyśleniu własnej implementacji:

Dodatkowe uwagi

Wydaje się, że masz dość powszechne (i całkowicie błędne) zrozumienie tego, czym jest Model w MVC. Model nie jest klasą . Jeśli masz nazwę klasy FooBarModellub coś, co dziedziczy AbstractModel, to robisz to źle.

We właściwym MVC Model jest warstwą zawierającą bardzo dużo klas. Dużą część zajęć można podzielić na dwie grupy ze względu na odpowiedzialność:

- Logika biznesowa domeny

( czytaj więcej : tutaj i tutaj ):

Instancje z tej grupy zajęć zajmują się obliczaniem wartości, sprawdzaniem różnych warunków, implementacją reguł sprzedażowych i całą resztą, którą nazwałbyś „logiką biznesową”. Nie mają pojęcia, w jaki sposób dane są przechowywane, gdzie są przechowywane, a nawet czy istnieje możliwość przechowywania na pierwszym miejscu.

Obiekt biznesowy domeny nie zależy od bazy danych. Podczas tworzenia faktury nie ma znaczenia, skąd pochodzą dane. Może pochodzić z SQL lub ze zdalnego interfejsu API REST, a nawet zrzut ekranu dokumentu MSWord. Logika biznesowa nie zmienia się.

- Dostęp do danych i ich przechowywanie

Instancje utworzone z tej grupy klas są czasami nazywane obiektami dostępu do danych. Zwykle struktury implementujące Data Mapper wzorzec (nie mylić z ORMami o tej samej nazwie .. brak relacji). To jest miejsce, w którym znajdowałyby się Twoje instrukcje SQL (lub może Twój DomDocument, ponieważ przechowujesz je w XML).

Oprócz dwóch głównych części istnieje jeszcze jedna grupa instancji / klas, o której należy wspomnieć:

- Usługi

Tutaj do gry wkraczają Twoje komponenty i komponenty innych firm. Na przykład możesz pomyśleć o „uwierzytelnianiu” jako o usłudze, którą można zapewnić samodzielnie lub za pomocą zewnętrznego kodu. Również „nadawca poczty” byłby usługą, która mogłaby łączyć jakiś obiekt domeny z PHPMailer lub SwiftMailer lub z twoim własnym komponentem wysyłającym pocztę.

Innym źródłem usług są abstrakcje w domenach i warstwach dostępu do danych. Tworzone są w celu uproszczenia kodu używanego przez kontrolery. Na przykład: utworzenie nowego konta użytkownika może wymagać pracy z kilkoma obiektami domeny i programami mapującymi . Ale korzystając z usługi, będzie potrzebować tylko jednej lub dwóch linii w kontrolerze.

Przy wykonywaniu usług należy pamiętać o tym, że cała warstwa ma być cienka . W usługach nie ma logiki biznesowej. Są tam tylko po to, aby żonglować obiektami domeny, komponentami i mapowaniem.

Jedną z ich cech wspólnych jest to, że usługi nie wpływają bezpośrednio na warstwę View i są autonomiczne do tego stopnia, że ​​mogą być (i często są przerywane) używane poza samą strukturą MVC. Również takie samowystarczalne struktury znacznie ułatwiają migrację do innego frameworka / architektury ze względu na wyjątkowo niskie sprzężenie między usługą a resztą aplikacji.

tereško
źródło
34
Czytając to ponownie, nauczyłem się więcej w ciągu 5 minut niż w ciągu miesięcy. Czy zgodziłbyś się z: cienkimi kontrolerami wysyłającymi do usług, które zbierają dane do przeglądania? Ponadto, jeśli kiedykolwiek przyjmiesz pytania bezpośrednio, wyślij mi wiadomość.
Stephane
2
Częściowo się zgadzam. Zbieranie danych z widoku odbywa się poza triadą MVC, podczas inicjalizacji Requestinstancji (lub jej odpowiednika). Administrator jedynie pobiera dane z Requestinstancji i większość z nich przekazuje do odpowiednich serwisów (część z nich też jest przeglądana). Usługi wykonują operacje, które im nakazałeś. Następnie, gdy widok generuje odpowiedź, żąda danych od usług i na podstawie tych informacji generuje odpowiedź. Wspomnianą odpowiedzią może być HTML utworzony z wielu szablonów lub tylko nagłówek lokalizacji HTTP. Zależy od stanu ustawionego przez kontroler.
tereško
4
Aby użyć uproszczonego wyjaśnienia: kontroler „zapisuje” w modelu i przegląda, przeglądaj „odczytuje” z modelu. Warstwa modelu to pasywna struktura we wszystkich wzorcach związanych z siecią, które zostały zainspirowane MVC.
tereško
@Stephane, jeśli chodzi o bezpośrednie zadawanie pytań, zawsze możesz wysłać mi wiadomość na Twitterze. A może pytałeś o coś w rodzaju „długiej formy”, której nie można wcisnąć w 140 znaków?
tereško
Czyta z modelu: czy to oznacza jakąś aktywną rolę dla modelu? Nigdy wcześniej tego nie słyszałem. Jeśli wolisz, zawsze mogę wysłać link przez Twittera. Jak widać, te odpowiedzi szybko zamieniają się w rozmowy, a ja starałem się szanować tę stronę i Twoich obserwujących na Twitterze.
Stephane
16

ACL i kontrolery

Po pierwsze: są to najczęściej różne rzeczy / warstwy. Kiedy krytykujesz przykładowy kod kontrolera, łączy on oba razem - najwyraźniej zbyt ciasno.

tereško przedstawił już sposób, w jaki można to bardziej oddzielić od wzoru dekoratora.

Cofnąłbym się najpierw o krok wstecz, aby znaleźć pierwotny problem, z którym się borykasz, i trochę go przedyskutować.

Z jednej strony chcesz mieć kontrolery, które po prostu wykonują pracę, do której są polecane (polecenie lub akcja, nazwijmy to polecenie).

Z drugiej strony chcesz mieć możliwość umieszczenia listy ACL w swojej aplikacji. Przedmiotem tych list ACL powinno być - jeśli dobrze zrozumiałem twoje pytanie - kontrolowanie dostępu do niektórych poleceń twoich aplikacji.

Ten rodzaj kontroli dostępu wymaga zatem czegoś innego, co łączy te dwa elementy. Na podstawie kontekstu, w którym polecenie jest wykonywane, rozpoczyna się lista ACL i należy podjąć decyzję, czy określone polecenie może zostać wykonane przez określony podmiot (np. Użytkownika).

Podsumujmy w tym miejscu, co mamy:

  • Komenda
  • ACL
  • Użytkownik

Komponent ACL jest tutaj centralny: musi wiedzieć przynajmniej coś o poleceniu (aby precyzyjnie zidentyfikować polecenie) i musi być w stanie zidentyfikować użytkownika. Użytkownicy są zwykle łatwo identyfikowani za pomocą unikalnego identyfikatora. Ale często w aplikacjach internetowych są użytkownicy, którzy nie są w ogóle identyfikowani, często nazywani gośćmi, anonimowymi, wszyscy itd. W tym przykładzie zakładamy, że lista ACL może zużywać obiekt użytkownika i hermetyzować te szczegóły. Obiekt użytkownika jest powiązany z obiektem żądania aplikacji i lista ACL może go używać.

A co z identyfikacją polecenia? Twoja interpretacja wzorca MVC sugeruje, że polecenie jest złożone z nazwy klasy i nazwy metody. Jeśli przyjrzymy się bliżej, dla polecenia są nawet argumenty (parametry). Więc ważne jest, aby zapytać, co dokładnie identyfikuje polecenie? Nazwa klasy, nazwa metody, liczba lub nazwy argumentów, a nawet dane zawarte w którymkolwiek z argumentów lub połączenie tego wszystkiego?

W zależności od poziomu szczegółowości potrzebnego do zidentyfikowania polecenia w liście ACL, może się to znacznie różnić. Na przykład zachowajmy to po prostu i określmy, że polecenie jest identyfikowane przez nazwę klasy i nazwę metody.

Zatem kontekst tego, w jaki sposób te trzy części (ACL, Polecenie i Użytkownik) należą do siebie nawzajem, jest teraz bardziej jasny.

Można powiedzieć, że z wyimaginowanym komponentem ACL możemy już wykonać następujące czynności:

$acl->commandAllowedForUser($command, $user);

Zobacz tylko, co się dzieje: dzięki umożliwieniu identyfikacji zarówno polecenia, jak i użytkownika, lista ACL może wykonać swoją pracę. Zadanie listy ACL nie jest związane z pracą obiektu użytkownika i konkretną komendą.

Brakuje tylko jednej części, to nie może żyć w powietrzu. I tak nie jest. Musisz więc zlokalizować miejsce, w którym musi zostać uruchomiona kontrola dostępu. Przyjrzyjmy się, co dzieje się w standardowej aplikacji internetowej:

User -> Browser -> Request (HTTP)
   -> Request (Command) -> Action (Command) -> Response (Command) 
   -> Response(HTTP) -> Browser -> User

Wiemy, że aby zlokalizować to miejsce, musi ono nastąpić przed wykonaniem konkretnego polecenia, więc możemy zredukować tę listę i wystarczy spojrzeć na następujące (potencjalne) miejsca:

User -> Browser -> Request (HTTP)
   -> Request (Command)

W pewnym momencie aplikacji wiesz, że określony użytkownik zażądał wykonania konkretnego polecenia. Wykonujesz już tutaj pewnego rodzaju listę ACL: Jeśli użytkownik zażąda polecenia, które nie istnieje, nie zezwalasz na jego wykonanie. Zatem gdziekolwiek zdarzy się to w Twojej aplikacji, może być dobrym miejscem na dodanie „prawdziwych” kontroli ACL:

Polecenie zostało zlokalizowane i możemy go zidentyfikować, aby lista ACL mogła sobie z nim poradzić. W przypadku, gdy polecenie nie jest dozwolone dla użytkownika, polecenie nie zostanie wykonane (akcja). Może CommandNotAllowedResponsezamiast CommandNotFoundResponsedla przypadku, żądanie nie może zostać przekształcone w konkretne polecenie.

Często nazywane jest miejsce, w którym mapowanie konkretnego żądania HTTPRequest jest mapowane na polecenie routingiem . Ponieważ Routing ma już zadanie zlokalizowania polecenia, dlaczego nie rozszerzyć go, aby sprawdzić, czy polecenie jest rzeczywiście dozwolone na liście ACL? Na przykład poprzez rozszerzenie Router do ACL świadomy routera: RouterACL. Jeśli twój router jeszcze nie zna User, Routerto nie jest to właściwe miejsce, ponieważ aby ACL działało nie tylko polecenie, ale także użytkownik musi być zidentyfikowany. Więc to miejsce może się różnić, ale jestem pewien, że możesz łatwo zlokalizować miejsce, które chcesz rozszerzyć, ponieważ jest to miejsce, które spełnia wymagania użytkownika i polecenia:

User -> Browser -> Request (HTTP)
   -> Request (Command)

Użytkownik jest dostępny od początku, najpierw polecenie z Request(Command).

Więc zamiast umieszczać kontrole ACL wewnątrz konkretnej implementacji każdego polecenia, umieszczasz je przed nim. Nie potrzebujesz żadnych ciężkich wzorców, magii lub czegokolwiek, ACL wykonuje swoją pracę, użytkownik wykonuje swoją pracę, a zwłaszcza polecenie wykonuje swoją pracę: tylko polecenie, nic więcej. Komenda nie ma interesu, aby wiedzieć, czy mają do niej zastosowanie role, czy jest gdzieś strzeżona, czy nie.

Więc po prostu oddzielaj rzeczy, które do siebie nie należą. Użyj nieznacznego przeformułowania zasady pojedynczej odpowiedzialności (SRP) : Powinien być tylko jeden powód do zmiany polecenia - ponieważ polecenie się zmieniło. Nie dlatego, że teraz wprowadzasz ACL do swojej aplikacji. Nie dlatego, że zmienisz obiekt użytkownika. Nie dlatego, że przeprowadzasz migrację z interfejsu HTTP / HTML do interfejsu SOAP lub wiersza poleceń.

ACL w twoim przypadku kontroluje dostęp do polecenia, a nie samo polecenie.

hakre
źródło
Dwa pytania: CommandNotFoundResponse i CommandNotAllowedResponse: czy przekazałbyś je z klasy ACL do routera lub kontrolera i spodziewałbyś się uniwersalnej odpowiedzi? 2: Gdybyś chciał uwzględnić metodę + atrybuty, jak byś sobie z tym poradził?
Stephane
1: Odpowiedź jest odpowiedzią, tutaj nie pochodzi z listy ACL, ale z routera, ACL pomaga routerowi znaleźć typ odpowiedzi (nie znaleziono, zwłaszcza: zabronione). 2: Zależy. Jeśli masz na myśli atrybuty jako parametry z akcji i potrzebujesz ACL z parametrami, umieść je pod ACL.
hakre
13

Jedną z możliwości jest zawinięcie wszystkich kontrolerów w inną klasę, która rozszerza kontroler i delegowanie wszystkich wywołań funkcji do opakowanej instancji po sprawdzeniu autoryzacji.

Możesz również zrobić to bardziej na początku, w dyspozytorze (jeśli twoja aplikacja rzeczywiście go ma) i wyszukać uprawnienia na podstawie adresów URL, zamiast metod kontrolnych.

edycja : czy potrzebujesz dostępu do bazy danych, serwera LDAP itp. jest ortogonalne do pytania. Chodziło mi o to, że można zaimplementować autoryzację na podstawie adresów URL zamiast metod kontrolera. Jest to bardziej niezawodne, ponieważ zazwyczaj nie będziesz zmieniać swoich adresów URL (adresy URL są rodzajem interfejsu publicznego), ale równie dobrze możesz zmienić implementacje kontrolerów.

Zwykle masz jeden lub kilka plików konfiguracyjnych, w których mapujesz określone wzorce adresów URL na określone metody uwierzytelniania i dyrektywy autoryzacji. Dyspozytor przed wysłaniem żądania do kontrolerów ustala, czy użytkownik jest uprawniony, a jeśli nie, przerywa wysyłkę.

Artefacto
źródło
Czy mógłbyś zaktualizować swoją odpowiedź i dodać więcej szczegółów na temat Dyspozytora. Mam dyspozytora - wykrywa przez URL jaką metodę kontrolera mam wywołać. Ale nie mogę zrozumieć, jak mogę uzyskać rolę (potrzebuję dostępu do bazy danych, aby to zrobić) w Dispatcher. Do usłyszenia wkrótce.
Kirzilla,
Aha, masz pomysł. Powinienem zdecydować, czy zezwolić na wykonanie, czy nie, bez dostępu do metody! Kciuki w górę! Ostatnie nierozwiązane pytanie - jak uzyskać dostęp do modelu z ACL. Jakieś pomysły?
Kirzilla,
@Kirzilla Mam te same problemy z kontrolerami. Wygląda na to, że zależności muszą gdzieś tam być. Nawet jeśli lista ACL nie jest, co z warstwą modelu? Jak możesz temu zapobiec?
Stephane