BenBE's humble thoughts Thoughts the world doesn't need yet …

05.07.2010

HTTP-Authentifizierung mit Mantis ertragbar machen

Filed under: Software — Schlagwörter: , , , , , , , , — BenBE @ 17:29:14

Mantis ist an sich ein sehr guter und gerade für Nicht-Informatiker gut geeigneter Bugtracker, der mit ein wenig Kreativität auch gut als Aufgabenverwaltung verwendet werden kann. Selber setze ich Mantis bereits geraume Zeit an verschiedenen Stellen ein; und so ist es nicht verwunderlich, dass nun auch für ein weiteres Projekt Mantis aufzusetzen war.

Für dieses Projekt gab es nun aber eine kleine Hürde, die bei Mantis bereits seit Jahren existiert: Die Authentifizierung gegen externe Systeme 😉 Mantis besitzt zwar von Haus aus die Möglichkeit, die Nutzerkonten aus einem LDAP-Server zu beziehen, erledigt dabei aber die Authentifizierung dennoch selber. Möchte man hingegen, dass Mantis stillschweigend den Nutzer frisst, der z.B. über den Apache oder ein Single-SignOn-System wie Shibbolleth angemeldet wurde, beißt man leider seit Jahren auf Granit. Ohne bei Mantis selber Hand anzulegen, kommt man leider nicht weit.

Um also für das vorliegende Projekt die Nutzer über ein zentrales Login auch an Mantis anmelden zu können, gab es im Internet eine Reihe von Vorschlägen. Einer der ausgereiftesten umfasste neben einer kurzen Erklärung auch eine gut nachvollziehbare Anleitung, was der Reihe nach zu patchen ist.

Fangen wir also mit einer funktionierenden Mantis-Installation an. Diese wird im Zuge dieser Installation vollständig auf die externe Authentifizierung über den Apache umgestellt. Da bei mir ein eGroupware als Login-Quelle verwendet wird, deute ich die dazu nötigen Schritte mit an; andere Login-Systeme funktionieren jedoch analog, wenn Sie Passwort-Hashes liefern, die vom Apachen (d.h. dem Unix-Crypt-Befehl) verstanden werden. In meinem Fall sind dies Salted MD5-Hashes (Die Hashes beginnen mit $1$), alternativ funktioniert aber auch das klassische 3DES-Crypt (13-Stellige Hashes) und alle anderen von Crypt unterstützen Passwörter (siehe Manpages zu crypt). Die Nutzung von Klartext-Passwörtern wird vom Apache zwar unterstützt, sollte aber aus Sicherheitsgründen NICHT verwendet werden …

Da Mantis nach diesem Schritt keine eigene Authentifizierung mehr vornimmt, sei an dieser Stelle zudem darauf hingewiesen, dass allein durch die externe Authentifizierung durch den Apache ALLE unberechtigten Benutzer bereits ausgesperrt werden müssen. Die Vergabe von Rechten, d.h. die Authorisierung) erfolgt weiterhin über Mantis (den Stress, das uzubauen tue ich mir nicht an).

Vorgeplänkel also geklärt, fangen wir mit der Anpassung von Mantis an. Der erste Schritt ist die Ergänzung der Authentication API von Mantis. Diese befindet sich im Verzeichnis /core in der Datei authentication_api.php. In dieser Datei suchen wir zuerst die Funktion auth_attemp_login.

        # --------------------
        # Attempt to login the user with the given password
        #  If the user fails validation, false is returned
        #  If the user passes validation, the cookies are set and
        #   true is returned.  If $p_perm_login is true, the long-term
        #   cookie is created.
        function auth_attempt_login( $p_username, $p_password, $p_perm_login=false ) {
                $t_user_id = user_get_id_by_name( $p_username );

                $t_login_method = config_get( 'login_method' );

                if ( false === $t_user_id ) {
                        if ( BASIC_AUTH == $t_login_method ) {
                                # attempt to create the user if using BASIC_AUTH
                                $t_cookie_string = user_create( $p_username, auth_generate_random_password(), $_SERVER['AUTHENTICATE_CONTACT_EMAIL']);

                                if ( false === $t_cookie_string ) {
                                        # it didn't work
                                        return false;
                                }

                                # ok, we created the user, get the row again
                                $t_user_id = user_get_id_by_name( $p_username );

                                if ( false === $t_user_id ) {
                                        # uh oh, something must be really wrong

                                        # @@@ trigger an error here?

                                        return false;
                                }
                        } else {
                                return false;
                        }
                }

                # check for disabled account
                if ( !user_is_enabled( $t_user_id ) ) {
                        return false;
                }

                # max. failed login attempts achieved...
                if( !user_is_login_request_allowed( $t_user_id ) ) {
                        return false;
                }

                $t_anon_account = config_get( 'anonymous_account' );
                $t_anon_allowed = config_get( 'allow_anonymous_login' );

                # check for anonymous login
                if ( !( ( ON == $t_anon_allowed ) && ( $t_anon_account == $p_username)  ) ) {
                        # anonymous login didn't work, so check the password

                        # Since basic auth is authoratative, and assuming that this page
                        # cannot be viewed unless it has already succeeded, then don't
                        # bother checking a password, and just do a valid login.
                        # -- BCV
                        if (BASIC_AUTH != $t_login_method && !auth_does_password_match( $t_user_id, $p_password ) ) {
                                user_increment_failed_login_count( $t_user_id );
                                return false;
                        }
                }

                # ok, we're good to login now

                # increment login count
                user_increment_login_count( $t_user_id );

                user_reset_failed_login_count_to_zero( $t_user_id );
                user_reset_lost_password_in_progress_count_to_zero( $t_user_id );

                # set the cookies
                auth_set_cookies( $t_user_id, $p_perm_login );
                auth_set_tokens( $t_user_id );

                return true;
        }

