Python für Webanwendungen 3: Türsteher
Posted by Jesco Freund at Oct. 26, 2010 11:29 a.m.
Mit ein wenig Phantasie kann die Anwendung aus Teil 2 hübsche HTML-Seiten rendern und verschiedene Themes verwenden. Doch was, wenn eine dieser Ansichten vor unbefugten Augen geschützt werden soll? Sicher, CherryPy bringt eine rudimentäre Authentifizierung über HTTP-Auth mit – keine sehr elegante Lösung. Von einer modernen Webanwendung erwartet man eher ein Login-Formular.
Das wäre mit einem hübschen Template und einer Controller-Methode ja noch fix erledigt. Doch wogegen authentifiziert eine Anwendung eigentlich? Viele Webanwendungen haben die in meinen Augen sehr schlechte Angewohnheit, auf eine eigene Userverwaltung zu bauen. Als Backend kommen häufig Datenbank-Tabellen in einem RDBMS zum Einsatz. Ich finde das äußerst nervig, denn i. d. R. lassen sich diese User-Informationen nicht oder nur mit aufwändigen Hacks von anderen Anwendungen nutzen – mit der Konsequenz, dass User für jede Applikation ein eigenes Login-Handle benötigen.
Sowohl für User als auch Admin ist es viel angenehmer, wenn jedem User (z. B. in einem Unternehmen, einer Organisation, einer Community, …) genau ein Handle zugeordnet ist. Nur ein Passwort, das vergessen werden kann, Änderungen von Attributen (Nickname, Name, Mail-Adresse, …) schlagen in allen Anwendungen durch, und als Admin muss man sich nur mit einer Userdatenbank herumschlagen. LDAP bietet genau diese Möglichkeit: Alle User-Handles an einem Ort zu speichern, und diese mit zusätzlichen Attributen auszustaffieren - je nachdem, was die verschiedenen Anwendungen so benötigen. Auch der Zugriff auf einzelne Anwendungen oder Anwendungsbereiche lässt sich über LDAP-Gruppen regeln.
In diesem Teil geht es daher um die Implementierung eines eigenen Authentifizierungs-Moduls für unsere Webanwendung, das gegen einen LDAP-Verzeichnisdienst authentifiziert. Um das Beispiel erst mal nicht zu komplex gestalten, soll das Modul nur die übergebenen Credentials prüfen, um den Zugang zur Applikation zu erlauben oder zu verwehren. Komplexere Prüfungen (etwa die Zugehörigkeit zu einer bestimmten LDAP-Gruppe) lassen sich dann später leicht einbauen.
Sinnvollerweise wird der gesamte Code für die Authentifizierung in einem eigenen Python-Modul gekapselt. Hier im Beispiel soll dies in der Datei myapp/auth.py passieren. Zunächst wird eine Klasse benötigt, die einen (sehr einfach gehaltenen User) beschreibt.
class User(object):
def __init__(self, username):
self._uname = username
@property
def username(self):
return self._uname
Als nächstes wird eine Funktion benötigt, welche die eigentliche Authentifizierung durchführt und entweder ein User-Objekt oder None zurückliefert. Für die einfachste Form einer Authentifizierung gegen LDAP werden allerdings noch zwei Informationen benötigt: Die URL des LDAP-Servers, und ein Muster, aus dem sich mit dem übergebenen Usernamen der DN des Users zusammenbasteln lässt. Diese Informationen können natürlich in den Konfigurationsdateien (config/devel.conf bzw. config/production.conf) abgelegt werden:
[global]
ldap.url = 'ldap://localhost'
ldap.tls = True
ldap.template = 'uid=%s,ou=people,dc=my-domain,dc=tld'
Zusätzlich ist hier im Beispiel noch die Möglichkeit gegeben, TLS für die Verbindung zum LDAP-Server zu fordern. Doch nun zur Implementierung der Authentifizierungs-Funktion, basierend auf der python-ldap Bibliothek.
import cherrypy
import ldap
def authenticate(username, password):
# Create LDAP context (but do not yet open a connection)
ctx = ldap.initialize(str(cherrypy.config.get('ldap.url')))
# If configured, try to open the context connection and switch to TLS
if cherrypy.config.get('ldap.tls', None):
try:
ctx.start_tls_s()
except ldap.LDAPError as e:
cherrypy.log(e.message['info'])
return None
# Try to authenticate
try:
ctx.simple_bind_s(str(cherrypy.config.get('ldap.template')) % username, password)
except (ldap.INVALID_CREDENTIALS, ldap.UNWILLING_TO_PERFORM):
return None
# Authentication seems to have worked
ctx.unbind_s()
return User(username)
Um möglichst viel Code aus einer späteren Login-Controllermethode in das auth-Modul zu verlagern, werden noch zwei weitere Funktionen benötigt:
def login(session, username, password):
user = authenticate(username, password)
if user:
session['user'] = user
return True
else:
return False
def logout(session):
if 'user' in session.keys():
del session['user']
Nun wird noch ein Login-Formular benötigt. Hier ein (sehr einfach gehaltenes) Template (themes/default/templates/login.html) – hübsch machen darf jeder selbst ![]()
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
xml:lang="en" lang="en">
<head>
<title>MyApp Login</title>
</head>
<body>
<h1>MyApp Login</h1>
<p py:if="login_error">
Bitte einen gültigen Benutzernamen und ein Passwort eingeben.
Beide Felder berücksichtigen die Groß-/Kleinschreibung.
</p>
<form action="" method="post">
<div>
<label for="id_username">Benutzername:</label>
<input type="text" name="username" id="id_username" />
</div>
<div>
<label for="id_password">Passwort:</label>
<input type="password" name="password" id="id_password" />
</div>
<div>
<label> </label>
<input type="submit" value="Anmelden" />
</div>
</form>
</body>
</html>
Damit stehen nun die Werkzeuge zur Verfügung, um einen einfachen Login-Controller zu realisieren. Der Einfachheit halber werden dazu einfach zwei Controller-Methoden im Root-Controller (myapp/controller.py) ergänzt:
import cherrypy
from myapp import template
from myapp import auth
class Root(object):
@cherrypy.expose
@template.output('hello.html')
def index(self):
return template.render(conf = cherrypy.config)
@cherrypy.expose
@template.output('login.html')
def login(self, username=None, password=None):
if cherrypy.request.method == 'GET':
return template.render(login_error = False)
elif cherrypy.request.method == 'POST'
if auth.login(cherrypy.session, username, password) == True:
url = cherrypy.session.get('login_url')
if url == None:
url = "/"
raise cherrypy.HTTPRedirect(url)
return template.render(login_error = True)
@cherrypy.expose
def logout(self):
auth.logout(cherrypy.session)
raise cherrypy.HTTPRedirect('/')
Die Controller-Methode login arbeitet folgendermaßen: Bei einem GET-Request wird das Login-Formular angezeigt. Bei einem POST-Request wird mit den übergebenen Credentials ein Login versucht. Schlägt dieser fehl, wird das Login-Formular erneut angezeigt, diemal mit einer Fehlermeldung. Gelingt der Login hingegen, wird wieder auf die Seite umgeleitet, die zuvor den Login gefordert hat.
Doch wie genau fordert eine Controller-Methode denn nun einen Login an? Genau wie die Zuordnung von Templates in Teil 2 soll es möglichst einfach sein, eine Controller-Methode als geschützt zu markieren – Code wie
if cherrypy.session.get("user", None):
return "Hübsche Seite"
else:
cherrypy.session['login_url'] = cherrypy.url()
raise cherrypy.HTTPRedirect('/login')
funktioniert zwar, ist aber weder elegant, noch huldigt er dem DRY-Prinzip. Hier kommt uns CherryPy zur Hilfe: Mit einem eigenen CherryPy-Tool lässt sich diese Herausforderung elegant lösen. Dieses findet seinen Platz wieder in myapp/auth.py und sieht folgendermaßen aus:
def auth_tool():
if not cherrypy.session.get("user", None):
cherrypy.session['login_url'] = cherrypy.url()
raise cherrypy.HTTPRedirect('/login', 307)
cherrypy.tools.auth = cherrypy.Tool('before_handler', auth_tool)
OK, der Code ist zugegebenermaßen etwas häßlich – eine hart codierte URL sollte in einem echten Projekt natürlich nicht zur Anwendung kommen, und statt cherrypy.url() wäre es vielleicht angebracht, Schema und Hostnamen zu entfernen. Zur Demonstration soll das obige Beispiel jedoch genügen.
Um nun eine Controller-Methode nur für angemeldete User zugänglich zu machen, kann das eben geschaffene CherryPy-Tool verwendet werden. Die Tool-API generiert nebenbei auch einen Dekorator, mit dem sich eine Controller-Methode problemlos auszeichnen lässt. Zur Demonstration legen wir eine weitere Controller-Methode innerhalb des Root-Controllers an:
@cherrypy.exposed
@cherrypy.tools.auth()
@template.output('hello.html')
def test(self):
return template.render(conf=cherrypy.config)
Bei dem Versuch, auf /test zuzugreifen, wird man nun auf die Login-Seite umgeleitet. Nach einem erfolgreichen Login gelangt man automatisch zurück zur ursprünglich angeforderten /test-Seite.
Damit ist nun alles vorhanden, um eine komplexere Webanwendung entwickeln zu können – eine strukturierte Ablage des Codes, Views in Form von Genshi-Templates und Authentifizierung für die Teile der Applikation, die nicht für jedermanns Augen bestimmt sind.
No comments | Defined tags for this entry: CherryPy, code, development, Genshi, python
Comments
No comments

Content is subject to the conditions of the