Elegante Rechteverwaltung

Als Programmierer steht man oft vor dem Problem eine Rechteverwaltung auf die Beine stellen zu müssen. Sei es für einen Webshop, ein CMS oder jegliche andere Form von Memberbereichen. Dieser beitrag soll zeigen, wie man ein performantes und flexibles System mit möglichst wenig Speicherverbrauch umsetzen kann

Eine Idee wäre in der entsprechenden Usertabelle neben Usernamen und Passwort jeweils ein Attribut hinzu zu fügen, was besagt „User darf Beitrag schreiben“, „User darf neues Lesen“ usw.
Überlegen wir uns, welcher Datentyp für dieses Vorhaben in Frage kommt. Da SQL oder Speziell MySQL keinen Booleanschen Datentyp implementiert muss man auf einen Ersatz zurück greifen. Die nächst kleineren wären CHAR(1) oder TINYINT. CHAR(1) hätte den Vorteil einen Wertebereich zu haben, mit dem man viele Eigenschaften sofort abdecken kann, z.B.:

a = „User darf Beitrag schreiben“
b = „User darf Beitrag nicht schreiben“
c = „User darf Beitrag editieren“
d = „User darf Beitrag nicht editieren“
e = „User darf Beitrag lesen“
f = „User darf Beitrag nicht lesen“

Okay, klingt doch gut oder? Was ist aber, wenn der User lesen und editieren darf? Somit würde unser Konzept nicht aufgehen.

Überlegen wir mal weiter. TINYINT ist sicher auch keine Lösung und nimmt 1 Byte ein, wo wir doch nur einen Wahrheitswert speichern wollen – einen booleanschen Wert.

Nehmen wir die Datentypen mal auseinander. TINYINT hat einen Wertebereich von -127 bis 128 (2^7) bzw. von 0 bis 255 (2^8) in der UNSIGNED Version. Diese Zahlen kommen durch 2er Potenzen zustande. Zweierpo…? – wir haben doch 2 Zustände, die wir speichern wollen. „Darf“ oder „Darf nicht“ – „Bit gesetzt“ oder „Bit nicht gesetzt“.

In einem TINYINT können demnach 8*2 solcher oben definierten Rechte gespeichert werden. In einem INT (4 Byte) könnte man demzufolge 32*2 solcher Rechte speichern.

Nur wie bekommt man die Bits gesetzt? Mit Mathematik.

Um ein Bit an Position „n“ zu setzen genügt:
$bit|(1<<$n)

