phpbb: Make sure PhpbbAuth::check_login always returns a tuple.
[matthijs/projects/wipi.git] / conf / auth / phpbb.py
index 5185eb3c345069eac3af6d02070b13a9e437deac..54763b3558731a9db66e165c7583a145a973ea2c 100644 (file)
@@ -4,10 +4,26 @@
 
     @copyright: 2008 Matthijs Kooijman
     @license: GNU GPL, see COPYING for details.
+
+    This plugin allows authentication (use accounts and password) and
+    authorization (use groups) against a phpbb Mysql database.
+
+    To use this plugin, you should put it in a place where the config python
+    file can "see" it (somewhere in your pythonpath, or in the same dir as the
+    config file). Then import the setup function and call it.
+
+    For example:
+
+    class FarmConfig:
+        import phpbb
+        (phpbb_auth, phpbb_groups) = phpbb.setup(...)
+        auth = [phpbb_auth]
+        groups = phpbb_groups
 """
 
 import MySQLdb
-import md5
+# Password encryption module. Python port of the method used by phpbb3.
+import phpass
 from MoinMoin import user
 from MoinMoin.auth import BaseAuth, ContinueLogin
 from MoinMoin.datastruct.backends import LazyGroupsBackend, LazyGroup
@@ -15,6 +31,41 @@ from MoinMoin import log
 logging = log.getLogger(__name__)
 
 
+def connect(dbhost=None, dbport=None, dbname=None, dbuser=None, dbpass=None, **kwargs):
+    # This code was shamelessly stolen from
+    # django.db.backends.mysql.base.cursor
+    kwargs = {
+        'charset': 'utf8',
+        'use_unicode': False,
+    }
+    if dbuser:
+        kwargs['user'] = dbuser
+    if dbname:
+        kwargs['db'] = dbname
+    if dbpass:
+        kwargs['passwd'] = dbpass
+    if dbhost.startswith('/'):
+        kwargs['unix_socket'] = dbhost
+    elif dbhost:
+        kwargs['host'] = dbhost
+    if dbport:
+        kwargs['port'] = int(dbport)
+
+    # End stolen code
+
+    try:
+        conn = MySQLdb.connect (**kwargs)
+    except:
+        import sys
+        import traceback
+        info = sys.exc_info()
+        logging.error("phpbb: authentication failed due to exception connecting to DB, traceback follows...")
+        logging.error(''.join(traceback.format_exception(*info)))
+        return False
+
+    return conn
+
+
 class PhpbbGroupsBackend(LazyGroupsBackend):
     class PhpbbGroup(LazyGroup):
         """
@@ -22,20 +73,19 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
         """
         pass
 
-    def __init__(self, request, auth):
+    def __init__(self, request, **kwargs):
         super(LazyGroupsBackend, self).__init__(request)
 
-        self.auth = auth
         self.request = request
+        self.dbconfig = kwargs
 
     def __iter__(self):
         """
         Return a list of group names.
         """
         return self.list_query("SELECT group_name \
-                                FROM `%sgroups` \
-                                WHERE group_single_user = 0"
-                                % self.auth.phpbb_prefix)
+                                FROM `%sgroups`"
+                                % self.dbconfig['phpbb_prefix'])
 
     def __contains__(self, group_name):
         """
@@ -44,8 +94,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
         return self.single_query("SELECT EXISTS ( \
                                       SELECT * \
                                       FROM `%sgroups` \
-                                      WHERE group_single_user = 0 \
-                                            AND group_name=%%s)" % self.auth.phpbb_prefix,
+                                      WHERE group_name=%%s)" % self.dbconfig['phpbb_prefix'],
                                  group_name)
 
     def __getitem__(self, group_name):
@@ -62,9 +111,8 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
         return self.list_query ("SELECT username \
                                  FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g  \
                                  WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \
-                                       AND ug.user_pending = 0 AND g.group_single_user = 0 \
-                                       AND g.group_name = %%s"
-                                 % (self.auth.phpbb_prefix, self.auth.phpbb_prefix, self.auth.phpbb_prefix),
+                                       AND ug.user_pending = 0 AND g.group_name = %%s"
+                                 % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']),
                                 group_name)
 
     def _group_has_member(self, group_name, member):