Wesentlich ist hierbei die Zeile mit dem Aufruf der Funktion user_create. Diese verwendet ursprünglich beim Anlegen neuer Benutzerkonten das von der HTTP-Authentifizierung übergebene Passwort. Da dieses aber in der Datenbank im Klartext abgelegt wird, wird der Aufruf ein wenig abgewandelt, so dass ein zufälliges Passwort erzeugt wird. Da dieses später eh ignoriert werden wird, besteht hier kein Grund, sich etwas anderes einfallen zu lassen. Ferner sorgt dieser Patch dafür, dass der Apache über eine Server-Variable die Email-Adresse für den anzulegenden Account festlegen kann, sollte der Account noch nicht existieren. Dazu aber später mehr.

Eine zweite Änderung befindet sich in der Abfrage von auth_does_password_match(). Diese wird durch die vorangestellte Abfrage für BASIC_AUTH übersprungen, womit allein die externe Authentifizierung in diesem Fall beachtet wird.

Nachdem nun die Anmeldung nun die Passwörter unter bestimmten Umständen ignoriert, geht es nun darum, die korrekten Nutzerdaten an Mantis weiterzuleiten, sowie eien Reihe von Unschönheiten der Session-Verwaltung auszubügeln. Hierzu öffnen wir als nächstes die Datei login.php im Hauptverzeichnis der Mantisinstallation.

<?php

        # Check login then redirect to main_page.php or to login_page.php

        require_once( 'core.php' );

        $f_username             = gpc_get_string( 'username', '' );
        $f_password             = gpc_get_string( 'password', '' );
        $f_perm_login           = gpc_get_bool( 'perm_login' );
        $f_return               = gpc_get_string( 'return', config_get( 'default_home_page' ) );
        $f_from                 = gpc_get_string( 'from', '' );

        if ( BASIC_AUTH == config_get( 'login_method' ) ) {
                $f_username = $_SERVER['REMOTE_USER'];
                $f_password = $_SERVER['PHP_AUTH_PW'];
        }

        if ( HTTP_AUTH == config_get( 'login_method' ) ) {
                if ( !auth_http_is_logout_pending() ) {
                        if ( isset( $_SERVER['PHP_AUTH_USER'] ) )
                                $f_username = $_SERVER['PHP_AUTH_USER'];
                        if ( isset( $_SERVER['PHP_AUTH_PW'] ) )
                                $f_password = $_SERVER['PHP_AUTH_PW'];
                } else {
                        auth_http_set_logout_pending( false );
                        auth_http_prompt();
                        return;
                }
        }

        if ( auth_attempt_login( $f_username, $f_password, $f_perm_login ) ) {
                $t_redirect_url = 'login_cookie_test.php?return=' . string_sanitize_url( $f_return );
        } else {
                $t_redirect_url = 'login_page.php?return=' . string_sanitize_url( $f_return ) . '&error=1';

                if ( HTTP_AUTH == config_get( 'login_method' ) ) {
                        auth_http_prompt();
                        exit;
                }
        }

        print_header_redirect( $t_redirect_url );
?>

