Strona na Drupalu otoczona płotem. Dostęp do niej chroniony jest szlabanem

Drupal node grants

Każdy, kto zajmuje się Drupal developmentem, prędzej czy później zetknie się z koniecznością zdefiniowania ściślejszej kontroli dostępu do treści. Standardowe mechanizmy ról i uprawnień są bardzo elastyczne, jednak w skomplikowanych projektach mogą nie wystarczyć. Gdy dostęp do węzłów zaczyna zależeć np. od pól przypisanych do użytkownika – wówczas należy sięgnąć po bardziej zaawansowane środki. Zarówno w Drupalu 7, jak i 8 możemy w tym celu wykorzystać hook hook_node_access() lub mechanizm tzw. grantów.

Kontrola uprawnień przez hook_node_access()

W przypadku zaawansowanej kontroli uprawnień, zdecydowanie najczęstszym wyborem jest hook hook_node_access(). Różni się on delikatnie między wersjami 7 i 8, ale zasada jego działania pozostaje z grubsza ta sama. Przyjmuje on trzy argumenty: $node, $op i $account.

Drupal przy każdej próbie dostępu do węzła wywoła mymodule_node_access() we wszystkich modułach i sprawdzi, czy bieżący użytkownik $account ma niezbędne uprawnienia do wykonania operacji $op na węźle $node. Hook powinien zwrócić jedną z następujących odpowiedzi:

return AccessResult::allowed(); // (lub NODE_ACCESS_ALLOW w wersji 7)
return AccessResult::forbidden(); // (lub NODE_ACCESS_DENY w wersji 7)
return AccessResult::neutral(); // (lub NODE_ACCESS_IGNORE w wersji 7)

Proste i skuteczne, prawda? Tak to wygląda w praktyce (D8):

use Drupal\Core\Access\AccessResult;
use Drupal\node\NodeInterface;
use Drupal\Core\Session\AccountInterface;

function mymodule_node_access(NodeInterface $node, $op, AccountInterface $account) {
  $type = $node->getType();
  if ($type == 'foo' && $op == 'view') {
    if(strstr($account->getEmail(), '@example.com')) {
        return AccessResult::allowed();
    }
    else {
      return  AccessResult::forbidden();
    }
  }
  return AccessResult::neutral();
}

Wydawałoby się, że kontrola dostępu za pośrednictwem hook_node_access() zaspokoi wszystkie zaawansowane potrzeby, jednakże – jak to mawiają – nie ma róży bez kolców. Sprawdzanie po kolei długiej listy węzłów z pewnością odbije się negatywnie na wydajności. Szczególnie moduły Views i Menu nie mogą pozwolić sobie na zbyt częste korzystanie z powyższego hooka. Z powodów optymalizacyjnych twórcy Drupala ograniczyli wywołanie hook_node_access() wyłącznie do węzłów pokazywanych w trybie “Full”. W pozostałych przypadkach należy zdać się na standardowy mechanizm ról i uprawnień lub używać znacznie mniej zasobożernych Access Grantów.

Access Grants

O ile zasadę działania hook_node_access() zrozumieć jest bardzo łatwo, o tyle Access Grants to temat nieco bardziej skomplikowany. Niektórzy autorzy blogów próbują stosować tu analogię do drzwi, zamków i kluczy. Ja jednak uważam, że wymaga to zbyt dużo naciągania rzeczywistości. Dlatego też zastosuję porównanie do tajnej wojskowej bazy.

Baczność! Co ma wspólnego placówka wojskowa z Drupalem? Ano i w jednym i w drugim mamy obszary zastrzeżone, do których dostać się mogą osoby o danym poziomie uprawnień. Co ważniejsze, i w jednym i w drugim użytkownicy otrzymują na wejściu przepustki definiujące zakres tego, co mogą zobaczyć lub zrobić.

W standardowym systemie uprawnień Drupala mamy jednego wartownika, który sprawdza, rolę użytkownika na jego przepustce. Rola zaś definiuje z góry możliwe do wykonania działania (np. odczyt węzła typu A, zapis i odczyt węzła typu B…). 

W hooku mymodule_node_access() mamy wielu wartowników, po jednym dla modułu. Każdy z nich legitymuje użytkownika i na podstawie wybranych kryteriów określa, czy zostanie przepuszczony dalej. Każdy wartownik ma pełny dostęp do danych użytkownika oraz do chronionego obszaru (węzła), na ich podstawie buduje swoją decyzję.

Co robią wspomniane Access Grants? Rozdzielają autoryzację na dwie osobne części, obsługiwane przez dwa hooki. Sposób ich użycia w wersjach Drupala 7 i 8 jest niemal identyczny.

1) hook_node_access_records()

Pierwszy z hooków uruchamiany jest przy zapisywaniu węzła. Posiada jeden argument $node. Zadaniem hooka jest określenie listy tzw. grantów, czyli przepustek koniecznych do okazania przy próbie dostępu do węzła. 

function mymodule_node_access_records(\Drupal\node\NodeInterface $node) {
  $grants = [
    [
      'realm' => 'realm1',
      'gid' => 12345,
      'grant_view' => 1,
      'grant_update' => 1,
      'grant_delete' => 1,
      'priority' => 0,
    ],
  ];
  return $grants;
}

Każdy z grantów ma swoją dziedzinę (realm). Użytkownik próbując dostać się do danego węzła, musi posiadać przynajmniej po jednym grancie (przepustce) z każdej zdefiniowanej dziedziny. Przykładowo, jeśli Drupal zbuduje na podstawie wszystkich znalezionych hooków mymodule_node_access_records() następującą listę grantów:

