phpbb: Add some module documentation.
[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         corresponding emailaddress, or False if authentication failed.
205         """
206         conn = connect(**self.dbconfig)
207
208         if not conn:
209             return False
210
211         # Get some data. Note that we interpolate the prefix ourselves, since
212         # letting the mysql library do it only works with values (it adds ''
213         # automatically). Note also that this allows possible SQL injection
214         # through the phpbb_prefix variable, but that should be a trusted
215         # value anyway.
216         cursor = conn.cursor ()
217         cursor.execute ("SELECT user_password,user_email FROM `%susers` WHERE username=%%s" % self.dbconfig['phpbb_prefix'], username)
218
219         # No data? No login.
220         if (cursor.rowcount == 0):
221             conn.close()
222             return False
223        
224         # Check password
225         row = cursor.fetchone()
226         conn.close()
227
228         if (md5.new(password).hexdigest() == row[0]):
229             return row[1]
230         else:
231             return False
232
233     def login(self, request, user_obj, **kw):
234         """
235         Handle a login. Called by moinmoin.
236         """
237         try:
238             username = kw.get('username')
239             password = kw.get('password')
240
241             logging.debug("phpbb_login: Trying to log in, username=%r " % (username))
242            
243             # simply continue if something else already logged in
244             # successfully
245             if user_obj and user_obj.valid:
246                 return ContinueLogin(user_obj)
247
248             # Deny empty username or passwords
249             if not username or not password:
250                 return ContinueLogin(user_obj)
251
252             email = self.check_login(request, username, password)
253             
254             # Login incorrect
255             if (not email):
256                 logging.debug("phpbb_login: authentication failed for %s" % (username))
257                 return ContinueLogin(user_obj)
258
259             logging.debug("phpbb_login: authenticated %s (email %s)" % (username, email))
260
261             u = user.User(request, auth_username=username, auth_method=self.name, auth_attribs=('name', 'password', 'email'))
262             u.email = email
263             #u.remember_me = 0 # 0 enforces cookie_lifetime config param
264             u.create_or_update(True)
265
266             return ContinueLogin(u)
267         except:
268             import sys
269             import traceback
270             info = sys.exc_info()
271             logging.error("phpbb_login: authentication failed due to unexpected exception, traceback follows...")
272             logging.error(''.join(traceback.format_exception(*info)))
273             return ContinueLogin(user_obj)
274
275     def login_hint(self, request):
276         """ Return a snippet of HTML that is displayed with the login form. """
277         return self.hint
278
279 def setup(**kwargs):
280     """
281     Setup the phpbb backend. Takes the following keyword arguments:
282     dbhost -- The database server host
283     dbport -- The database server portname
284     dbname -- The database name
285     dbuser -- The username to log in
286     dbpass -- The password to log in
287     phpbb_prefix -- The table name prefix used for this phpbb installation
288     name -- The name to use for the auth backend
289     hint -- A hint to show in the login interface (HTML string) 
290
291     This function can be called multiple times to create backends for
292     different phpbb installations
293
294     Returns a tuple (auth, groups) containing an (instantiated) auth backend
295     and a groups backend (constructor). These can be put directly into the
296     "auth" (as part of the list) and "groups" (directly) config directives.
297
298     e.g.,
299     
300     class FarmConfig:
301         (phpbb_auth, phpbb_groups) = phpbb.setup(...)
302         auth = [phpbb_auth]
303         groups = phpbb_groups
304
305     (actual configuration parameters to setup() are omitted in this example)
306     """
307     
308     # Create a "constructor" to create a phpbb_groups instance, while
309     # passing ourselves to it.
310     groups = lambda config, request: PhpbbGroupsBackend(request, **kwargs)
311     # Create an instantiated auth backend.
312     auth = PhpbbAuth(**kwargs)
313
314     return (auth, groups)
315     
316 # vim: set sw=4 expandtab sts=4:vim