This post comes from the first version of this blog.
Please send me an email if anything needs an update. Thanks!
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
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
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:
|
|
W kolejnej przechowamy dostępne w systemie uprawnienia:
|
|
Ostatnia zaś będzie zawierała relacje pomiędzy użytkownikami a uprawnieniami:
|
|
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:
|
|
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:
|
|
Potrzebne będą także relacje do tabeli uprawnień [zbiorowe uprawnienia grup użytkowników]:
|
|
oraz do tabeli użytkowników [użytkownicy przypisani do grup]:
|
|
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:
|
|
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:
|
|
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: ;]
|
|
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:
|
|
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.