W ostatnich wpisach z tej serii skupialiśmy się na zarządzaniu uprawnieniami pewnych grup użytkowników naszej aplikacji. Obszary dostępu to nic innego jak podział na dwie sztywne grupy posiadające [lub nie] dostępu do pewnych zabezpieczonych elementów systemu, zaś poziomy to tylko wykorzystanie abstrakcji do złożenia w jednym miejscu wielu obszarów. Opisując te sposoby “celowo zapominałem” o tym, że nie zawsze da się podzielić użytkowników na odpowiednie grupy, ponadto nie zawsze nadane uprawnienia muszą być sztywne - czasem chcemy np. dać dodatkowo jednemu zaufanemu użytkownikowi dostęp do statystyk witryny, albo przeglądania artykułów - wtedy musielibyśmy stworzyć albo oddzielny obszar albo nowy poziom, który notabene zburzyłby dotychczas istniejącą strukturę. Co zrobić z takim problemem?

Wstęp.

Oczywiście potrzebujemy jeszcze innego, nowego systemu zarządzania uprawnieniami, który pozwoliłby na zachowanie istniejących zysków i przy okazji wyeliminowałby słabości tych, które testowaliśmy do tej pory. W szczególności powinien umożliwiać:
  • nadawanie indywidualnych uprawnień
  • nadawanie grupowych uprawnień
  • reagować na fakt posiadania uprawnienia przez użytkownika lub przypisanej mu grupy
Drodzy Czytelnicy, oto przed Wami najnowszy cud techniki - macierze uprawnień! Ale nie wyprzedzajmy zbytnio materiału - wszystko ma swoją kolej. ;]

W naszym systemie możemy wyróżnić kilka podstawowych bytów i ich zadań:

  • użytkownik - żąda dostępu do modułu / akcji
  • moduł / akcja - wymaga odpowiedniego uprawnienia
  • uprawnienie - pozwala na dostęp do modułu/ akcji
Zatem mechanizm polega na tym, że w przypadku próby dostępu do modułu sprawdzana jest informacja o tym, czy dany użytkownik ma prawo do wykonania określonej akcji. Możemy to oczywiście zrealizować na kilka sposobów, z tym, że opisane dzisiaj metody będą opierały się wyłącznie na bazie danych - w kwestii przeniesienia części logiki [szczególnie danych dotyczących modułów] polecam zapoznanie się z poprzednim wpisem o poziomach. Zatem do dzieła!

Macierz uprawnień.

Biorąc pod uwagę wyróżnione wyżej byty, będziemy przechowywać informacje o nich w trzech tabelach, pierwszą z nich będzie tabela użytkowników:

CREATE TABLE `users` (
 `id` int(11) NOT NULL auto_increment,
 `login` varchar(255) NOT NULL,
 `password` varchar(255) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

W kolejnej przechowamy dostępne w systemie uprawnienia:

CREATE TABLE `perms` (
 `id` int(11) NOT NULL auto_increment,
 `name` varchar(255) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

Ostatnia zaś będzie zawierała relacje pomiędzy użytkownikami a uprawnieniami:

CREATE TABLE `user_perms` (
 `id` int(11) NOT NULL auto_increment,
 `user` int(11) NOT NULL,
 `perm` int(11) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

Autoryzacja będzie odbywała się w bardzo prosty sposób. Użytkownik podczas logowania jest uwierzytelniany przy użyciu danych z tabeli users, następnie przy próbie dostępu do jednego z modułów będziemy musieli tylko sprawdzić, czy istnieje powiązanie pomiędzy nim a wymaganym uprawnieniem:

SELECT up.user AS uid, up.perm AS pid, pm.name AS perm
FROM user_perms up
INNER JOIN perms pm ON pm.id = :perm
WHERE up.user = :user AND up.perm = :perm

Brak wyników [rekord powiązania nie istnieje] traktujemy jako brak uprawnień wymaganych do dostępu. Tradycyjna macierz jest najprostszym sposobem zarządzania uprawnieniami, przy czym należy zaznaczyć, że na dłuższą metę ciężko jest zarządzać takim systemem. Wynika to z faktu, że każdy użytkownik posiada swój własny zestaw wpisów w tabeli, więc nie jest możliwe “masowe” nadanie / zabranie określonych przywilejów wybranej grupie [pomijając zapytania z klauzulą WHERE id = 34 OR id =35 OR id = 36 i dalej ;]].

Grupy.

Jak wspomniałem wyżej tradycyjna macierz uprawnień nie sprawdzi się w przypadku próby masowego zarządzania użytkownikami. Potrzeba nam zatem narzędzia, które będzie funkcjonowało “obok” macierzy uprawnień użytkowników i pozwalało na spełnienie pozostałych dwóch postawionych w pierwszym akapicie wymagań. Takim narzędziem są właśnie grupy, które wprowadzimy jako kolejny byt obok tych wspomnianych wyżej. Ze względu na poczynione założenia będziemy potrzebowali trzech nowych tabel w bazie danych. Jako pierwszą stworzymy tabelę przechowującą dane grup:

CREATE TABLE `groups` (
 `id` mediumint(11) NOT NULL auto_increment,
 `name` varchar(255) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

Potrzebne będą także relacje do tabeli uprawnień [zbiorowe uprawnienia grup użytkowników]:

CREATE TABLE `group_perms` (
 `id` int(11) NOT NULL auto_increment,
 `group` int(11) NOT NULL,
 `perm` int(11) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

oraz do tabeli użytkowników [użytkownicy przypisani do grup]:

CREATE TABLE `user_groups` (
 `id` int(11) NOT NULL auto_increment,
 `user` int(11) NOT NULL,
 `group` int(11) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

W tym wypadku autoryzacja użytkownika do określonego zasobu “nieco” się utrudnia, ponieważ oprócz sprawdzenia, czy użytkownik sam posiada wymagane uprawnienia, będziemy musieli także sprawdzić, czy żadna z grup, do której jest przypisany nie posiada czasem podobnego wpisu w bazie danych. Sprawdzanie uprawnień samego użytkownika omówiliśmy już wyżej, dlatego poniższy kod wyświetli nam pasujące uprawnienia grup, do których przypisany jest podany użytkownik:

SELECT gp.perm AS perm_id, gp.group AS group_id, gr.name AS group_name, pm.name AS perm_name
FROM group_perms gp
INNER JOIN groups gr ON ((gp.group = gr.id) AND (gr.id IN (SELECT `group` FROM user_groups ug WHERE ug.user = :user)))
INNER JOIN perms pm ON gp.perm = pm.id
WHERE gp.perm = :perm

Jak widać, sprawdzenie wszystkich informacji wymaga dwóch dosyć złożonych zapytań, ale mamy pełną kontrolę nad wszystkim. Trochę pogłówkowałem i udało mi się także stworzyć wersję z jednym zapytaniem, ale wymaga ona złączenia podanych wyżej dwóch grupowych tabel relacyjnych w jedną z flagą typu relacji. Oto ona:

CREATE TABLE `auth` (
 `id` int(11) NOT NULL auto_increment,
 `type` int(11) NOT NULL,
 `perm` int(11) NOT NULL,
 `item` int(11) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

Pole type przyjmuje dwie wartości - 0 dla relacji użytkownika i 1 dla relacji grupy. Mając strukturę bazy danych wykonaną w taki sposób nie musimy wykonywać dwóch zapytań [z jedną tabelą jest to niemożliwe - musielibyśmy wykonać jedno zapytanie SELECT naraz na dwóch tabelach / zbiorach danych, które nie są ze sobą powiązane]. Oto magia we własnej osobie, czyli zapytanie nad którym spędziłem trochę czasu: ;]

SELECT au.id, au.perm, pr.name, au.type, us.login, gr.name
FROM auth au
LEFT JOIN perms pr ON au.perm = pr.id
LEFT JOIN users us ON (au.type = 0 AND au.item = us.id)
LEFT JOIN groups gr ON (au.type = 1 AND au.item = gr.id)
WHERE ((au.type = 0 AND us.id = :user) OR (au.type = 1 AND au.item IN (SELECT `group` FROM user_groups ug WHERE ug.user = :user))) AND au.perm = :perm

Uff… aż zrobiło mi się gorąco. ;]

Hierarchia grup.

Do opisania pozostaje jeszcze jeden problem, który z pozoru jest bardzo prosty, jednak mam co do niego “mieszane uczucia” - hierarchia grup. Hierarchia jest bardzo naturalną relacją pozwalającą na redukcję złożoności danego zbioru danych, w tym wypadku polegałoby to na tym, że każda grupa miałaby także pole parent:

CREATE TABLE `groups` (
 `id` mediumint(11) NOT NULL auto_increment,
 `parent` int(11) NOT NULL,
 `name` varchar(255) NOT NULL,
 PRIMARY KEY  (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

które zawierałoby identyfikator grupy nadrzędnej. W tym wypadku drugie zapytanie sprawdzające uprawnienia grup, musiałoby rekursywnie przejść przez wszystkie “nadrzędne” dla niej grupy i zatrzymać się w momencie znalezienia tej “uprawnionej”. Nie wiem jednak czy da się to wykonać bez stosowania procedur składowanych, dlatego pozwoliłem sobie pominąć ten temat, aczkolwiek zaznaczając, że problem istnieje i na pewno istnieje jakieś rozwiązanie - niestety ostatnio chronicznie brakuje mi czasu [zbliża się sesja i wszystkie zaliczenia] i niestety nie mogę wejść aż tak głęboko w temat, żeby to wyjaśnić. Być może napiszę jeszcze jeden wpis o tym jak już dokładnie przejrzę możliwości i napiszę działające zapytania.

Podsumowanie.

Tak jak obiecałem, dzisiaj została opublikowana [na razie ;]] merytorycznie ostatnia część serii wpisów na temat systemów uprawnień na stronach internetowych. Mam nadzieję, że zawarte tutaj materiały uznaliście za przydatne i zastosujecie te informacje przy pisaniu własnych aplikacji. Jeśli macie jakiekolwiek pytania, zapraszam do komentowania tego wpisu bądź “skrobnięcia” maila do mnie - adres znajdziecie w dziale “Kontakt”. Do zobaczenia za tydzień!