@@ -76,9 +124,9 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
                                        SELECT * \
                                        FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \
                                        WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \
-                                             AND ug.user_pending = 0 AND g.group_single_user = 0 \
+                                             AND ug.user_pending = 0 \
                                              AND g.group_name = %%s AND u.username = %%s)"
-                                   % (self.auth.phpbb_prefix, self.auth.phpbb_prefix, self.auth.phpbb_prefix),
+                                   % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']),
                                   (group_name, member))
         
     def groups_with_member(self, member):
@@ -89,9 +137,8 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
         return self.list_query ("SELECT g.group_name \
                                  FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \
                                  WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \
-                                       AND ug.user_pending = 0 AND g.group_single_user = 0 \
-                                       AND u.username = %%s"
-                                % (self.auth.phpbb_prefix, self.auth.phpbb_prefix, self.auth.phpbb_prefix),
+                                       AND ug.user_pending = 0 AND u.username = %%s"
+                                % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']),
                                 member)
 
     def single_query(self, *args):
@@ -102,7 +149,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
         conn = None
         cursor = None
         try:
-            conn = self.auth.connect(self.request)
+            conn = connect(**self.dbconfig)
             cursor = conn.cursor()
             cursor.execute(*args)
 
@@ -121,7 +168,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend):
         conn = None
         cursor = None
         try:
-            conn = self.auth.connect(self.request)
+            conn = connect(**self.dbconfig)
             cursor = conn.cursor()
             cursor.execute(*args)
 
@@ -137,7 +184,7 @@ class PhpbbAuth(BaseAuth):
     logout_possible = True
     login_inputs    = ['username', 'password']
     
-    def __init__(self, name='phpbb', dbhost=None, dbuser=None, dbpass=None, dbname=None, dbport=None, phpbb_prefix='', hint=None):
+    def __init__(self, name='phpbb', hint=None, **kwargs):
         """
             Authenticate using credentials from a phpbb database
 
@@ -145,83 +192,47 @@ class PhpbbAuth(BaseAuth):
 
             The hint parameter is a snippet of HTML that is displayed below the login form.
         """
-        self.dbhost  = dbhost
-        self.dbuser  = dbuser
-        self.dbpass  = dbpass
-        self.dbname  = dbname
-        self.dbport  = dbport
-        self.phpbb_prefix = phpbb_prefix
+        self.dbconfig = kwargs
         self.name    = name
         self.hint    = hint
+        self.hash    = phpass.PasswordHash()
 
-        # Create a "constructor" to create a phpbb_groups instance, while
-        # passing ourselves to it.
-        self.groups_backend = lambda config, request: PhpbbGroupsBackend(request, self)
-    
     def check_login(self, request, username, password):
         """ Checks the given username password combination. Returns the
-        corresponding emailaddress, or False if authentication failed.
+        real username and corresponding emailaddress, or (False, False)
+        if authentication failed. Username checks are case insensitive,
+        so the real username (with the real casing) is returned (since
+        ACL checks _are_ case sensitive).
         """
-        conn = self.connect(request)
+        conn = connect(**self.dbconfig)
 
         if not conn:
-            return False
+            return (False, False)
 
         # Get some data. Note that we interpolate the prefix ourselves, since
         # letting the mysql library do it only works with values (it adds ''
         # automatically). Note also that this allows possible SQL injection
         # through the phpbb_prefix variable, but that should be a trusted
         # value anyway.
+        # Finally note that by default, the phpbb database specifies a
+        # case insensitive collaction for the username field, so
+        # usernames are checked in case insensitive manner.
         cursor = conn.cursor ()
-        cursor.execute ("SELECT user_password,user_email FROM `%susers` WHERE username=%%s" % self.phpbb_prefix, username)
+        cursor.execute ("SELECT user_password,user_email,username FROM `%susers` WHERE LOWER(username)=LOWER(%%s)" % self.dbconfig['phpbb_prefix'], username)
 
         # No data? No login.
         if (cursor.rowcount == 0):
             conn.close()