Neu ist in dieser Datei insbesondere der Teil bzgl. der Nutzerdaten durch Übernahme dieser aus den Server-Variablen PHP_AUTH_USER und PHP_AUTH_PW. Das Passwort ist an dieser Stelle zwar eigentlich egal (weil es später durch auth_attemp_login() eh ignoriert wird, aber wenn, dann patchen wir hier mal sauber 😉

Nach dem nun die Anmeldung bereits klappen sollte, gilt es nun noch eine Reihe Unschönheiten zu beheben. Die erste dieser Unschönheiten ist der Drang von Mantis, de Benutzer einen Login-Dialog zeigen zu wollen. Diese Unart lässt sich durch Uschreiben des zugehörigen Redirects in der index.php (Hauptveryeichnis) aber einfach korrigieren:

<?php require_once( 'core.php' ) ?>
<?php
        if ( auth_is_user_authenticated() ) {
                print_header_redirect( config_get( 'default_home_page' ) );
        } else if ( BASIC_AUTH == config_get( 'login_method' ) ) {
                print_header_redirect( 'login.php' );
        } else {
                print_header_redirect( 'login_page.php' );
        }
?>

Hierbei wird automatisch statt auf die login_page.php auf die login.php weitergeleitet, die für die HTTP-Basierten Authentifizierungsverfahren zuständig ist. Bliebe noch der Logout. Dieser funktioniert bisher nicht, ist aber gerade in einer Umgebung, wo User auch einmal kurz gewechselt werden müssen, unverzichtbar. Daher gibt es auch für diese Situation noch den passenden Patch, der in der logout_page.php eingebaut werden muss:

<?php
        require_once( 'core.php' );

        auth_logout();

        if ( HTTP_AUTH == config_get( 'login_method' ) ) {
                auth_http_set_logout_pending( true );
        }

        if ( BASIC_AUTH == config_get( 'login_method' ) ) {
                if ( !auth_http_is_logout_pending() ) {
                        auth_http_set_logout_pending( true );
                        header('Status: 401 Authentication required');
                        header('WWW-Authenticate: basic realm="MantisBT"');
                        echo 'Hier geht\'s weiter';
                } else {
                        auth_http_set_logout_pending( false );
                        header('status: 301 Moved temporarily');
                        header('Location: index.php');
                }
        } else {
                print_header_redirect( config_get( 'logout_redirect_page' ), /* die */ true, /* sanitize */ false );
        }

?>

Nach dem nun Mantis für die Nutzung der externen Authentifizierung vorbereitet ist, gilt es nun noch, diese auch bereitzustellen. Da eGroupware, was in meinem Fall die Logins bereitstellt, eine Datenbank besitzt, bietet sich die Nutzung von authn_db für den Apache regelrecht an. Hierfür müssen wir beim Apache den Datenbanklayer, sowie die zugehörigen Authentifzierungsmodule laden. Dies geht als root auf der Konsole mit

a2enmod dbd authn_dbd

Danach muss die Authentifizierung noch konfiguriert werden. In einem VHost (ODER, wenn gewünscht auch global) muss hierzu die Datenbank-Verbindung zu eGroupware konfiguriert werden.

# mod_dbd configuration
DBDriver        mysql
DBDParams       "host=localhost user={username} pass={password} dbname={dbnae}"
DBDMin          4
DBDKeep         8
DBDMax          20
DBDExptime      300

Dieser Block darf nicht in einem Directory oder Location-Kontext auftauchen, da der Datenbank-Layer des Apache auf VHost-Ebene limitiert ist. Dies stört aber in der Regel nicht weiter. Zu guter Letzt muss nun lediglich noch die eingerichtete Datenbank-Verbindung auch benutzt werden. Für das Verzeichnis von Mantis reicht hierfür ein Block wie in folgendem Beispiel:

<Location /mantis>
        # mod_authn_dbd SQL query to authenticate a user
        AuthBasicProvider dbd
        AuthDBDUserPWQuery "SELECT acc.account_pwd, ab.contact_email FROM egw_accounts AS acc, egw_acl AS acl, egw_accounts AS g, egw_addressbook AS ab WHERE acc.account_id = acl.acl_account AND acl.acl_appname = 'phpgw_group' AND acl.acl_location = -g.account_id AND (g.account_lid = 'Mantis') AND acc.account_type = 'u' AND g.account_type='g' AND acc.account_status = 'A' AND g.account_status = 'A' AND acc.account_lid = %s AND acc.account_id = ab.account_id"

        AuthType Basic
        AuthName "MantisBT"

        Require valid-user
</Location>

Die verwendete Abfrage gewährt nun jedem eGroupware-Benutzer, der in der Gruppe „Mantis“ ist Zugang zum Bugtracker. Die Vergabe von Berechtigungen anhand von Subgruppen wird in dieser Form NICHT unterstützt und lässt sich auch SEHR schlecht realisieren.

Was durch diese Konfiguration jedoch sehr leicht realisierbar ist, ist die Übernahme der eGroupware-Kontakt-Email des jeweiligen Nutzers, womit ich auf die oben erwähnte Server-Variable zurückkomme, die ich bisher unkommentiert gelassen habe. Diese wird im Zuge der Login-Abfrage durch den Apache gesetzt. Der DBD-Layer erwartet in der ersten Spalte der Abfrage das Passwort; liefert die Abfrage jedoch weitere Spalten, so werden diese als Server-Variablen an die aufgerufene Website übergeben: In unserem Fall halt Mantis.

Bliebe eigentlich nur noch eines zu erledigen:

apache2ctl restart

That’s it!

Flattr this!

Keine Kommentare »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress