About Modellierung

This page contains an archive of all entries posted to /blog/wvk in the Modellierung category. They are listed from oldest to newest.

Linux is the previous category.

Philosophisches und anderer Schmalz is the next category.

Many more can be found on the main index page or by looking through the archives.

Powered by
Movable Type 3.31

Main

Modellierung Archives

September 17, 2007

Apache+/-MySQL

Der mächtige Indianer stellt sich in Sachen Konfiguration via MySQL etwas quer. Zwar gibt es drei Module, die die VirtualHost-Verwaltung über MySQL ermöglichen sollen, aber es sieht noch nicht danach aus, als kämen diese in Frage für meinen Server. Das eine Projekt, mod_v2h scheint schon länger tot zu sein. Das zweite, mod_vdbh, hab ich vor einiger Zeit mal zu installieren versucht, aber bin gescheitert (OK, ich habe mich vielleicht nicht soo intensiv bemüht...). Das dritte und meistversprechende Projekt ist mod_vhs. Es schien mir bislang etwas konfus von der Konfiguration her, aber ich überlege ernsthaft, es einmal intensiver auszuprobieren.
Allen Modulen gemein ist, dass es keine fertigen DEB-Binaries gibt, was die Systempflege etwas aufwändiger macht. Da nicht die Systemeinrichtung und die damit verbundenen Eingriffe ins Innenleben mancher Software, sondern die Entwicklung einer übergreifenden Hostingserver-Administrationsoberfläche Ziel dieser Arbeit ist, würde ich gerne so weit es geht auf nicht im Standard-Debian System enthaltene Software verzichten. Das bedeutet, dass ich vorerst mit normalen Apache-Konfigurationsdateien arbeiten und mir die Möglichkeit, VirtualHosts zentral über die Datenbank abzuwickeln, für später aufheben werde.
Aus diesem Grund habe ich mir letzte Woche einen Parser für Apache-Configdateien geschrieben. Momentan kann der noch nichts weiter als syntaktisch korrekte Configdateien einlesen, die einzelnen Werte in einer Baumstruktur abpeichern (siehe Klassendiagramm) und veränderte sowie unveränderte Daten in der korrekten Reihenfolge wieder zurückspeichern, aber das wird sich sehr bald ändern. Als nächstes werde ich eine einfache Oberfläche zur Bearbeitung der Werte erschaffen, danach eine saubere Möglichkeit zur Validierung der Werte. Es wäre schön, wenn die ganzen Konfigurations-Direktiven sich so handhaben ließen als wären es ActiveRecord-Objekte aus einer Datenbank, aber ob das nicht vielleicht zu aufwändig zu implementieren ist (und wie sinnvoll), muss noch erörtert werden...

Das Klassendiagram "so far":
class%20diagram.png

Das UML-Modell zum Config-Parser gibt's unter http://chaos.hobby-astronomie.net/dipl/configParser/configParser.html in seiner jeweils aktuellsten Version. Die XMI-Variante gibt's unter http://chaos.hobby-astronomie.net/dipl/configParser.xmi

September 19, 2007

Ruby + Mailman

Mailman ist eine sehr mächige Mailinglisten-Verwaltungssoftware, außerdem eine der beliebtestens in der Unix-Welt. Weiterhin stellt diese Software eine der Hauptmotivationen für meine eigene Serververwaltungssoftware dar: Sie ist nicht mittels z.B. Confixx administrierbar.
Mailman bietet eine umfangreiche Webbasierte Administrationsoberfläche, die aber relativ fest mit der Software selbst verwachsen ist (das Grundgerüst der Seite ist fest im Python-Quelltext verdrahtet). Man hat zwar die Möglichkeit, HTML-Vorlagen pro Liste anzulegen, aber so ganz frei ist man in der Seitengestaltung nicht. Außerdem ist die Integration der Mailinglistenverwaltung in eine andere Administrationsumgebung schwierig.

Mailman ist in Python geschrieben, einer Sprache die Ruby nicht unähnlich ist.

Mailman bietet eine Menge an Konsolenskripten zur Mailinglistenadministration an.
"Hey," dachte ich mir da gestern, "Dann ist es ja ein leichtes, die Änderungen an Mailinglisten vorzunehmen indem ich einfach diese Skripte von meinem Ruby-Programm aufrufe". Möglich wäre es auf jeden Fall, aber bei jeder Änderung erst eine Instanz einer Shell zu erstellen, dann diesem einen zusammengefrickelten string zu übermitteln und den Rückgabewert abzufangen... das ist nicht sauber.
Aber es gibt eine begeisterungsfähige Lösung, auf die ich soeben gestoßen bin:
Mailman bietet ein Skript namens withlist, wo entweder interaktiv via Konsole, oder aber (und darum geht es mir!) programmatisch, also mittels eines eigenen Python-Scripts, direkt Änderungen an Mailinglisten vorgenommen werden können. Nun ist Python aber nicht Ruby, doch dafür gibts eine Bibliothek namens Ruby/Python. Damit lassen sich Python-Objekte und -Funktionen in Ruby wie Ruby-Objekte/-Methoden verwenden.
Was werde ich nun also tun? Ich werde das withlist abspecken und nach Ruby portieren, dann mittels Ruby/Python die von Mailman zur Verfügung gestellten Libs einbinden und dann eine perfekt in Consolvix integrierte Mailinglistenadministrationsoberfläche (38 chars! ;-)) haben.

<added date="2007-09-20 20:39 UTC+2">Leider hat das alles nicht so geklappt, wie ich es mir vorgestellt hätte. Python/Ruby lässt sich nach Anpassen der Bibliothekspfade zwar einwandfrei kompilieren, aber sobald man davon in einer .rb-Datei gebrauch machen möchte, wirft die python.so fröhlich mit Segfaults und Meldungen über fehlende Objektreferenzen um sich. Nunja, ein Versuch war's wert. Ich werde jetzt doch die Methode mit den Shellscripten anwenden, auch wenn das einiges an zusätzlichem Overhead bedeutet...</added>

Nur leider geht eines nicht, und zwar die Verlagerung der Mailman-Konfiguration in eine Datenbank. Es gibt zwar Bemühungen diesezüglich, aber die Beziehungen seien einfach zu komplex laut eines Beitrags in der Mailman-Mailingliste. Ob ich die Daten dennoch redundant in einer Datenbank abspeichere, ist noch nicht entschieden. Vermutlich jedoch eher nicht, da sonst die Listenadministration via Mailman's eigener Oberfläche oder via E-Mail nicht mehr abgeglichen würde. Natürlich könnte man ein Synchronisations-Script schreiben, welches ab und an per Cron getriggert wird, aber dann sind wir ja wieder da, wo ich eigentlich von weg wollte.

September 20, 2007

Benutzerverwaltung

Nach einem befruchtenden Gespräch mit Herrn Prof. Klingspor am heutigen Nachmittag bin ich nun in einer zentralen Frage weitergkommen, die im wesentlichen die Hauptbremse für weitere Überlegungen zum Zentralen Modul meiner Software darstellte.
Die Frage: wie gestalte ich meine Benutzerverwaltung? Genauer: Wie ist die Abhängigkeit zwischen den betreffenden Klassen?
Das Problem: Das Gesamtsystem unterscheidet zwischen den folgenden Benutzer-Arten: Systembenutzer (der UNIX-Systemaccount), Hosting-Benutzer (ein Systembenutzer mit der Möglichkeit, Seiten zu hosten und weitere e-Mailadressen anzulegen; verwendet von der Webanwendugn und dem System), E-Mail-Account (sowohl für Systembenutzer als auch virtuell; verwendet von der Mailsoftware), Systemadministrator (vermutlich auch ein Systemaccount, aber mit zusätzlichen Administrationsrechten innerhalb von Consolvix), Reseller (Ein Hostungbenutzer mit der Möglichkeit, weitere Webbenutzer zu administrieren).
Die Lösung: Erst hatte ich gedacht, das als Klassendiagramm vielleicht so abzubilden:
user_classes_inherit.png
Die Vererbung würde dann durch Single Table Inheritance geregelt. Für jede Art der Authentifizierung und Authorisierung würde dann auf die Methoden/Eigenschaften des "User"-Elternobjektes zurückgegriffen werden. Durch Setzen des entsprechenden "type"-Feld in der Datenbanktabelle könnte man dan auch sehr einfach einen Hostingbenutzer zum Reseller machen usw. Nachteil ist allerdings, dass diese eine Benutzertabelle eine große Zahl an NULL-Werten aufweisen würde und Äpfel und Birnen gewissermaßen alle in einen Topf mit "Obst" geworfen würden.

Nach einiger Überlegung, wie die E-Mailaccounts verwaltet werden sollten, kam ich zu der Entscheidung, dass die sauberste Lösung durch möglichst wenig Vererbung und mehr Aggregation/Komposition erreicht wird:
user_classes_final.png
So gibt es nun eine abstrakte "Benutzer"-Elternklasse. Diese ist letztendlich nur für den E-Mailversand interessant, denn sowohl virtuelle E-Mailbenutzer (alle mit der UID/GID 5000) als auch Systembenutzer besitzen ein E-Mailkonto. Auf Datenbankebene wird diese "Klasse" als View implementiert, welche die benötigten Attribute aus der Systemenutzer- und E-Mailkonten-Tabelle zusammenführt. Auf SW-Ebene kann man sagen, dass Sowohl die virtuellen als auch die Systembenutzer eher ein E-Mail-Konto "implementieren" als "sind", insofern macht es Sinn, den EmailUser als Interface oder abstrakte Klasse zu betrachten.
Die Systembenutzertabelle wird sowohl vom PAM-System als auch von Consolvix zur Authentifizierung verwendet.
Da die Zusatzangaben, wie z.B. alles was mit Hosting und Administrationsrechten zu tun hat, nicht wirklich logisch zur Klasse "Benutzer" gehören, werden diese nun aus anderen Tabellen aggregiert. Dass dieses Design wesentlich flexibler ist als eine strikte Klassenhierachie, dürfte dem geschulten Auge sofort klar sein ;-). Ich werde den Sachverhalt in der Diplomarbeit wohl noch weiter ausführen.

September 22, 2007

MySQL + ProFTP, oder: Consolvix + FTP?!

Es begab sich zu einer Zeit, als das ARPANET noch blühte und ein Hacker noch ein Hacker und kein von kriminellen Konotationen bevorurteilter Zeitgenosse war, als -- aus heutiger Sicht -- eines der größten Verbrechen der Computer-Industrie begangen wurde: das File Transfer Protocol wurde erfunden und standardisiert. Seitdem sind vermutlich Hunderttausende armer Internetznutzer bereits Opfer der -- aus heutigen Sicht -- hirnrissigen Tatsache geworden, dass FTP ein Plain Text-Protokoll ohne Kennwortverschlüsselung ist. Und während Telnet mittlerweile ein Schattendasein im Lichte von SSH führt (tut es das?!), findet sich noch immer auf jedem Shared Hosting System standardmäßig ein FTP-Zugang für jeden. Von SFTP, RSync oder WebDAV über SSH, SCP und Fish scheint noch nie ein Mensch zuvor gehört zu haben.

Ein Grund mehr also, für mein "imaginäres" Hostingunternehmen Consolving ein weiteres Alleinstellungsmerkmal aufzupolieren: "Bei uns greifen sie stets über erwiesenermaßen sichere Verschlüsselungstechnologien auf Ihre Daten zu!" -- klingt irgendwie gut, weckt Kundenvertrauen, macht das Ganze für erfahrenere Internetnutzer umso attraktiver usw.

Also ran und gleich mal SFTP eingerichtet, das ist mit RSSH (Restricted SSH) sehr einfach. Einfach in /etc/rssh.conf eintragen, dass alle betreffenden Benutzer rsync, scp und sftp nutzen dürfen, und bei den betreffenden Benutzern /usr/bin/rssh als Shell eintragen. Einen Haken hat die Geschichte, und die kann sehr wohl auch zu einem internen Sicherheitsproblem werden: SSH erlaubt es in der "out of the Box"-Variante nicht, die Benutzer in ihr Homeverzeichnis zu "Chrooten" (man verzeihe bitte die vielen Sprachvergewaltigungen an dieser Stelle...). Für eine Shared Hosting-Umgebung mit mehreren Duzend oder gar Hunderten von Kunden ist das natürlich unhaltbar.

