Nadszedł czas na pierwszą implementację tej struktury w sql, oczywiście nie jest to czego byśmy oczekiwali ponieważ ma pewne braki które mogły by się wydawać dla większego użytku wprost odwrotnie proporcjonalne do zastosowania role based access control. Otóż jest trochę uproszczona struktura, dlaczego? Mieliśmy w pracy z kolegą burzę mózgów na ten temat gdyż go bardzo zainteresował i doszedł do wniosku iż konstruowanie kolosa może mieć negatywny wpływ na jego interfejs, i prawdę powiedziawszy po głębszym zastanowieniu się oraz doświadczeniu wyniesionym z używania phpbb (taki skrypt forum) doszedłem do wniosku iż ma chłop rację. Więc na pierwszy ogień skonstruowaliśmy taką oto strukturę rbac’a, ale moje ego nie pozwala mi na zakończenie prac nad tym i będę kontynuował rozwój do takiego stopnia że stwierdzę iż większego kolosa się zbudować nie da ponieważ zawiera już wszystko.
Struktura SQL Role Based Access Control
Także do dzieła, struktura bazy danych:
BEGIN; DROP TABLE IF EXISTS "rbac_user" CASCADE; CREATE TABLE "rbac_user" ( "idUser" SERIAL PRIMARY KEY, "userName" VARCHAR( 255 ) NOT NULL ); DROP TABLE IF EXISTS "rbac_group" CASCADE; CREATE TABLE "rbac_group" ( "idGroup" SERIAL PRIMARY KEY, "groupName" VARCHAR( 255 ) UNIQUE NOT NULL ); DROP TABLE IF EXISTS "rbac_user_to_group"; CREATE TABLE "rbac_user_to_group" ( "idUser" INT REFERENCES "rbac_user"( "idUser" ) NOT NULL, "idGroup" INT REFERENCES "rbac_group"( "idGroup" ) NOT NULL, PRIMARY KEY( "idUser", "idGroup" ) ); DROP TABLE IF EXISTS "rbac_module" CASCADE; CREATE TABLE "rbac_module" ( "idModule" SERIAL PRIMARY KEY, "moduleName" VARCHAR( 255 ) UNIQUE NOT NULL ); DROP TABLE IF EXISTS "rbac_action" CASCADE; CREATE TABLE "rbac_action" ( "idAction" SERIAL PRIMARY KEY, "idModule" INT REFERENCES "rbac_module"( "idModule" ) NOT NULL, "actionName" VARCHAR( 255 ) NOT NULL, "inherited" BOOLEAN DEFAULT FALSE NOT NULL, UNIQUE( "actionName", "idModule" ) ); DROP TABLE IF EXISTS "rbac_privilages"; CREATE TABLE "rbac_privilages" ( "idGroup" INT REFERENCES "rbac_group"( "idGroup" ) NOT NULL, "idModule" INT REFERENCES "rbac_module"( "idModule" ) NOT NULL, "idAction" INT REFERENCES "rbac_action"( "idAction" ) NOT NULL, "allow" BOOLEAN DEFAULT FALSE NOT NULL, PRIMARY KEY( "idGroup", "idModule", "idAction" ) ); COMMIT;
Użyłem transakcji, w posgresql można
W MySQL też, pod warunkiem że typ tabeli to InnoDB. Ale nie teraz o tym rzecz. Co jest co?
rbac_user – tabela przechowująca użytkowników naszego systemu
rbac_group – tabela przechowująca grupy w naszym systemie
rbac_user_to_group – tabela wiążąca użytkowników z grupami, relacja typu many-to-many (wiele do wielu)
rbac_module – tabela przechowująca nazwy naszych modułów, można to rozumieć jako controllery naszej aplikacji
rbac_action – tabela przechowująca nazwy naszych akcji, trzeba to rozumieć jako metody w naszych controllerach, relacja typu many-to-one (wiele do jednego, to samo co jeden do wielu [one-to-many])
rbac_privilages – a to jest tabela wiążąca grupę użytkowników, moduł i akcję.
Demo Role Based Access Control
Teraz jak to ustrojstwo wypełnić danymi? Bardzo proszę:
BEGIN; INSERT INTO "rbac_user" ( "userName" ) VALUES ( 'przemek' ), ( 'maciej' ), ( 'andrzej' ), ( 'grzesiek' ); INSERT INTO "rbac_group" ( "groupName" ) VALUES ( 'admin' ), ( 'user' ), ( 'moderator' ); INSERT INTO "rbac_user_to_group" VALUES ( 1, 1 ), ( 2, 3 ), ( 3, 2 ), ( 4, 2 ); INSERT INTO "rbac_module" ( "moduleName" ) VALUES ( 'news' ), ( 'articles' ), ( 'download' ), ( 'music' ); INSERT INTO "rbac_action" ( "idModule", "actionName", "inherited" ) VALUES ( 1, 'index', FALSE ), ( 1, 'edit', FALSE ), ( 1, 'add', FALSE ), ( 1, 'delete', FALSE ), ( 1, 'block', FALSE ), ( 2, 'index', TRUE ), ( 2, 'edit', TRUE ), ( 2, 'add', TRUE ), ( 2, 'delete', TRUE ), ( 2, 'block', TRUE ), ( 3, 'index', TRUE ), ( 3, 'edit', FALSE ), ( 3, 'add', TRUE ), ( 3, 'delete', FALSE ), ( 3, 'block', FALSE ), ( 3, 'download', TRUE ), ( 4, 'index', TRUE ), ( 4, 'edit', TRUE ), ( 4, 'add', FALSE ), ( 4, 'delete', FALSE ), ( 4, 'block', FALSE ), ( 4, 'download', FALSE ), ( 4, 'play', TRUE ); INSERT INTO "rbac_privilages" VALUES ( 1, 1, 1, TRUE ), ( 1, 1, 2, TRUE ), ( 1, 1, 3, TRUE ), ( 1, 1, 4, TRUE ), ( 1, 1, 5, TRUE ), ( 2, 1, 1, TRUE ), ( 2, 1, 2, FALSE ), ( 2, 1, 3, TRUE ), ( 2, 1, 4, FALSE ), ( 2, 1, 5, FALSE ), ( 3, 1, 1, TRUE ), ( 3, 1, 2, TRUE ), ( 3, 1, 3, TRUE ), ( 3, 1, 4, TRUE ), ( 3, 1, 5, FALSE ), ( 1, 2, 6, TRUE ), ( 1, 2, 7, TRUE ), ( 1, 2, 8, TRUE ), ( 1, 2, 9, TRUE ), ( 1, 2, 10, TRUE ), ( 2, 2, 6, TRUE ), ( 2, 2, 7, FALSE ), ( 2, 2, 8, TRUE ), ( 2, 2, 9, FALSE ), ( 2, 2, 10, FALSE ), ( 3, 2, 6, TRUE ), ( 3, 2, 7, TRUE ), ( 3, 2, 8, TRUE ), ( 3, 2, 9, TRUE ), ( 3, 2, 10, FALSE ), ( 1, 3, 11, TRUE ), ( 1, 3, 12, TRUE ), ( 1, 3, 13, TRUE ), ( 1, 3, 14, TRUE ), ( 1, 3, 15, TRUE ), ( 1, 3, 16, TRUE ), ( 2, 3, 11, TRUE ), ( 2, 3, 12, FALSE ), ( 2, 3, 13, TRUE ), ( 2, 3, 14, FALSE ), ( 2, 3, 15, FALSE ), ( 2, 3, 16, TRUE ), ( 3, 3, 11, TRUE ), ( 3, 3, 12, TRUE ), ( 3, 3, 13, TRUE ), ( 3, 3, 14, FALSE ), ( 3, 3, 15, TRUE ), ( 3, 3, 16, TRUE ), ( 1, 4, 17, TRUE ), ( 1, 4, 18, TRUE ), ( 1, 4, 19, TRUE ), ( 1, 4, 20, TRUE ), ( 1, 4, 21, TRUE ), ( 1, 4, 22, TRUE ), ( 1, 4, 23, TRUE ), ( 2, 4, 17, TRUE ), ( 2, 4, 18, FALSE ), ( 2, 4, 19, TRUE ), ( 2, 4, 20, FALSE ), ( 2, 4, 21, FALSE ), ( 2, 4, 22, TRUE ), ( 2, 4, 23, TRUE ), ( 3, 4, 17, TRUE ), ( 3, 4, 18, TRUE ), ( 3, 4, 19, TRUE ), ( 3, 4, 20, FALSE ), ( 3, 4, 21, TRUE ), ( 3, 4, 22, TRUE ), ( 3, 4, 23, TRUE ); COMMIT;
Znowu transakcję, jakoś tak szybciej działa to
Jak pewnie zdążyliście zauważyć tabela rbac_action posiada kolumnę „inherited” która w późniejszym etapie mojej implementacji tego paradygmatu będzie potrzebna do tego by w hierarchii definiować czy dana akcja ma być dziedziczona dla pod grup. Taki ficzer.
Teraz tak, żeby to działało co jest wyżej przedstawione na gotowca, czyli copy-paste wymagana jest baza danych postgresql > 8.2. Dlaczego? A otóż dlatego że:
DROP "tabela" IF EXISTS;
jest zaimplementowane od tej versji. Oj brakowało tego brakowało… Nie wiem jak z wierszami podawanymi po przecinku, gdyż w 8.1 to nie działa (mowa tutaj o klauzuli INSERT).
Sprawdzanie uprawnień
Pozostało nam już tylko jak to pobrać, dla przykładu nie tworzyłem żadnych testów wydajnościowych, chodź przyznam że łączymy parę tabel ale utworzone indeksy są w pełni wykorzystywane, przykład jak sprawdzić uprawnienia:
SELECT "allow" FROM "rbac_privilages" p LEFT JOIN "rbac_module" m ON ( p."idModule" = m."idModule" ) LEFT JOIN "rbac_action" a ON ( p."idAction" = a."idAction" ) LEFT JOIN "rbac_user_to_group" u2g ON ( p."idGroup" = u2g."idGroup" ) WHERE u2g."idUser" = 2 AND m."moduleName" = 'music' AND a."actionName" = 'index';
Możemy sobie z tego ładny widoczek utworzyć, ale może o tym kiedy indziej. A teraz co wykazało explain?
Nested Loop (cost=8.28..14.17 rows=1 width=1) -> Nested Loop (cost=8.28..13.19 rows=1 width=5) JOIN Filter: (p."idModule" = m."idModule") -> Hash JOIN (cost=8.28..10.26 rows=3 width=9) Hash Cond: (p."idAction" = a."idAction") -> Seq Scan ON rbac_privilages p (cost=0.00..1.69 rows=69 width=13) -> Hash (cost=8.27..8.27 rows=1 width=4) -> INDEX Scan USING "rbac_action_actionName_key" ON rbac_action a (cost=0.00..8.27 rows=1 width=4) INDEX Cond: (("actionName")::text = 'index'::text) -> INDEX Scan USING "rbac_module_moduleName_key" ON rbac_module m (cost=0.00..0.97 rows=1 width=4) INDEX Cond: ((m."moduleName")::text = 'music'::text) -> INDEX Scan USING rbac_user_to_group_pkey ON rbac_user_to_group u2g (cost=0.00..0.97 rows=1 width=4) INDEX Cond: ((u2g."idUser" = 2) AND (u2g."idGroup" = p."idGroup")) (13 rows)
A co explain z analyze?
Nested Loop (cost=8.28..14.17 rows=1 width=1) (actual time=0.449..0.480 rows=1 loops=1) -> Nested Loop (cost=8.28..13.19 rows=1 width=5) (actual time=0.355..0.449 rows=3 loops=1) JOIN Filter: (p."idModule" = m."idModule") -> Hash JOIN (cost=8.28..10.26 rows=3 width=9) (actual time=0.083..0.288 rows=12 loops=1) Hash Cond: (p."idAction" = a."idAction") -> Seq Scan ON rbac_privilages p (cost=0.00..1.69 rows=69 width=13) (actual time=0.012..0.099 rows=69 loops=1) -> Hash (cost=8.27..8.27 rows=1 width=4) (actual time=0.049..0.049 rows=4 loops=1) -> INDEX Scan USING "rbac_action_actionName_key" ON rbac_action a (cost=0.00..8.27 rows=1 width=4) (actual time=0.027..0.036 rows=4 loops=1) INDEX Cond: (("actionName")::text = 'index'::text) -> INDEX Scan USING "rbac_module_moduleName_key" ON rbac_module m (cost=0.00..0.97 rows=1 width=4) (actual time=0.006..0.008 rows=1 loops=12) INDEX Cond: ((m."moduleName")::text = 'music'::text) -> INDEX Scan USING rbac_user_to_group_pkey ON rbac_user_to_group u2g (cost=0.00..0.97 rows=1 width=4) (actual time=0.005..0.006 rows=0 loops=3) INDEX Cond: ((u2g."idUser" = 2) AND (u2g."idGroup" = p."idGroup")) Total runtime: 0.576 ms (14 rows)
I skąd te 14 wierszy? Bladego pojęcia nie mam. (Edit już wiem ;p) Także czekam na Wasze konstruktywne opinie, chce zaznaczyć że całość nie jest tylko moim pomysłem ale również kolegi z pracy Maćka.


%H:%i
14 wierszy bo explain analyze ma 14 wierszy.