Umfassendes Rollen- und Rechte-Management für eigene Anwendungen

6 | 18714 Aufrufe
Sie können diese Wikiseite nach der Anmeldung auf Webmasterpro bearbeiten. Helfen Sie mit und verbessern Sie "Umfassendes Rollen- und Rechte-Management für eigene Anwendungen" mit Ihrem Wissen!

Anzeige Hier werben

Konzept

Mit dem hier vorgestellten Konzept lassen sich einem Benutzer eine oder mehrere Rollen zuweisen (bspw. "Administrator", "Moderator" oder "Benutzer"). Diesen Rollen werden Rechte zugewiesen, die in der Web-Anwendung bestimmte Aktionen für den Benutzer erlauben/verbieten. In einem Redaktionssystem könnte es z.B. die Rechte "Artikel anlegen", "Bild hochladen" oder "fremde Artikel bearbeiten" geben. Ein Benutzer darf eine Aktion ausführen, wenn er aufgrund einer (oder mehrerer) seiner Rollen das Recht zugewiesen bekommen hat. Zudem soll es möglich sein, die Rechte eines Benutzers individuell anzupassen. Hiermit können Rechte einem einzelnen Benutzer gezielt aberkannt oder zugewiesen werden. Diese individuellen Anpassungen überschreiben die Einstellungen der Rolle(n) des Benutzers.

Umsetzung

Benötigte Tabellen

Es werden folgende Datenbank-Tabellen benötigt (in eckigen Klammern stehen die Felder der Tabelle, Primärschlüssel sind kursiv):

  1. rights [rightID (int), rightName (varchar 100)]
    • Enthält alle in der Web-Anwendung verfügbaren Rechte. Der Name des Rechts ("Bild hochladen") steht in rightName
  2. roles [roleID (int), roleName (varchar 100)]
    • Enthält alle in der Web-Anwendung verfügbaren Rollen ("Administrator", "Moderator", etc)
  3. role_rights [roleID (int), rightID (int)]
    • Speichert, welche Rechte eine Rolle besitzt
  4. account_roles [accountID (int), roleID (int)]
    • Speichert die Rolle(n) eines Benutzers.
  5. account_rights_adjust [accountID (int), rightID (int), adjustment (int)]
    • Speichert die individuellen Rechte-Anpassungen eines Benutzers. Hierzu wird neben dem betroffenen Benutzer (accountID) und Recht (rightID) ein "adjustment"-Wert eingetragen. Ist dieser "1", bekommt der Benutzer das entsprechende Recht zugewiesen. Ist er "-1" bekommt er ein ihm zugeteiltes Recht aberkannt.

Rechte abfragen

Der Trick ist nun, die Rechte eines Benutzers mit einer einzigen Abfrage auslesen zu können - so dass alle Rollen und individuellen Anpassungen berücksichtigt sind. Dies geschieht mit folgender SQL-Query:

 
sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SELECT final.rightID, final.rightName, IF(SUM(final.hasRight)>=1, 1, 0) AS hasRight FROM (
    (SELECT r.rightID, r.rightName, IF(SUM(counter)>=1, 1, 0) AS hasRight
    FROM rights r 
        LEFT JOIN 
            (SELECT rr.*, 1 AS counter
            FROM role_rights rr, account_roles ar
            WHERE ar.roleID = rr.roleID
            AND ar.accountID = 1) rr 
        ON r.rightID = rr.rightID
    GROUP BY r.rightID)

    UNION

    (SELECT r.rightID, r.rightName, a.adjustment AS hasRight
    FROM rights r, account_rights_adjust a
    WHERE   r.rightID = a.rightID
        AND a.accountID = 1)
    ) AS final
GROUP BY final.rightID

Das Ergebnis ist eine Tabelle der folgenden Art:

rightID
rightName
hasRight
1
Artikel anlegen
1
2
Artikel löschen
0
3
Bild hochladen
1

Obenstehende SQL-Query ist für accountID = 1. Also bedeutet die Ergebnistabelle, dass der Benutzer mit der ID "1" das Recht hat, Artikel anzulegen und Bilder hochzuladen, nicht jedoch das Recht, einen Artikel zu löschen.

Was steckt dahinter?

Doch wie funktioniert die Query genau? Wir betrachten sie hierzu stückweise. Die Query ist so aufgebaut, dass zwei innere Queries verbunden werden (mit UNION) und auf diese Verbindung dann ein weiteres SELECT ausgeführt wird.

Teil 1: Rollenbasierte Rechte berücksichtigen