-            return False
+            return (False, False)
        
         # Check password
         row = cursor.fetchone()
         conn.close()
 
-        if (md5.new(password).hexdigest() == row[0]):
-            return row[1]
+        if self.hash.check_password(password, row[0]):
+            return (row[1], row[2])
         else:
-            return False
-
-    def connect(self, request):
-        # This code was shamelessly stolen from
-        # django.db.backends.mysql.base.cursor
-        kwargs = {
-            'charset': 'utf8',
-            'use_unicode': False,
-        }
-        if self.dbuser:
-            kwargs['user'] = self.dbuser
-        if self.dbname:
-            kwargs['db'] = self.dbname
-        if self.dbpass:
-            kwargs['passwd'] = self.dbpass
-        if self.dbhost.startswith('/'):
-            kwargs['unix_socket'] = self.dbhost
-        elif self.dbhost:
-            kwargs['host'] = self.dbhost
-        if self.dbport:
-            kwargs['port'] = int(self.dbport)
-
-        # End stolen code
-
-        try:
-            conn = MySQLdb.connect (**kwargs)
-        except:
-            import sys
-            import traceback
-            info = sys.exc_info()
-            logging.error("phpbb_login: authentication failed due to exception connecting to DB, traceback follows...")
-            logging.error(''.join(traceback.format_exception(*info)))
-            return False
-
-        return conn
+            return (False, False)
 
     def login(self, request, user_obj, **kw):
         """
@@ -242,16 +253,19 @@ class PhpbbAuth(BaseAuth):
             if not username or not password:
                 return ContinueLogin(user_obj)
 
-            email = self.check_login(request, username, password)
+            (email, real_username) = self.check_login(request, username, password)
             
             # Login incorrect
             if (not email):
                 logging.debug("phpbb_login: authentication failed for %s" % (username))
                 return ContinueLogin(user_obj)
 
-            logging.debug("phpbb_login: authenticated %s (email %s)" % (username, email))
+            logging.debug("phpbb_login: authenticated %s (email %s, real username %s)" % (username, email, real_username))
 
-            u = user.User(request, auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email'))
+            # We use the username from the database (real_username)
+            # here, since the username from the request might have
+            # "wrong" casing (and ACL checks are case sensitive).
+            u = user.User(request, auth_username=real_username, auth_method=self.name, auth_attribs=('name', 'password', 'email'))
             u.email = email
             #u.remember_me = 0 # 0 enforces cookie_lifetime config param
             u.create_or_update(True)
@@ -269,4 +283,41 @@ class PhpbbAuth(BaseAuth):
         """ Return a snippet of HTML that is displayed with the login form. """
         return self.hint
 
+def setup(**kwargs):
+    """
+    Setup the phpbb backend. Takes the following keyword arguments:
+    dbhost -- The database server host
+    dbport -- The database server portname
+    dbname -- The database name
+    dbuser -- The username to log in
+    dbpass -- The password to log in
+    phpbb_prefix -- The table name prefix used for this phpbb installation
+    name -- The name to use for the auth backend
+    hint -- A hint to show in the login interface (HTML string) 
+
+    This function can be called multiple times to create backends for
+    different phpbb installations
+
+    Returns a tuple (auth, groups) containing an (instantiated) auth backend
+    and a groups backend (constructor). These can be put directly into the
+    "auth" (as part of the list) and "groups" (directly) config directives.
+
+    e.g.,
+    
+    class FarmConfig:
+        (phpbb_auth, phpbb_groups) = phpbb.setup(...)
+        auth = [phpbb_auth]
+        groups = phpbb_groups
+
+    (actual configuration parameters to setup() are omitted in this example)
+    """
+    
+    # Create a "constructor" to create a phpbb_groups instance, while
+    # passing ourselves to it.
+    groups = lambda config, request: PhpbbGroupsBackend(request, **kwargs)
+    # Create an instantiated auth backend.
+    auth = PhpbbAuth(**kwargs)
+
+    return (auth, groups)
+    
 # vim: set sw=4 expandtab sts=4:vim