Single-Sign-On mit Moinmoin und Mailman 3
Marvin Gülker · 27.07.2019
Der Artikel erklärt anhand eines konkreten Beispiels (TSC-Projekt), wie man eine einheitliche Anmeldung (Single-Sign-On) mit Moinmoin und Mailman 3 ohne Zuhilfenahme einer Drittsoftware wie etwa eines LDAP-Servers umsetzt.
Kategorien: Software
Problemstellung
Bekanntlich leitet der Verf. die Entwicklung des freien und quelloffenen Computerspiels „The Secret Chronicles of Dr. M.“ (TSC). In der Frühzeit dieses Projekts gab es ein Wiki, welches aber nach einiger Zeit wegen zweier Probleme eingestellt werden mußte: zum einen gab es ein immenses Problem mit Spam-Bots, zum anderen war jeder Nutzer gezwungen, für das TSC-Wiki einen eigenen Account anzulegen, weil eine einheitliche Anmeldung für alle TSC-Dienste (sog. Single sign on) bis zum Schluß nicht umgesetzt wurde. Nachdem zuletzt wieder frischer Wind in die TSC-Entwicklung gekommen und eine Veröffentlichung einer neuen Version 2.1.0 absehbar geworden ist, war es an der Zeit, das Thema Wiki und einheitliche Anmeldung wieder aufzuwärmen. Bereits das erste Wiki lief mit Moinmoin weitestgehend klaglos, sodaß es nahe lag, auch diesmal wieder auf die Software zu setzen. Auf der anderen Seite steht Hyperkitty: das Web-Interface von Mailman 3, das für TSC Forenfunktionen bereitstellt und hoffentlich einmal allen an TSC Interessierten ein Austauschmedium jenseits des IRC-Kanals bieten wird. Beide Dienste sollten mit denselben Anmeldeinformatiouen genutzt werden können. Da Hyperkitty bereits voll in Gebrauch war, lag es nahe, Moinmoin an Hyperkitty anzubinden und nicht umgekehrt.
Dieser Artikel zeigt die gewählte Vorgehensweise auf, die dieses Problem für die Anforderungen des TSC-Projekts hinreichend gut gelöst hat. Sie ist bei weitem nicht perfekt, jedoch ist dies wohl nicht zuletzt der Tatsache geschuldet, daß der Verf. eigentlich kein Python programmiert. Erfahrene Python-Programmierer werden über die gewählte Vorgehensweise wahrscheinlich die Hände über dem Kopf zusammenschlagen. Das ist berechtigt; Verbesserungsvorschläge können gern gemacht werden. Schon jetzt wird aber darauf hingewiesen, daß die Server-Konfiguration im TSC-Projekt bewußt konservativ gewählt ist. TSC hat ein sehr kleines Entwicklungs-Team und ist darauf angewiesen, daß der Server weitestgehend wartungsarm ist. Wichtigste Konsequenz hieraus ist, daß auf dem Server keine Software an den offiziellen Repositorien vorbei installiert wird. Weder für Mailman 3 noch für Moinmoin sind deshalb die für Python sonst typischen virtuellen Umgebungen (virtualenv) im Einsatz. Dies sollte man bei der Lektüre im Hinterkopf behalten. Eine andere Folge aus dieser Zielsetzung ist die angestrebte Problemlösung überhaupt: denn schließlich gibt es mit LDAP, PAM oder gar Kerberos speziell auf einheitliche Anmeldung über mehrere Dienste hinweg zugeschnittene Software, welche sonst auch oft empfehlenswert ist. Für das TSC-Projekt bedeutete ihr Einsatz aber wieder eine Komponente mehr, die Wartung und Aufsicht benötigt, für welche einfach die Kapazitäten fehlen. Mit der hier beschriebenen Lösung treten zu Mailman 3, Moinmoin und den zur ihrem Einsatz ohnehin erforderlichen Dienstprogrammen dagegen keine größeren Komponenten mehr hinzu.
Abb. 1 skizziert die in die Lösung involvierten Programme und ihre Kommunikation untereinander. An der Spitze befindet sich zunächst der HTTP-Server, Apache httpd‚ der die eingehenden HTTP-Anfragen entgegen nimmt und diejenigen für dynamische Inhalte u.a. auf Moinmoin und Hyperkitty verteilt. Dies geschieht über WSGI, wobei die beiden WSGI-Endpunkte von uwsgi bereitgestellt werden. Bezüglich Hyperkitty als Web-Interface ergibt sich nichts weiter besonderes (rechter Teil der Abbildung). Insoweit handelt es sich wohl um eine recht übliche Art, Python-Webanwendungen bereitzustellen11 Diese „Reverse Proxy“ genannte Konfiguration mit Frontend-HTTP- und Backend-Anwendungs-Server ist so auch für Ruby-Webanwendungen üblich, wenngleich dort statt WSGI meist ebenfalls einfach auf HTTP zur Kommunikation zwischen Frontend- und Backend-Server zurückgegriffen wird. ‚ die weiterer Erläuterung nicht bedarf. Auf der linken Seite der Abbildung ist der Teil abgebildet, der vorliegend von Interesse ist. Moinmoin kommuniziert über ein Plugin mit Hyperkitty. Hyperkitty liest dann die eigentlichen Zugangsdaten aus der Datenbank und vergleicht die Paßwort-Hashes (wofür Hyperkitty auf für das Webframework Django übliche Techniken zurückgreift, die hier nicht weiter interessieren sollen). Nachfolgend wird die Konfiguration dieser Programme näher erläutert.