Dieser Abschnitt behandelt folgenden Teil obiger Query:

 
sql
1
2
3
4
5
6
7
8
9
(SELECT r.rightID, r.rightName, IF(SUM(counter)>=1, 1, 0) AS hasRight
    FROM rights r 
        LEFT JOIN 
            (SELECT rr.*, 1 AS counter
            FROM role_rights rr, account_roles ar
            WHERE ar.roleID = rr.roleID
            AND ar.accountID = 1) rr 
        ON r.rightID = rr.rightID
    GROUP BY r.rightID)

Die erste der beiden inneren Queries liest sämtliche Rechte aus und prüft für jedes Recht, ob der Benutzer es aufgrund seiner Rolle(n) besitzt. Dabei wird bewusst ein LEFT JOIN für die Verbindung der Tabellen genutzt, da so alle verfügbaren Rechte im der Ergebnis landen - also auch alle die, die keiner Rolle des Benutzers zugewiesen wurden. Würde man einen normalen JOIN benutzen, würden die nicht zugewiesenen Rechte nicht im Ergebnis erscheinen.

Das Gegenstück der Tabelle rights im LEFT JOIN (also das, womit die Tabelle gejoint wird), ist ebenfalls eine Abfrage. Diese ermittelt sämtliche Rollen eines Benutzers über account_roles und fragt für diese Rollen alle ihnen zugewiesene Rechte ab. Zusätzlich wird eine Hilfsspalte namens counter in die Ergebnismenge eingefügt, die immer "1" enthält. Diese "1" wird später benötigt, um zu zählen, wie oft ein Recht in den Rollen-Zuweisungen auftaucht. Sprich: Ist ein Benutzer zwei Rollen zugewiesen, denen beide Recht 5 zugewiesen wurde, würde SUM(counter) "2" enthalten für Recht 5. Die Anzahl der Zuweisungen ist für die weitere Verarbeitung jedoch etwas hinderlich. Wir brauchen in der Ergebnis-Tabelle lediglich die Information, ob ein Benutzer das Recht hat ("1") oder nicht ("0"). Dies wird mit dem IF-Statement realisiert.

Das Ergebnis dieses Teils der Query ist also eine Tabelle, die sämtliche Rechte enthält und für jedes Recht entweder eine "1" (= der Benutzer hat das Recht aufgrund einer oder mehrerer seiner Rollen) oder eine "0" (= er hat das Recht nicht) aufführt.

Teil 2: Individuelle Rechte-Anpassungen berücksichtigen

Dieser Abschnitt behandelt nun die zweite innere Query:

 
sql
1
2
3
4
(SELECT r.rightID, r.rightName, a.adjustment AS hasRight
    FROM rights r, account_rights_adjust a
    WHERE   r.rightID = a.rightID
        AND a.accountID = 1)

Diese Query ist deutlich einfacher - sie fragt einfach nur sämtliche Rechte-Anpassungen des Benutzers mit der Benutzer-ID "1" ab. Der Join mit der Tabelle rights ist nötig, da im letzten Schritt diese Query mit der vorigen zusammengeführt werden muss und hierzu beide identische Spalten zurückgeben müssen. Da die erste Query "rightID, rightName und hasRight" zurückgibt, muss diese das ebenfalls tun.

Teil 3: Abfragen zusammenführen und Ergebnistabelle erstellen

Als letztes müssen noch die Queries aus Teil 1 und 2 mit UNION zusammengeführt werden. Zusätzlich wird die Summe über die Spalte hasRight für jedes Recht gebildet. Dadurch werden die individuellen Rechte-Anpassungen berücksichtigt. Denn: Wenn ein Benutzer ein Recht aberkannt bekommt, wird ein Wert "-1" in die Datenbank geschrieben. Bildet man nun die Summe, wird zu der "1" (aus der Query für rollenbasierte Rechte) eine "-1" addiert, sodass "0" herauskommt.

Theoretisch könnte es vorkommen, dass ein Benutzer ein Recht aberkannt bekommt, das er zum aktuellen Zeitpunkt sowieso nicht mehr besitzen würde (z.B. weil die Rechte seiner Rolle geändert wurden). Dies würde zu einer "-1" in der endgültigen Tabelle führen. Ebenso ist der umgekehrte Fall denkbar: Ein Benutzer bekommt ein Recht individuell zugewiesen, später wird dieses Recht jedoch auch seiner Rolle zugewiesen. Das Ergebnis wäre hier "2". Aus Konsistenzgründen wird die Ergebnis-Tabelle erneut mit einem IF so verändert, dass für alle Werte >=1 eine "1" und sonst eine "0" erscheint.

Damit hätte man in einer einzigen SQL-Abfrage eine komplette Liste aller Rechte inklusive der Berücksichtigung von Rollen und Spezial-Zuweisungen für einen konkreten Benutzer erzeugt.