$grants[] = 
[ 'realm' => 'realm1', 'gid' => 123, … ],
[ 'realm' => 'realm1', 'gid' => 456, … ],
[ 'realm' => 'realm2', 'gid' => 789, … ];

To użytkownik przy dostępie do węzła musi mieć co najmniej jedną przepustkę z dziedziny realm1 oraz jedną przepustkę z dziedziny realm2.

Skomplikowane? Użyję może naszej wojskowej analogii. Załóżmy, że w tajnej bazie znajduje się kilka wydziałów (to będą wspomniane wcześniej dziedziny – realms). Każdy z wydziałów ustawia przy zastrzeżonym obszarze swojego wartownika. Użytkownik żądający dostępu do obszaru podchodzi kolejno do każdego wartownika i okazuje posiadane przepustki (czyli granty). Jeśli wartownik stwierdzi, że jedna z posiadanych przepustek jest zgodna z wymaganiami nałożonymi przez jego wydział – przekazuje użytkownika dalej. Jeśli użytkownik nie posiada pasującej przepustki – nie ma prawa wstępu do obszaru, dalsi wartownicy zaś już go nie sprawdzają.

Dlaczego tak jest szybciej? Bo wartownik nie musi znać szczegółów autoryzacji (ani mieć dostępu do węzła), wystarczy, że porówna rodzaje przepustek.

Czym jest właściwie dziedzina (realm)?

Dziedziną (lub w wojskowej analogii – wydziałem) może być dowolny ciąg znaków, najczęściej stosuje się tu po prostu nazwę modułu. Takie podejście sprawia, że system grantów staje się nieco czytelniejszy – użytkownik musi posiadać po jednym grancie (przepustce) do każdego modułu, który definiuje własne reguły dostępu do węzła poprzez hook_node_access_records(). Pozostań przy tej konwencji jeśli tylko nie jest ona dla Ciebie zbytnim uproszczeniem.

Wszystko albo nic

Przy korzystaniu z mechanizmów autoryzacji czasami istnieje konieczność zastosowania dwóch skrajnych scenariuszy: dostępu dla wszystkich lub tylko dla administratora. W pierwszym przypadku wystarczy zwrócić w hooku pustą tablicę grantów. Będzie to oznaczało, że użytkowników w danej dziedzinie nie obowiązują żadne “przepustki”. W drugim przypadku należy celowo zdefiniować grant, którego nie otrzyma żaden użytkownik.

2) hook_node_grants()

Kto nadaje granty (przepustki) użytkownikom? Otóż jest za to odpowiedzialny hook hook_node_grants(), uruchamiany przy każdej próbie dostępu do węzła. Przyjmuje w argumencie konto użytkownika $account oraz wykonywaną operację $op. Na tej podstawie określa tablicę przyznanych grantów, identyfikowanych po dziedzinie (klucz) i identyfikatorze (wartość).

use Drupal\Core\Session\AccountInterface;

function mymodule_node_grants(AccountInterface $account, $op) {
  if ($op == 'view') {
    $grants['realm1'] = array(123);
    return $grants;
  }
}

Zauważ, że powyższy hook nie ma dostępu do obiektu $node. To właśnie dlatego mechanizm grantów jest o wiele szybszy. Nie wymaga bowiem każdorazowego ładowania pełnego węzła. Dzięki temu zdefiniowane przez Ciebie granty mogą być brane pod uwagę przy budowie widoków oraz podczas generowania menu.

Reset uprawnień

Zaczynając pracę z mechanizmem grantów możesz napotkać kilka niespodziewanych problemów. Jeśli autoryzacja działa błędnie, w pierwszej kolejności spróbuj przebudować tabelę node_access w bazie danych Drupala. Dokonasz tego przy pomocy funkcji node_access_rebuild() lub odwiedzając adres /admin/reports/status/rebuild (link odnajdziesz w Raporcie o stanie witryny w panelu administracyjnym).

Jeśli po przebudowie granty nadal nie działają w zamierzony sposób – przejrzyj tabelę node_access i upewnij się, że nie zawiera ona wiersza z wartościami nid=0, gid=0, realm=all. Wpis ten dodawany jest domyślnie podczas instalacji Drupala i powoduje wyłączenie systemu grantów. Obecność tego wpisu nie wynika z żadnego błędu – bez niego dostęp do treści na całej stronie byłby przyznany domyślnie wyłącznie dla administratora.

Rodzaje operacji

Zapewne nie umknął Twojej uwadze fakt, że przy opisie grantów pominąłem kwestię rodzaju operacji dokonywanej na węźle. O tym jakich operacji może dokonać użytkownik (odczytu/zapisu/usunięcia), możesz zdecydować w obu przedstawionych powyżej hookach (spójrz tylko na przykłady). Pamiętaj: jeśli do danej dziedziny pasować będzie kilka grantów – zawsze zostanie wybrany ten z najwyższym poziomem dostępu.

Podsumowanie

Na koniec artykułu przypomnę Ci, że wszystkie mechanizmy uprawnień w Drupalu działają niejako równolegle do siebie. Możesz więc dowolnie mieszać ze sobą wszystkie metody autoryzacji dostępu do treści. Warto wspomnieć, że na stronie drupal.org trwają dyskusje nad przekuciem grantów w coś łatwiejszego do zrozumienia i bardziej pasującego do podejścia obiektowego. Trudno jednak spodziewać się w najbliższym czasie rewolucyjnych zmian.
 
Odmaszerować :-)!
 

3. Najlepsze praktyki zespołów programistycznych