2 # Copyright (C) 1998-2008 by the Free Software Foundation, Inc.
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
20 """Handle passwords and sanitize approved messages."""
22 # There are current 5 roles defined in Mailman, as codified in Defaults.py:
23 # user, list-creator, list-moderator, list-admin, site-admin.
25 # Here's how we do cookie based authentication.
27 # Each role (see above) has an associated password, which is currently the
28 # only way to authenticate a role (in the future, we'll authenticate a
29 # user and assign users to roles).
31 # Each cookie has the following ingredients: the authorization context's
32 # secret (i.e. the password, and a timestamp. We generate an SHA1 hex
33 # digest of these ingredients, which we call the `mac'. We then marshal
34 # up a tuple of the timestamp and the mac, hexlify that and return that as
35 # a cookie keyed off the authcontext. Note that authenticating the user
36 # also requires the user's email address to be included in the cookie.
38 # The verification process is done in CheckCookie() below. It extracts
39 # the cookie, unhexlifies and unmarshals the tuple, extracting the
40 # timestamp. Using this, and the shared secret, the mac is calculated,
41 # and it must match the mac passed in the cookie. If so, they're golden,
42 # otherwise, access is denied.
44 # It is still possible for an adversary to attempt to brute force crack
45 # the password if they obtain the cookie, since they can extract the
46 # timestamp and create macs based on password guesses. They never get a
47 # cleartext version of the password though, so security rests on the
48 # difficulty and expense of retrying the cgi dialog for each attempt. It
49 # also relies on the security of SHA1.
59 from types import StringType, TupleType
60 from urlparse import urlparse
67 from Mailman import mm_cfg
68 from Mailman import Utils
69 from Mailman import Errors
70 from Mailman.Logging.Syslog import syslog
72 from Mailman.Utils import md5_new, sha_new
78 from Mailman import SecurityManager
88 class ForgeSecurityManager(SecurityManager.SecurityManager):
89 def __init__(self,mlist):
91 # We used to set self.password here, from a crypted_password argument,
92 # but that's been removed when we generalized the mixin architecture.
93 # self.password is really a SecurityManager attribute, but it's set in
94 # MailList.InitVars().
95 self.mod_password = None
97 self.__mlist.passwords = {}
99 def AuthContextInfo(self, authcontext, user=None):
100 # authcontext may be one of AuthUser, AuthListModerator,
101 # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
104 # user is ignored unless authcontext is AuthUser
106 # Return the authcontext's secret and cookie key. If the authcontext
107 # doesn't exist, return the tuple (None, None). If authcontext is
108 # AuthUser, but the user isn't a member of this mailing list, a
109 # NotAMemberError will be raised. If the user's secret is None, raise
111 key = self.__mlist.internal_name() + '+'
112 if authcontext == mm_cfg.AuthUser:
115 raise TypeError, 'No user supplied for AuthUser context'
116 secret = self.__mlist.getMemberPassword(user)
117 userdata = urllib.quote(Utils.ObscureEmail(user), safe='')
118 key += 'user+%s' % userdata
119 elif authcontext == mm_cfg.AuthListModerator:
120 secret = self.mod_password
122 elif authcontext == mm_cfg.AuthListAdmin:
123 secret = self.__mlist.password
126 elif authcontext == mm_cfg.AuthSiteAdmin:
127 sitepass = Utils.get_global_password()
128 if mm_cfg.ALLOW_SITE_ADMIN_COOKIES and sitepass:
132 # BAW: this should probably hand out a site password based
133 # cookie, but that makes me a bit nervous, so just treat site
134 # admin as a list admin since there is currently no site
135 # admin-only functionality.
136 secret = self.__mlist.password
142 def Authenticate(self, authcontexts, response, user=None):
143 # Given a list of authentication contexts, check to see if the
144 # response matches one of the passwords. authcontexts must be a
145 # sequence, and if it contains the context AuthUser, then the user
146 # argument must not be None.
148 # Return the authcontext from the argument sequence that matches the
149 # response, or UnAuthorized.
150 for ac in authcontexts:
151 if ac == mm_cfg.AuthCreator:
152 ok = Utils.check_global_password(response, siteadmin=0)
154 return mm_cfg.AuthCreator
155 elif ac == mm_cfg.AuthSiteAdmin:
156 ok = Utils.check_global_password(response)
158 return mm_cfg.AuthSiteAdmin
159 elif ac == mm_cfg.AuthListAdmin:
160 def cryptmatchp(response, secret):
163 if crypt and crypt.crypt(response, salt) == secret:
167 # BAW: Hard to say why we can get a TypeError here.
168 # SF bug report #585776 says crypt.crypt() can raise
169 # this if salt contains null bytes, although I don't
170 # know how that can happen (perhaps if a MM2.0 list
171 # with USE_CRYPT = 0 has been updated? Doubtful.
173 # The password for the list admin and list moderator are not
174 # kept as plain text, but instead as an sha hexdigest. The
175 # response being passed in is plain text, so we need to
176 # digestify it first. Note however, that for backwards
177 # compatibility reasons, we'll also check the admin response
178 # against the crypted and md5'd passwords, and if they match,
179 # we'll auto-migrate the passwords to sha.
180 key, secret = self.AuthContextInfo(ac)
183 sharesponse = sha_new(response).hexdigest()
185 if sharesponse == secret:
187 elif md5_new(response).digest() == secret:
189 elif cryptmatchp(response, secret):
192 save_and_unlock = False
193 if not self.__mlist.Locked():
195 save_and_unlock = True
197 self.__mlist.password = sharesponse
202 self.__mlist.Unlock()
205 elif ac == mm_cfg.AuthListModerator:
206 # The list moderator password must be sha'd
207 key, secret = self.AuthContextInfo(ac)
208 if secret and sha_new(response).hexdigest() == secret:
210 elif ac == mm_cfg.AuthUser:
213 if self.__mlist.authenticateMember(user, response):
215 except Errors.NotAMemberError:
218 # What is this context???
219 syslog('error', 'Bad authcontext: %s', ac)
220 raise ValueError, 'Bad authcontext: %s' % ac
221 return mm_cfg.UnAuthorized
223 def WebAuthenticate(self, authcontexts, response, user=None):
224 # Given a list of authentication contexts, check to see if the cookie
225 # contains a matching authorization, falling back to checking whether
226 # the response matches one of the passwords. authcontexts must be a
227 # sequence, and if it contains the context AuthUser, then the user
228 # argument should not be None.
230 # Returns a flag indicating whether authentication succeeded or not.
231 for ac in authcontexts:
232 ok = self.CheckCookie(ac,user)
236 ac = self.Authenticate(authcontexts, response, user)
238 print self.MakeCookie(ac, user)
242 def MakeCookie(self, authcontext, user=None):
243 key, secret = self.AuthContextInfo(authcontext, user)
244 if key is None or secret is None or not isinstance(secret, StringType):
247 issued = int(time.time())
248 # Get a digest of the secret, plus other information.
249 mac = sha_new(secret + `issued`).hexdigest()
250 # Create the cookie object.
251 c = Cookie.SimpleCookie()
252 c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
253 # The path to all Mailman stuff, minus the scheme and host,
254 # i.e. usually the string `/mailman'
255 path = urlparse(self.__mlist.web_page_url)[2]
256 c[key]['path'] = path
257 # We use session cookies, so don't set `expires' or `max-age' keys.
258 # Set the RFC 2109 required header.
259 c[key]['version'] = 1
262 def ZapCookie(self, authcontext, user=None):
263 # We can throw away the secret.
264 key, secret = self.AuthContextInfo(authcontext, user)
265 # Logout of the session by zapping the cookie. For safety both set
266 # max-age=0 (as per RFC2109) and set the cookie data to the empty
268 c = Cookie.SimpleCookie()
270 # The path to all Mailman stuff, minus the scheme and host,
271 # i.e. usually the string `/mailman'
272 path = urlparse(self.__mlist.web_page_url)[2]
273 c[key]['path'] = path
274 c[key]['max-age'] = 0
275 # Don't set expires=0 here otherwise it'll force a persistent cookie
276 c[key]['version'] = 1
279 def CheckCookie(self, authcontext, user):
280 cookiedata = os.environ.get('HTTP_COOKIE')
283 c = parsecookie(cookiedata)
284 if authcontext == mm_cfg.AuthUser:
289 usernames = self.__mlist.db_cookie_to_mail(c)
291 for user in usernames:
292 ok = self.__checkone(c, authcontext, user)
296 prefix = self.__mlist.internal_name() + '+user+'
298 if k.startswith(prefix):
299 usernames.append(k[len(prefix):])
300 # If any check out, we're golden. Note: `@'s are no longer legal
301 # values in cookie keys.
302 for user in [Utils.UnobscureEmail(urllib.unquote(u))
304 ok = self.__checkone(c, authcontext, user)
309 return self.__checkone(c,authcontext, user)
311 def __checkone(self, c, authcontext, user):
313 key, secret = self.AuthContextInfo(authcontext, user)
314 except Errors.NotAMemberError:
316 user_id = self.__mlist.db_cookie_to_id(c)
318 monitored = self.__mlist.db_id_to_monitored(user_id)
319 isAdmin = self.__mlist.db_isAdmin(user_id)
320 isSiteAdmin = self.__mlist.db_isSiteAdmin(user_id)
321 if authcontext == mm_cfg.AuthUser:
326 elif authcontext == mm_cfg.AuthListAdmin:
331 elif authcontext == mm_cfg.AuthListModerator:
336 elif authcontext == mm_cfg.AuthSiteAdmin:
341 if not c.has_key(key) or not isinstance(secret, StringType):
343 # Undo the encoding we performed in MakeCookie() above. BAW: I
344 # believe this is safe from exploit because marshal can't be forced to
345 # load recursive data structures, and it can't be forced to execute
346 # any unexpected code. The worst that can happen is that either the
347 # client will have provided us bogus data, in which case we'll get one
348 # of the caught exceptions, or marshal format will have changed, in
349 # which case, the cookie decoding will fail. In either case, we'll
350 # simply request reauthorization, resulting in a new cookie being
351 # returned to the client.
353 data = marshal.loads(binascii.unhexlify(c[key]))
354 issued, received_mac = data
355 except (EOFError, ValueError, TypeError, KeyError):
357 # Make sure the issued timestamp makes sense
361 # Calculate what the mac ought to be based on the cookie's timestamp
362 # and the shared secret.
363 mac = sha_new(secret + `issued`).hexdigest()
364 if mac <> received_mac:
370 splitter = re.compile(';\s*')
374 for line in s.splitlines():
375 for p in splitter.split(line):
377 k, v = p.split('=', 1)