Schließlich bin ich doch wieder auf FTP zurückekommen, denn was ProFTP so alles bietet (MySQL-Unterstützung, umfangreiches Quota-Handling, detaillierte und flexible Zugriffsverwaltung pro Host, Gruppe, Klasse, Benutzer, Verzeichnis bis hin zur einzelnen Datei), deckt sich perfekt mit meinen Systemanforderungen. Und als Sagnehäubchen kommt mit mod_tls auch noch verschlüsseltes FTP (FTPS, nicht zu verwechseln mit SFTP, welches ein SSH-Subsystem ist!) hinzu. Das einzige Problem mit FTPS ist, dass längst nicht jeder FTP-Client es untersützt, und außerdem nicht auf Port 20/21 (Daten/Steuerung), sondern auf Port 989 und 990. Diese werden aber duch die meisten Firewalls standardmäßig blockiert (*winkewink* an DVZ@FH Bochum! *grml*) und während viele Internetnutzer bei "Port 21" noch eine leise Glocke bimmeln hören, dürfte "Port 990" vollkommene Ahnungslosigkeit hervorrufen.
Nichtsdestoweniger habe ich nun probehalber einen sicheren FTP-Server eingerichtet (sicher heißt, eine Verbindung ist ausschließlich über TLS möglich) und das funktioniert soweit einwandfrei. Dass in einer Willkommensmail an zukünftige Kunden erklärt wird, wie dieser mit dieser vom de Facto-Standard abweichenden Konfiguration umzugehen hat, versteht sich von selbst :-)

Was nun als nächstes mit Hilfe von ProFTP sehr elegant umgesetzt (und in Consolvix integriert!) werden wird, ist ein Quotasystem und die Benutzerverwaltung. ProFTP ermöglicht den Einsatz virtueller Benutzerkonten über MySQL, was es auch sinnvoll und attraktiv macht, jedem Kunden beliebig viele FTP-Zugänge zur Verfügung zu stellen. Die Idee dahinter: Der Kunde (sprich der ihm zugeordnete Systembenutzer) erhält maximal Q MB Plattenspeicher zur Verfügung gestellt. Das wird in die Quotatabelle eingetragen (und nicht, wie ich erst plante, in die Benutzertabelle). Der Benutzer kann nun neue, virtuelle FTP-Benutzer anlegen und ihnen X MB (< Q, natürlich) Platenplatz zuordnen, bis ΣX = Q. ProFTP's Quotasystem sorgt dafür, dass sowohl die einzelnen Benutzer ihr X, als auch alle Benutzer zusammen (vorgegeben über eine Gruppen-Quota-Regel in der Quotatabelle) Q niemals oder nur kurzzeitig überschreiten.

Durch die geschickte Verwendung von Aggregation (Klassendiagramm folgt später) und die Verwendung der passenden Views auf DB-Ebene kann können so buchhalterische Vorgaben und deren technische einhaltung durch Setzen eines einzigen Wertes, also ohne jegliche Möglichkeit der Dateninkonsistenz, umgesetzt werden.
Doch damit nicht genug: Apache ermöglicht es, Loggingausgaben direkt in eine MySQL-Datenbank zu schreiben. Um das (z.B. monatliche) HTTP-Transfervolumen eines Kunden zu ermitteln, braucht man also nicht mehr als ein SELECT SUM(size) FROM transfer WHERE hostname=[host eines Kunden].

So langsam fängt sie Sache an, wirklich Spaß zu machen ;-)

September 23, 2007

Neues aus der Modellier-Ecke

Ich möchte mal behaupten, dass die letzte Nacht und der heutige Tag äußerst erfolgreich verlaufen sind. Gestern Nacht stieß ich bei der Suche nach etwas ganz anderem auf eine Sache, die ich eigentlich schon aufgegeben hatte: mod_vhost_dbi. In Kombination mit mod_dbi_pool ermöglicht es Apache2-VirtualHosts in eine beliebige (!) Datenbank zu speichern. Das Modul ließ sich einwandfrei Kompilieren und funktionierte auf Anhieb. Nun dachte ich also, doch von den lästigen Apache-Konfigurationsdateien los zu kommen. Freude!
Doch die ernüchterung kam gegen 2:00 Uhr: tatsächlich lassen sich nur der DocumentRoot, der User (Sichwort SuExec) und der ServerName einstellen. Grundsätzlich reicht das für eine einfache Konfiguration auch aus, aber gerade wenn man wegen Rails o.Ä. einige Spezialeinstellungen vornehmen möchte, muss man dann doch wieder auf die herkömmliche hartkodierte Variante zugreifen.
Gegen 2:10 Uhr entschloss ich mich dann dazu, mal einen Blick in den Quelltext des Moduls zu werfen. Tatsächlich habe ich es dann auch geschafft, immerhin noch 1-2 Zusatzsangaben wie den ServerAdmin in die Datenbank zu verfrachten. Als ich allerdings gegen 5:40 mit dem Durchpflügen der Apache Runtime API weitestgehend fertig war, war mir klar dass es einen guten Grund gibt, warum ein so praktisches Modul zur VortualHost-Verwaltung in einer Datenbank nicht in dem Umfang existiert, wie ich es mir wünschen würde: Es ist unglaublich aufwändig zu programmieren, da die Apache-API (bzw. eigentlich die betroffenen Module) dafür nicht an den benötigten Stellen "Hooks" (kann man als Broadcast-Funktionsaufrufe sehen, wo jedes Modul sich an bestimmte Stellen ins Servergeschehen einhängen kann) aufweisen. Ich habe bei einem anderen Modul (mod_vhost) versucht, etwas Quelltext abzukupfern, aber für einen Sonntagvormittag nach nur 6 Stunden Schlaf wurde es einfach zu schnell zu komplex.
Dennoch werde ich nun auf die VirtualHost-in-Datenbank-Lösung setzen, und alle weiteren Einstellungen nicht in der Datenbank, sondern über .htaccess-Dateien im Homeverzeichnis der User erledigen (sofern ein globaler default nicht ausreicht).

Da nun Apache-VirtualHosts auch als ActiveRecord-Objekte in Consolvix auftauchen dürften, habe ich mir überlegt, dass es Sinn macht, jedem Benutzer die Möglichkeit zu geben, beliebig viele ("beliebig" im technischen, nicht buchhalterischen Sinne) VirtualHosts innerhalb seines Homeverzeichnisses anzulegten. Das macht für Subdomains etc. durchaus Sinn. In Kombination mit den ProFTP-VirtualHosts (praktischerweise verwenden Apache und ProFTP die selbe Syntax in ihren Config-Dateien, dafür kann ich also die bereits vorhandene Klassensamlung verwenden :-)) kann ein Kunde nun also mehrere unterschiedeliche Domains in mehreren Unterverzeichnissen hosten und dazu noch mehrere FTP-Zugänge (beliebig viele) einrichten -- macht z.B. für eine Vereinsseite Sinn.
In diesem Licht macht die Betrachtung Unterscheidung HostingUser/SystemUser/FTPUser wenig Sinn, ebenso wie wenn ein SystemUser z.B. Webhosting-Eigenschaften Aggregiert. Ich betrachte VirtualHosts nun einfach als zusätzliche Konten: Ein normaler SystemUser bekommt automatisch (durch das Datenbankschema bedingt) einen Master-FTP-Zugang zu seinem Homeverzeichnis. Je nach dem welche Rechte für ihn freigeschaltet sind, kann er nun weitere FTP-Accounts für Unterverzeichnisse anlegen und diese (sofern berechtigt) für den HTTPD freigeben oder sperren.

So, und hier eine kleine Klassenübersicht:

SystemClasses.png

October 3, 2007

Ein paar Gedanken zu Benutzerrollen

Da neulich ja schon das Thema Benutzerrollen angesprochen wurde, habe ich nun mal ein paar Spezifikationen zur Benutzerverwaltung in meiner Software zusammengetragen. Diese Liste ist vorläufig, sie kann sich jederzeit ändern.

Folgende Benutzer-Rollen sind in Consolvix vorgesehen: Buchhaltungs-Administrator, Systemadministrator, Kunde, Reseller, E-Mailbenutzer, Hostingbenutzer, FTP-Benutzer, Systembenutzer. Rollen können sich überschneiden; manche Rollen sind Unter-Rollen von anderen Rollen. Buchhalterische und technische Rollen sind von einander getrennt.

Detaillierte Beschreibung

Der Kunde
erfüllt rein buchhalterische Aufgaben wie Abrechnungswesen, Domain-Eigentum etc. Der Kunde übernimmt die rechtliche Verantwortung für alle ihm zugeordneten Benutzer-Accounts und deren Inhalte. Im System (also technisch) spielt der Kunde keine Rolle.
Der Systembenutzer
ist die zentrale Entität. Jede Art von (Hosting-)Account wird durch Erweiterung der Rechte des Systembenutzers gebildet. Ein Systembenutzer erhält ein eigenes Heimat-Verzeichnis und einen System-E-Mailaccount (@). Zugriffsrechte, Quota-Regelungen etc. werden an den Systembenutzer vergeben. Der Systembenutzer spielt sowohl im (Betriebs-)System als auch in der Verwaltungssoftware (Consolvix) eine wichtige Rolle.
Der Systemadministrator
ist eine nur im Kontext der Verwaltungssoftware existierende Entität, die sämtliche Zugriffsrechte sowohl auf administrativer (kann sämtliche Konfigurationseinstellungen vornehmen) als auch auf Benutzerdatenebene besitzt. Der Systemadministrator hat jedoch a priori keinen Zugriff auf buchhalterische Daten (lässt sich durch eine Konfigurationsoption aber ändern). Administrationsrechte können Modulweise vergeben werden.
Der Buchhaltungs-Administrator
ist verantwortlich für das Rechnungswesen. Im Rahmen der Diplomarbeit nicht weiter relevant.
Reseller
sind Kunden mit zusätzlichen Rechten: sie dürfen andere Kunden und Systembenutzer anlegen und sind für deren Verwaltung verantwortlich. Reseller können im Rahmen ihrer eigenen Verbrauchsgrenzen (Quotas) auch solche für ihre eigenen Benutzer anlegen. Sie können jedoch keine technischen Verwaltungsaufgaben übernehmen (s. Sysadmin). Ein Reseller muss mind. einen Systembenutzeraccount und eine Domain besitzen
E-Mailbenutzer
sind in erster Linie für den E-Mailserver relevant. Über die Verwaltungssoftware können Konten erstellt, konfiguriert und gelöscht werden, das Abrufen von E-Mails geschieht jedoch über vorerst nur über den Mailserver direkt (via IMAP oder POP3).
Hostingbenutzer
sind Systembenutzer mit der Möglichkeit, Inhalte im Web freizugeben, sprich VirtualHosts einzurichten.
FTP-Benutzer
sind virtuelle Benutzer, die auf Systemebene nur für den FTP-Server relevant sind. Auf Verwaltungsebene können einem Systembenutzer beliebig viele FTP-Benutzerkonten zugeordnet werden.

Die Rollen werden hier nicht (nur) über einzelne Rechte-Flags, sondern teilweise über ganze eigene Entitäten in der Datenbank definiert. Beispielsweise wird ein Systembenutze rnicht einfach zum Hostingbenutzer indem er Hosting-Rechte über ein Boolean Flag freigeshaltet bekommt, sondern indem das Systembenutzer-Objekt einen ganzen Satz neuer Eigenschaften aggregiert, die diese Rechte und Grenzen quantitativ erfassen. Ebenso beim Reseller: ein ganzer Datensatz in der Reseller-Tabelle beschreibt, welche neuen Rechte ein Reseller-Kunde bekommt. Fehlt dieser Datensatz (bzw. ist der Fremdschlüsselverweis im Kunden-Objekt gleich NULL), dann handelt es sich nicht um einen Reseller-Kunden.