Bereitstellung der Daten aus Hyperkitty
Sowohl Hyperkitty als auch Moinmoin sind Python-Programme. Was läge da näher, als einfach Hyperkittys Code in Moinmoin zu laden, um die Authentifizierung durchzuführen? In der Tat mag das eine gangbare Variante sein, allerdings kann man die möglichen Folgen eines gleichzeitigen Ladens zweier ganz verschiedener Programme überhaupt nicht abschätzen. Um subtile, später schwer verständliche Probleme zu vermeiden, wurde deshalb entschieden, die Ausführung beider Programme streng voneinander zu trennen. Die deshalb erforderliche Kommunikation über ein externes Medium erfolgt mithilfe der Kommandozeile. Moinmoin erhält ein (in Python geschriebenes) Plugin, welches durch Kommando-Ausführung Hyperkitty die Authentisierung durchführen läßt. Das Skript bekommt dabei zwei Argumente: den Nutzernamen und das übermittelte Paßwort. Via Exitcode gibt es dann zu erkennen, ob die Authentisierung erfolgreich war, d.h. die beiden Paßwort-Hashes übereinstimmten. Zusätzlich wird im Erfolgsfalle auf der Standardausgabe die E-Mail-Adresse ausgegeben, damit Moinmoin diese erfährt. Dies führt zu dem – erwünschten – Seiteneffekt, daß die Nutzer ihre E-Mail-Adresse nur über Hyperkitty wirksam ändern können (siehe dazu noch unten III.).
Das von Hyperkitty genutzte Django-Framework bietet eine einfache
Möglichkeit an, beliebigen Code in Form von Python-Skripten im Kontext
der Webanwendung auszuführen. Jedes Django-Projekt enthält ein zentrales
Verwaltungsskript, manage.py=‚ welches eine Reihe administrativer
Befehle bereitstellt. Dazu gehört auch ein Befehl
[[https://django-extensions.readthedocs.io/en/latest/runscript.html][=runscript=]],
welcher wie der Name nahelegt ein Python-Skript ausführt. Allerdings muß
man dabei beachten, daß diese Skripte in einem dem Python-Paketformat
entsprechenden Verzeichnis abgelegt werden müssen, widrigenfalls sie
nicht gefunden werden. Die Django-Dokumentation selbst schlägt vor, dazu
im Projektverzeichnis einen Unterordner „scripts“ anzulegen. Das ist
insofern mißlich, als daß auf dem TSC-Server Hyperkitty ja aus dem
Repositorium installiert wurde und man unterhalb von =/usr
gemeinhin
keine Änderungen vornehmen sollte. An sich sollte man das Problem durch
Ergänzung des Python-Suchpfades um ein Verzeichnis außerhalb von /usr
mithilfe von uwsgi beheben können, doch genügt das manage.py
aus
welchen Gründen auch immer nicht (derselbe Trick funktioniert dagegen
bei Moinmoin tadellos, wie unten unter III. beschrieben werden wird). Um
die Auswirkungen so gering wie möglich zu halten, legt man deshalb einen
Symlink von /usr/share/mailman3-web/scripts
nach z.B.
/etc/mailman3/customscripts
an.
In diesem Verzeichnis kann man dann den Anweisungen der
Django-Dokumentation zu runscript
folgend eine leere Datei
__init__.py
anlegen, um das Python-Paketformat zu wahren. Das
eigentliche Authentisierungsskript legt man dann mit dem folgenden
Inhalt unter /etc/mailman3/customscripts/authtest.py
(oder einem
beliebigen anderen Namen) ab.
from django.contrib.auth import authenticate def run(*args): user = authenticate(username=args[0], password=args[1]) if user is not None: # A backend authenticated the credentials print(user.email) else: print("Invalid credentials") exit(1) # No backend authenticated the credentials
Das Skript ist so einfach, daß man es auch ohne vertiefte Python-Kenntnisse gut verstehen kann. Die übergebenen Argumente werden zur Authentisierung genutzt und bei Erfolg wird die E-Mail-Adresse des Nutzers ausgegeben. Die Methode authenticate() wird von Django bereitgestellt. Ausführen kann man das Skript dann ganz einfach von der Kommandozeile:
$ python manage.py runscript authtest --script-args johndoe s33cretp4ssw0rd
Gibt es Probleme, kann man mithilfe der zusätzlichen Option
--traceback
die vollständige Fehlermeldung erhalten.
Abruf in Moinmoin
Die eigentliche Herausforderung besteht nun darin, Moinmoin um eine neue Authentisierungsmöglichkeit zu erweitern, welche das soeben erstellte Skript aufruft. Glücklicherweise ist Moinmoins Authentisierungsmechanismus modular aufgebaut, d.h. jeder Mechanismus zur Authentisierung stellt ein eigenes Modul dar. Wie das Moinmoin-Wiki darlegt, kann man sich das zu Nutze machen, um ein eigenes Authentisierungs-Modul zu schreiben. Was sich zunächst schwierig anhört, ist aber tatsächlich gar nicht so kompliziert, wenngleich es natürlich schöner wäre, wenn Moinmoin bereits von sich aus ein Shellskript-Modul anböte. Schließlich wäre das universell einsetzbar und nicht auf TSC beschränkt. Jedenfalls kann und sollte man sich von den bestehenden Authentisierungs-Modulen in Moinmoins Quelltext inspirieren lassen. Besonders hilfreich sind etwa die LDAP- und Interwiki-Authentisierungs-Module. Nach deren Vorbild und mit der für einen Nicht-Python-Programmierer gehörigen Frickelei ist dabei das folgende Modul entstanden:
""" MoinMoin - Custom HyperKitty authentication """ import subprocess from MoinMoin import log logging = log.getLogger(__name__) from MoinMoin import user from MoinMoin.auth import BaseAuth, ContinueLogin, CancelLogin class HyperkittyAuth(BaseAuth): """ Authentication against HyperKitty's database """ name = 'hyperkittyauth' logout_possible = True login_inputs = ['username', 'password'] def __init__(self): BaseAuth.__init__(self) def login(self, request, user_obj, **kw): username = kw.get('username') password = kw.get('password') _ = request.getText if not username or not password: return ContinueLogin(user_obj, _('Missing user name or password.')) logging.debug("Trying to authenticate %r" % username) email = self.check_hyperkitty(username, password) if not email is None: logging.debug("Authentication successful. E-Mail is %r" % email) u = user.User(request, name=username, auth_method=self.name, auth_attribs=('name', 'password', 'email')) u.name = username u.email = email u.create_or_update(True) logging.debug("Completed authentication for %r" % username) return ContinueLogin(u) else: logging.debug("Authentication failed.") return ContinueLogin(user_obj, _('Incorrect user name or password.')) def check_hyperkitty(self, username, password): try: return subprocess.check_output(["python", "/usr/share/mailman3-web/manage.py", "runscript", "--traceback", "authtest", "--script-args", username, password]).rstrip() except: logging.debug("Hyperkitty says no") return None
Die login()
-Methode wird von Moinmoin zwecks Authentisierung
aufgerufen. Diese extrahiert die übergebenen Zugangsdaten und prüft
durch Aufruf von check_hyperkitty()
diese auf ihre Gültigkeit. Sind
sie gültig, wird ein neues Nutzer-Objekt angelegt oder ein bestehendes
aktualisiert. Letzteres führt dazu, daß in Hyperkitty vorgenommene
Änderungen der E-Mail-Adresse eines Nutzers bei der Anmeldung
automatisch nach Moinmoin (welches jene etwa für Benachrichtungen
benötigt) übernommen werden. So muß sich der Nutzer nur um die
Verwaltung seines Hyperkitty-Kontos kümmern. Übergibt man
ContinueLogin()
eine User
-Instanz, so wertet Moinmoin die
Authentisierung offenbar als gelungen, im Übrigen nicht. Wichtig: wird
im letzteren Fall ein String als zweites Argument übergeben, dann wird
dieser dem Nutzer als Fehlermeldung angezeigt. Die aufgerufene Methode
HyperkittyAuth.check_hyperkitty()
nun führt den oben unter II. in
Aussicht gestellten Shellout durch und ruft das dort beschriebene Skript
über manage.py
auf. Die Auswertung ist weitgehend selbsterklärend.
rstrip()
wird benötigt, um das abschließende Zeilenumbruchzeichen,
welches von print()
stammt, zu verwerfen. Schließlich gehört es nicht
zur E-Mail-Adresse.
Das Skript kann erfreulicherweise außerhalb von /usr
abgespeichert
werden, solange man dabei die Regeln für Python-Pakete einhält. Im Falle
von TSC habe ich mich für
/etc/moin/hyperkittyauth/hyperkittyauth/__init__.py
entschieden. Das
Verzeichnis /etc/moin/hyperkittyauth
ist dann dem Python-Suchpfad für
Moinmoin hinzuzufügen. Dies geschieht in Falle von TSC mithilfe von
uwsgi (siehe unten IV.). Es ist darauf zu achten, daß Moinmoin für
dieses Skript Leserechte erhält.
Moinmoin muß nun noch so konfiguriert werden, daß es nur auf das neu
erstellte Modul hyperkittyauth zurückgreift, wenn es Nutzer
authentisieren muß. Dazu ist die Wiki-Konfiguration anzupassen, welche
sich bei der hier Verwendung findenden Debian/Ubuntu-Paketierung von
Moinmoin unter /etc/moin/mywiki.py
befindet. In die Konfiguration ist
zunächst das neue Modul zu importieren:
import hyperkittyauth
Anschließend ist die Klasse Config
um folgende Zeile zu ergänzen:
auth = [hyperkittyauth.HyperkittyAuth()]
Durch vollständiges Überschreiben von auth
wird gewährleistet, daß
wirklich nur auf dieses Modul zur Authentisierung zurückgegriffen wird.
Konfiguration von httpd und uwsgi
Was bleibt, ist die Konfiguration von Apache httpd und uwsgi. Zu ersterem gibt es nur wenig zu sagen. Moinmoin wird wie Hyperkitty auch per Reverse Proxy über das WSGI-Protokoll angebunden. Das sieht im Falle von TSC so aus (Einsatz eines UNIX-Domain-Sockets):
Alias /static /usr/share/moin/htdocs ProxyPass /static ! ProxyPass / unix:/run/uwsgi/app/moinmoin/socket|uwsgi://localhost/
Bemerkenswert an der folgend wiedergegebenen uwsgi-Konfiguration ist vor
allem die Direktive pythonpath
. Sie ergänzt den Python-Suchpfad um das
Verzeichnis /etc/moin/hyperkittyauth
, in welchem sich das unter oben
III. vorgestellte neue Authentisierungsmodul befindet. So kann Moinmoin
dieses auffinden. Im Übrigen bestehen keine Besonderheiten, sieht man
davon ab, daß Moinmoin seine WSGI-Datei unter
/usr/share/moin/server/moin.wsgi
bereitstellt (was der Paketierung
geschuldet ist).
[uwsgi] uwsgi-socket = /run/uwsgi/app/moinmoin/socket enable-threads = true chdir = /usr/share/moin/server wsgi-file = moin.wsgi master = true process = 2 threads = 2 uid = www-data gid = www-data plugins = python pythonpath = /etc/moin/hyperkittyauth
Schluß
Der Artikel zeigt, daß die Verbindung von Mailman 3 mit Moinmoin mit etwas Aufwand gut zu bewerkstelligen ist. Erfreulich ist, daß durch Verzicht auf eine zusätzliche komplexe Komponente wie einen LDAP-Server der Wartungsaufwand gering gehalten wird. Lediglich zwei kleine Skripte treten hinzu, die man schnell nachvollziehen kann. Trotzdem wird eine vollwertige Single-Sign-On-Lösung umgesetzt, die es den Nutzern fortan ermöglichen wird, ihren TSC-Foren-Account auch für das korrespondierende Wiki zu verwenden.
Genutzte Software-Versionen:
- Ubuntu 18.04
- Moinmoin 1.9.9
- Mailman 3.1.1
- uwsgi 2.0.15
- Apache httpd 2.4.29
Fußnoten:
Diese „Reverse Proxy“ genannte Konfiguration mit Frontend-HTTP- und Backend-Anwendungs-Server ist so auch für Ruby-Webanwendungen üblich, wenngleich dort statt WSGI meist ebenfalls einfach auf HTTP zur Kommunikation zwischen Frontend- und Backend-Server zurückgegriffen wird.