Crivens!

eigene repoze.who-plugins in turbogears verwenden

Montag, 03. Mai 2010, Themen:

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.

Die Suche

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:

Daher muss man "einfach" nur die add_auth_middleware-Methode überschreiben, um eigene Plugins zu verwenden.

tg.configuration.AppConf ableiten

Zuerst 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.

eigene repoze.who-Plugins

Bei den eigenen Plugins folge ich ganz der Linie von repoze.who.plugins.sa

Basisklasse

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

Authenticator Plugin

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']

Metadata-Provider-Plugin

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 anpassen

Im 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.

Kommentare:
blog comments powered by Disqus