Ein paar Anforderungen:

  • Jeder Kunde bekommt 1...n Systembenutzer zugeordnet; Standard ist 1.
  • Ein Kunde kann Reseller-Rechte bekommen; Reseller sind Kunden mit Reseller-Rechten.
  • Ein Reseller ist berechtigt, andere Kunden und Systembenutzer anzulegen.
  • Jeder Kunde gehört zu genau einem Reseller.
  • Jeder Benutzer kann sein eigenes Kennwort/persönliche Daten ändern, Reseller die ihrer Kunden, Kunden die aller ihnen direkt zugeordneter Benutzer
  • Benutzer- und Resellerkonten können gesperrt werden.
  • Bei Sperrung eines Resellerkontos werden dessen Kundenkonten ebenfalls gesperrt
  • Für einen Kunden können alle E-Mail, FTP- und Systemkonten einzeln oder kollektiv gesperrt werden.
  • Kunden haben ein Benutzerkonto für das Online-Vertrags- und Rechnungswesen.

October 7, 2007

Unklare Gedanken zum Thema "Module"

Es folgt eine wirre Sammlung an Gedanken zum Thema Module/Plugins. Ideen und Ratschläge sind stets herzlich willkommen!

Ein Modul betseht aus einer Menge von Klassen, die Aufgaben aus einem Bestimmten Bereich übernehmen sollen. Beispiele:
- HTTP: CRUD von Virtual Hosts, sperren von VirtualHosts, CRUD von Subdomains, Zuweisung von Domains zu VirtualHosts, setzen von Zugriff-Limits (Quota)
- FTP: CRUD von FTP-Accounts, setzen von Quota
- Benutzerverwaltung ...

Fragen:
- Soll jedes Modul Zugriff haben auf sämtliche in der gesamten Software bestehenden Models/Controller?
- Welche Authorisierungs-Methoden braucht jedes Modul?
- Welche Module müssen einander kennen?
- Welche Module sind voneinander abhängig?
- Macht es Sinn, abhängige Module zu gestalten, oder ist das ein Zeichen für schlechte Aufgabentrennung?
- Lassen sich alle Aufgabenbereiche überhaupt so genau abgrenzen? [s. erste Frage]
- Sollen Module aus Benutzer-, Administrator- oder Entwicklersicht erstellt werden? Ein Benutzer sieht den Unteschied zwischen einer "Domain", einem "VirtualHost" und einem "FTP-Account" vielleicht nicht (=> möglichst einfaches Interface, gutes Maß an Abstraktion der darunterliegenden Technologien/Anwendungen, hohes Maß an Automatisierung und sinnvolle Default-Werte). Ein Administrator sollte (?) jedoch tiefergehende Möglichkeiten haben und auf Applikationsebene Einstellungen vornehmen können (=> möglichst viele Einstellungsmöglichkeiten, auch auf Technischer Ebene). Ein Entwickler benötigt hingegen eine streng abgegrenzte Schnittstelle auf Code-Ebene, betrachtet "das Model Domain" und "das Model VirtualHost" als Datenbankentitäten usw. (=> ?????)
- Module sollen natürlich (?) bei der Installation keine Änderungen an anderen Modulen vornehmen dürfen. Um eine gute Erweiterbarkeit der Funktionalität bei stark vernetzten Modulen zu gewährleisten, müsste man Gebrauch machen von Callback-Funktionen. Rails bietet hierzu ein hervorragendes Mittel: Filter-Funktionen. Beispielsweise kann man Filter-Objekte (oder Methoden, Procs, ...) bauen, die vor, nach, oder um Methoden herum aufgerufen werden (also davor und danach), oder den Aufruf einer Funktion gänzlich ersetzen oder verhindern. So könnte ein Modul ein Callback-Objekt bei einem anderen Modul registrieren (z.B. für Authentifikationsaufgaben). Solche Möglichkeiten müssten in der API gut dokumentiert werden.

October 9, 2007

More impressive Views without Schuppen vor den Augen

Hallo Welt, ich lebe noch! Und heute kam ich mal wieder etwas mehr voran als die letzten Tage, wo ich im wesentlichen am Klassendesign herumgefeilt und mir gedanken zum Aufbau meiner Programm-Module gemacht habe. Zweiteres wäre einen eigenen Eintrag wert (der auch schon seit drei Tagen in Mache ist...), auf ersteres werde ich jetzt eingehen.
Mittlerweile besteht das Datenbankschema für Consolvix aus etwas über 40 Tabellen und Views, was sich natürlich auch auf Applikationsebene wiederspiegelt. Nicht gerade wenig, und viele Tabellen speichern doch in etwa das gleiche: Logindaten. Einfach, weil ich zu blind war um zu sehen wie man diesen ominösen universellen "User" nun auf Datenbankebene abbilden kann (siehe "Neues aus der Modellier-Ecke"). Nun fiel es mir heute wie Schuppen von den Augen, als ich ich feststellte dass trotz reiflicher Überlegungen zur Klassenhierarchie bei den Benutzerkonten (siehe "Benutzerverwaltung") meine Überlegungen nicht weit genug geführt hatte: Aus Polymorphie zwischen User, EmailAccount und SystemUser mach Aggregation, und zwar nicht vom Benutzer ausgehend, sondern zum Benutzer hin. Auf Datenbankebene bedeutet das: mache eine Tabelle User(id, name, real_name, uid, gid,...) und erstelle für jeden FTP-Account, E-Mail-Account etc. einen neuen Datensatz, dessen Inhalt dann aggregiert wird. Auf Applikationsebene "hat" ein EMailAccount dann ein User-Objekt, wo Logindaten etc. drinstehen. Für die Anwendungen wie der Mailserver, die direkt auf die Daten in einer Tabelle zugreifen können müssen, kommen dann einfach Views zum Tragen.
Eine schöne View ist z.B. die, die aus Systembenutzern, Benutzern und FTP-Kontodaten die Tabelle für die FTP-VirtualHosts bzw. FTP-Accounts zusammenbaut:

CREATE VIEW proftp_virtual_hosts AS
SELECT
  `users`.`name`     AS name,
  `users`.`password` AS password,
  IF (`ftp_accounts`.`home_dir`,
    `ftp_accounts`.`home_dir`,
    `system_users`.`home_dir`
  ) AS document_root,
  `ftp_accounts`.`login_count`   AS login_count,
  `ftp_accounts`.`last_accessed` AS last_accessed,
  `ftp_accounts`.`last_modified` AS last_modified,
  IF (`system_users`.`shell`,
    `system_users`.`shell`,
    '/bin/false'
  ) AS shell,
  `users`.`system_uid` AS uid,
  `users`.`system_gid` AS gid
FROM
  `ftp_accounts`,
  `system_users` RIGHT JOIN `users` ON (`system_users`.`user_id` = `users`.`id`)
--   access_rights   AS a
WHERE
      `users`.`id`                  = `ftp_accounts`.`user_id`
  AND `ftp_accounts`.`is_disabled?` = 0
  AND `users`.`is_disabled?`        = 0

Resultat:
| Field | Type | Null | Default |
| id | int(10) | No | |
| login_count | int(11) | No | 0 |
| last_accessed | datetime | No | 0000-00-00 00:00:00 |
| last_modified | datetime | No | 0000-00-00 00:00:00 |
| is_disabled? | tinyint(1) | No | 0 |
| user_id | int(11) | No | |
| home_dir | varchar(35) | No | |

Der FTP-Server kann dann auf die Daten zugreifen, als wäre es eine ganz normale Tabelle (ja, Zugriffszeiten und login_count lässt sich auch über die View aktualisieren), aber auf Applikationsebene ergeben sich durch das Auslagern der gemeinsamen "Benutzer"-Daten erhebliche Vorteile in der Rechteverwaltung. So kann dort bei der Authentisierung immer mit dem Benutzer-Objekt ohne Unterscheidung, ob es nun ein E-Mail oder FTP-Konto ist, gearbeitet werden. Im Klassendiagramm, sprich auf Applikationsebene, sieht obiger Zusammenhang so aus:

ftp-users-details

Ganz ähnlich, nur noch die eine oder andere Tabelle mehr erfordernd, sieht es mit den E-Mailkonten aus. Bei der Unten aufgeführten Lösung wird nicht mehr zwischen "virtuellen" und "echten" E-Mail-Konten unterschieden. Jedes E-Mailkonto "ist" bzw. "hat" auch einen Benutzer ("hate", weil Komposition -- obwohl eigentlich der "ist ein"-Gedanke durch Vererbung hadinter steckt und die Komposition an die Stelle der Polymorphie tritt). Dem E-Mailserver ist es egal, ob dieser "E-Mailbenutzer" nun auch ein Systemkonto besitzt (eindeutige UID/GID) oder ob es sich dabei um einen "virtuellen Benutzer" handelt, der in diesem Fall immer die UID/GID 5000 hat. Auch Consolvix' Authorisierungsfunktionen ist es herzlich egal, ob sich da via Web-Interface nun ein reiner E-Mailbenutzer oder etwas anderes einloggt, denn diese benutzen nur die im User-Objekt gespeicherten Informationen -- insbesondere sind sämtliche Zugriffsrechte allein am User gekoppelt.

Nundenn, so sieht der wichtigste Teil des E-Mail-"Subsystems" aus:

email-classes-details.png

Die E-Mailadresse stellt eine eigene Entität dar, weil offensichtlich mehrere E-Mailadresse für das gleiche POP/IMAP-Konto bestimmt sein können. Dass die Domain in der E-Mailadresse nochmal über einen Fremdschlüssel referenziert ist, macht die ganze Sache noch eine kleine Stufe komplifizierter, aber es soll ja ein konsistentes Datenbankschema sein. Dennoch beinhaltet obiges Scheme noch ein u.U. signifikantes Problem, und wer es herausfindet der kriegt von mir einen HTTP-Keks ;-)

Wie auch immer, der E-Mailserver benötigt eine einzige schön aufbereitete Tabelle, um seine Mails zu liefern. Deswegen übernimmt auch hier eine hübsche kleine View das. Bis auf die Subqueries, auf die ich unten kurz eingehe, sollte alles auch ohne Kommentare halbwegs selbsterklärend sein, ansonsten einfach einen Kommentar schreiben, ich erläutere gerne mehr.

CREATE VIEW `postfix_accounts` AS
SELECT
  CONCAT(`name`, '@', (SELECT `value` FROM `app_settings` WHERE `key` = 'mail_domain_name')) AS `account`,
  `name`,
  `real_name`,
  `password`,
  `quota`,
  IF (`email_accounts`.`mail_dir`,
      `email_accounts`.`mail_dir`,
      CONCAT(`home_dir`, '/mail/', `name`, '/Maildir/')
  ) AS `maildir`,
  (SELECT `system_uid`
    FROM `users` JOIN `app_settings` ON (`users`.`name` = `app_settings`.`value`)
    WHERE `app_settings`.`key` = 'mailbox_user_name'
  ) AS `uid`,
  (SELECT `system_gid`
    FROM `system_groups` JOIN `app_settings` ON (`system_groups`.`name` = `app_settings`.`value`)
    WHERE `app_settings`.`key` = 'mailbox_group_name'
  ) AS `gid`
FROM
 (`system_users` RIGHT JOIN `users` ON `system_users`.`user_id` = `users`.`id`),
  `email_accounts`
WHERE
      `users`.`id` = `email_accounts`.`user_id`
  AND `email_accounts`.`is_disabled?` = 0;

Resultat:
| Field | Type | Null | Default |
| account | varchar(301) | Yes | NULL |
| name | varchar(50) | No | |
| real_name | varchar(32) | No | |
| password | varchar(40) | No | x |
| quota | varchar(10) | No | |
| maildir | varchar(97) | Yes | NULL |
| uid | int(11) | Yes | NULL |
| gid | int(11) | Yes | NULL |

So, und was hat das mit den Subqueries auf sich? Wie ich noch nirgends erwähnt habe, gibt es für Consolvix eine eigene Entität, AppSettings, in der konfigurierbare Einstellunen gespeichert werden. Man kann ja nicht erwarten, dass die Systemkonfiguration in die Datenbank ausgelagert wird und dann die verwaltende Applikation für sich selbst wieder auf Konfigurationsdateien zurückgreift :-) -- jedenfalls wird in der Tabelle app_settings jeweils ein Schlüssel-Wert-Paar gespeichert, mit Angabe des zugehörigen Moduls. Innerhalb der Applikation ermöglicht die folgende Methode, diese Einstellungen schnell und einfach zu erhalten:

  def setting(setting_name)
    setting = AppSetting.find_by_key(setting_name,
                            :conditions => {:module_id => @current_module.id,
                                            :version   => @current_module.version} )
    setting.value
  end

Und wer genau hinschaut, entdeckt im wesentlichen genau die SQL-Abfrage als Subquery in den Views. Was im Datenbankschema noch nicht umgesetzt ist, ist jedoch die Versionierung der Module etc. Überhaupt bin ich noch unentschlossen, was die Versionierung der Applikationseinstellungen angeht. Denn nochmals je eine Subquery um Modul-ID und aktuelle Version herauszufinden scheint mir gegenärtig etwas übertrieben, auch wenn man da mit Stored Procedures ja einiges machen kann. Da mich zur Zeit aber ohnehin das Gefühl beschleicht, mehr ER-Modellierung als RoR zu machen, werde ich es erstmal dabei belassen.

Und für die UML-Liebhaber, hier ein Schnappschuss des wichtigsten Teils des Klassendiagramms:
SystemClasses-071010.png

Stay tuned, Folks!

October 22, 2007

Die Geschichte mit der Vererbung

Wie mittlerweile klar rübergekommen sein dürfte, umfasst Consolvix diverse Arten von Benutzern. EmailAccount, FtpAccount, Client etc. aggregieren alle ein User-Objekt, um vom System authentifiziert werden zu können. Um nun all diesen Objekten nach außen hin das "Aussehen" eines von User erbenden Objektes zu verpassen, verwende ich jetzt einfach sowas wie ein Proxy-Pattern, realisiert über ein "Mixin", also ein Ruby-Modul. Am Beispiel SystemUser HAS_ONE User sieht das so aus:

module UserInheriter
  # übernehme alle aus der Tabelle ermittelten Attribute ohne ID
  attributes = User.new.attribute_names - ['id']

  attributes.each do |attr|
    # generiere Lesemethode:
    define_method("#{attr}") do
      return self.user.send("#{attr}")
    end

    # generiere Schreibmethode:
    define_method("#{attr}=") do |param|
      return self.user.send("#{attr}=", param)
    end
  end
end

class SystemUser < ActiveRecord::Base
    has_one :user, :dependent => :destroy
  # ...
  include UserInheriter
  # ...
end

So wird jede Klasse, die das Modul UserInheriter importiert, um die Lese- und Schreibmethoden der User-Klasse erweitert. Natürlich bleibt der direkte Zugriff auf das User-Objekt weiterhin möglich.

Was später noch implermentiert werden sollte, ist die Unterscheidung zwischen public und protected Attribute sowie die Weiterreichung von Aggregierten Objekten. Das tut momentan aber noch nicht Not und darum wird's vorerst bei diesem Umfang bleiben.

October 26, 2007

Neue Benutzer Schritt für Schritt

Die letzten zwei Tage habe ich mich mal von meinem Modell losgerissen und endlich eines der umfangreicheren Use-Cases implementiert: Das Schrittweise Anlegen eines neuen Kunden mit (fast) allem drum und dran. Meine Anforderungen an die Implementierung waren in etwa die folgenden:

  • Übersichtlichkeit: Der Administrator oder Reseller, der den Kunden anlegt, soll nicht von einem Monster-Formular erschlagen werden, sondern Schritt für Schritt durch den Prozess geführt werden.
  • REST lässt grüßen: Sowohl der Gesamtprozess als auch jeder einzelne Unterschritt ist direkt über eine eindeutige URL zu erreichen. Beim Aufruf des Gesamtprozesses (/admin/create_hosting_account) wird auf die letzte noch nicht bearbeitete Eingabemaske gewechselt (wenn z.B. Schritt 1 und 2 bearbeitet sind, springe zu Maske 3). Beim expliziten Aufruf eines Schrittes (z.B. /admin/create_hosting_account/3) wird, sofern alle vorangehenden Schritte erfolgreich waren, zu diesem gewechselt.
  • Es sollte jederzeit möglich sein, in eine bereits bearbeitete Maske zu wechseln und dort Änderungen vorzunehmen, ohne dass Eingaben aus einer anderenMaske dadurch verändert werden müssten. Nach Bearbeiten einer Maske wird immer in die letzte noch nicht fertig bearbeitete Maske gesprungen.
  • Überprüfung auf Fehler erfolgt beim Absenden jeder einzelnen Maske, nicht erst am Ende des Prozesses
  • Bis zum letzten Schritt werden keinerlei Daten in der Datenbank abgespeichert. Und selbst im letzten Schritt wird durch eine Datenbanktransaktion dafür gesorgt, dass entweder alles oder gar nichts übernommen wird -- im zweiteren Fall wird wieder zum letzten Schritt gewechselt.
  • Die Umsetzung im Code sollte mit möglichst wenig Wiederholung (DRY ;-)) und im Hinblick auf zukünftige Erweiterung um weitere Schritte geschehen.
  • nochmal REST: Wird ein Schritt mit GET aufgerufen, wird dieser angezeigt ohne jegliche Fehlermeldungen, sollten sich in der aktuellen Maske noch Fehler befinden. Geschieht der Aufruf mit POST (und hoffentlich Formular-Eingaben), wird versucht, diesen Schritt abzuschließen indem die eigegebenen Daten validiert und in der Session zwischengespeichert werden. Bei DELETE-Aufrufen wird die gesamte "Transaktion" abgebrochen, d.h. alle bisher getätigten Eingaben werden gelöscht und es wird zum ersten Schrit gesprungen. PUT wird nicht verwendet.

Das Oberflächendesign lässt sicherlich noch den einen oder anderen Wunsch offen, denn momentan verwende ich einfach noch die Standard-Formulare des Rails-Scaffolds (wer sich die fieldsets genauer anschaut, wird evtl. feststellen, dass sie sich genau mit den Datenbankentitäten decken -- im ersten Schritt wärden das Client und Address). Außerdem sind noch keinerlei "dynamischen" Elemente implementiert, so lässt sich z.B. erst eine Domain und ein E-Mailaccount (E-Mailadresse, Adresse,...) im Laufe des Prozesses anlegen. Ziel ist es, diese auf eine beliebige Zahl zu erhöhen (Stichwort Formularerweiterung mit JS).

Ansonsten funktioniert eigentlich alles wie gewünscht, besonders die garantierte Vollständigkeit aller vorherigen Schritte bei der Anzeige eines beliebigen Schrites (Die Überprüfung auf Vollständigkeit von Schritt n erfordert die vorherige Überprüfung von Schritt n-1, rekursiv bis Schritt 1 -- es ist so nicht möglich, dass irgend eine Überprüfung "aus Versehen" beim Programmieren ausgelassen werden könne).

Und hier noch ein paar Screenshots:

snap7.png Erster Schritt: Eingabe Der Kontaktdaten des Kunden

snap8.png Upps, da hat sich wohl wer vertan: Ausführliche Fehlermeldungen eind selbstverständlich.

snap9.png Alle Eingaben OK, weiter geht's mit dem zweiten Schritt.

October 29, 2007

Konfigurationsmanagement

Eine wesentliche Funktion, die das Kernmodul von Consolvix bereit stellen soll (und die es in der Tat mittlerweile tut ;-)), ist das einheitliche Auslesen und Speichern von Programmeinstellungen (AppSettings). Beispiele hierfür sind etwa die Anzahl der standardmäßig anzuzeigenden Einträge in einer Liste, das default-Homeverzeichnis der Hostingkunden, die UID des Mailservers und vieles mehr. Angefangen hatte es also mit einer Tabelle in der Form

appsettings(id_, key, value).

Doch um etwas mehr Übersicht zu schaffen, ist es natürlich sinnvoll, die einzelnen Werte an das jeweilige Modul zu binden, wo sie ihre Gültigkeit erhalten. So würde es z.B. wenig Sinn ergeben, die default-Webroots der Webbenutzer zu laden, wenn man sich gegenwärtig im Modul "email" aufhält. Und wenn man schon von verschiedenen Modulen spricht, die ja auch noch in verschiedenen Versionen auftauchen können, kommt zusätzlich gleich noch eine Versionierungsinformation hinzu. Also wird aus der obigen Tabelle die folgende:

appsettings(id, key, value, version, moduleid).

Nebenbei bemerkt, die Modul-Tabelle sieht so aus:

consolvixmodule(id, name, description, version, isenabled?, navigationentry, controllername, created_at)

Es war so angedacht, dass bei einem Update über das [version]-Feld entschieden werden sollte, ob ggf. eine neuere Modulversion zum Download zur Verfügung steht. Wenn diese dann installiert wäre, so würde dann (je nach antwort auf die Frage ob die alte Config übernommen werden soll) für jedes AppSetting ein neuer Eintrag mit identischen/aktualisierten Attributen und der neuen Versionsnummer (der des neuen Moduls) angelegt werden. Das würde Downgrades einfacher machen, denn die alten Einstellungen blieben weiterhin erhalten.

Aber sowas kann man ja genausogut über das Datum machen: dieses wird nämlich beim Herunterladen der Modulliste vom Downloadserver gleich mitgeliefert.

Dieses Konzept geriet arg ins Wanken, als ich heute die Funktion zum Auslesen der Konfigurationswerte weiter auscodete. Um einen Hauch dessen zu vermitteln, was da auf mich zugekommen wäre, möchte ich nur die API-Kommentare anführen, die ich (VOR! dem Erstellen der Funktion) dazu schrieb:

class ApplicationController < ActionController::Base @@module_name = 'core'

# Get an application setting, optionally with module name and version.
# * setting_name: the name of the setting to be retrieved, either string or symbol
# * module_name: name of the module where to look for the setting.
#              If none specified, use current module.
#              If not found, return default value or throw exception (see options)
# * options: Optional ;-)
#  * :version (Integer) => look for this version.
#                          If none specified or 0, use current module version (default)
#                          If positive, look for this exact version (return default value or throw exception if not found).
#                          If -1, look for highest version available.
#                          If not found, raise not found exception (if no default value given)
#  * :default (Object)  => return this value instead of throwing an exception
#  * :core (true|false) => let the core option take precedence over the default value, if specified.

def setting(setting_name, module_name=@@module_name, *options)
  # ...
end

end

Das hätte schätzungsweise 50 zeilen Code bedeutet und irgendwie schien mir die Herangehensweise etwas dubios. Irgendwie so wie "hole dir den wert einer Einstellung, wenn du sie hier nicht findest schau mal dort, oder dann vielleicht drüben, oder sonstwo, notfalls bei 'ner alten version die eigentlich gar nicht mehr existieren sollte -- und wenn das alle snix wird, dann schmeiß eben 'ne Exception". Toll, was? :)

Also wenn Konfigurationsmanagement, dann bitte auch vernünftig. Das ist nun herausgekommen: Benenne "version" in "configuration_id" um. Mache eine neue Entität Configuration und lasse für jedes Modul sauber angelegte Konfigurationen zu, die ein Admin nach Belieben zusammenstellen und aktivieren/deaktivieren kann. Wenn ein neues Modul installiert wird, so kann entweder eine neue Konfiguration angelegt werden, oder allenfalls neu hinzugekommene AppSettings werden einfach in die bestehende Konfiguration übernommen. Wenn bestimmte Einstellungen in der neuen Modulversion nicht mehr verwendet werden, dann stören sie auch keinen, wenn sie weiterhin in der Datenbank herumgeistern (es gibt da allerdings auch eine "Cleanup Module Configuration"-Option ;-)). Die configurations-Tabelle schaut so aus:

