Entwurf eines komplexen Authorisierungs-Systems

benneq

Fleet Admiral
Registriert
Juli 2010
Beiträge
12.620
Moin,

ich plane gerade eine Eierlegende-Woll-Milch-Sau zu erschaffen. Prinzipiell ist die Sprache erstmal egal (ich werde es aber später dann (hoffentlich) in Ruby on Rails umsetzen).

Ganz grob geht es um Folgendes: Ich habe ein System mit vielen Usern, ein User kann einer oder mehreren Gruppen angehören und jede Gruppe hat bestimmte Rechte. Das Ganze soll vollkommen dynamisch sein, d.h. alles wird in einer Datenbank abgelegt und muss zur Laufzeit manipulierbar sein. Außerdem muss es auf dem MVC-Pattern aufbauen und somit natürlich objektorientiert geschrieben sein.


Also fangen wir mit einem kleinen Beispiel an:
Code:
CLASS Eintrag {
  METHODE neu { ... }
  METHODE bearbeiten { ... }
  METHODE löschen { ... }
  METHODE anzeigen { ... }
}
Die Datenbank sieht in etwa so aus:
Code:
USER: id
GRUPPE: id
BERECHTIGUNG: id, klasse, methode
### VERKNÜPFUNGS TABELLEN: ###
USER_GRUPPE: user_id, gruppe_id
GRUPPE_BERECHTIGUNG: gruppe_id, berechtigung_id
Jetzt lässt sich einfach festlegen, welche Gruppe auf welche Methoden der jeweiligen Klasse zugreifen darf. Die Methoden beinhalten zum einen natürlich die Logik, aber der Einfachheit halber auch gleich das nötige Formular. Es wäre ja unsinnig, wenn ich einen Eintrag erstellen kann, aber das Formular nicht sehen darf oder andersrum ;)


Nun soll jeder Eintrag noch eine Liste von Kommentaren erhalten:
Code:
CLASS Eintrag {
  METHODE neu { ... }
  METHODE bearbeiten { ... }
  METHODE löschen { ... }
  METHODE anzeigen { ... }
  METHODE getKommentare { ... }
}
Die neue Methode gibt einfach alle zugehörigen Kommentare zurück, jetzt könnte man z.B. in der Datenbank festlegen, dass nur eingeloggte Benutzer das dürfen. Die Klasse dazu sieht im Prinzip wieder genauso aus, wie die der Einträge:
Code:
CLASS Kommentar {
  METHODE neu { ... }
  METHODE bearbeiten { ... }
  METHODE löschen { ... }
  METHODE anzeigen { ... }
}
In Rails könnte man die URLs so betrachten:
Code:
/eintraege/new  # Formular für neuen Eintrag erstellen
/eintraege/{id}/edit   # Formular zum Editieren von Eintrag mit der id={id}
/eintraege/{id}   # URL zum Löschen von Eintrag mit der id={id}
/eintraege/{id}   # URL zum Anzeigen von Eintrag mit der id={id}
/eintraege   # URL zum Anzeigen von allen Einträgen
### ZUGEHÖRIGE KOMMENTARE: ###
/eintraege/{id}/kommentare/new   # Formular um neuen Kommentar zum Eintrag mit id={id} zu erstellen
/eintraege/{id1}/kommentare/{id2}/edit   # Formular zum Editieren von Kommentar mit id={id2} zum Eintrag mit id={id1}
/eintraege/{id1}/kommentare/{id2}   # URL zum Löschen von Kommentar mit id={id2} zum Eintrag mit id={id1}
/eintraege/{id}/kommentare   # URL zum Anzeigen aller Kommentare zum Eintrag mit id={id}
Jetzt kommt das erste Problem: Die Kommentare stellen eine Eigenständige Klasse dar, d.h. ich könnte das Leserecht für die Einträge verweigern, aber innerhalb der Kommentar-Klasse trotzdem lesen. Das Problem lässt sich relativ einfach lösen: Um an die Kommentare zu gelangen MUSS man über die Einträge gehen, dabei wird in der Eintragsklasse die Methode getKommentare aufgerufen. Also sperrt man auch diese Methode über die Datenbank.


Soweit ist alles kein Problem. Nun wird es tricky und ich habe noch keine hunderprozentige Idee, wie ich es umsetzen könnte...:
Angenommen wir haben ein paar User, die in der Gruppe der Moderatoren sind. Die Moderatoren sind die einzigen, die Einträge verfassen, editieren und löschen dürfen. Jetzt kann Moderator1 einen Eintrag verfassen und Moderator2 kann diesen editieren oder löschen.
Also die Frage lautet: Wie bau ich in das Datenbankschema ein, dass in einer bestimmten Klasse, eine bestimmte Gruppe nur das editieren und/oder löschen darf, was sie auch selbst erstellt hat?

