controleren van rechten - idee?
Poolse (of prefix) notatie. Het voordeel hiervan is dat ik rechtstreeks rechten kan koppelen aan een resource, en hier ook direct bij opslaan (een expressie die aangeeft of iets leesbaar is, een expressie die aangeeft of iets schrijbaar is et cetera).
Ik heb het in PHP al voor elkaar, alleen soms zal er waarschijnlijk de behoefte ontstaan om alleen die resources op te halen uit mijn database waarvoor de gebruiker die ze opvraagt voldoende rechten heeft. Ik wil dus de selectie al in mijn database maken en niet achteraf filteren.
Hiertoe heb ik de volgende MySQL functie gemaakt (zowel deze code als de PHP variant volgt in grote lijnen de abstracte implementatie op de WIKI-pagina):
Hierbij heb ik wel wat truuks uit moeten halen en moet ik bepaalde randgevallen correct afhandelen. Bovenstaande code kan desgewenst worden toegelicht.
Omdat dit wellicht nogal abstract is een voorbeeld.
Stel ik heb een resource die als volgt is ingesteld: de resource is toegankelijk als iemand recht 1 heeft, OF recht 2, maar dan mag die persoon het recht 3 niet hebben. De "rechtenboom" ziet er dan als volgt uit:
Deze kun je ook op de bovenstaande pagina nabouwen (give it a try).
De geserialiseerde expressie (in prefix notatie) wordt dan (| is de logische OR, & de logische AND, ! de logische NOT):
Als we vervolgens wat SELECTs doen:
(gebruiker heeft recht 1 - toegang verleend)
(gebruiker heeft recht 2 - toegang verleend)
(gebruiker heeft recht 2 en 3 - toegang geweigerd)
Nu heb ik wat vragen:
- wat vind je van dit idee (een geserialiseerd predikaat opslaan bij een resource die aangeeft wat (moet blijken uit de kolom waar je dit in opslaat) en onder welke condities (moet blijken uit de validatie middels has_rights()) hier iets mee gedaan mag worden)
- MySQL kent geen arrays, een alternatief hiervoor zijn tijdelijke tabellen ofzo (dan moet ik er waarschijnlijk een PROCEDURE van maken); bovenstaande code is een beetje wollig, maar lijkt te werken; zijn er ergens nog dingen aan te verbeteren / te veranderen?
Om resources in mijn website af te schermen maak ik gebruik van geserialiseerde rechten-expressies, een soort van predikaat in string-vorm dus, opgesteld in een (semi) Ik heb het in PHP al voor elkaar, alleen soms zal er waarschijnlijk de behoefte ontstaan om alleen die resources op te halen uit mijn database waarvoor de gebruiker die ze opvraagt voldoende rechten heeft. Ik wil dus de selectie al in mijn database maken en niet achteraf filteren.
Hiertoe heb ik de volgende MySQL functie gemaakt (zowel deze code als de PHP variant volgt in grote lijnen de abstracte implementatie op de WIKI-pagina):
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
DELIMITER ;;
DROP FUNCTION IF EXISTS has_rights;;
CREATE FUNCTION has_rights(expression VARCHAR(255), rights VARCHAR(255)) RETURNS BOOL DETERMINISTIC
BEGIN
DECLARE stack VARCHAR(255);
DECLARE token VARCHAR(5);
DECLARE val1 VARCHAR(5);
DECLARE val2 VARCHAR(5);
IF (expression = '') THEN
RETURN TRUE;
END IF;
SET stack = '1,';
SET expression = REVERSE(CONCAT(',&,', expression));
WHILE (LOCATE(',', expression) > 0) DO
SET token = SUBSTRING_INDEX(expression, ',', 1);
SET expression = SUBSTRING(expression, LENGTH(token) + 2);
IF (LENGTH(token) > 0) THEN
IF (token = '|' OR token = '&') THEN
SET val1 = SUBSTRING_INDEX(stack, ',', 1);
SET stack = SUBSTRING(stack, LENGTH(val1) + 2);
SET val2 = SUBSTRING_INDEX(stack, ',', 1);
SET stack = SUBSTRING(stack, LENGTH(val2) + 2);
IF (token = '|') THEN
SET stack = CONCAT((val1 OR val2), ',', stack);
ELSE
SET stack = CONCAT((val1 AND val2), ',', stack);
END IF;
ELSEIF (token = '!') THEN
SET val1 = SUBSTRING_INDEX(stack, ',', 1);
SET stack = CONCAT(NOT(val1), ',', SUBSTRING(stack, LENGTH(val1) + 2));
ELSE
SET val1 = LOCATE(CONCAT(',', token, ','), CONCAT(',', rights, ',')) > 0;
SET stack = CONCAT(val1, ',', stack);
END IF;
END IF;
END WHILE;
SET val1 = SUBSTRING_INDEX(stack, ',', 1);
RETURN (val1 = 1);
END;;
DELIMITER ;
DROP FUNCTION IF EXISTS has_rights;;
CREATE FUNCTION has_rights(expression VARCHAR(255), rights VARCHAR(255)) RETURNS BOOL DETERMINISTIC
BEGIN
DECLARE stack VARCHAR(255);
DECLARE token VARCHAR(5);
DECLARE val1 VARCHAR(5);
DECLARE val2 VARCHAR(5);
IF (expression = '') THEN
RETURN TRUE;
END IF;
SET stack = '1,';
SET expression = REVERSE(CONCAT(',&,', expression));
WHILE (LOCATE(',', expression) > 0) DO
SET token = SUBSTRING_INDEX(expression, ',', 1);
SET expression = SUBSTRING(expression, LENGTH(token) + 2);
IF (LENGTH(token) > 0) THEN
IF (token = '|' OR token = '&') THEN
SET val1 = SUBSTRING_INDEX(stack, ',', 1);
SET stack = SUBSTRING(stack, LENGTH(val1) + 2);
SET val2 = SUBSTRING_INDEX(stack, ',', 1);
SET stack = SUBSTRING(stack, LENGTH(val2) + 2);
IF (token = '|') THEN
SET stack = CONCAT((val1 OR val2), ',', stack);
ELSE
SET stack = CONCAT((val1 AND val2), ',', stack);
END IF;
ELSEIF (token = '!') THEN
SET val1 = SUBSTRING_INDEX(stack, ',', 1);
SET stack = CONCAT(NOT(val1), ',', SUBSTRING(stack, LENGTH(val1) + 2));
ELSE
SET val1 = LOCATE(CONCAT(',', token, ','), CONCAT(',', rights, ',')) > 0;
SET stack = CONCAT(val1, ',', stack);
END IF;
END IF;
END WHILE;
SET val1 = SUBSTRING_INDEX(stack, ',', 1);
RETURN (val1 = 1);
END;;
DELIMITER ;
Hierbij heb ik wel wat truuks uit moeten halen en moet ik bepaalde randgevallen correct afhandelen. Bovenstaande code kan desgewenst worden toegelicht.
Omdat dit wellicht nogal abstract is een voorbeeld.
Stel ik heb een resource die als volgt is ingesteld: de resource is toegankelijk als iemand recht 1 heeft, OF recht 2, maar dan mag die persoon het recht 3 niet hebben. De "rechtenboom" ziet er dan als volgt uit:
Deze kun je ook op de bovenstaande pagina nabouwen (give it a try).
De geserialiseerde expressie (in prefix notatie) wordt dan (| is de logische OR, & de logische AND, ! de logische NOT):
Als we vervolgens wat SELECTs doen:
Code (php)
1
2
3
4
5
6
7
2
3
4
5
6
7
mysql> SELECT has_rights('|,1,&,2,!,3', '1');
+--------------------------------+
| has_rights('|,1,&,2,!,3', '1') |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.00 sec)
+--------------------------------+
| has_rights('|,1,&,2,!,3', '1') |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.00 sec)
(gebruiker heeft recht 1 - toegang verleend)
Code (php)
1
2
3
4
5
6
7
2
3
4
5
6
7
mysql> SELECT has_rights('|,1,&,2,!,3', '2');
+--------------------------------+
| has_rights('|,1,&,2,!,3', '2') |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.00 sec)
+--------------------------------+
| has_rights('|,1,&,2,!,3', '2') |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.00 sec)
(gebruiker heeft recht 2 - toegang verleend)
Code (php)
1
2
3
4
5
6
7
2
3
4
5
6
7
mysql> SELECT has_rights('|,1,&,2,!,3', '2,3');
+----------------------------------+
| has_rights('|,1,&,2,!,3', '2,3') |
+----------------------------------+
| 0 |
+----------------------------------+
1 row in set (0.00 sec)
+----------------------------------+
| has_rights('|,1,&,2,!,3', '2,3') |
+----------------------------------+
| 0 |
+----------------------------------+
1 row in set (0.00 sec)
(gebruiker heeft recht 2 en 3 - toegang geweigerd)
Nu heb ik wat vragen:
- wat vind je van dit idee (een geserialiseerd predikaat opslaan bij een resource die aangeeft wat (moet blijken uit de kolom waar je dit in opslaat) en onder welke condities (moet blijken uit de validatie middels has_rights()) hier iets mee gedaan mag worden)
- MySQL kent geen arrays, een alternatief hiervoor zijn tijdelijke tabellen ofzo (dan moet ik er waarschijnlijk een PROCEDURE van maken); bovenstaande code is een beetje wollig, maar lijkt te werken; zijn er ergens nog dingen aan te verbeteren / te veranderen?
Voor wie het interesseert:
Regel 15 moet vervangen worden door het volgende:
Dit omdat expression initieel omgedraaid wordt. Tokens die uit meer dan 1 karakter bestaan (rechten-id's groter dan 9) moeten dus opnieuw omgedraaid worden anders staan deze achterstevoren :).
Werkt dus nu ook voor rechten-id's groter dan 9 :D.
Code (php)
1
2
3
4
5
6
7
2
3
4
5
6
7
mysql> SELECT has_rights('&,96,|,98,97', '98,96');
+-------------------------------------+
| has_rights('&,96,|,98,97', '98,96') |
+-------------------------------------+
| 1 |
+-------------------------------------+
1 row in set (0.00 sec)
+-------------------------------------+
| has_rights('&,96,|,98,97', '98,96') |
+-------------------------------------+
| 1 |
+-------------------------------------+
1 row in set (0.00 sec)
Sorry voor de bump maar kon helaas niet anders.
Gewijzigd op 28/03/2015 16:37:10 door Thomas van den Heuvel
Voor de geïnteresserde(n): ik heb inmiddels een nieuwe variant in elkaar gezet.
En als je wat minder interesse hebt in alle technische mumbo jumbo maar wilt zien wat je er nu eigenlijk mee kunt doen: het stelt je in staat om eenvoudig rechten-bomen in elkaar te klikken die later weer geëvalueerd kunnen worden zodat bepaald kan worden of je voldoende rechten hebt om bepaalde acties uit te voeren of dingen in te mogen zien.
Op- en aanmerkingen welkom, vooral als je denkt te weten hoe er nog wat milliseconden van de executietijd afgeschaafd kunnen worden, al is het volgens mij al redelijk snel ;).
Grappig, dit lijkt heel erg op de menubomen die we op het werk gebruiken in elkaar te draaien. Selecties gebeuren op eigenschappen, dus bijvoorbeeld type product = telescoop AND merk = Meade.
Maw: ik zie de meerwaarde hier niet zo van (een oplossing op zoek naar een probleem?).
eerste deel. De expressies die je bouwt zijn in wezen geserialiseerde predicaten (in Poolse Notatie). Dit stelt je in staat om een soort van if-statement met rechtenchecks dynamisch te koppelen aan willekeurige content / resources, deze is niet langer gebonden aan de code / hard-coded.
Dit is niet voor iedere gebruiker uiteraard maar meer voor beheerders. Het nut staat misschien beter uitgelegd in het Ik vind het een leuke oplossing, en ik zie ook wel de voordelen als je het heel dynamisch wilt doen, maar je bent hier gewoon "programmeerwerk" naar de gebruiker aan het verplaatsen, en dat gaat meestal maar beperkt goed ("ze snappen het niet", of in ieder geval niet goed genoeg en gaan fouten maken). Een rechten-/profielenstructuur snapt "iedereen". Misschien niet zo flexibel, dus om de flexibiliteit van dit systeem op te vangen heb je heel veel profielen nodig, maar dat slaan ze gewoon op in een Excel sheet, en desnoods krijgt elke gebruiker z'n eigen profiel, en dan is iedereen weer tevreden.
En wat je dan dus eigenlijk gedaan hebt is je hele flexibele systeem eenmalig "plat slaan" naar aparte/unieke rechten. Voordeel is dan dat je niet elke keer je Poolse notatie uit hoeft te werken (en dat is dus altijd sneller ;-) ).
Maarrr, mocht je de boel toch willen versnellen is het misschien een idee om de resultaten te cachen in een (RAM) tabel. Kolommen: expression, rights, en result. Bij meer dan een x aantal rijen truncaten (en opnieuw beginnen). Voorkomt dat als je voor dezelfde gebruiker (steeds zelfde rights) een hele waslijst aan resources moet doorlopen (met dus naar mijn idee veelal dezelfde expressions) je steeds dezelfde riedel moet uitvoeren.
Het hangt natuurlijk van de toepassing af of dit handig en efficient is, en voor andere varianten zijn CRUD-aanpakken of (simpele) profielen of wat dan ook wellicht handiger. Maar deze opzet geeft vooralsnog volledige vrijheid.
Nogmaals, normale gebruikers zullen hier niet aankomen, dit is echt voor de inrichting/beheerkant van backends.
Code wat aangepast en validatiefunctie herschreven. Deze zit nu ook ingebouwd in de demo, dus je krijgt nu ook foutmeldingen over wat er eventueel niet klopt aan de expressie.
Code (php)
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
//dit:
OR--+--1
|
+--AND--2
|
+--NOT--3
//wordt dan (ik omsluit de "rechten" met punthaken ivm afbakening):
/<1>|<2>(?!.*<3>)/
OR--+--1
|
+--AND--2
|
+--NOT--3
//wordt dan (ik omsluit de "rechten" met punthaken ivm afbakening):
/<1>|<2>(?!.*<3>)/
De gebruikersrechten moet je dan wel (op de een of andere manier - hier numeriek) sorteren:
Code (php)
1
2
3
4
2
3
4
$regex = '/<1>|<2>(?!.*<3>)/';
print((preg_match($regex,'<1>') ? 'OK' : 'no') . "\n");
print((preg_match($regex,'<2>') ? 'OK' : 'no') . "\n");
print((preg_match($regex,'<2><3>') ? 'OK' : 'no') . "\n");
print((preg_match($regex,'<1>') ? 'OK' : 'no') . "\n");
print((preg_match($regex,'<2>') ? 'OK' : 'no') . "\n");
print((preg_match($regex,'<2><3>') ? 'OK' : 'no') . "\n");
Zelfde resultaat. Ik vermoed sneller dan nu.
In MySQL kun je dan:
In r.expression staat dan dus de regex, rights zijn de gesorteerde rechten (incl punthaken). Ik vermoed ook sneller dan nu.
Moet je dus nog wel een "regex compiler" toevoegen. Maakt het wel weer complexer. Of beter: een invoerhulp die meteen een regex in elkaar knutselt :-)
Of dat de regex variant makkelijk uitbreidbaar is op het moment dat je extra operatoren introduceert (Joost mag weten waarom, maar ik ken rechtenconstructies met XOR operatoren).
Of hoe de lengte van de expressie zich verhoudt tot performance. Of de vorm (duik je met groeperingshaken de recursie in? De Poolse notatie blijft lineair).
Ik denk dat je i.i.g. wat aan transparantie verliest op het moment dat je regexes introduceert. Dus zelfs als dit (mogelijk marginaal) sneller is dan de huidige opzet, zou ik daar uit overwegingen van simpliciteit misschien toch niet aan beginnen.
Vraag is ook, kun je het resultaat van een REGEXP vergelijking in MySQL cachen zonder extra handelingen? Volgens mij doet een DETERMINISTIC functie dat automatisch. Als de invoer hetzelfde is als een reeds eerdere ingevoerde combinatie van rechten en een expressie, dan is het resultaat al bekend.
Je zou met benchmarks moeten kijken of dit echt sneller is. Meten = weten.