configurations(id, name, description, moduleid, userid, createdat, updatedat)

Es folgt nun ein Wenig Pseudocode, der in etwa klar machen soll, nach welchen Regeln nun Einstellungen geladen werden:

if modulname_angegeben? and modul_vorhanden?(modulname)
  modul = modulname
else
  if not core_option_angegeben? and modul_vorhanden?(@@module_name)
    modul = @@module_name
  elsif core_option_angegeben?
    modul = 'core'
  end
end
if modul.nil?
  raise "Modul nicht vorhanden"
end

if einstellung_vorhanden?(für_modul) and modul != core
  return einstellung(für_modul)
elsif core_option_angegeben? or modul == core
  if einstellung_vorhanden?(für_core)
    return einstellung(für_core)
  end
end
if default_option_angegeben?
  return :default
end
raise Exception(nicht gefunden)

Die Versionsnummer taucht nirgends mehr auf, darum kümmert sich wie gesagt nun das Configuration-Objekt. Damit lassen sich Konfigurationen auch benennen. Beispielsweise ist es damit auch möglich, während Wartungsphasen den Zugriff au alle Module durch Auswahl einer entsprechenden Konfiguration zu sperren. Ach überhaupt ist vieles denkbar. Ich denke mal, dass sich heute wieder einiges an Flexiblität in der Anwendung etabliert hat ;-)

November 29, 2007

wvk-4, consolvix+trac+svn

Hallo Welt! Um vier Weisheitszähne erleichtert und mit Kühlakkus an den Hamsterbacken ist es mir nun eine kleine Freude, ein paar Worte zu den aktuellen Errungenschaften im Rahmen von Consolvix zu verlieren. Zuerst einmal muss ich feststellen, dass die "Kern-Infrastruktur" nicht so wirklich wachsen will. Außerdem habe ich noch kein schlüssiges Konzept für eine brauchbare GUI. Zwar kann man alle in der Datenbank vorhandene Entitäten wunderbar CRUDden, aber damit ist es ja nicht getan. Ich bin jedoch zuversichtlich, dass sich das innerhalb der nächsten Wochen klären lässt.

Was hat sich also ereignet? Ich habe mich im Rahmen einer akuten Notwendigkeit zu folgendem verleiten lassen: eine einfache Weboberfläche um Subversion-Repositories zu verwalten und ihnen ein Trac-Projekt zuzuordnen sowie letztere auch detailliert konfigurieren können, zu schreiben. Das an sich ist nichtt besonders anspruchvoll, nur Auslesen einiger Shellscript-Ausgaben (svnlook z.B.), lesen einiger verzeichnisinhalte und lesen und schreiben einer Konfigurations- (INI-) Datei. In meinem judendlichen Übereifer von den eleganten Fähigkeiten des allmächtigen ActiveRecord geblendet, sollten "natürlich" SvnRepositories und TracEnvironments als solche Objekte behandelt werden, damit die Konfiguartion auch schon einfach ist. OK, zugegebenermaßen wäre das bei SvnRepository nicht nötig gewesen, aber auf die Errungenschaften bei TracEnvironment bin ich schon ein wenig stolz ;-)...

OK, was zeichnet eine TracEnvironment aus?

  • ist ein Verzeichnis, liegt zusammen mit anderen projekten in /var/trac => "Datenbank"
  • hat eine INI-Datei zur Konfiguration => Werte = Spalten
  • wird aufgesetzt durch den Aufruf von "trac-admin initenv " => "save"
  • muss nachträglich über ein Webformular mit Validierung berarbeitet werden können => klassische ActiveRecord-bezogene Aufgabe
  • beim Anlegen via "tracadmin initenv" wird _nur die Projektumgebung angelegt, nicht die Konfiguration. Also muss aus einer "sample"-Datei die konfiguration generiert werden, aber nur beim Anlegen => schöne Aufgabe für before_create
  • bis auf die initial anzugebenden Argumente (s.o.) sind alle anderen Konfigurationsdirektiven optional und theoretisch auch beliebig erweiterbar => "Spalten" müssen irgendwie dymanisch generiert werden können.

Zuerst: hier kam wieder ActiveRecord::BaseWithoutTable zum Tragen. Alle bei der Initialisierung zwingend notwendigen Spalten wurden "fest verdrahtet", der Rest wird nachher dynamisch über die Konfigurationsdatei gemacht. Alo reicht zur Datendefinition folgendes:

class TracEnvironment < ActiveRecord::BaseWithoutTable
  TRAC_BASE_PATH     = '/var/trac'
  TRAC_CONFIG_FILE   = 'conf/trac.ini'
  TRAC_TEMPLATE_PATH = '/usr/share/trac/templates'

  column :name,                   :string
  column :path,                   :string
  column :trac__database,         :string, 'sqlite:db/trac.db'
  column :trac__repository_type,  :string, 'svn'
  column :trac__repository_dir,   :string
  column :trac__templates_dir,    :string, TRAC_TEMPLATE_PATH

  validates_presence_of :name, :trac__repository_dir
  validates_inclusion_of :trac__repository_type,
                        :within => ['svn'],
                        :message => 'currently, only "svn" is supported.'
  before_create :initenv
end

die Konstanten dürften selbsterklärend sein. Doch warum diekomischen Spaltennamen mit "__"? Bei denen handelt es sich um Werte, die nachher in die Konfigurationsdatei geschrieben werden. Das Problem mit der Konfigurationsdatei ist, dass diese in "Abschnitte" unterteilt ist, also z.B. sowas:

[abschnitt1]
var1 = val1
var2 = val2
[abschnitt2]
var3 = val3
...

das ist eine zweidimensionale Struktur, die ich über die Konvention ActiveRecord-Spaltenname = "#{Abschnittsname}__{Einstellunsname}" auf das @attributes-Array des ActiveRecord-Objektes abbilde. Unter Angabe der definierten Spalten kann man eine TracEnvironment erstellen, ganz genau wie man ein ganz normales AR-objekt in der Datenbank erstellt -- mit den gleichen Methoden, callbacks etc:

...
@trac_environment = TracEnvironment.new params[:trac_environment]
@trac_environment.save
...

Handelt es ich um ein neues Objekt (@newrecord==true), wird vor dem Speichern (übe beforecreate) erst die ganze Ordnerstruktur etc. angelegt, danach die Konfiguration geschrieben:

  def update
    temp_file_name = "#{self.config_file}~"
    file = File.new(temp_file_name, 'w')
    self.config_file_struct.each do |section, value|
      file << "\n[#{section}]\n"
      value.each do |attr, attr_val|
        file << "#{attr} = #{self.send "#{section}__#{attr}"}\n"
      end
    end
    FileUtils.move temp_file_name, self.config_file
    @new_record = false
    self
  end

  def create
    command = "trac-admin #{self.path} initenv #{self.name} #{self.trac__database} #{self.trac__repository_type} #{self.trac__repository_dir} #{self.trac__templates_dir}"
    @initialisation_log = "<em>#{command}</em>\n" + `#{command}`
    case $?.exitstatus
    when 0 # everything alright
      return true
    when 1 # general failure, just clean up and return
      self.destroy
      return false
    when 2 # environment exists already
      return false
    else # unknown exit status (however, not a success): cleanup and issue an error
      raise "trac-admin initenv returned exit code #{$?.exitstatus}"
      self.destroy
    end
    self
  end

ich spare mir jetzt eine detaillierte Erklärung, fragt mich wenn etwas unklar ist. Das Lesen der Konfiguration geschieht hier:

def parse_config_file!(file=nil)
  config  = {}
  section = ''
  begin
    file = File.new(file||self.config_file)
    file.each_line do |line|
      if /^[([a-z0-9_]*)]$/.match line
        section = $1
      elsif /^\s*([a-zA-Z0-9_]*)\s*=\s*(.*)$/.match line
        self.send "#{section}__#{$1}=", $2
      end
    end
  rescue Errno::ENOENT
  end
  config
end

im Zusammenspiel mit einer überschriebenen methodmissing, die die Setter-"Methoden" für die in der Konfigurationsdatei vorhandenen Werte zu überschreiben. Das ist nötig, weil die eigentliche methodmissing eine spalte nicht anlegt, wenn diese nicht explizit definiert wurde (in diesem Fall über "column ...", ansonsten über auslesen der Datenbanktabelle). Die überschribene Methode fügt neue, unbekannte attribute einfach ins @attributes-Array ein, danach kann ActiveRecord damit arbeiten wie mit allen anderen Attributen.

def method_missing(name, *args)
  method_name = name.to_s
  if /^([a-z0-9_]+)__([a-z0-9_]+)=$/.match(method_name)
    begin
      super
    rescue NoMethodError
      key = "#{$1}__#{$2}"
      @attributes[key] = args[0]
    end
  else
    super
  end
end

Alles in allem ist das Konzept, Dateisystemartige Strukturen mit Konfigurationswerten auf ActiveRecord abzubilden, in meinen Augen ziemlich praktisch. Auf Applikationsebene arbeitet man mit genau den gleichen Strukturen wie an anderer Stelle auch, und man spart sich jede Menge Ärger mit Validierung, Flusskontrolle über Callback-Methoden, usw. Nur ein Wermutstropfen bleibt noch: so schöne Aufrufe wie

TracEnvironment.find_by_trac__repository_type "svn"

gehen NOCH nicht. Vielleicht macht es Sinn, das später einmal zu implementieren, so viel Aufwand wäre es ja nicht!

Achja, und was wäre ein so langer Post ohne ein Bild:

snap24.png

snap26.png

snap27.png

Vielen Dank für's Lesen, Ich freue mich auf Kommentare!

December 8, 2007

Transaktionen braucht Consolvix

Ein sehr guter Punkt, auf den mich Phillip gestern Abend hingewisen war, ist nun umgesetzt: Das Anlegen von neuen Kunden/Hostingaccounts ist nun in Transaktionen gekapselt, die voneinander vollkommen unabhängig sind. Gleichzeitig habe ich die Speicherung der temporären Transaktionsdaten aus der session-Variable herausgeholt und als normale AR-Entität in die Datenbank ausgelagert. Das geschah mit dem Hintergedanken, dass so eine Transaktion ja universell sein soll und es durchaus vorkommen könnte, dass man mal Arbeit anfängt und sich dann für die Mittagspause ausloggt. Oder eine Transaktion könnte Schritte enthalten, die von verschiedenen Benutzern ausgeführt werden müssen, z.B. die Bestätigung eines neuen Kennworts: Der Admin startet die Transaktion, eine Mail wird an den Kunden verschickt, dieser bestätigt sein neues Kennwort und damit ist die Transaktion erst abgeschlossen.

Nun wo also Arbeit über mehrere Sitzungen verteilt werden kann, macht es Sinn, die angefangenen Transaktionen gleich auf der Admin-Startseite anzeigen zu lassen. Dazu gibt's jetzt also ein neues Applet was genau dieses tut:

Diese Liste umfasst einen Link zu den jeweils offenen Transaktionen. Dazu wird pro Transaktion gespeichert, welcher Controller und welche Aktion aufgerufen werden muss, um die Arbeit weiterzuführen. Außerdem werden die Zeiten gespeichert, an denen die Transaktion gestartet bzw. bearbeitet wurde und von wem. Summa summarum schaut die Entity also so aus:

consolvix_transactions(id, number, user_id, controller, action, description, data, created_at, updated_at)

Das data-Feld enthält die als YAML serialisierten "Sitzungs"-Daten.

