Momentan bin ich dabe eine Datenbank für ein Orchester zu programmieren. Ok, genauer gesagt eine schon in die Jahre gekommene Datenbank neu umzusetzen; und hierzu verwende ich Turbogears
Der Turbogears-Quickstart enthält schon eine vorgefertigte Benutzer- und Rechteverwaltung. Leider stimmt diese nicht mit der Struktur der Datenbank überein, wie ich sie gerne hätte. Da mit vielen Freiwilligen gearbeitet wird, die sich dazu noch von Projekt zu Projekt unterscheiden können, war die Idee die bereits in der Datenbank registrieten Personen einfach zu Benutzern machen zu können. Da sich später vielleicht auch Mitspieler anmelden sollen um z.B. die eigenen Telefonnummern zu ändern oder eine Zusage für ein Projekt zu geben wollte ich die vorhanden Email-Adressen als Benutzernamen verwenden.
Um einen Benutzer anhand seiner Email-Adresse anmelden zu können – ohne dass
diese Adresse doppelt in der Datenbank abgelegt wird – muss ich eine Abfrage
über mehrere Tabellen machen: users->persons->emails (in Wirklichkeit noch
ein wenig komplizierter, aber lassen wir es dabei :-)
Der erste Versuch war die repoze.who und repoze.what Konfiguration
unangetastet zu lassen und mit SQLAlchemy-Comparators zu arbeiten – ich
bin dabei kläglich gescheitert. Und spätestens wenn die oben genannte Funktion
für die Musiker hinzukommen sollte, werden eigene Plugins für repoze.who und /
oder repoze.what benötigt – also fange ich doch gleich damit an…
Leider ist die Dokumentation dazu auf der Turbogears-Seite etwas spärlich und verstreut. Teilweise mit Links auf das repoze-Projekt, das wiederum auf Turbogears verweist. Auch die such im allwissenden Netz hat keine einfaches Tutorial hervorgebracht. Daher will ich hier beschreiben werden welche Schritte nötig sind, um die Integration eigener Plugins in Turbogears 2 zu ermöglichen.
Nach dem ich mich durch das Setup von Turbogears gewühlt habe und ich endlich mit den richtigen Begriffen die Google-Treffer so weit einschränken konnte, dass sie mir etwas sinnvolles liefern bin ich über diese Seite gestolpert: http://blog.axant.it/archives/tag/turbogears. Darin wird zwar beschrieben, wie man eine Authentifizierung über mongodb hinbekommt, war aber trotzdem sehr hilfreich.
Die wichtigsten Erkenntnisse kurz zusammengefasst:
config.app_cfg der Wert
base_config.auth_backend = 'sqlalchemy' gesetzt ist, wird die
repoze.who-middleware in der richtigen Reihenfolge von Turbogears
eingebunden. tg.configuration.AppConfig in der
Methode make_base_app wiederum eine Methode self.add_auth_middleware()
aufgerufen. Dies ist kann nicht konfiguriert werden. tg.configuration.AppConfig.add_auth_middleware() letztendlich konfiguriert
repoze.what durch repoze.what.quickstart.setup_sql_auth(). Daher muss man "einfach" nur die add_auth_middleware-Methode überschreiben,
um eigene Plugins zu verwenden.
tg.configuration.AppConf ableitenZuerst muss eine eigene Klasse für die AppConf geschaffen werden, die sich aus
dieser ableitet. Die einzige Methode die überschrieben werden muss ist
add_auth_middleware(). Hierbei wurde nur der Aufruf
app = setup_derived_auth(app, skip_authentication=skip_authentication, […]
geändert um eine eigen Funktion aufzurufen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import logging from copy import copy from tg.configuration import AppConfig class DerivedAppConfigAuth(AppConfig): """ Own App-Config class to implement custom repoze.(who|what) plugins The class tg.configuration.AppConfig is derived to implement custom plugins for authentification and authorization. """ def add_auth_middleware(self, app, skip_authentication): """ Configure authentication and authorization. :param app: The TG2 application. :param skip_authentication: Should authentication be skipped if explicitly requested? (used by repoze.who-testutil) :type skip_authentication: bool """ from repoze.what.plugins.quickstart import setup_sql_auth from repoze.what.plugins.pylonshq import booleanize_predicates # Predicates booleanized: booleanize_predicates() # Configuring auth logging: if 'log_stream' not in self.sa_auth: self.sa_auth['log_stream'] = logging.getLogger('auth') # Removing keywords not used by repoze.who: auth_args = copy(self.sa_auth) pprint(auth_args) if 'password_encryption_method' in auth_args: del auth_args['password_encryption_method'] if not skip_authentication: if not 'cookie_secret' in auth_args.keys(): msg = "base_config.sa_auth.cookie_secret is required " \ "you must define it in app_cfg.py or set " \ "sa_auth.cookie_secret in development.ini" print msg raise ConfigurationError(message=msg) app = setup_derived_auth(app, skip_authentication=skip_authentication, **auth_args) return app |
repoze.what.quickstart.setup_sql_auth()Als nächstes muss man eine Funktion erstellen, welche die gleichen Aufgaben
übernimmt wie repoze.what.quickstart.setup_sql_auth(). Hierzu habe ich einfach
die Funktion kopiert, umbenannt und in die gleiche Datei eingefügt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | from repoze.what.plugins.quickstart import find_plugin_translations from repoze.what.plugins.sql import configure_sql_adapters from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin from repoze.who.plugins.friendlyform import FriendlyFormPlugin from repoze.what.middleware import setup_auth def setup_derived_auth(app, user_class, group_class, permission_class, dbsession, form_plugin=None, form_identifies=True, cookie_secret='secret', cookie_name='authtkt', login_url='/login', login_handler='/login_handler', post_login_url=None, logout_handler='/logout_handler', post_logout_url=None, login_counter_name=None, translations={}, **who_args): """ Configure :mod:`repoze.who` and :mod:`repoze.what` with SQL-only authentication and authorization, respectively. […] """ plugin_translations = find_plugin_translations(translations) if group_class is None or permission_class is None: group_adapters = permission_adapters = None else: source_adapters = configure_sql_adapters( user_class, group_class, permission_class, dbsession, plugin_translations['group_adapter'], plugin_translations['permission_adapter']) group_adapters = {'sql_auth': source_adapters['group']} permission_adapters = {'sql_auth': source_adapters['permission']} # Setting the repoze.who authenticators: # using custom class here, removed translations for this class if 'authenticators' not in who_args: who_args['authenticators'] = [] orchestra_auth = OrchestraDbTeamUserAuthenticatorPlugin(user_class, dbsession) who_args['authenticators'].append(('orchestra_auth', Orchestraauth)) cookie = AuthTktCookiePlugin(cookie_secret, cookie_name) # Setting the repoze.who identifiers if 'identifiers' not in who_args: who_args['identifiers'] = [] who_args['identifiers'].append(('cookie', cookie)) if form_plugin is None: form = FriendlyFormPlugin( login_url, login_handler, post_login_url, logout_handler, post_logout_url, login_counter_name=login_counter_name, rememberer_name='cookie') else: form = form_plugin if form_identifies: who_args['identifiers'].insert(0, ('main_identifier', form)) # Setting the repoze.who challengers: if 'challengers' not in who_args: who_args['challengers'] = [] who_args['challengers'].append(('form', form)) # Setting up the repoze.who mdproviders: # using custom class here, removed translations for this class orchestra_user_md = OrchestraDbTeamUserMDPlugin(user_class, dbsession) if 'mdproviders' not in who_args: who_args['mdproviders'] = [] who_args['mdproviders'].append(('orchestra_user_md', orchestra_user_md)) middleware = setup_auth(app, group_adapters, permission_adapters, **who_args) return middleware |
Die wichtigsten Änderungen betreffen die Bereiche für den
repoze.who.Authenticator und repoze.who.metadataprovider. Die neuen
repoze.who-Plugins OrchestraDbTeamUserAuthenticatorPlugin und
OrchestraDbTeamUserMDPlugin müssen natürlich noch erstellt werden.
repoze.who-PluginsBei den eigenen Plugins folge ich ganz der Linie von repoze.who.plugins.sa
Analog zu repoze.who.plugins.sa wird zuerst eine Basisklasse definiert, die
vor allem eine Methode bietet den Benutzer anhand der Email-Adresse abzufragen.
Hierzu verwende ich eine Klassenmethode in meinem Model, da ich diese öfters
brauche, z.B. für eine "Passwort vergessen" Funktion. Ich habe dabei darauf
geachtet, dass die Abfrage im User-Model die SQLAlchemy-Funktion .one() benutzt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound class _BaseRepozeWhoOrchestraDdPlugin(object): """ Baseclass for the custom authenticator and metadata provider """ def __init__(self, user_class, dbsession): """ Setup the plugin. :param user_class: The SQLAlchemy/Elixir class for the users. :param session: The SQLAlchemy/Elixir session. """ self.user_class = user_class self.dbsession = dbsession def get_user(self, email): try: return self.user_class.by_email_address(email) except (NoResultFound, MultipleResultsFound): # As recommended in the docs for repoze.who, it's important to # verify that there's only _one_ matching userid. return None |
Als nächstes kommt das Plugin zur Authentifizierung. Die einzig benötigte
Methode hierbei ist authenticate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from zope.interface import implements from repoze.who.interfaces import IAuthenticator class OrchestraDbTeamUserAuthenticatorPlugin(_BaseRepozeWhoOrchestraDdPlugin): """ :mod:`repoze.who` custom authenticator for OrchestraDb """ implements(IAuthenticator) # IAuthenticator def authenticate(self, environ, identity): if not ('login' in identity and 'password' in identity): return None user = self.get_user(identity['login']) if user: if user.validate_password(identity['password']): return identity['login'] |
Nun noch das Plugin für den Metadata-Provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from repoze.who.interfaces import IMetadataProvider class OrchestraDbTeamUserMDPlugin(_BaseRepozeWhoOrchestraDdPlugin): """ :mod:`repoze.who` custom metadata provider that loads the SQLAlchemy-powered object for the current user in the OrchestraDb. It loads the object into ``identity['user']``. """ implements(IMetadataProvider) def add_metadata(self, environ, identity): identity['user'] = self.get_user(identity['repoze.who.userid']) |
app_cfg.py anpassenIm letzten Schritt muss man natürlich noch dafür sorgen, dass die abgeleitete AppConfig auch verwendet wird:
1 2 3 | # from tg.configuration import AppConfig from orchestra.config.derived_app_config_auth import DerivedAppConfigAuth base_config = DerivedAppConfigAuth() |
So, ich hoffe das hilft jemandem weiter, der vor dem gleichen Problem stand. Anbei noch ein Zip der finalen Datei.