Tipp zur Implementierung

Puffern der Abfrage-Ergebnisse

Da diese Query etwas komplexer und größer ist, sollte sie nicht für jede Rechte-Überprüfung ausgeführt werden. Besonders aufgrund der Tatsache, dass die Abfrage sämtliche Rechte zurückliefert bietet es sich an, das Ergebnis während der Laufzeit zwischenzuspeichern.

Wird beispielsweise in einem Script zuerst geprüft, ob der aktuell angemeldete Benutzer Recht 5, danach ob er Recht 3 und anschließend ob er Recht 10 hat, sollte lediglich die erste Rechte-Überprüfung die Datenbankabfrage ausführen. Hier sollte das Ergebnis dann in einem Array bspw. zwischengespeichert werden, sodass alle weiteren Abfragen darauf zurückgreifen können.

Wichtig ist hierbei, dass dieses Array für den jeweiligen Benutzer gespeichert werden muss (beispielsweise als Eigenschaft eines Account-Objekts). Andernfalls würden Rechte-Prüfungen eines Benutzers X womöglich auf die Rechte-Liste von Benutzer Y zugreifen.

Außerdem ist darauf zu achten, dass, falls in einem Script zwischendurch Rechte neu gesetzt werden, das Array geleert wird, damit die neuen Zuweisungen berücksichtigt werden können.

Optimierung der Abfrage

Obenstehende Abfrage liefert neben der Rechte-ID und dem Flag, ob der Benutzer das entsprechende Recht besitzt, auch noch den Namen des Rechts. Im Produktiv-Einsatz ist letzteres oft nicht nötig, da in einem Script meist einfach nur festgestellt werden muss, ob ein Benutzer Recht X besitzt - und nicht wie Recht X benannt ist.

Für obenstehende Query bedeutet das, dass man sich in der zweiten Teil-Query einen Join sparen kann: nämlich den der Adjustment-Tabelle mit der Rechte-Tabelle. Dieser Join dient nur dazu, den Rechte-Namen in der zweiten Teil-Query zu ermitteln. Die optimierte Fassung der gesamten Query lautet somit:

 
sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SELECT final.rightID, IF(SUM(final.hasRight)>=1, 1, 0) AS hasRight 
FROM (
    (SELECT r.rightID, IF(SUM(counter)>=1, 1, 0) AS hasRight
    FROM rights r 
        LEFT JOIN 
            (SELECT rr.*, 1 AS counter
            FROM role_rights rr, account_roles ar
            WHERE ar.roleID = rr.roleID
            AND ar.accountID = 1) rr 
        ON r.rightID = rr.rightID
    GROUP BY r.rightID)

    UNION

    (SELECT a.rightID, a.adjustment AS hasRight
    FROM account_rights_adjust a
    WHERE a.accountID = 1)
    ) AS final
GROUP BY final.rightID

Was sich geändert hat: In sämtlichen SELECT's wurde die Spalte rightName entfernt. Dies ist nötig, da bei einem UNION die beiden zu verbindenden Queries im selben "Format" vorliegen müssen (sprich, sie müssen dieselben Spalten zurückgeben). Zudem wurde der Join in der zweiten Teil-Query entfernt.

Das o.g. Beispiel für das Ergebnis dieser Abfrage enthält somit nur noch:

rightID
hasRight
1
1
2
0
3
1

Fazit

In diesem Artikel wurde gezeigt, wie man ein umfassendes Rechtesystem mit wenigen Tabellen aufsetzen kann. Ferner wurde beschrieben, wie man mittels einer einzigen Query die Daten dieses Rechtesystems auswerten und benutzen kann.

Für Fragen, Kritik und Anregungen kann nachfolgender Kommentar-Bereich genutzt werden :)


Wikiseite bearbeiten

Diese Seite kann von jedem registrierten Benutzer bearbeitet werden. Bisher hat 1 Person an der Seite "Umfassendes Rollen- und Rechte-Management für eigene Anwendungen" mitgewirkt.

Sie haben einen Fehler entdeckt oder möchten etwas ergänzen? Dann können Sie nach der Anmeldung "Umfassendes Rollen- und Rechte-Management für eigene Anwendungen" hier bearbeiten.

Mitarbeiter
  • Max ist schon ein paar Jährchen als treuer User bei Webmasterpro dabei. Seine Interessen hier sind hauptsächlich die Entwicklung von Web-Applikationen sowie hobbymäßige Kreativ-Aufgaben mit Photoshop. In letzter Zeit hat er bedingt durch sein Studium leider nur noch Zeit, ab und zu mal im Reallife-Bereich des Forums herumzugeistern.