Als API-Funktionen stehen jedem Controller nun start_transaction, transaction_started?, cleanup_transaction', 'load_transaction sowie save_transaction zur Verfügung.:

  • start_transaction(description, action, controller=nil) erstellt ein neues Transaktionsobjekt mit den angegebenen Daten.
  • load_transaction lädt, sofern als Parameter eine gültige TID angegeben wurde, die Daten aus der Datenbank nach @transaction, welches danach dem Controller/den Views zur Verfügung steht.
  • transaction_started? prüft nur, ob dieses Objekt NIL ist oder nicht.
  • load_transaction sollte als before_filter aller Aktionen eingesetzt werden, die auf die transaktionen zugreifen möchten. Eingebunden wird der Filter über use_transaction_for :aktion1, :aktion2, ...
  • save_transaction kommt entsprechend als after_filter an die Aktionen, die die Daten möglicherweise verändert haben könnten. Dies kann über save_transaction_for :aktion1, :action2, ... bewerkstelligt werden.

Am Beispiel "Anlegen eines Hosting-Accounts" passiert nun Folgendes: wird create_hosting_account aufgerufen, so wird eine neue Transaktion gestartet (sofern nicht als parameter bereits eine gültige TID übergeben wurde) und nach cha_do_create/1 weitergeleitet. Wird cha_do_create/[schritt-nummer] ohne gültige TID aufgerufen, wird erst geschaut, ob offene Transaktionen (für den aktuell eingeloggten Benutzer) vorhanden sind. Wenn nicht, wird nach create_hosting_account umgeleitet. Wenn eine Transaktion vorhanden ist, wird diese fortgeführt. Wenn mehrere Transaktionen offen sind, wird nach cha_select_transaction umgeleitet, wo der Benutzer die gewünschte Transaktion auswählt und schließlich wieder nach cha_do_create umgeleitet wird. Jederzeit kann abort_create aufgerufen werden, was die Transaktion beendet und löscht.

Ich werde jetzt mal sehen inwieweit es Sinn macht, alle "erstellen"- und "bearbeiten"-Dialoge in Transaktionen zu verpacken. Ich denke aber, dass es nur bei Aktionen mit mehr als einer Eingabemaske Sinn macht, da die zu speichernden Daten mindestens einmal gePOSTet werden müssen. Aber man könnte natürlich genausogut bei ein-formularigen Aktionen einen "Zwischenspeichern"-Button einbauen. Was sagen die Usability- und anderen Experten dazu?

December 13, 2007

Transaktionen die Zweite

Hurra, es ist vollbracht! Nach zwei Tagen des frustgeladenen Programmierens bis in die späten Morgenstunden hinein scheint meine Transaktions-API nun relativ solide geworden zu sein. Es haben sich einige Änderungen gegenüber dem ergeben, was ich bereits in meinem letzten Eintrag zu dem Thema schrieb. Doch alles der Reihe nach:

  • Der gesamte Transaktionsrelevante Code liegt jetzt in einem eigenen Modul: Consolvix::TransactionAPI. Wenn also nun ein Controller für gewisse Aktionen Transaktionsfunktionalität braucht, kann er diese über include Consolvix::TransactionAPI einbinden.
  • Um einen aus mehreren Schritten aufgebauten Transaktionshandler zu erstellen, dient die methode transaction_handler. Diese generiert aus den angegebenen Daten alle Unterschritte und Überpfügungsmethoden als Stubs.
  • das Laden und Speichern der aktuellen Transaktion geschieht automagisch über in transaction_handler eingefügte before- bzw. afterfilter (Danke an Daniel für die Zeit die du dir genommen hast, nach dem Gepräch hat's irgendwie funktioniert -- darüber reden hilft oft schon g)
  • um die korrekte Weiterleitung an die richtigen actions kümmert sich der beim Aufruf von transaction_handler generierte Code ebenfalls vollständig selbst, im Wesentlichen immer noch so wie im letzten Eintrag beschrieben. Nur eben schön versteckt für den Programmierer. Die Einzige wichtige Änderung, die sich ergeben hat, ist dass die Schrittnummer jetzt als parameter und nicht mehr als ID-Teil der URI mitgegeben wird -- aus dem Grunde, weil z.B. eine "bearbeite Kunde soundsoviel" eine ID-Angabe benötigt...

Nehmen wir nun also an, im HostingAccountsController sollen die beiden Aktionen new und edit als mehrschrittige Transaktion ausgeführt werden. Dazu definiere man dann zuerst das Folgende:

class HostingAccountsController < ApplicationController
  transaction_handler 'Edit Hosting Account',
                      :edit,
                      :worker   => :update,
                      :selector => :select_update_transaction
                      1 => [:edit_step1, nil],
                      2 => [:edit_step2, :step1_done?],
                      3 => [:edit_step3, :step2_done?],
                      4 => [:edit_save,  :step4_done?]

  transaction_handler 'Create Hosting Account for existing Client',
                      :new,
                      :worker => :create,
                      1 => [:cha_step1, nil],
                      2 => [:cha_step2, :step1_done?],
                      3 => [:cha_step3, :step2_done?],
                      4 => [:cha_save,  :step4_done?]

end

Die Argumente bedeuten folgendes:

  • Menschenlesbare Beschreibung, die z.B: beim Auswählen unfertiger Transaktionen angezeigt wird.
  • Name der Haupt-Aktion = "Name der Transaktion", im folgenden main_action genannt. Diese erstellt eine neue Transaktion und leitet anschließend an die worker_action weiter.
    • :worker (optional, default = "do#{mainaction}"): die Aktion, an die im späteren Verlauf jeweils alle Daten gesendet werden, unter Angabe des aktuellen Schrittes.
    • :selector (optional, default = "selecttransact#{main_action}"): die Aktion, die aufgerufen wird wenn keine (gültige) Transaktions-ID an die worker_action übergeben wurde UND wenn mehrere Transaktionen des verlangten Typs offen sind.

  • 1...n => [action, prerequisite]: dieser hash hat als Schlüsel die Schritt-Nummer und als Wert den Namen des auszuführenden Unterschrittes sowie seine Vorbedingung, die erfüllt sein soll. Die hier angegebenen tatsächlichen Unterschritte der Transaktion sind protected und nicht direkt aufrufbar. Der Zugriff wird über die worker_action geregelt, die bei jedem Aufruf prüft, ob zur Aufgerufenen Schritt-Nummer auch die Vorbedingung erfüllt ist. Vorbedingungen sind Methoden, die einen boolean-Wert zurück geben. Wenn sie nicht in der Controller-Klasse explizit implementiert werden, dan werden die vordefinierten Stubs aufgerufen, die immer FALSE liefern -- so merkt man als Entwickler schnell mal, dass man was vergessen hat zu implementieren ;-)

Für die ganz harten unter der werten Leserschaft dieses Blogs hänge ich gleich mal den Code von TransactionAPI an. Wer die Muße hat, ihn durchzulesen und zu verstehen (müsste machbar sein, er ist sogar kommentiert ;-)) und mir dazu Ideen und Verbesserungsvorschläge oder einfach nur fiese Detailfragen zu stellen, der sei an dieser Stelle herzlich dazu eingeladen.

January 3, 2008

Benutzerauthentifizierung und-Autorisierung Teil 1

Vorweg:

  • Authentifizierung (Authentication) = Der Vorgang des Feststellens, ob jemand der ist, den er vorgibt zu sein
  • Autorisierung (Authorisation) = Der Vorgang der Überprüfung, ob jemand das darf, was er machen will
  • Login = Beginn einer Sitzung, während der Befehle von ein und demselben authentifizierten Benutzer aufgerufen werden
  • Web-Applikationsentwiklung = Die Kunst, über ein statusloses Protokoll, welches laut REST-Paradigma auch statuslose Sitzungen verwenden sollte, dem Benutzer gleichzeitig die Illusion einer statusbehafteten Arbeitssitzung als auch die Illusion jener Statuslosigkeit zu schaffen, dass er jederzeit, von überall aus, parallel und ohne Schritte wie Login und "starte Transaktion A" wiederholen zu müssen, jede beliebige Aktion, welche zu genau jenem Zwecke immer eindeutig adressierbar sein soll, zur Erfüllung seiner Arbeit aufrufen kann.

Habe ich ein Problem? Neiiin.. es ist nur... wie kombiniere ich Statuslosigkeit mit der Statushaftigkeit, damit sowohl Otto Dummuser als auch REST-Fetischisten wunschlos glücklich sind? Also Wie verwende ich gleichzeitig die Möglichkeit, ein enorm schickes und ergonomisches HTML-Loginformular als auch HTTP Basic Authorization zur Autorisierung der Benutzer anzuwenden?

Mit dieser Frage habe ich mich (oh, ist heute schon wieder Freitag?!?) nun bald eine Woche herumgeschlagen. Und die frohe Botschaft ist: Beides geht nun. Man kann sich sowohl über ein Sitzungs-Cookie als auch über HTTP Basic Auth. Zugang zu einer Aktion verschaffen. Und das Tolle ist: wenn die Aktion nicht autorisiert ist, wird sowohl ein schickes Loginformular als auch ein HTTP 401 Unauthorized und ein WWW-Authenticate geschickt.

Nun sieht der Authentifizierungs-Prozess also folgendermaßen aus:

    • Wenn der Aufruf mittels POST geschah und das Array params[:auth_user] vorhanden ist, wird also ein formularbasierter Login versucht -- mit diesen Daten wird eine Authentifizierung durchgeführt.
    • Wenn ein Request mit HTTP Basic-Logindaten im Header ankommt, werden diese ausgewertet, sofern core::use_http_basic_auth wahr ist.
  1. Wenn kein gültiger User gefunden wurde (die Authentifizierung soweit also fehlgeschlagen ist), wird in der Session geschaut, ob dort ein authentifizierter Benutzer zu finden ist.
    • wenn alles obige fehlschlägt, wird je nach dem ob core::use_http_basic_auth wahr oder falsch ist, ein WWW-Authenticate-Header losgeschickt und gleichzeitig aber das Loginformular gerendert (dieses wird angezeigt, sobald ein Besucher die HTTP-Autorisierung abbricht). Ansonsten wird nur das Loginformular angezeigt.
    • wenn ein gültiger Benutzer gefunden wurde, wird fortgefahren mit der aufgerufenen Aktion.

Unabhängig davon, ob die Autorisierung über HTTP Basic oder Formular geschieht, wird immer der eingeloggte Benutzer in der Session gespeichert. Ist core::use_http_basic_auth falsch, wird ein Logout in klassischer Weise über das Beenden der Sitzung und ein Redirect zum Login-Formular bewerkstelligt.

Bei HTTP Basic Authentication ist ein Logout bekanntlich nicht wirklich möglich. Deswegen wird beim Aufruf von /logout geprüft, ob noch eine Benutzer-ID in der Session steht. Wenn ja, wird die Session beendet und ein neuer HTTP Basic Auth.-Header gesetzt. Wenn nicht, dann muss die Session bereits beendet worden (und dies somit der zweite Aufruf von /logout) sein -- dann wird mit den neu eingegebenen Benutzerdaten ein neuer Authentifizierungsversuch gestartet und auf /index umgeleitet.

Unterm Strich kann man sich nun also sowohl formularbasiert als auch über HTTP Basic einloggen und zumindest aus der üblichen Benutzersicht auch ausloggen. Ob HTTP Basic (und später auch Digest-) Autorisierung verwendet werden soll, kann der Admin über die Weboberfläche festlegen.

Stay tuned. mehr im nächsten Post!

January 7, 2008

Benutzerauthentifizierung und-Autorisierung Teil 2

Soo, nach der Einführung in die oberflächliche Funkionsweise des Loginprozesses unter Consolvix kann nun erst der richtige Spaß anfangen, denn: Ob der Benutzer sich einloggen darf, hängt von einer ganzen Reihe an Faktoren ab, nicht nur ob er sein Kennwort richtig eingegeben hat!

Beispiele:

  • ist das Benutzerkonto an sich gesperrt?
  • ist das Benutzerkennwort abgelaufen?
  • verfügt der Benutzer über ausreichende Rechte, um das verlangte Consolvix-Modul zu verwenden?
  • verfügt der Benutzer über ausreichende Rechte, um diese Aktion aufzurufen?
  • befindet sich das System im Wartunsgmodus? (dann sind nur Administratorlogins erlaubt)
  • ...

