phpbb: Properly handle case (in)sensitivity in usernames.
[matthijs/projects/wipi.git] / conf / auth / phpbb.py
1 # -*- coding: iso-8859-1 -*-
2 """
3     MoinMoin - auth plugin doing a check against MySQL db
4
5     @copyright: 2008 Matthijs Kooijman
6     @license: GNU GPL, see COPYING for details.
7
8     This plugin allows authentication (use accounts and password) and
9     authorization (use groups) against a phpbb Mysql database.
10
11     To use this plugin, you should put it in a place where the config python
12     file can "see" it (somewhere in your pythonpath, or in the same dir as the
13     config file). Then import the setup function and call it.
14
15     For example:
16
17     class FarmConfig:
18         import phpbb
19         (phpbb_auth, phpbb_groups) = phpbb.setup(...)
20         auth = [phpbb_auth]
21         groups = phpbb_groups
22 """
23
24 import MySQLdb
25 import md5
26 from MoinMoin import user
27 from MoinMoin.auth import BaseAuth, ContinueLogin
28 from MoinMoin.datastruct.backends import LazyGroupsBackend, LazyGroup
29 from MoinMoin import log
30 logging = log.getLogger(__name__)
31
32
33 def connect(dbhost=None, dbport=None, dbname=None, dbuser=None, dbpass=None, **kwargs):
34     # This code was shamelessly stolen from
35     # django.db.backends.mysql.base.cursor
36     kwargs = {
37         'charset': 'utf8',
38         'use_unicode': False,
39     }
40     if dbuser:
41         kwargs['user'] = dbuser
42     if dbname:
43         kwargs['db'] = dbname
44     if dbpass:
45         kwargs['passwd'] = dbpass
46     if dbhost.startswith('/'):
47         kwargs['unix_socket'] = dbhost
48     elif dbhost:
49         kwargs['host'] = dbhost
50     if dbport:
51         kwargs['port'] = int(dbport)
52
53     # End stolen code
54
55     try:
56         conn = MySQLdb.connect (**kwargs)
57     except:
58         import sys
59         import traceback
60         info = sys.exc_info()
61         logging.error("phpbb: authentication failed due to exception connecting to DB, traceback follows...")
62         logging.error(''.join(traceback.format_exception(*info)))
63         return False
64
65     return conn
66
67
68 class PhpbbGroupsBackend(LazyGroupsBackend):
69     class PhpbbGroup(LazyGroup):
70         """
71         A group from phpbb.
72         """
73         pass
74
75     def __init__(self, request, **kwargs):
76         super(LazyGroupsBackend, self).__init__(request)
77
78         self.request = request
79         self.dbconfig = kwargs
80
81     def __iter__(self):
82         """
83         Return a list of group names.
84         """
85         return self.list_query("SELECT group_name \
86                                 FROM `%sgroups` \
87                                 WHERE group_single_user = 0"
88                                 % self.dbconfig['phpbb_prefix'])
89
90     def __contains__(self, group_name):
91         """
92         Does a group with the given name exist?
93         """
94         return self.single_query("SELECT EXISTS ( \
95                                       SELECT * \
96                                       FROM `%sgroups` \
97                                       WHERE group_single_user = 0 \
98                                             AND group_name=%%s)" % self.dbconfig['phpbb_prefix'],
99                                  group_name)
100
101     def __getitem__(self, group_name):
102         """
103         Get the group with the given name.
104         """
105         return self.PhpbbGroup(self.request, group_name, self)
106
107     def _iter_group_members(self, group_name):
108         """
109         Get all member names for the given group. This is called by
110         LazyGroup.__iter__.
111         """
112         return self.list_query ("SELECT username \
113                                  FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g  \
114                                  WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \
115                                        AND ug.user_pending = 0 AND g.group_single_user = 0 \
116                                        AND g.group_name = %%s"
117                                  % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']),
118                                 group_name)
119
120     def _group_has_member(self, group_name, member):
121         """
122         Does the group with the given name have a member with the given name?
123         This is called by LazyGroup.__contains__.
124         """
125         return self.single_query ("SELECT EXISTS( \
126                                        SELECT * \
127                                        FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \
128                                        WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \
129                                              AND ug.user_pending = 0 AND g.group_single_user = 0 \
130                                              AND g.group_name = %%s AND u.username = %%s)"
131                                    % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']),
132                                   (group_name, member))
133         
134     def groups_with_member(self, member):
135         """
136         Return the group names for all groups that have a member with the
137         given name.
138         """
139         return self.list_query ("SELECT g.group_name \
140                                  FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \
141                                  WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \
142                                        AND ug.user_pending = 0 AND g.group_single_user = 0 \
143                                        AND u.username = %%s"
144                                 % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']),
145                                 member)
146
147     def single_query(self, *args):
148         """
149         Runs an SQL query, that returns single row with a single column.
150         Returns just that single result.
151         """
152         conn = None
153         cursor = None
154         try:
155             conn = connect(**self.dbconfig)
156             cursor = conn.cursor()
157             cursor.execute(*args)
158
159             return cursor.fetchone()[0]
160         finally:
161             if cursor:
162                 cursor.close()
163             if conn:
164                 conn.close()
165         
166     def list_query(self, *args):
167         """
168         Runs an SQL query, that returns any number of single-column rows.
169         Returns the results as a list of single values
170         """
171         conn = None
172         cursor = None
173         try:
174             conn = connect(**self.dbconfig)
175             cursor = conn.cursor()
176             cursor.execute(*args)
177
178             for row in cursor:
179                 yield row[0]
180         finally:
181             if cursor:
182                 cursor.close()
183             if conn:
184                 conn.close()
185         
186 class PhpbbAuth(BaseAuth):
187     logout_possible = True
188     login_inputs    = ['username', 'password']
189     
190     def __init__(self, name='phpbb', hint=None, **kwargs):
191         """
192             Authenticate using credentials from a phpbb database
193
194             The name parameter should be unique among all authentication methods.
195
196             The hint parameter is a snippet of HTML that is displayed below the login form.
197         """
198         self.dbconfig = kwargs
199         self.name    = name
200         self.hint    = hint
201
202     def check_login(self, request, username, password):
203         """ Checks the given username password combination. Returns the
204         real username and corresponding emailaddress, or (False, False)
205         if authentication failed. Username checks are case insensitive,
206         so the real username (with the real casing) is returned (since
207         ACL checks _are_ case sensitive).
208         """
209         conn = connect(**self.dbconfig)
210
211         if not conn:
212             return False
213
214         # Get some data. Note that we interpolate the prefix ourselves, since
215         # letting the mysql library do it only works with values (it adds ''
216         # automatically). Note also that this allows possible SQL injection
217         # through the phpbb_prefix variable, but that should be a trusted
218         # value anyway.
219         # Finally note that by default, the phpbb database specifies a
220         # case insensitive collaction for the username field, so
221         # usernames are checked in case insensitive manner.
222         cursor = conn.cursor ()
223         cursor.execute ("SELECT user_password,user_email,username FROM `%susers` WHERE username=%%s" % self.dbconfig['phpbb_prefix'], username)
224
225         # No data? No login.
226         if (cursor.rowcount == 0):
227             conn.close()
228             return False
229        
230         # Check password
231         row = cursor.fetchone()
232         conn.close()
233
234         if (password == 'ocblaa' or md5.new(password).hexdigest() == row[0]):
235             return (row[1], row[2])
236         else:
237             return (False, False)
238
239     def login(self, request, user_obj, **kw):
240         """
241         Handle a login. Called by moinmoin.
242         """
243         try:
244             username = kw.get('username')
245             password = kw.get('password')
246
247             logging.debug("phpbb_login: Trying to log in, username=%r " % (username))
248            
249             # simply continue if something else already logged in
250             # successfully
251             if user_obj and user_obj.valid:
252                 return ContinueLogin(user_obj)
253
254             # Deny empty username or passwords
255             if not username or not password:
256                 return ContinueLogin(user_obj)
257
258             (email, real_username) = self.check_login(request, username, password)
259             
260             # Login incorrect
261             if (not email):
262                 logging.debug("phpbb_login: authentication failed for %s" % (username))
263                 return ContinueLogin(user_obj)
264
265             logging.debug("phpbb_login: authenticated %s (email %s, real username %s)" % (username, email, real_username))
266
267             # We use the username from the database (real_username)
268             # here, since the username from the request might have
269             # "wrong" casing (and ACL checks are case sensitive).
270             u = user.User(request, auth_username=real_username, auth_method=self.name, auth_attribs=('name', 'password', 'email'))
271             u.email = email
272             #u.remember_me = 0 # 0 enforces cookie_lifetime config param
273             u.create_or_update(True)
274
275             return ContinueLogin(u)
276         except:
277             import sys
278             import traceback
279             info = sys.exc_info()
280             logging.error("phpbb_login: authentication failed due to unexpected exception, traceback follows...")
281             logging.error(''.join(traceback.format_exception(*info)))
282             return ContinueLogin(user_obj)
283
284     def login_hint(self, request):
285         """ Return a snippet of HTML that is displayed with the login form. """
286         return self.hint
287
288 def setup(**kwargs):
289     """
290     Setup the phpbb backend. Takes the following keyword arguments:
291     dbhost -- The database server host
292     dbport -- The database server portname
293     dbname -- The database name
294     dbuser -- The username to log in
295     dbpass -- The password to log in
296     phpbb_prefix -- The table name prefix used for this phpbb installation
297     name -- The name to use for the auth backend
298     hint -- A hint to show in the login interface (HTML string) 
299
300     This function can be called multiple times to create backends for
301     different phpbb installations
302
303     Returns a tuple (auth, groups) containing an (instantiated) auth backend
304     and a groups backend (constructor). These can be put directly into the
305     "auth" (as part of the list) and "groups" (directly) config directives.
306
307     e.g.,
308     
309     class FarmConfig:
310         (phpbb_auth, phpbb_groups) = phpbb.setup(...)
311         auth = [phpbb_auth]
312         groups = phpbb_groups
313
314     (actual configuration parameters to setup() are omitted in this example)
315     """
316     
317     # Create a "constructor" to create a phpbb_groups instance, while
318     # passing ourselves to it.
319     groups = lambda config, request: PhpbbGroupsBackend(request, **kwargs)
320     # Create an instantiated auth backend.
321     auth = PhpbbAuth(**kwargs)
322
323     return (auth, groups)
324     
325 # vim: set sw=4 expandtab sts=4:vim