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