Mein Ziel ist es, nämtliche Zugriffsberechtigungsfragen an ein austauschbares Authoriser-Modul zu binden. Beispielsweise existieren bislang Auth::Authoriser sowie AuthPam::Authoriser. Ersteres Modul verwendet direkt die auch vom System verwendeten MySQL-Tabellen, und ist gleichzeitig fester Bestandteil des Kernmoduls con Consolvix. Das davon abgeleitete Modul AuthPam::Authoriser verwendet die Ruby/PAM Bibliothek um direkt auf die PAM-Schnittstelle des Betriebssystems zugreifen zu können. Da mein System ja aber die gleichen MySQL-Tabellen verwendet wie sie Consolvix verwalten soll (was ja überhaupt erst die Sinnhaftigkeit meiner Diplomarbeit ausmacht ;-)), ist damit noch nichts gewonnen. Doch wer weiß, wie sich Consolvix noch entwickeln wird...

Das Authoriser-Modul gibt auf die allgemeine Frage "Darf dieser Benutzer das?" eine Ja/Nein-Antwort. Momentan lautet sie genau dann "Ja", wenn das Kenwort richtig, das Konto aktiv und das Kennwort nicht abgelaufen ist. Ich arbeite an einer ausgefeilteren Authentifizierung und Autorisierung, die nach dem gleichen Muster wie PAM aufgebaut ist (es soll ja auch später PAM als Autorisierungsquelle verwendet werden können). Die PAM-API liefert hier eine schöne Referenz für ein mächtiges Zugriffsgegelwerk, bestehend aus folgenden Methoden:

  1. authenticate: Authentifizierung -- hier: Stimmt das Kennwort (aus HTTP, POST oder Session)?
  2. set_credentials: laden von Gruppenzugehörigkeiten, Zugriffsrechten etc.
  3. account_management: Überprüfung, ob das Konto verfügbar ist -- Je nach dem, was diese Methode zurück liefert, können verschiedene Aktionen nötig sein. Ein Beispiel wäre eine Auffurderung, ein neues Kennwort zu setzen. Ein anderes, den Login-Vorgang komplett abzubrechen (z.B. weil Login derzeit nicht möglich/erlaubt). Momentan wird bei "active" fortgefahren, ansonsten einfach eine entsprechende Fehlermeldung ausgegeben und zum Login redirected.
  4. open_session: Hier kommt alles rein, was zu Beginn der autorisierten Sitzung abgefackelt werden sollte. Ein Beispiel wäre das Setzen des Benutzerobjektes und seiner Gruppen in den Kontext des Controllers (vorher existiert dieses nur im Kontext des Authorisers)
  5. close_session: alles, was am Ende einer Sitzung (hier vermutlich eher im Sinne einer Aktion/eines Seitenaufrufs als im Sinne der Cookie-basierten Session, aber das muss ich noch evaluieren) bereinigt und gespeichert werden soll. Momentan wird einfach das Benutzerobjekt auf nil gesetzt
  6. change_authentication_token: vereinfacht ausgedrückt: Setze neues Kennwort. Mit Iris-Scans und Smartcards werde ich wohl nicht sooo viel zu tun haben :)

Der Standard-Authoriser kann bislang alle Benutzer aus der users-Tabelle authentifizieren, wobei es keine Rolle spielt ob das Kennwort mit UNIX-crypt (also DES), MD5-crypt, Blowfish-crypt oder Plaintext gespeichert ist. Beim Login wird an Hand von RegExps versucht, festzustellen wie das Kennwort gehashed wurde und ggfs das Salt extrahiert. Anschließend wird damit das eingegebene Kennwort gehashed und mit dem wert aus der Tabelle verglichen. Beim Setzen neuer Kennwörter (set_credentials) wird auf die Applikationseinstellung core::passwordhashmethod zurückgegriffen um die hash/crypt-Methode zu bestimmen.

Wenn mein Umbrello nun mitspielt werde ich ein kleines Klassendiagrämmchen von dem ganzen Auth*-System basteln, ansonsten wird's langsam Zeit, schlafen zu gehen.

Tatsache, es hat geklappt :)

January 15, 2008

Etwas Modellierarbeit oder: es lebe die 42!

Schon von Anfang an hadere ich mit mir, was die Modellierung der Benutzerklassen angeht (siehe anfängliche Posts...). Wie einigen aufmerksamen Lesern dieses Blogs bekannt sein dürfte, ist das Datenmodell nun so gestaltet, dass jede Entität, die ind er Wahren Welt auch ein auch ein Benutzer "ist", nun ein Benutzerobjekt aggregiert. Wenn ein Benutzer beispielsweise gleichzeitig einen E-Mail-, FTP- und Systemaccount hat, so sind das eben drei verschiedene Entitäten, die allesamt dasselbe Benutzerobjekt aggregieren. Dass es auch mit diesem Design zu Problemen kommen würde, war ja von Anfang an sonnenklar. Denoch werde ich diese Linie weiter verfolgen, zumal ja schon ein größerer Teil der Applikation darauf aufbaut. Doch nun gab's zwei kleine Änderungen:

  • Entgegen aller Regeln der Redundanzvermeidung und Normalisierung wurde im User ein Fremdschlüssel auf den Clientgelegt. So kommt man vom Benutzer aus wenigstens einfach an den "Besitzer", was ansonsten mit ganzen drei Joins bewerkstelligt werden musste.
  • FtpAccountsController, EmailAccountsController und SystemAccountController sind nun allesamt Kindsklassen von UsersController. Alles was man mit einem User machen kann, kann man logischerweise auch mit den anderen User-Arten machen -- darum dieser Schritt.

Die Klassenhierachie für die Benutzertypen schaut also nun so aus:

user-controllers-diagram.png

Wer sich über den Einschub von Consolvix::Application wundert, dessen Verwunderung sei hoffentlich mit der Erklärung besänftigt, dass ich das nicht etwa gemacht habe um die Fläche im Klassendiagramm etwas weiter aufzufüllen, sondern einfach um im ApplicationController etwas Übersicht zu schaffen. Alle privaten und internen Funktionen stehen nun weitestgehend in Consolvix::Application, während im (Application)Controller im Wesentlichen nur noch Actions stehen, die der Besucher direkt aufrufen kann.

Und was um alles in der Welt hat das alles mit der 42 zu tun? Nun, Dies ist der 42. Eintrag. In dem Sinne Lang lebe das Universum! ;-)

January 19, 2008

Verdeckte Transaktionen

Wie schon vor einiger Zeit in diesem Eintrag beschrieben, bietet Consolvix eine durchaus ausgeklügelte Möglichkeit, Transaktionen über mehrere Schritte zu implementieren. Gestern Nacht kamen mir kurz vorm Einschlafen dazu noch zwei Ideen, die ich vielleicht noch umsetzen möchte (hier wäre mir die Meinung eines Experten sehr willkommen).

  1. Momentan speichere ich die Daten zu einer Transaktion als YAML serialisiert im data-Feld einer ConsolvixTransaction ab. Wird die Transaktion abgebrochen, werden alle temporären Daten also gelöscht und gut ist. Aber eigentlich, ja EIGENTLICH, ist das ja nicht ganz sauber -- wenn man doch ein User-Objekt hat, und sei es nur ein temporäres, dann gehört das ja eigentlich in die users-Tabelle und in der Transaktion sollte nur die ID und der Typ des Objektes gespeichert werden. Wenn dann die Transaktion gelöscht wird, sollten diese temporären Objekte ebenfalls gelöscht werden. An sich kein Problem, ich bin mir sogar sicher, dass sich das äußerst elegant über HABTM und polymorphische Aggregation lösen ließe. Aber: So lange die Transaktion nicht abgeschlossen ist, befinden sich also unter den "gültigen" Objekten auch "ungültige", also solche, die überhaupt noch nirgends in der restlichen Applikation auftauchen sollten! Also müsste dann doch irgendwie jedes temporäre Objekt als solches markiert werden -- und dann hört die Sache wieder auf, elegant zu sein -- ganz im Gegenteil gar. Dabei sei angemerkt, dass theoretisch jede Entität (aus 20, 30, vielleicht auch irgendwann mal über 50 möglichen) in einer Transaktion vorkommen kann. Also doch lieber bei der jetzigen Methode (also dump/serialisieren) bleiben? Wie werden solche vorläufigen Daten in Businessapplikationen "der Großen" gehandhabt? Hilfe...?
  2. "Points of no Return" einbauen. Bitte was? Gerade bei Webhosting-Angelegenheiten gibt es Aktionen, die die Bestätigung des Benutzers/Kunden erfordern. Beispiel: neue Bestellung, bestehend aus vier Schritten. Schritt 1: Kunde wählt z.B. "5GB mehr Speicher". Schritt 2: Kunde bestätigt Bestellung und erklärt sich mit irgendwelchen Bedingungen einverstanden. Dann wird eine E-Mail an die Buchhaltung geschickt, diese bestätigt Schritt 3, sprich "Kunde bekommt nun 5 GB Speicher mehr abgerechnet". Dann wird eine E-Mail an den Admin geschickt, dieser bestätigt Schritt 4, dass die 5 GB auch tatsächlich freigeschaltet werden. Diese Transaktion hätte zwei "Points of no return": Nachdem der Kunde Schritt 2 bestätigt hat, kann er nicht mehr zu Schritt 1 zurück, was mit der aktuellen Transaktionsverwaltung aber jederzeit möglich wäre. Der zweite PonR wäre, nachdem die Buchhaltung ihren Teil bestätigt hat -- danach kann auch nicht mehr zu Schritt 3 zurückgesprungen werden, sondern entweder der Admin bestätigt auch Schritt 4, oder er bricht die ganze Transaktion ab und alles bleibt beim alten. Ein anderes Beispiel wäre z.B. das Registrieren eines neuen Accounts: der potentielle Kunde füllt drei Seiten Formularkram aus, schickt diese ab und kann danach nichts mehr an Schritt 1..3 ändern, auch wenn die Transaktion an sich erst abgeschlossen ist, wenn die Buchhaltung und der Admin (kann meinetwegen die gleiche Person sein) noch ihre 1...n Schritte abgeschlossen haben. Mir scheint es eigentlich sinnvoll und durchaus elegant, neben der Definition einer Sequenz von Actions als Schritte in einer Transaktion auch "Synchronisationspunkte" zu definieren, nach denen entweder abgebrochen oder weitergemacht, nicht aber zurückgesprungen werden kann. Wird das bei Datenbanktransaktionen nicht auch im Grunde genommen so verwendet? Stating the obvious? Denke ich nun wieder zu kompliziert? Meinungen? Buh-Rufe? Faule Eier?

Jedenfalls kommen noch weitere Änderungen an der bestehenden Transaktions-API hinzu, weil ich gemerkt habe, dass sich noch einige Komplifikationen ergeben, sobald verschiedene Teile einer Transaktion von unterschiedlichen Benutzern (teilweise noch nicht einmal vorhandenen!) ausgeführt werden können müssen.

...

und ich glaube, Deutsch ist tatsächlich die einzige Sprache, in der vier aufeinander folgende Verben ein grammatikalisch korrektes Konstrukt bilden können... ;-)

January 20, 2008

Von etwas komplizierteren Beziehungen

Wenn A viele B hat, und C viele B hat, kann C auch viele A haben, wenn C :through benutzt. Wenn A aber viele B und B viele C hat, außerdem C zu vielen B gehört, dann kann C nicht auch viele A haben, selbst wenn B :through benutzt.

Ich wollt's nur mal erwähnt haben, sollte jemand von euch das auch mal probieren wollen.

Wie Bahnhof? Gut, dann nochmal kurz:

:through geht nicht bei hasandbelongstomany-Assoziationen.

Mein Problem:

class AccessRight
  # ...
end

class AccessRightGrant
  belongs_to :acces_right
  belongs_to :subject,
             :polymorphic => true
