1 # Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
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.
59 from urlparse
import urlparse
61 from Mailman
import Defaults
62 from Mailman
import Errors
63 from Mailman
import Utils
64 from Mailman
import passwords
65 from Mailman
.configuration
import config
67 log
= logging
.getLogger('mailman.error')
68 dlog
= logging
.getLogger('mailman.debug')
74 class SecurityManager
:
75 def AuthContextInfo(self
, authcontext
, user
=None):
76 # authcontext may be one of AuthUser, AuthListModerator,
77 # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
80 # user is ignored unless authcontext is AuthUser
82 # Return the authcontext's secret and cookie key. If the authcontext
83 # doesn't exist, return the tuple (None, None). If authcontext is
84 # AuthUser, but the user isn't a member of this mailing list, a
85 # NotAMemberError will be raised. If the user's secret is None, raise
87 key
= urllib
.quote(self
.fqdn_listname
) + '+'
88 if authcontext
== Defaults
.AuthUser
:
91 raise TypeError('No user supplied for AuthUser context')
92 secret
= self
.getMemberPassword(user
)
93 userdata
= urllib
.quote(Utils
.ObscureEmail(user
), safe
='')
94 key
+= 'user+%s' % userdata
95 elif authcontext
== Defaults
.AuthListModerator
:
96 secret
= self
.mod_password
98 elif authcontext
== Defaults
.AuthListAdmin
:
99 secret
= self
.password
102 elif authcontext
== Defaults
.AuthSiteAdmin
:
103 sitepass
= Utils
.get_global_password()
104 if config
.ALLOW_SITE_ADMIN_COOKIES
and sitepass
:
108 # BAW: this should probably hand out a site password based
109 # cookie, but that makes me a bit nervous, so just treat site
110 # admin as a list admin since there is currently no site
111 # admin-only functionality.
112 secret
= self
.password
118 def Authenticate(self
, authcontexts
, response
, user
=None):
119 # Given a list of authentication contexts, check to see if the
120 # response matches one of the passwords. authcontexts must be a
121 # sequence, and if it contains the context AuthUser, then the user
122 # argument must not be None.
124 # Return the authcontext from the argument sequence that matches the
125 # response, or UnAuthorized.
126 for ac
in authcontexts
:
127 if ac
== Defaults
.AuthCreator
:
128 ok
= Utils
.check_global_password(response
, siteadmin
=False)
130 return Defaults
.AuthCreator
131 elif ac
== Defaults
.AuthSiteAdmin
:
132 ok
= Utils
.check_global_password(response
)
134 return Defaults
.AuthSiteAdmin
135 elif ac
== Defaults
.AuthListAdmin
:
136 # The password for the list admin and list moderator are not
137 # kept as plain text, but instead as an sha hexdigest. The
138 # response being passed in is plain text, so we need to
139 # digestify it first.
140 key
, secret
= self
.AuthContextInfo(ac
)
143 if passwords
.check_response(secret
, response
):
145 elif ac
== Defaults
.AuthListModerator
:
146 # The list moderator password must be sha'd
147 key
, secret
= self
.AuthContextInfo(ac
)
148 if secret
and passwords
.check_response(secret
, response
):
150 elif ac
== Defaults
.AuthUser
:
153 if self
.authenticateMember(user
, response
):
155 except Errors
.NotAMemberError
:
158 # What is this context???
159 log
.error('Bad authcontext: %s', ac
)
160 raise ValueError('Bad authcontext: %s' % ac
)
161 return Defaults
.UnAuthorized
163 def WebAuthenticate(self
, authcontexts
, response
, user
=None):
164 # Given a list of authentication contexts, check to see if the cookie
165 # contains a matching authorization, falling back to checking whether
166 # the response matches one of the passwords. authcontexts must be a
167 # sequence, and if it contains the context AuthUser, then the user
168 # argument should not be None.
170 # Returns a flag indicating whether authentication succeeded or not.
171 for ac
in authcontexts
:
172 ok
= self
.CheckCookie(ac
, user
)
176 ac
= self
.Authenticate(authcontexts
, response
, user
)
178 print self
.MakeCookie(ac
, user
)
182 def _cookie_path(self
):
183 script_name
= os
.environ
.get('SCRIPT_NAME', '')
184 return SLASH
.join(script_name
.split(SLASH
)[:-1]) + SLASH
186 def MakeCookie(self
, authcontext
, user
=None):
187 key
, secret
= self
.AuthContextInfo(authcontext
, user
)
188 if key
is None or secret
is None or not isinstance(secret
, basestring
):
191 issued
= int(time
.time())
192 # Get a digest of the secret, plus other information.
193 mac
= sha
.new(secret
+ repr(issued
)).hexdigest()
194 # Create the cookie object.
195 c
= Cookie
.SimpleCookie()
196 c
[key
] = binascii
.hexlify(marshal
.dumps((issued
, mac
)))
197 c
[key
]['path'] = self
._cookie
_path
()
198 # We use session cookies, so don't set 'expires' or 'max-age' keys.
199 # Set the RFC 2109 required header.
200 c
[key
]['version'] = 1
203 def ZapCookie(self
, authcontext
, user
=None):
204 # We can throw away the secret.
205 key
, secret
= self
.AuthContextInfo(authcontext
, user
)
206 # Logout of the session by zapping the cookie. For safety both set
207 # max-age=0 (as per RFC2109) and set the cookie data to the empty
209 c
= Cookie
.SimpleCookie()
211 c
[key
]['path'] = self
._cookie
_path
()
212 c
[key
]['max-age'] = 0
213 # Don't set expires=0 here otherwise it'll force a persistent cookie
214 c
[key
]['version'] = 1
217 def CheckCookie(self
, authcontext
, user
=None):
218 # Two results can occur: we return 1 meaning the cookie authentication
219 # succeeded for the authorization context, we return 0 meaning the
220 # authentication failed.
222 # Dig out the cookie data, which better be passed on this cgi
223 # environment variable. If there's no cookie data, we reject the
225 cookiedata
= os
.environ
.get('HTTP_COOKIE')
228 # We can't use the Cookie module here because it isn't liberal in what
229 # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and
230 # you get a CookieError. :(. All we care about is accessing the
231 # cookie data via getitem, so we'll use our own parser, which returns
233 c
= parsecookie(cookiedata
)
234 # If the user was not supplied, but the authcontext is AuthUser, we
235 # can try to glean the user address from the cookie key. There may be
236 # more than one matching key (if the user has multiple accounts
237 # subscribed to this list), but any are okay.
238 if authcontext
== Defaults
.AuthUser
:
243 prefix
= urllib
.quote(self
.fqdn_listname
) + '+user+'
245 if k
.startswith(prefix
):
246 usernames
.append(k
[len(prefix
):])
247 # If any check out, we're golden. Note: '@'s are no longer legal
248 # values in cookie keys.
249 for user
in [Utils
.UnobscureEmail(u
) for u
in usernames
]:
250 ok
= self
.__checkone
(c
, authcontext
, user
)
255 return self
.__checkone
(c
, authcontext
, user
)
257 def __checkone(self
, c
, authcontext
, user
):
258 # Do the guts of the cookie check, for one authcontext/user
261 key
, secret
= self
.AuthContextInfo(authcontext
, user
)
262 except Errors
.NotAMemberError
:
264 if key
not in c
or not isinstance(secret
, basestring
):
266 # Undo the encoding we performed in MakeCookie() above. BAW: I
267 # believe this is safe from exploit because marshal can't be forced to
268 # load recursive data structures, and it can't be forced to execute
269 # any unexpected code. The worst that can happen is that either the
270 # client will have provided us bogus data, in which case we'll get one
271 # of the caught exceptions, or marshal format will have changed, in
272 # which case, the cookie decoding will fail. In either case, we'll
273 # simply request reauthorization, resulting in a new cookie being
274 # returned to the client.
276 data
= marshal
.loads(binascii
.unhexlify(c
[key
]))
277 issued
, received_mac
= data
278 except (EOFError, ValueError, TypeError, KeyError):
280 # Make sure the issued timestamp makes sense
284 # Calculate what the mac ought to be based on the cookie's timestamp
285 # and the shared secret.
286 mac
= sha
.new(secret
+ repr(issued
)).hexdigest()
287 if mac
<> received_mac
:
294 splitter
= re
.compile(';\s*')
298 for line
in s
.splitlines():
299 for p
in splitter
.split(line
):
301 k
, v
= p
.split('=', 1)