1
# Copyright (C) 1998-2004 by the Free Software Foundation, Inc.
3
# This program is free software; you can redistribute it and/or
4
# modify it under the terms of the GNU General Public License
5
# as published by the Free Software Foundation; either version 2
6
# of the License, or (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
"""Handle passwords and sanitize approved messages."""
20
# There are current 5 roles defined in Mailman, as codified in Defaults.py:
21
# user, list-creator, list-moderator, list-admin, site-admin.
23
# Here's how we do cookie based authentication.
25
# Each role (see above) has an associated password, which is currently the
26
# only way to authenticate a role (in the future, we'll authenticate a
27
# user and assign users to roles).
29
# Each cookie has the following ingredients: the authorization context's
30
# secret (i.e. the password, and a timestamp. We generate an SHA1 hex
31
# digest of these ingredients, which we call the `mac'. We then marshal
32
# up a tuple of the timestamp and the mac, hexlify that and return that as
33
# a cookie keyed off the authcontext. Note that authenticating the user
34
# also requires the user's email address to be included in the cookie.
36
# The verification process is done in CheckCookie() below. It extracts
37
# the cookie, unhexlifies and unmarshals the tuple, extracting the
38
# timestamp. Using this, and the shared secret, the mac is calculated,
39
# and it must match the mac passed in the cookie. If so, they're golden,
40
# otherwise, access is denied.
42
# It is still possible for an adversary to attempt to brute force crack
43
# the password if they obtain the cookie, since they can extract the
44
# timestamp and create macs based on password guesses. They never get a
45
# cleartext version of the password though, so security rests on the
46
# difficulty and expense of retrying the cgi dialog for each attempt. It
47
# also relies on the security of SHA1.
56
from types import StringType, TupleType
57
from urlparse import urlparse
65
from Mailman import mm_cfg
66
from Mailman import Utils
67
from Mailman import Errors
68
from Mailman.Logging.Syslog import syslog
78
class SecurityManager:
80
# We used to set self.password here, from a crypted_password argument,
81
# but that's been removed when we generalized the mixin architecture.
82
# self.password is really a SecurityManager attribute, but it's set in
83
# MailList.InitVars().
84
self.mod_password = None
88
def AuthContextInfo(self, authcontext, user=None):
89
# authcontext may be one of AuthUser, AuthListModerator,
90
# AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
93
# user is ignored unless authcontext is AuthUser
95
# Return the authcontext's secret and cookie key. If the authcontext
96
# doesn't exist, return the tuple (None, None). If authcontext is
97
# AuthUser, but the user isn't a member of this mailing list, a
98
# NotAMemberError will be raised. If the user's secret is None, raise
100
key = self.internal_name() + '+'
101
if authcontext == mm_cfg.AuthUser:
104
raise TypeError, 'No user supplied for AuthUser context'
105
secret = self.getMemberPassword(user)
106
key += 'user+%s' % Utils.ObscureEmail(user)
107
elif authcontext == mm_cfg.AuthListModerator:
108
secret = self.mod_password
110
elif authcontext == mm_cfg.AuthListAdmin:
111
secret = self.password
114
elif authcontext == mm_cfg.AuthSiteAdmin:
115
sitepass = Utils.get_global_password()
116
if mm_cfg.ALLOW_SITE_ADMIN_COOKIES and sitepass:
120
# BAW: this should probably hand out a site password based
121
# cookie, but that makes me a bit nervous, so just treat site
122
# admin as a list admin since there is currently no site
123
# admin-only functionality.
124
secret = self.password
130
def Authenticate(self, authcontexts, response, user=None):
131
# Given a list of authentication contexts, check to see if the
132
# response matches one of the passwords. authcontexts must be a
133
# sequence, and if it contains the context AuthUser, then the user
134
# argument must not be None.
136
# Return the authcontext from the argument sequence that matches the
137
# response, or UnAuthorized.
138
for ac in authcontexts:
139
if ac == mm_cfg.AuthCreator:
140
ok = Utils.check_global_password(response, siteadmin=0)
142
return mm_cfg.AuthCreator
143
elif ac == mm_cfg.AuthSiteAdmin:
144
ok = Utils.check_global_password(response)
146
return mm_cfg.AuthSiteAdmin
147
elif ac == mm_cfg.AuthListAdmin:
148
def cryptmatchp(response, secret):
151
if crypt and crypt.crypt(response, salt) == secret:
155
# BAW: Hard to say why we can get a TypeError here.
156
# SF bug report #585776 says crypt.crypt() can raise
157
# this if salt contains null bytes, although I don't
158
# know how that can happen (perhaps if a MM2.0 list
159
# with USE_CRYPT = 0 has been updated? Doubtful.
161
# The password for the list admin and list moderator are not
162
# kept as plain text, but instead as an sha hexdigest. The
163
# response being passed in is plain text, so we need to
164
# digestify it first. Note however, that for backwards
165
# compatibility reasons, we'll also check the admin response
166
# against the crypted and md5'd passwords, and if they match,
167
# we'll auto-migrate the passwords to sha.
168
key, secret = self.AuthContextInfo(ac)
171
sharesponse = sha.new(response).hexdigest()
173
if sharesponse == secret:
175
elif md5.new(response).digest() == secret:
177
elif cryptmatchp(response, secret):
180
save_and_unlock = False
181
if not self.Locked():
183
save_and_unlock = True
185
self.password = sharesponse
193
elif ac == mm_cfg.AuthListModerator:
194
# The list moderator password must be sha'd
195
key, secret = self.AuthContextInfo(ac)
196
if secret and sha.new(response).hexdigest() == secret:
198
elif ac == mm_cfg.AuthUser:
201
if self.authenticateMember(user, response):
203
except Errors.NotAMemberError:
206
# What is this context???
207
syslog('error', 'Bad authcontext: %s', ac)
208
raise ValueError, 'Bad authcontext: %s' % ac
209
return mm_cfg.UnAuthorized
211
def WebAuthenticate(self, authcontexts, response, user=None):
212
# Given a list of authentication contexts, check to see if the cookie
213
# contains a matching authorization, falling back to checking whether
214
# the response matches one of the passwords. authcontexts must be a
215
# sequence, and if it contains the context AuthUser, then the user
216
# argument should not be None.
218
# Returns a flag indicating whether authentication succeeded or not.
219
for ac in authcontexts:
220
ok = self.CheckCookie(ac, user)
224
ac = self.Authenticate(authcontexts, response, user)
226
print self.MakeCookie(ac, user)
230
def MakeCookie(self, authcontext, user=None):
231
key, secret = self.AuthContextInfo(authcontext, user)
232
if key is None or secret is None or not isinstance(secret, StringType):
235
issued = int(time.time())
236
# Get a digest of the secret, plus other information.
237
mac = sha.new(secret + `issued`).hexdigest()
238
# Create the cookie object.
239
c = Cookie.SimpleCookie()
240
c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
241
# The path to all Mailman stuff, minus the scheme and host,
242
# i.e. usually the string `/mailman'
243
path = urlparse(self.web_page_url)[2]
244
c[key]['path'] = path
245
# We use session cookies, so don't set `expires' or `max-age' keys.
246
# Set the RFC 2109 required header.
247
c[key]['version'] = 1
250
def ZapCookie(self, authcontext, user=None):
251
# We can throw away the secret.
252
key, secret = self.AuthContextInfo(authcontext, user)
253
# Logout of the session by zapping the cookie. For safety both set
254
# max-age=0 (as per RFC2109) and set the cookie data to the empty
256
c = Cookie.SimpleCookie()
258
# The path to all Mailman stuff, minus the scheme and host,
259
# i.e. usually the string `/mailman'
260
path = urlparse(self.web_page_url)[2]
261
c[key]['path'] = path
262
c[key]['max-age'] = 0
263
# Don't set expires=0 here otherwise it'll force a persistent cookie
264
c[key]['version'] = 1
267
def CheckCookie(self, authcontext, user=None):
268
# Two results can occur: we return 1 meaning the cookie authentication
269
# succeeded for the authorization context, we return 0 meaning the
270
# authentication failed.
272
# Dig out the cookie data, which better be passed on this cgi
273
# environment variable. If there's no cookie data, we reject the
275
cookiedata = os.environ.get('HTTP_COOKIE')
278
# We can't use the Cookie module here because it isn't liberal in what
279
# it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and
280
# you get a CookieError. :(. All we care about is accessing the
281
# cookie data via getitem, so we'll use our own parser, which returns
283
c = parsecookie(cookiedata)
284
# If the user was not supplied, but the authcontext is AuthUser, we
285
# can try to glean the user address from the cookie key. There may be
286
# more than one matching key (if the user has multiple accounts
287
# subscribed to this list), but any are okay.
288
if authcontext == mm_cfg.AuthUser:
293
prefix = self.internal_name() + '+user+'
295
if k.startswith(prefix):
296
usernames.append(k[len(prefix):])
297
# If any check out, we're golden. Note: `@'s are no longer legal
298
# values in cookie keys.
299
for user in [Utils.UnobscureEmail(u) for u in usernames]:
300
ok = self.__checkone(c, authcontext, user)
305
return self.__checkone(c, authcontext, user)
307
def __checkone(self, c, authcontext, user):
308
# Do the guts of the cookie check, for one authcontext/user
311
key, secret = self.AuthContextInfo(authcontext, user)
312
except Errors.NotAMemberError:
314
if not c.has_key(key) or not isinstance(secret, StringType):
316
# Undo the encoding we performed in MakeCookie() above. BAW: I
317
# believe this is safe from exploit because marshal can't be forced to
318
# load recursive data structures, and it can't be forced to execute
319
# any unexpected code. The worst that can happen is that either the
320
# client will have provided us bogus data, in which case we'll get one
321
# of the caught exceptions, or marshal format will have changed, in
322
# which case, the cookie decoding will fail. In either case, we'll
323
# simply request reauthorization, resulting in a new cookie being
324
# returned to the client.
326
data = marshal.loads(binascii.unhexlify(c[key]))
327
issued, received_mac = data
328
except (EOFError, ValueError, TypeError, KeyError):
330
# Make sure the issued timestamp makes sense
334
# Calculate what the mac ought to be based on the cookie's timestamp
335
# and the shared secret.
336
mac = sha.new(secret + `issued`).hexdigest()
337
if mac <> received_mac:
344
splitter = re.compile(';\s*')
348
for p in splitter.split(s):
350
k, v = p.split('=', 1)