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

3 | 12 Kommentare | 16659 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.

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

Neuen Kommentar schreiben
Wie in einem Array speichern?
Beantworten

Hi, Wie kann ich das in einem Array schreiben und wieder abrufen? könntet ihr mir da mal einen Tip geben? :)

haut bei mir nicht wirklich hin

nobbie am 21.04.2013 um 17:49
SQL-Dump
Beantworten

Genial, jedoch - obwohl ich mich nun doch schon eine Weile mit MySQL beschäftige, bekomme ich einfach keine vernünftigen Ergebnisse aus dem Query. Wahrscheinlich liegt es daran, dass ich das mit der role_rights irgendwie nicht kapiere ....

Also, schön wäre ein beispielhafter SQL-Dump indem ein paar Rollen mit den Rechten zu sehen sind. Bitte bitte :)

Danke und Sorry.

schamane am 16.04.2010 um 20:31
Re: SQL-Dump
Beantworten

Mh, hat nach etwas Fummelei doch noch geklappt. Eine Lösung von esviko wäre natürlich auch schön. Ansonsten: Vielen Dank für den Artikel :)

schamane am 17.04.2010 um 18:58
Re: SQL-Dump
Beantworten

Meinst du das Problem das du hattest könnten auch andere haben? Dann schreib doch eben kurz wo's gehakt hat und die dazugehörige Lösung :)

Ansonsten: Danke, freut mich :)

Max B am 21.04.2010 um 09:18
Re: SQL-Dump
Beantworten

Ach, das war nur ein dummer Fehler - ich habe zwar in der Zeile 17 mit verschiedenen ID's rumprobiert, dann aber vergessen, diese auch in Zeile 9 anzupassen. Einfach nur ein dummer Flüchtigkeitsfehler - selbstverständlich müssen dann beide ID's angepasst werden.

Momentan bin ich daran, eine zweite Tabelle mit abzufragen - also z.B. könnte es in Deinem Beispiel eine Rechtetabelle dieseRights geben und dazu auch noch eine Tabelle jeneRights. In der role_rights und in der role_rights_adjust müssen deshalb ein weiteres Feld für die Tabelle jeneRights eingefügt werden - und - naja, eine intelligente Selectabfrage für diese Tabelle. Naja, und da sitze ich und sitze ich :)

Das Problem ist, das Dein Beispiel so genial ist, dass man dadurch richtig Lust bekommt das aufzubohren (wenn man denn kann).

Also nochmal - super Beispiel! Und Danke!

schamane am 04.05.2010 um 20:16
Erweiterung für Gruppen-Management
Beantworten

Hallo allerseits,

bin noch recht neu in der Materie Rollen- und Rechte-Management. Stehe vor der Herausforderung, das ganze Ding auf mehrere Gruppen auszuweiten - d.h. in einem Forum soll es mehrere 'Sub-Foren' geben. Ein Admin des Sub-Forum A soll im Sub-Forum B nicht zwangsläufig die Rechte haben, die er im Sub-Forum A hat. Darüberhinaus soll es einen SuperAdmin geben, der 'seine Nase überall reinstecken' können soll ;-)

Meine Idee/Ansatzt sieht in etwa so aus:

neue Tabelle:

groups [groupID (int), groupName (int)]

Die Tabelle

account_roles [accountID (int), roleID (int)]

wird erweitert

account_roles_groups [accountID (int), roleID (int), groupID (int)]

Die Tabelle

account_rights_adjust [accountID (int), rightID (int), adjustment (int)]

wird erweiter

account_rights_adjust [accountID (int), rightID (int), groupID (int), adjustment (int)]

... was denkt ihr?

esviko am 15.03.2010 um 15:36
Re: Erweiterung für Gruppen-Management
Beantworten

Puh, müsste ich mich mal in Ruhe reindenken. Wenn du einen funktionierenden Entwurf hast kannst du ihn ja oben mal unter "Erweiterung" oder so einfügen. Ist ja ne Wiki-Seite.

Oder du machst n neuen Artikel auf und linkst den in diesen hier rein, um die Trennung klarer zu halten

Bin mal gespannt! Sicher ne sinnvolle Idee!

Max B am 15.03.2010 um 21:56
Super Artikel
Beantworten

Ein sehr schönes Systems was ich bei mir auch implementieren möchte.

Nur habe ich mal eine Frage, bei "role_rights" muss ich für jedes Recht einer Rolle eine neuen Eintrag hinzufügen?

roleID 1 | rightID 4 (Artikel schreiben)
roleID 1 | rightID 6 (Bilder hochladen)

Nach diesem Schema?

Roman Martin am 10.07.2009 um 12:21
Re: Super Artikel
Beantworten

Danke :)

Ja, in der Tabelle "role_rights" müssen einer Rolle alle Rechte zugewiesen, die die Rolle besitzen soll.

Max B am 10.07.2009 um 12:29
Re: Super Artikel
Beantworten

ok, da fehlt noch ein "werden" im Satz, aber ich denke die Aussage ist klar ;)

Max B am 10.07.2009 um 12:30
Dankeschön
Beantworten

Dankeschön, das ist leicht verständlich. Schöne, flexible Lösung!

Carsten Witt am 23.06.2009 um 19:35
titel.
Beantworten

Klasse Artikel, mir fallen keine Fehler auf und die herrangehensweise gefällt mir.. sowas ähnliches werde ich sicher mal gebrauchen können, danke!

Benutzer gelöscht am 16.06.2009 um 13:55