Eine Idee, die mir nach langem Überlegen kam, war, dass man dafür eine neue Methode einführt, dann sähe die Klasse quasi so aus:
Code:
CLASS Eintrag {
  METHODE neu { ... }
  METHODE bearbeiten { ... }
  METHODE löschen { ... }
  METHODE anzeigen { ... }
  METHODE getKommentare { ... }
  METHODE eigenenBearbeiten { ... }
  METHODE eigenenLöschen { ... }
}
Innerhalb der Methode kann man dann prüfen, ob der aktuelle User auch der Eigentümer ist. Das hat dann aber mindestens 3 Nachteile:
1. Wenn (aus Leichtsinn) 'bearbeiten' und 'eigenenBearbeiten' in der Datenbank steht, dann darf jeder in der Gruppe immernoch überall reinschreiben. Ist natürlich nur eine Konfigurationsgeschichte und nicht wirklich schwerwiegend.
2. Für das User-Interface fragt man natürlich immer in der Datenbank ab, ob nun auch der Knopf zum 'Bearbeiten' oder 'Löschen' angezeigt werden soll. Da müsste man nun immer auf 2 Variablen prüfen.
3. Das System soll dynamisch werden. D.h. ich will nicht für jede potenzielle Klasse 2 solche Methoden einbauen. Das Ganze müsste/sollte sich auch irgendwie einfach über die Datenbank regeln lassen. Klar, die Einfachste Methode wäre hier noch ein Flag einzuführen:
Code:
USER: id
GRUPPE: id
BERECHTIGUNG: id, klasse, methode, nur_eigene
### VERKNÜPFUNGS TABELLEN: ###
USER_GRUPPE: user_id, gruppe_id
GRUPPE_BERECHTIGUNG: gruppe_id, berechtigung_id
Das wäre soweit natürlich kein Problem und es wird vermutlich recht häufig benutzt, also sollte das eine Byte da ganz gut aufgehoben sein. Aber vielleicht gibt's ja noch eine schönere Lösung?!


Jetzt gibt's allerdings noch ein richtiges Problem:
Angenommen, wir wollen unsere Einträge noch weiter gruppieren - nach Themen. Also erstellen wir eine neue Klasse:
Code:
CLASS Thema {
  METHODE neu { ... }
  METHODE bearbeiten { ... }
  METHODE löschen { ... }
  METHODE anzeigen { ... }
  METHODE getEinträge { ... }
}
Jedes Thema enthält nun also eine Reihe von Einträgen. Die URLs in Rails sähen grob so aus:
Code:
 # Alle Themen anzeigen:
/themen/
# Alle Einträge eines Themas anzeigen:
/themen/{thema_id}/eintraege/
# Alle Kommentare eines Eintrags eines Themas:
/themen/{thema_id}/eintraege/{eintrag_id}/kommentare/
Nun will ich jedem Moderator einen oder mehrere Themenbereiche zuordnen können, in denen sie arbeiten dürfen. Alle anderen Themen sind nur lesbar (oder nicht mal das, soll ja frei konfigurierbar sein)...
Aber wie stell ich das an?

Wenn ich den Moderatoren nun Schreibrechte für Themen gebe, können sie in allen Themen alles tun was sie wollen.
Eine Möglichkeit wäre 4 Methoden in die Klasse zu setzen, eine die schaut ob der Moderator da lesen darf, dann noch eine für's Schreiben, eine fürs Editieren und eine für's Löschen. Das ist natürlich wieder hässlich.
Möglichkeit 2 wäre eine Art Bedingung mit in die DB zu setzen:
Code:
USER: id
GRUPPE: id
BERECHTIGUNG: id, klasse, methode, bedingung
### VERKNÜPFUNGS TABELLEN: ###
USER_GRUPPE: user_id, gruppe_id
GRUPPE_BERECHTIGUNG: gruppe_id, berechtigung_id
Die Datenbank könnte dann so aussehen:
Code:
### USER ###
id = 9
### USER_GRUPPE ###
user_id = 9
gruppe_id = 33
### GRUPPE ###
id = 33
### GRUPPE_BERECHTIGUNG ###
gruppe_id = 33
berechtigung_id = 20
### BERECHTIGUNG ###
id = 20 
klasse = 'Eintrag'
methode = 'neu'
bedingung = 'IF moderators_for_thema CONTAINS user_id WHERE thema_id = eintrag.thema_id'
Um die Moderatoren einem Thema zuzuweisen muss ja eh irgendwo eine Tabelle sein mit den Zuweisungen und genau die habe ich in der Bedingung mit 'moderators_for_thema' angesprochen. Aber so wirklich zufriedenstellend finde ich das Ganze nicht...


