About January 2008

This page contains all entries posted to /blog/wvk in January 2008. They are listed from oldest to newest.

December 2007 is the previous archive.

February 2008 is the next archive.

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

Powered by
Movable Type 3.31

« December 2007 | Main | February 2008 »

January 2008 Archives

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 4, 2008

Auf zwei Schienen fährt man besser...

...so dachte ich mir, und stand vor der Entscheidung, ob ich nun den gleichen kapitalen Fehler begehen sollte, den ich schon während der Bachelorarbeit (nur damals mit PHP) begangen hatte: Sollte ich mitten während der Entwicklung einer Software das Framework aktualisieren? Bislang arbeitete ich ja mit Rails 1.2.6 und war gut zufrieden, doch dann wurden ein paar neue Features (wie eingebautes HTTP Basic Auth... Blog-Artikel dazu ist in Arbeit!) doch zu verlockend.... und ich tat es.

Ja, ich fahre nun auch auf Rails 2.0.

Und der Umstieg gelang erstaunlich reibungslos! Meine Schritte waren:

  • Lesen des Artikels mit den Änderungen in Rails 2.0 aus dem InnoQ-Wiki
  • Deinstallieren von Rails 1.2.6 (über APT, denn ich hatte Rails nicht über Gem installiert)
  • Installieren von Rails 2.0 (über Gem, denn Rails 2.0. gab's noch nicht in meinen Apt-Repos)
  • Deployen einer plain vanilla Rails App irgendwohin
  • Kopieren von /config/{boot, environment, routes, initializers}, /script/* und /public/{dispatch.*, *.html} von vanilla nach Consolvix
  • Korrigieren der Pfadangaben zu den includes in dispatch.fcgi, denn sonst spackte Apache
  • Umbenennen aller Views mit u.g. Script, das vielleicht auch jemand anderem irgendwann eine Hilfe sein könnte.

#!/usr/bin/env ruby
# rename all *.rhtml files within the /app/views directory ro *.html.erb
# Author: WvK <[email protected]>
require 'fileutils'
Dir.new('.').each do |entry|
  if File.directory? entry and not entry[0..0] == '.'
    Dir.new("./#{entry}").each do |file|
      if /.*.rhtml/.match file
        old_path = "./#{entry}/#{file}"
        new_path = old_path.gsub /.rhtml/, '.html.erb'
        if FileUtils.mv old_path, new_path
          p "mv #{old_path} -> #{new_path}"
        else
          p "ERROR RENAMING #{old_path} to #{new_path}!"
        end
      end
    end
  end
end

Tja, und was soll ich sagen: läuft! Bis auf die Sache mit der pagination, derer ich mich noch nicht angenommen habe, läuft alles weitestgehend tadellos. Ich bin froh, dass ich ab und an mal die Deprecation-Warnungen in /log/development.log zu Herzen genommen hatte!

Nun wo der Schritt also vollbracht ist, ist Consolvix bestimmt besser als je zuvor den Änderungen, welche die Zukunft bescheren wird, gewachsen ;-)

Desktop-Icons die Zweite + Navigation

Gerade fiel mir etwas auf, was schon lange implementiert ist, wozu ich aber noch kein Wort geschrieben habe (vermutlich eingeschüchtert durch die vielen gut gemeinten Ermahnungen, ich solle mich mehr um's Innenleben als um die Oberfläche meiner Software kümmern...).

Richtig geraten, es geht um die Oberfläche. Vor einiger Zeit hatte ich das Konzept der Deskop-Icons, die man per Drag&Drop auch als Applets einsetzen kann. Nun schien das einigen Menschen nicht sehr intuitiv und bei anderen funktionierten entweder die Links oder das Drag&Drop, deswegen gabs dort ein paar Änderungen:

  • Es wird nun unterschieden zwischen normalen "Navigations"-Icons und Applet-Icons. Applet-Icons sind nicht mit einem Link belegt und können per Drag&Drop auf die Applet-Flächen auf dem Desktop gezogen werden. die anderen Icons sehen ähnlich aus, liegen aber auf einer separaten Fläche und können nur angeklickt werden (sind also "gepimpte" Links).
  • Nicht alle Funktionen können als Applet aufgerufen werden. Bislang wurde im Aufruf einer Aktion als Applet bei der proxy-Funktion (callfunctionas_applet(controller, action)) aus einem fest verdrahteten Array gelesen, ob die verlangte Funktion als Applet angezeigt werden kann. Jetzt kann über den Klassen-Methodenaufruf callable_as_applet :action1, :action2, ... pro Controller festgelegt werden, welche Aktionen als Applets angezeigt werden können.
  • Navigations-Icons werden nicht mehr fest verdrahtet vie vorher, sondern dynamsich aus der Datenbank geladen. Der Administrator kann selbst bestimmen, welche URI er mit welchem Icon verbinden will, welcher Text und Tooltipp es haben soll, in welchem Modul und bei welchen Benutzer-Typen es angezeigt werden soll (momentan sind dies: admin, user, reseller). Um ein Desktop-Icon im Controller ein @icon = ConsolvixDesktopIcon.find XYZ und in der View <%= desktop_icon @icon %>.
  • Navigations-Icons erscheinen nicht nur bei /XXX/index auf dem "Desktop" von XXX, sondern auch bei /XXX/YYY als Navigationseintrag (siehe Bild)


Jedes Desktop-Icon entspricht auch einem Haupt-Navigationseintrag für das jeweilige Modul. Desktop-Applets sind nun von den anderen Icons getrennt.


Auf Modul-Index-Seiten kann z.B. wieder ein Modul-Desktop mit Aktionen für den aktuellen Kontext angezeigt werden


Im Kontext einer Resource (z.B. Clients) wird immer eine Liste von zur Verfügung stehenden Elementen angezeigt. Im Kontext eines Elements wird eine Liste mit zur Verfügung stehenden Aktionen ausgeklappt.

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 14, 2008

Neue Hardware braucht der Mensch

Neues Jahr, neuer Rechner, so dachte ich mir. Endlich war es dann letzte Woche auch so weit, dass hier ein Paket nach dem anderen eintrudelte und sich im Laufe eines Nachmittags ein neuer Rechner zusammenfügte. Nach vielen Jahren erfüllte sich endlich mal wieder ein kleiner Traum, nämlich die von schneller Hardware und t<\epsilon Reaktionszeit für flüssiges Arbeiten. Man mag staunen, aber ein AMD Athlon mit 1200MHz und 1GB Speicher ist nicht mehr ausreichend, um mit KDE flott Webentwicklung betreiben zu können. Nun steht hier also ein schöner Core2Quad mit 6GB Speicher und der rennt mir sozusagen davon -- sogar mit transparenten Fenstern und Schatten und allem trallala :-). Der Bootvorgang dauert keine Minute und 4-6 Sekunden nach dem Login ist KDE vollständig geladen. WOW!

Aber es hat leider mehr als drei Tage gedauert, bis dieses System mal vernünftig lief (und einen Tag bis sich überhaupt erstmal ein Installationssystem booten ließ). Ich sage nur: 64Bit sind so viel Fluch wie Segen. Von allen Live-CDs die Philipp und ich durchprobiert haben (Debian, Knoppix, Ububntu, SuSE, Sidux) wollten gerade mal zwei starten und bei keinem funktionierten die Onboard-LAN-Ports -- Stichwort nVidia nForce. Das forcedeth-Modul was zum Betrieb der Ethernetports nötig ist, wurde zwar unter Sidux geladen, auch ließen sich beide Ports einrichten und ansteuern, aber tatsächlich wurde nichts über die Leitung geschickt. Ein Glück dass hier noch eine alte 3Com-Karte herumschwirrte, mit der bin ich jetzt online. Schließlich gelang es, über Philipps Mac eine 64-bit Debian-Netinst-CD zu laden (Hier der Hinweis an potentielle Nachahmer: ia64 ist NICHT, wie es der Name nahe legt und ich es erst vermutete, die Archikektur eines Intel Core2Duo/Quad -- es SOLLTE i386 gehen (ging dann auch, nachdem im BIOS gerumgedreht wurde, ganz komisch alles g), richtig ist aber amd64, auch wenn's ein Intel-Prozi ist. Damit lief jedenfalls der Rechner ziemlich bald und mit einem selbst kompilierten Kernel (t_compile < 30 min) ohne die ganzen optionalen Komponenten rennt er gar.

Irgendwie muss man aber die überschüssige Rechenleistung auch wieder los werden, und somit erachte ich es als selbstverständlich, die als RAID1 geschalteten Platten zu verschlüsseln. der neue Debian Sid-Installer macht es gar möglich, schon bei der Installation ein Software-RAID und Verschlüsselung mit allem drum und dran einzurichten. Das Problem ist nur: es funktioniert nicht so wie man erwartet. Der Installer ermöglichte es mir problemlos, /dev/sda1 und /dev/sdb1 zu einem RAID1-Verbund zusammenzufassen, das entstandene /dev/md0 zu verschlüsseln und darauf wiederum zwei Partitionen (/ und swap) anzulegen. Das System ließ sich absolut einwandfrei installieren. Nur leider konnte das System danach nicht booten, da der Device-Mapper, der für das mounten verschlüsselter Partitionen zuständig ist, noch nicht geladen sein kann, bevor der Kernel das RAID zusammengefügt hat, und das RAID nicht zusammengefügt werden kann, bevor / gemountet ist -- jedenfalls ist das ein bekannter und gemeldeter Bug und ich vermute stark, dass das nächste Release von Debian diese Möglichkeit nicht mehr im Installer eingebaut haben wird. Jedenfalls liegt / jetzt weiterhin unverschlüsselt auf der Platte, aber die restlichen Dateisysteme sind allesamt mittels dm-crypt verschlüsselt. Ich kann nicht behaupten, dass dadurch wie Performanz des System in irgendwelcher Weise beeinträchtigt wird.

So, doch nun: auf ins (endlich wieder) produktive Arbeiten! Nebenbei kann ja mal KDE 4 compilen, bin gespannt wie lange das dauert (Qt alleine brauchte 15 min mit 10 Threads)...

January 15, 2008

Neues System, neue Probleme

Mal wieder ging ein potentiell produktiver Tag ohne ein produktives Ergebnis zu Ende, weil mein Apache nicht mit den Rails-Anwendungen spielen wollte. mod_cgi meldete das übliche "premature end of script headers", was im Wesentlichen nichts weiter aussagt als "irgentwas stimmt da nicht". mod_fcgid hatte anschienend noch viel weniger Lust, denn eine Instanz nach dem Anderen hängte sich einfach nach einem "unexpected Signal 11" auf, später gab's dann auch noch lustige Segfaults dazu. Neuinstallieren von Apache und Modulen und trallala brachte alles nichts, auch die Schreib-, Lese- und Ausführrechte waren alle korrekt gesetzt FRUST.

Schließlich und endlich rang ich mich dann dazu durch, den Apachen mitsamt des Fcgid-Moduls selber zu kompilieren, denn vielleicht gab es ja tatsächlich Probleme, weil der eine oder andere Teil noch im 32 Bit-Modus lief. Glücklicherweise ist selber bauen und dann installieren unter Debian sehr sauber und einfach:

$ cd /usr/src
$ apt-get source apache2 libapache2-mod-fcgid
  ... [ratter] ...

das holt erstmal die ganzen Sourcen. Anschließend wird kompiliert:

$ cd apache2-2.2.6
$ fakeroot debian/rules binary
  ... [ratter ... ratter ... ratter] ...

dito für mod_fcgid. Im Verzeichnis /usr/src liegen anschließend einige .debs:

apache2_2.2.6-3_all.deb
apache2.2-common_2.2.6-3_amd64.deb
apache2-dbg_2.2.6-3_amd64.deb
apache2-doc_2.2.6-3_all.deb
apache2-mpm-event_2.2.6-3_amd64.deb
apache2-mpm-perchild_2.2.6-3_all.deb
apache2-mpm-prefork_2.2.6-3_amd64.deb
apache2-mpm-worker_2.2.6-3_amd64.deb
apache2-prefork-dev_2.2.6-3_amd64.deb
apache2-src_2.2.6-3_all.deb
apache2-threaded-dev_2.2.6-3_amd64.deb
apache2-utils_2.2.6-3_amd64.deb

Installiert werden sollten jedoch nicht alle (genauer gesagt geht das gar nicht). Es reicht, common, mpm-worker, utils und fcgid zu installieren.

# dpkg -i apache2.2-common_2.2.6-3_amd64.deb \
   apache2-mpm-prefork_2.2.6-3_amd64.deb \
   apache2-utils_2.2.6-3_amd64.deb

wenige Minuten später ... siehe da ... der Apache startet (OK, das hätte ich auch erwartet) und Rails rennt wieder. Geschafft!

Vielleicht noch ein paar Worte zu den verschiedenen MPM-Paketen oben: Seit der Version 2.X besteht der Apache aus zwei Teilen, einmal einem common-Paket und einem mpm-*-Paket. Die eigentliche Arbeit, nämlich das Bedienen der Clients, macht je ein "Arbeiter". Da der Apache bekanntlich durchaus 1000+ Anfragen pro Sekunde verarbeiten können muss, versteht sich von selbst, dass da irgendwie parallele Prozesse am Werk sein müssen. Und tatsächlich, "MPM" steht für Multi-Processing Module.

MPM-PREFORK ist das klassische Apache-Worker-Modell, wo sich einfach beim Start des Servers schon mehrere Apache-Instanzen im Speicher einnisten. Jede Instanz für sich genommen arbeitet singlethreaded. Dieses "Thread"-Modell ist sehr ausgereift und meines Wissens neben MPM-ITK das einzige, was mit Rails (und auch PHP5) zusammenarbeitet.

MPM-ITK erlaubt es, eine eigene UID und GID pro VirtualHost anzulegen, also ähnlich wie mod_suexec es für PHP tut. Das Klingt auf jeden Fall für meinen Server schonmal sehr interessant...

Die anderen MPM-Modelle sind entweder nicht sonderlich ausgereift oder sonstwie ungeeignet, deswegen werde ich nicht weiter drauf eingehen und mich einfach freuen, dass der alte Indianer wieder läuft :-)

INI-Files

Heute Nacht saß irgendwann wie betäubt vor meinem Quelltext und machte ein apathisches Auditing, bis ich auf die Stelle stieß, wo ich im TracEnviironment-Model die Trac.ini verarbeite. Wie bereits anderswo beschrieben, werden die Werte aus der ini-Datei als Attribute der jeweiligen TracEnvironmant-Instanz abgelegt, damit ich darauf zugreifan kann wie auf ein ActiveRecord-Objekt. Das übernimmt diese Methode:

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

Zurückschreiben geht ähnlich, halt einfach den gleichen Weg rückwärts. Das funktioniert alles Prima so weit udn ich denke auch nicht, dass ich das so bald ändern werde. Dennoch packte mich die Neugier, ob es nicht vielleicht ein Rails-Plugin oder ein Gem zurm INI-Dateihandling gibt. Und siehe da: das inifile-Gem tut genau das: https://rubyforge.org/projects/inifile. Intern werden die "Sections" als Hash gespeichert und auf die Sections wird ebenfalls wie ein Hash zugegriffen.

Alles sehr nett. Viel Spaß damit, sollte jemand das mal brauchen...

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 16, 2008

Neues Aussehen

Wie man sieht ist diese Nacht nicht ganz spurlos an diesem Blog vorbeigezogen. Ich hoffe das neue Layout gefällt dem einen oder anderen. Anregungen. Kritik und scharfe Munition nehme ich gerne entgegen, dafür gibt's ja die praktische Kommentierfunktion.

Und: huch, wer hätte das gesacht, auch ich kann kurze Posts schreiben! ;-)

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 21, 2008

Ein weiterer Rails-Bug

Vielleicht erinnert sich der eine oder andere aus der Schnittmenge der treuen Leser dieses Blogs und der regelmäßig bei unserer Diplomandenrunde in Dortmund Anwesenden noch daran, dass wir "damals" erwähnt hatten, dass es manchmal, aber nicht immer, in Rails zu einer Exception kommt, wenn man z.B. sowas hier probiert:

@client.user.system_groups.access_rights

obwohl alles ganz sicher korrekt mit has_many usw. definiert wurde. Ich werde hier nichts breittreten, was mich grob geschätzt die letzten 4 Stunden und viel mehr Nerven als diese Stunden Sekunden haben, gekostet hat, sondern es in einem einfachen Satz zusammenfassen:

NIEMALS in Join-Tables einen Primärschlüssel namens 'id' verwenden!!

das Problem ist, dass bei einem Join wie diesem:

User.find(17).system_groups

dieses SQL generiert wird:

SELECT * FROM `system_groups` 
  INNER JOIN groups_users 
    ON system_groups.id = groups_users.group_id 
WHERE (groups_users.user_id = 17 )

Na, wem fällt's auf? genau, da steht SELECT * FROM ..., und nicht, wie es sich gehört, SELECT system_groups.* .... Damit tritt der Spaltenname id nämlich zweimal im Resultat auf (einmal von der Gruppentabelle und einmal von der Join-Table), wobei letzten Endes die ID von der Join-Table von Rails verarbeitet wird und dann als FALSCHE system_group_id eingesetzt wird.

Nach dem Entfernen der id Spalte aus groups_users lief alles wie erwartet.

Man mag mich bei Gelegenheit dafür steinigen, dass ich in Join-Tables eine künstliche ID-Spalte verwende, aber Rails' Verhalten bezeichne ich in diesem Fall als schlichtweg falsch. I hereby declare it a bug.

January 25, 2008

Rubys Objektmodell

OK, was ich jetzt hier schreibe ist nichts neues und auch schon auf viel bessere Art und Weise von Why the Lucky Stiff beschrieben worden. Jedenfalls trieb mich ein Bug in Consolvix dazu, mich nochmals intensiver mit dem Thema Metaprogrammierung und dem Ruby-Objektmodell auseinanderzusetzen. Es folgen ein Paar Notizen.

  • Objekte in Ruby können nur Variablen aufnehmen -- Ein Ruby-Objekt hat keine Methoden
  • Objekte sind in erster Linie dies: Objekte. Erst in zweiter Linie sind sie Instanzen einer Klasse. Auch Klassen sind Objekte (ABER auf C-Quellcode-Ebene sind Object und Class zwei verschiedene Structs -- ich halte diese Information für wichtig, wenn man wirklich blicken will, warum diese Class-Objekt-Abhängigkeit keine unendliche Rekursion mit sich bringt)
  • Objekt-/Instanz-Methoden befinden sich in der Klasse, von der sich das Objekt ableitet. Eine Klasse ist nicht ein statisches, abstraktes Dingda, sondern ein Singleton-Objekt. Ein Objekt, das (Klassen-)Variablen UND Methoden aufnehmen kann. Eine Klasse eben. (Und hier würde dann die Rekursion einsetzen ;-))
  • Klassenmethoden befinden sich -- nein, nicht in der Klasse, die ja ein Objekt ist, sondern in der Klasse, die meistens Metaklasse genannt wird.
  • Klassen vererben ihre Methoden ihren Unterklassen. Somit steht, wenn man eine Klasse um eine neue Methode bereichert, sofort sämtlichen Unterklassen und sämtlichen Instanzen der Klasse diese Methode zur Verfügung. Wenn man aber einem einzelnen Objekt an dieser Stelle eine andere Implementierung dieser Methode geben möchte, so kann man dem Objekt diese Methode über die Metaklasse dieses Objektes zur Verfügung stellen. Methoden werden zuerst in der Metaklasse dann erst in der Klassenhierachie gesucht.
  • Das Konzept der Vererbung erweitert eine Klassenhierachie vertikal, während Module die Klassenhierachie horizontal erweitern -- man könne also sagen, dass das Konzept der Metaklassen die Hierachie in der tiefe erweitern -- aber das klingt gleich wieder so esotherisch...
  • Metaklassen können, da diese wiederum Objekte sind, selbst auch wieder Metaklassen enthalten. Eine Veränderung der Metaklasse einer Metaklasse einer Klasse zieht jedoch keinerlei Konsequenzen für die Klasse nach sich. Außerdem ist es sehr unwahrscheinlich, dass von diesem Konzept jemals jemand Gebrauch machen wird (OK, ich werde meinen kranken Geist darauf ansetzen, in einer freien Minute etwas passendes zu finden ;-))

Folgendes ER-Diagramm hab ich nach meinem Verständnis des Sachverhaltes zusammengestellt. Wenn jemand darin einen grundlegenden Fehler entdeckt, bitte sofort melden!

Dieses Klassendiagramm habe ich nach dem ASCII-Diagram aus einem (vermutlich bekannten) Post in der Ruby-Talk-Mailingliste gebastelt. Es stellt die Abhängigkeit der Klassen und Objekte untereinander dar. Wie man sieht, erbt letztendlich alles von Object. Die Klassen mit eingeklammerten Namen sind die jeweiligen Meta-Klassen (bzw. Singleton-Klassen-Instanzen...).

RubyObjectModelInheritances.png

Ich finde, dass das Bild etwas klarer wird, wenn man die Vererbung durch eine gerichtete Beziehung der Objekte untereinander ersetzt. super zeigt somit immer auf das Objekt der Elternklasse während self auf das Objekt der Singleton-Klasse zeigt.

RubyObjectModelReferences.png

So hat man im Wesentlichen die Abbildung des Ruby-Objektmodells auf C-Ebene vor sich: Vererbung und "magische Meta-Dingsda" sind nichts weiter als Structs, von denen einige lediglich Variablen, andere auch Methoden referenzieren können... der ganze Zauber gelüftet, aber eine Menge mehr Klarheit geschaffen :)

Zu dem ganzen ist anzumerken, dass nicht zu jedem Objekt a priori eine Singletonklasse existiert -- das würde unendlich viel Speicher erfordern. Eine Singletonklasse wird nur dann erstellt, wenn sie explizit angefordert wird, und zwar über

class Foo
  # ...
end

obj = Foo.new

class << obj
  # hier sind wir im Kontext der Metaklasse von obj!
end

class << Foo
  # hier sind wir im Kontext der Metaklasse von Foo!
end

class Foo
  class << self
    class << self
      # im Kontext der Metaklasse der Metaklasse von Foo ;-)
    end
  end
end

Alles klar? Nicht? Dann hilft nur lesen des o.g. Artikels von Why the Lucky Stiff oder sich einfach mit anderen Dingen beschäftigen...

January 27, 2008

Ein paar Worte zu Modulen in Ruby

Als Ergänzung zu meinem letzten Eintrag zum Thema "Rubys Objektmodell" folgen ein paar Notizen dazu, wie sich Module in das Gefüge einfügen (müssen sie ja irgendwie, sonst wäre es kein Gefüge ;-))

  • Wird ein Modul mittels include eingebunden, werden alle Modulmethoden zu Instanzmethoden der einbindenden Klasse.
  • Wird ein Modul mittels extend eingebunden, werden alle Modulmethoden zu Klassenmethoden der einbindenden Klasse.
  • es ist (in Rails) durchaus üblich, ein Modul Foo zu definieren, welches dann mit include Foo eingebunden wird, sowie ein Untermodul Foo::ClassMethods, welches mittels extend Foo::ClassMethods eingebunden wird. Diese Praxis habe ich einfach mal so übernommen.

So viel zu den Methoden. Doch was ist mit den Variablen? Wie wir alle wissen, werden Klassenvariablen weitervererbt, Instanzvariablen jedoch nicht (irgendwie logisch...). Aber bei Modulen...?

  • Da ein Modul nicht instanziiert werden kann, kann es auch keine Instanzvariablen (@foo) haben -- ganz einfach. Wird ein Modul mittels include eingebunden, müssen alle @foo's also Instanzvariablen der Objekte werden; wird es mit extend eingebunden, sind es eben Instanzvariablen der Klasse (und NICHT etwa Klassenvariablen oder so...)
  • Klassenvariablen (@@bar) werden bei include einfach übernommen -- sprich, was im Modul eine "Klassen"variable war, wird auch in (den Instanzen) der einbindenden Klasse als Klassenvariable erhalten bleiben. Bei extend jedoch sind alle im Modul definierten Klassenvariablen nur für die im Modul definierten Methoden erreichbar! Alle Versuche, aus dem Kontekt der Klasse (oder einer ihrer Instanzen) an sie heranzukommen, sind bei mir bislang gescheitert.

An einem Beispiel zeigt sich so was immer schön:

module Foo
  @@truth = 42

  def say_the_truth
    p @@truth
  end
end

class Bar
  include Foo
end
Bar.new.say_the_truth # 42 *SMILE*

class FooBar < Bar
  def say_something
    p @@truth
  end
end
FooBar.new.say_something # 42 *SMILE*

class Baz
  extend Foo

  def say_something
    p @@truth
  end
end
Baz.say_the_truth # 42 *SMILE*
Baz.new,say_something # PENG! WUMMS! "NameError: uninitialized class variable @@truth in Baz" ... *Autsch*

Alles klar?

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.