Um ein Bit an Position „n“ zu löschen genügt:
$bit & (~(1<<$n)

Fehlt eigentlich nur noch die Möglichkeit zu Prüfen ob ein Bit gesetzt ist:
$bit & (1<<$n)

Diese Funktion in MySQL als Stored Function implementiert und man hat die Möglichkeit komfortabel ein Rechtesystem aufzubauen:

CREATE FUNCTION ISBIT(bit INT, n INT)
RETURNS INT
RETURN bit&amp;(1&lt;&lt;n);
 
CREATE FUNCTION SETBIT(bit INT, n INT, init BOOLEAN)
RETURNS INT
RETURN IF(init=1, bit|(1&lt;&lt;n), bit&amp;(~(1&lt;&lt;n)))

Nun noch ein Beispiel zum Setzen und Lesen von Bits:

SELECT ISBIT(Rechte, 3) FROM USER WHERE ID=123;
 
UPDATE USER SET Rechte=SETBIT(Rechte, 3, 1) WHERE ID=123

Ich habe mir die Möglichkeit natürlich nicht ausgedacht. Die Rechteverwaltung von Dateisystemen arbeitet auf die selbe Weise.

Die Möglichkeit zwei Zustände zu speichern reicht oft nicht aus. Versteht man die binäre Logik, kann man das System auch erweitern um mehrere Optionen performant verfügbar zu haben bzw. zu speichern.

Bald wird noch ein Beitrag von daryl folgen, wie man ein ähnliches System aufbaut, das aber die Binär-Logik in den PHP-Bereich verlagert. Ähnlich wie es bei UNIX gemacht wird. Dieses System wird wohl aber nicht performanter, maximal etwas verständlicher, denn wie so schon einige Male hier im Blog beschrieben, ist es durchaus schneller, Logik- und Arithmetik-Operationen auf der Datenbank auszuführen.

crypt hat 2 Beiträge geschrieben

16 Kommentare zu “Elegante Rechteverwaltung

  1. Carsten sagt:

    Ich hatte hierletzt einen ähnlichen Ansatz.

    z.B. hat man eine dezimale Zahl, deren Stellen wie folgt belegt sind:

    111
    ||\- darf Beitrag lesen
    |\– darf Beitrag schreiben
    \— darf Beitrag bearbeiten

    Die Dezimalzahl wird dann mit der PHP-Funktion bindec() in eine binäre Zahl umgewandelt und in der Datenbank gespeichert.

    Zum Beispiel hätte ein normaler User nur das Recht einen Beitrag zu lesen => binär: 100 => dezimal: 4

    Schreiberling: binär: 110 => dezimal: 6

    Admin: binär: 111 => dezimal: 7

  2. GhostGambler sagt:

    Ich persönlich muss sagen, dass ich von diesen binären Spielereien nichts halte.
    Man quetscht etwas in eine Spalte, in der Hoffnung, dass die Länge eben dieser für die nähere Zukunft reicht (Was macht man, wenn die Länge vom Integer irgendwann nicht mehr reicht? Oder die von bigint?), macht es vollkommen unleserlich für das menschliche Auge, wenn man nicht den entsprechenden Code plus Funktionen gerade im Kopf hat, und wofür das alles? Weil man nicht in der Lage ist das ganze etwas geschickter per PHP umzusetzen?

    Mit zwei JOINs beim Login, plus Cache in der Session lässt sich das ganze über 6 Tabellen mit Einzelrechten und Gruppenrechten leserlich aufteilen.
    Mit einer weiteren HEAP/Memory-Tabelle, kann man optional den Cache steuern, falls dieser mal tatsächlich zeitnah neu aufgebaut werden muss – ansonsten geschieht dies beim nächsten Login ja eh automatisch.
    Die Abfrage selber im Code lässt sich dann über ein
    if (in_array(„write_post“, $_SESSION[‚rights‘]))
    realisieren – optional in irgendwelchem Klassen-Code oder einer Funktion „versteckt“.
    Um das ganze noch weiter zu beschleunigen, gibt es eine Standard-Gruppe für alle Benutzer – mal ehrlich, wie viele VERSCHIEDENE Rechte-Konstellationen gibt es? Bei einer Community mit 100.000 Benutzern, haben 99.900 doch genau die gleichen Rechte – für die braucht man nicht alles einzeln speichern!

    Die Abfragen auf die Rechte ständig in der Datenbank auszuführen ist indes Perlen vor die Säue…
    Sobald man einmal den Integer-Wert ausgelesen hat, kann man genauso gut mit PHP auf die Bits prüfen. Das spart der Datenbank das Parsen von Queries und den Weg zwischen DB und PHP. Und die Datenbank hat ja eigentlich nun wirklich anderes zutun, als sich um Bitoperationen zu kümmern~

    Ehrlich gesagt fällt mir, abgesehen der Performance wegen, kein vernünftiger Grund für diese vollkommen undurchsichtige Lösung ein.
    Und Performance ist ja bekanntlich nicht alles und ein sehr sehr vages Konstrukt… Arbeitszeit indes ist deutlich teurer als zusätzliche Hardware! Das sollte man auch nie vergessen.

    Meine Meinung zu dem Thema – mag jeder sehen wie er will~

  3. Bjoern sagt:

    Ich schließe mich meinem Vorredner an, binäre „Spielereien“ sind voller Fallstricke … so haben sich auch schon im Beitrag ein paar Fehler eingeschlichen, z.B.

    > TINYINT hat einen Wertebereich von -127 bis 128

    Der Wertebereich ist -128 bis 127

    > In einem TINYINT können demnach 8*2 solcher oben
    > definierten Rechte gespeichert werden. In einem
    > INT (4 Byte) könnte man demzufolge 32*2 solcher
    > Rechte speichern.

    Wieso mal zwei? Ich denke hier wird vermutet, daß positive und negative Bits zur Verfügung stehen … dabei ist es in aller Regel so, daß das Vorzeichen selbst ein Bit ist, beim Ändern des MSB (des höchstwertigsten Bits) einer vorzeichenbehafteten Variable gilt es also immer: Obacht!

    Die beiden Beispiele zeigen schon: Der Mehraufwand und die Fehleranfälligkeit rentieren sich nur in Ausnahmefällen – dies es aber durchaus gibt.

    Bjoern

  4. David sagt:

    Die Methode ist für mich leider auch nichts, als Wissensspielerei.
    Viel sinnvoller ist es hier gleich ein ENUM/SET mit ‚0‘,’1′ zu machen und statt dem User direkt die Einstellungen zuzuordnen, dafür eine eigene Tabelle „rights“ zu eröffnen.
    Allein schon wegen der Übersicht würde ich die von mir genannte Methode verwenden.

    Wegen 0,05s PHP-Performance mach ich mir auch nicht in die Hose 😉

  5. Christoph sagt:

    Also ich mache es ganz primitiv. Eine Tabelle mit zwei Spalten, „user_id“ (SMALLINT) und „right_name“ (VARCHAR(30)). Jedes Recht, was ein Benutzer bekommt, wird eingetragen. Hat ein Benutzer keine Rechte, hat er in der Tabelle auch keinen Eintrag.

    Die Richtigkeit der Rechte wird im ACP per JS geregelt. So muss immer das _view-Recht bestehen, damit derjenige Benutzer überhaupt den Bereich X einsehen darf. Ohne x_view kann er nicht x_edit oder x_remove haben (klar: Wenn für ihn der Bereich nicht sichtbar ist, wie sollte er dann Daten löschen oder bearbeiten?).

    Ob das performant ist … puh, keine Ahnung. Es funktioniert jedenfalls wunderbar, ist auch ohne PHP in der DB gut lesbar und wartbar.

    Die Spielerein mit den Bits halte ich auch nicht gerade für sinnvoll. Immerhin gibt es in manchen System von mir über 60 Rechte, also bräuchte ich ja so grob 60 Bit in einem Bitfeld/Integer. Neeee…

  6. admin sagt:

    @David: Die meisten Tipps in diesem Blog sind dazu da, um genau diese 0,05 s Performance rauszuholen. Die Zielgruppe sind deshalb auch große Seiten und nicht irgendwelche mit paar Hundert Besuchern im Monat.

    @Björn und Christoph:
    Natürlich kann man eine Extra-Usertabelle machen und für jedes Recht eine einzelne Spalte machen. Dabei wird allerdings jede Menge Performance und Speicherplatz vergeudet! Dieser Blog soll ja gerade neue Wege aufzeigen, denn oft sind es gerade die Sachen, die nicht jedem gleich einfallen, die am schnellsten sind. Oder frag mal jemanden auf der Straße wie er eine Liste sortieren würde – kommt ganz oft Bubblesort und fast nie Quicksort. Immer mal bissl mehr die grauen Zellchen anstrengen, kann sich für große Seiten wirklich lohnen!

  7. GhostGambler sagt:

    Bei großen Websiten braucht der Server für solch Dinge höchstens 0.005 Sekunden 😉

    Aber auch, oder gerade bei solchen Websiten, steht eindeutig die Lesbarkeit des Codes und die einfache Wartbarkeit im Vordergrund.
    Hardware ist halt einfach billiger als Arbeitszeit.

    Wenn der immer-währende JOIN zu langsam ist, dann wird er beschleunigt. Wie ich zum Beispiel schon andeutete – Cache in der Session.

    Was mir jetzt (also vor 4 Stunden beim Mittag) noch einfiel ist ein Cache mit eben der hier beschriebenen Variante.
    So bleiben die Daten nach Außen der Klasse lesbar (if ($user->has_right(„admin“)), nach „hinten“ in die Datenbank bleiben sie auch lesbar (vernünftige Tabellenstruktur), und die Performanz kann der PHP-Entwickler der Klasse in der Mitte durch eben diese binären Spielereien raus holen. So beschränkt sich die Unleserlichkeit auf einen kleinen Teil vom Projekt und zieht sich nicht durch alle Teilbereiche. (Stichwort: Kapselung)

    Das wäre mMn eine adäquate Lösung für eine große Website.

  8. Leif sagt:

    Also ich finde die Sache aus Performancesicht auch interessant. Man muss es ja nicht für Rechteverwaltung nutzen (auf die hier ohnehin nicht wirklich eingegangen wird). Die Bitoperationen und die Stored Procedures sind auch woanders anwendbar.

    @admin: der Quciksort kann im Worst Case das Laufzeitverhalten O(n^2) haben, der Heapsort nicht – und der ist ähnlich schnell.

  9. admin sagt:

    Hehe korrekt, Heapsort ist u.U. schneller. Tut hier aber nix zur Sache. Wollte nur den Nörglern entgegentreten und sagen, dass die meisten Tipps hier für Profis und solche, die es werden wollen, sind.

    Es folgt wie gesagt bald noch ein Beitrag, wo die Binär-Logik auf PHP-Ebene liegt. Das kann man dann auch hübsch mit Funktionen machen, wie GhostGambler vorgeschlagen hat. Dann bleibt der Code sauber (obwohl ich das hier eigentlich auch recht gekapselt finde, denn man ruft ja lediglich die Stored Function auf – aber ok, in der DB ists dann nicht mehr ganz so einfach lesbar).

  10. Leif sagt:

    Was ich noch anmerken muss, und was ich sehr wichtig finde: Der Titel „Elegante Rechteverwaltung“ ist total irreführend. Man erwartet eine Struktur, um Rechte zu verwalten. Vielmehr ist das Thema aber „Mehrere Booleans in einem Integer zusammenfassen“, und es wurde nur am Beispiel Rechteverwaltung gezeigt, wozu das gut sein kann. Die Rechteverwaltung an sich ist gar nicht Thema. Und es hätte auch andere Beispiele geben können, aber die dürfen nicht titelgebend sein, weil es darum ja gar nicht geht.
    Ich denke auch, für eine spätere Suche nach diesen Bitoperationen sollte der Titel des Beitrags entsprechend umbenannt werden.

  11. Caron sagt:

    Also, vielleicht mal kurz zu eleganter Sprache:
    Entweder ‚boolean‘ oder ‚boole(‚)sch‘. Aber nicht ‚booleansch‘. Das hieße ja nichts anders als ‚booleschsch‘.
    Das ganze geht auf einen Herrn George Boole zurück.

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>