Hoffentlich macht sich jemand die Mühe und liest den ganzen Mist und kann mir helfen.

Grüße und Danke!
 
Mach dich mal mit dem Rechtesystem von Linux vertraut.
http://pmhahn.de/linux/permission/index.html
In der Mitte gibt es ein Tool um es leichter zu verstehen.

Kurz gesagt, ich würde jedem Kommentar / Eintrag noch folgendes mitgeben. z.B.
Besitzer: user1
Gruppe: gruppe1
Rechte: 750

Der Besitzer darf alles machen, die Gruppe darf allerdings nur lesen und jeder andere darf gar nichts.
 
Schreit nach ACL.

http://de.wikipedia.org/wiki/Access_Control_List

Sprich du hast Benutzer, jeder Benutzer hat eine RollenId. Diese sind in einer anderen Tabelle hinterlegt mit namen. Dann hast du noch 3 weitere Tabellen. Eine für actions, diese können zum beispiel "list","create","edit","delete" sein. dann eine mit resourcen, sei es seiten auf die man zugreifen kann oder dateien. Beispiele sind "admin","shop","benutzerverwaltung".
Die letzte Tabelle ist ein zusammenschluss. Dort sagst du RollenId 1 darf auf Resource 1 ("benutzerverwaltung") benutzer löschen (action "delete")
 
@Suxxess: Ich nutze seit über 10 Jahren Linux ;) Und das System ist ungenügend, weil es auch möglich sein muss dass ich einer Aktion z.B. 2 User zuweisen möchte. Nimm einfach mal das Beispiel, dass sich 2 Leute einen Blog teilen. Klar könnte man die in die selbe Gruppe stecken und dann der Gruppe die Rechte geben, aber dann kommt eben das nächste Beispiel: Ich möchte 2 Gruppen die Rechte geben.

Das Problem wird an sich mit Hilfe von ACL gelöst, wie chriz0101 schon sagt.

...
...
...
Ich glaub ich war gestern einfach viel zu übermüdet :D Denn das Problem existiert eigentlich gar nicht! :D

Also angenommen, ich habe meine Einträge, die nach Themen sortiert sind. Dann habe ich in meinem MVC-Code natürlich diese Beziehung irgendwo eingebaut. Und wenn ich die Moderatoren einem oder mehreren Themen zuweisen möchte, dann muss das auch irgendwo im Model vermerkt sein.
Code:
### DATENBANK ###
USER: id
THEMA: id
EINTRAG: id, thema_id
### VERKNÜPFUNG ###
EINTRAG_THEMA: thema_id, eintrag_id   # Ein Eintrag kann zu mehreren Themen gehören
USER_THEMA: user_id, thema_id   # Wer moderiert das Thema?
Das heißt, dass es mit ACL funktionieren müsste (evtl. dann mit dem zusätzlichen Flag, dass sich diese Option nur auf die eigenen Beiträge bezieht)


EDIT: Der Wahnsinn geht weiter:
Ich hab mir eben das CanCan Framework für Rails angeschaut, die haben es so gelöst, dass sie in einer Datei festlegen, wer was darf:
Code:
if user.is_admin?
  can :manage, :all
elsif user.is_moderator?
  can :read, :all
  can :create, Article
  can :update, Article
  can :delete, Article
else
  can :read, :all
end
'manage' ist die Abkürzung für read, create, update, delete. Zusätzlich lassen sich auch Bedingungen benutzen:
Code:
can :read, Project, user_id: current_user.id
Also der Benutzer kann jedes Projekt lesen bei dem 'user_id' gleich der ID des eingeloggten Users ist.
Code:
can :read, Project, category: { visible: true }
Also der Benutzer kann auf ein beliebiges Projekt zugreifen, wenn die Kategorie, zu der das Projekt gehört, visible=true hat.
Code:
can :manage, Project
cannot :destroy, Project
Der User kann also alles mit jedem Projekt machen, außer Löschen.

Damit kann man schon mal lustig quer durch die Datenbank seine Bedingungen setzen. Im Prinzip ziemlich einfach. Schwieriger wird es nun, wenn man das Ganze dynamisch machen will... Und vor allem das Speicherformat. Vermutlich sollte man weg von der Datenbank und es als ausführbaren Code speichern oder?
 
Zuletzt bearbeitet:
Zurück
Oben