end

class SystemGroup
  has_many :access_right_grants,
           :as => :subject
  has_many :access_rights,
           :through => :access_right_grants
  has_and_belongs_to_many :users
end

class User
  has_and_belongs_to_many :system_groups
  has_many :access_right_grants,
           :as => :subject
  has_many :user_access_rights,
           :class_name => 'AccessRight'
           :through => :access_right_grants
  has_many :access_rights,
           :class_name => 'AccessRight',
           :through => system_groups
end

... geht also NICHT.

Was ich erreichen möchte, ist dies: User hat AccessRights, SystemGroup hat AccessRights, User hat alle AccessRights, die SystemGroup auch hat. User.access_rights soll also alle , nicht nur des User's AccessRights zurückliefern (in obigem Code sollten erstmal nur die AccessRights der Gruppen geladen werden, nicht alle). AccessRightGrant ist die Linking Table zwischen AccessRight und User/SystemGroup, wobei letztere über die polymorphische subject-Spalte gelinkt werden.

Dem mit o.g. Code generierten SQL nach zu urteilen, liegt das Problem bei der HABTM-Beziehung. Bestätigt hat das ein Tauchgang in den Rails-Source, der übrigens mit seinen n Metaprogrammier-Ebenen mehr als faszinierend und beeindruckend ist, wenn man sich mal etwas Zeit für ihn nimmt. Bei einer normalen has_many-Beziehung zwischen User und SystemGroupkönnte es funktionieren, ausprobiert habe ich das jedoch nicht.

Aber es lässt sich für alles eine Lösung finden und bis ich hierfür eine elegante Lösung gefunden habe, werde ich einfach ganz skrupellos brute-force-Methoden wie "lade alle Rechte und durchsuche das Array" benutzen. Tja, Rails, das haste nun davon :-)

January 30, 2008

Ideen für die Konfigurationsverwaltung

Nach einem äußerst interessanten Vortrag in Essen über Themen aus der Quantenmechanik von Prof. Dr. Anton Zeilinger persönlich ergab es sich vorhin, dass Philipp und ich und ich zu einer kleinen Diskussionsrunde zusammenfanden, um ein Wenig über das Thema Konfigurationsverwaltung auf Webservern zu diskutieren. Dabei kam es, wie in solchen Diskussionen "leider" üblich, zu jeder Menge neuer Ideen, die ich am liebsten schon von Anfang an in meine Diplomarbeit eingebaut hätte.

Nachdem ich nun in meiner Serververwaltungs-Applikation so etwas wie Konfigurationsversionen eingebaut habe, stellte sich eine weitere, alles andere als triviale Frage: Wie ist das mit Konfigurationsdateien? Bekanntlich habe ich es zur Prämisse gemacht, dass Konfigurationsdateien, sofern sie sich nicht durch Datenbankeinträge ersetzen lassen, direkt von Consolvix bearbeitet werden und nicht jedes Mal aus Datenbankeinträgen generiert werden (uns somit händische Änderungen womöglich rückgängig machen). Um hier auch verschiedene Konfigurationen mit verschiedenen Einstellungen zu erlauben, wäre es denkbar, jeder Konfigurationsdatei z.B. eine entsprechende Endung zu geben die mit dem Schlüssel der jeweils aktiven Konfiguration in der Datenbank übereinstimmt. Diesen Gedanken habe ich nicht weiter verfolgt, da er mir etwas umständlich erschien. Wesentlich besser finde ich die Idee, generell alle Konfigurationsdateien (z.B. das komplette /etc-Verzeichnis) mit Subversion zu verwalten. So könnte man Konfigurationen ändern wie man lustig ist, und wenn mal irgendwann das halbe System dadurch abgeschossen sein sollte, dann checkt man eben eine ältere Konfiguration aus und versucht es von neuem. Um dann noch aus verschiedenen Systemkonfigurationen (Produktion, Wartung, ...) wählen zu können, könnte man immer noch mit Branching arbeiten: Für jede mögliche Konfiguration wird einfach ein neuer Zweig des Repositorys angelegt. Nachdem ich dann mal kurz Gebrauch vom allwissenden Dämon Google gemacht hatte, stieß ich auf ein e Beschreibung, wie man mittels SWIG die Subversion-Bindings für Ruby installiert (für Debian-Benutzer ist es noch einfacher: apt-get install libsvn-ruby ;-)). Besser noch: nach weiterem Googlen fand ich dann das Rails-Plugin acts_as_subversioned für versioniertes ActiveRecord, das Datenbankentitäten versioniert abspeichert! Etwas derartiges wäre eigentlich für mein Gesamtsystem von Anfang an sehr praktisch gewesen -- ich fürchte aber, dass es etwas zu viel Zeit kosten würde, das jetzt noch einzubauen (nach Ende der Diplomarbeit werde ich es aber auf jeden Fall weiter verfolgen!)

Ein weiteres Thema des o.g. Brainstormings war: Wie lasse ich Consolvix Veränderungen am System vornehmen, wenn diese nicht über die Datenbank abgefackelt werden? Momentan habe ich den Benutzer www-data einfach in die Gruppen gepackt, die Zugriff auf die zu den Diensten gehörenden Ordner hat, die es konfigurieren soll (Beispielsweise Gruppe subversion für /var/svn) Auf lange Sicht müsste dazu Consolvix aber root-Reche bekommen -- und spätestens hier sollten sämtliche Alarmglocken losklingeln. Also werde ich vermutlich folgende Lösung verwenden: Für alle durchzuführenden Änderungen im System wird ein Shell-Kommando oder -Script erstellt. Dieses wird an einen kleinen Dämon mit setuid=root weitergereicht, der genau zwei Kommandos ausführt: sudo <Benutzer> und das angegebene Kommando/Script. <Benutzer> ist hierbei immer die UID des gerade in Consolvix eingeloggten Benutzers. So wird sichergestellt, dass Consolvix selbst immer ein unprivilegierter Dienst bleibt und alle durchzuführenden Kommandos immer als der User ausgeführt werden, der gerade in Consolvix eingeloggt ist. Das geht, weil System- und Consolvix-Benutzer identisch sind.

secure_consolvix_exec0.png

Eine Idee war es, die so generierten Kommandos zuerst in ein Subversion-Repository einzuchecken, damit diese vom o.g. Dämon zuerst ausgecheckt und dann durchgeführt werden. Der Hintergedanke dazu war der, dass das erste Zielsystem für Consolvix aus einem Test- und einem Produktionssystem bestehen wird. Im Testsystem werden iterativ Änderungen vorgenommen und erst wenn das System zufriedenstellend lauft, werden diese auch am Produktionssystem vorgenommen. Hier wäre die Idee mit Subversion nicht schlecht, denn so würde sichergestellt werden, dass, ähnlich wie bei Rails-Migrations, immer alle Änderungen in der richtigen Reihenfolge und genau so durchgeführt werden.

secure_consolvix_exec1.png

Allerdings verletzt das die andere Arbeitsprämisse, nämlich dass manuelle Änderungen im System genauso behandelt werden sollen wie von Consolvix durchgeführte -- und jedes Mal manuell ein Shell-Skript zu erstellen, dieses einzuchecken und dann ausführen zu lassen ist gelinde gesagt etwas umständlich. Im Mainframe-Bereich ist das zwar Praxis, aber wir betreiben ja nur einen kleinen Linux-Server... Ich denke, dass in die Richtung noch mehr Gedanken folgen werden, aber bis auf Weiteres werde ich erstmal so weitermachen wie bisher.

February 3, 2008

Ein Paar Dia-Gramm

OK, diese Diagramme wurden nicht mit Dia erstellt, sondern mit Umbrello. Aber nachdem Philipp schon solch schöne UML-Bildchen online gestellt hat, muss ich natürlich nachziehen ;-)

Das folgende Diagramm fasst noch einmal bildlich zusammen, was ich in einem anderen Post zum Ablauf einer Transaktion beschrieben hatte. Übrigens wäre ich noch immer SEHR dankbar, wenn mir jemand der Erfahrung mit dem Thema hat, ein paar Kommentarzeilen zu dem Eintrag da lassen würde!

Ablauf einer Transaktion

Das zweite Diagramm soll dem unbedarften Leser einen ungefähren Überblick über den Ablauf der Request-Verarbeitung verschaffen. In Wahrheit passieren noch ein paar unwesentliche Schritte mehr, aber ich denke nicht, dass das Weglassen dieser Schritte einen falschen Eindruck vermitteln könnte:

Ablauf eines Requests

Fragen? Ideen? => dafür gibt's das Kommentat-Formular unten ;-)

January 20, 2009

@shop.products.published.for_category(@category).since(3.years.ago).bought_by(@me)

Wenn es etwas gibt, was uns im aktuellen Projekt die Haut gerettet hat, dann ist es vermutlich named_scope. Gerade in dem Moment, wo Vladimir und ich eigentlich entschieden hatten, dass wie die Grenze des mit ActiveRecord elegant lösbaren überschritten hatten, sprang es mir ins Gesicht.

Hintergrund: Hat man ein komplexes Datenmodell mit vielen n-ären Abhängigkeiten und einer entsprechend kombinatorischen Anzahl an interessanten Abfragen, so stößt man mit find_by_x_and_y(:conditions => '...', :joins => '...') doch schnell an seine Grenze. Beispiel:

Category.all(
  :select     => 'categories.*, COUNT(DISTINCT categories_products.product_id) AS product_count,
                  AVG(product_reports.popularity) AS category_popularity',
  :joins      => 'INNER JOIN categories_products
                    ON categories_products.category_id=categories.id
                  INNER JOIN products
                    ON categories_products.product_id=products.id
                  INNER JOIN product_groups
                    ON products.group_id=product_group.id
                  INNER JOIN product_reports
                    ON product_reports.product_tribe_id=product_groups.id
                  INNER JOIN shop_products
                    ON shop_products.product_id=products.id',
  :group      => 'category_id',
  :conditions => 'shop_products.workflow_state=\'published\' AND categories.shop_id=42',
  :order      => 'category_popularity')

Wer findet heraus, was diese Query liefert?

Die Lösung für dieses Problem lautet, wie schon gesagt, Named Scopes. Diese erlauben es, Bedingungen bei Abfragen beliebig zu kombinieren. Ein Beispiel:

Category.for_shop(42).published.by_popularity

das liefert das gleiche Ergebnis wie die erste Query und ist gleichzeitig um längen flexibler. Was steckt dahinter? Dieses:

class Category
  named_scope :published,
    :conditions => "shop_products.workflow_state='published'"

  named_scope :by_popularity,
    :order  => 'AVG(product_reports.popularity)',
    :group  => 'category_id',
    :joins  => 'INNER JOIN categories_products
                  ON categories_products.category_id=categories.id
                INNER JOIN products
                  ON categories_products.product_id=products.id
                INNER JOIN product_groups
                  ON products.group_id=product_group.id
                INNER JOIN product_reports
                  ON product_reports.product_tribe_id=product_groups.id
                INNER JOIN shop_products
                  ON shop_products.product_id=products.id'

  named_scope :for_shop, lambda {|shop_id|
    {:conditions => ['categories.shop_id=?', shop_id]}
  }
end

Ein Named Scope ist also eine Art abgespeckter Beziehungsdefinition (has_many, has_one etc. definieren intern auch Scopes), die zur Generierung einer komplexeren SQL-Query herangezogen werden kann. Somit lässt sich die Komplexität der Queries wieder schön kapseln und außer an der Stelle ihrer Definition hat man nirgends in der Anwendung mit SQL zu tun, sondern mit gut lesbaren Konstrukten wie @shop.products.published.for_current_shop.since(3.years.ago).bought_by(@current_user). Außerdem spart man sich das händische Erstellen vieler spezialisierter find-Methoden!

Lang lebe ActiveRecord ;)

*) Anmerkung: Beim Projekt handelt es sich nicht, wie vllt angedeutet, um ein Shopsystem. Der Code wurde quasi anonymisiert.