summaryrefslogtreecommitdiff
path: root/src/mailman/attic/SecurityManager.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/attic/SecurityManager.py')
-rw-r--r--src/mailman/attic/SecurityManager.py306
1 files changed, 306 insertions, 0 deletions
diff --git a/src/mailman/attic/SecurityManager.py b/src/mailman/attic/SecurityManager.py
new file mode 100644
index 000000000..8d4a30592
--- /dev/null
+++ b/src/mailman/attic/SecurityManager.py
@@ -0,0 +1,306 @@
+# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Handle passwords and sanitize approved messages."""
+
+# There are current 5 roles defined in Mailman, as codified in Defaults.py:
+# user, list-creator, list-moderator, list-admin, site-admin.
+#
+# Here's how we do cookie based authentication.
+#
+# Each role (see above) has an associated password, which is currently the
+# only way to authenticate a role (in the future, we'll authenticate a
+# user and assign users to roles).
+#
+# Each cookie has the following ingredients: the authorization context's
+# secret (i.e. the password, and a timestamp. We generate an SHA1 hex
+# digest of these ingredients, which we call the 'mac'. We then marshal
+# up a tuple of the timestamp and the mac, hexlify that and return that as
+# a cookie keyed off the authcontext. Note that authenticating the user
+# also requires the user's email address to be included in the cookie.
+#
+# The verification process is done in CheckCookie() below. It extracts
+# the cookie, unhexlifies and unmarshals the tuple, extracting the
+# timestamp. Using this, and the shared secret, the mac is calculated,
+# and it must match the mac passed in the cookie. If so, they're golden,
+# otherwise, access is denied.
+#
+# It is still possible for an adversary to attempt to brute force crack
+# the password if they obtain the cookie, since they can extract the
+# timestamp and create macs based on password guesses. They never get a
+# cleartext version of the password though, so security rests on the
+# difficulty and expense of retrying the cgi dialog for each attempt. It
+# also relies on the security of SHA1.
+
+import os
+import re
+import sha
+import time
+import urllib
+import Cookie
+import logging
+import marshal
+import binascii
+
+from urlparse import urlparse
+
+from Mailman import Defaults
+from Mailman import Errors
+from Mailman import Utils
+from Mailman import passwords
+from Mailman.configuration import config
+
+log = logging.getLogger('mailman.error')
+dlog = logging.getLogger('mailman.debug')
+
+SLASH = '/'
+
+
+
+class SecurityManager:
+ def AuthContextInfo(self, authcontext, user=None):
+ # authcontext may be one of AuthUser, AuthListModerator,
+ # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
+ # context.
+ #
+ # user is ignored unless authcontext is AuthUser
+ #
+ # Return the authcontext's secret and cookie key. If the authcontext
+ # doesn't exist, return the tuple (None, None). If authcontext is
+ # AuthUser, but the user isn't a member of this mailing list, a
+ # NotAMemberError will be raised. If the user's secret is None, raise
+ # a MMBadUserError.
+ key = urllib.quote(self.fqdn_listname) + '+'
+ if authcontext == Defaults.AuthUser:
+ if user is None:
+ # A bad system error
+ raise TypeError('No user supplied for AuthUser context')
+ secret = self.getMemberPassword(user)
+ userdata = urllib.quote(Utils.ObscureEmail(user), safe='')
+ key += 'user+%s' % userdata
+ elif authcontext == Defaults.AuthListModerator:
+ secret = self.mod_password
+ key += 'moderator'
+ elif authcontext == Defaults.AuthListAdmin:
+ secret = self.password
+ key += 'admin'
+ # BAW: AuthCreator
+ elif authcontext == Defaults.AuthSiteAdmin:
+ sitepass = Utils.get_global_password()
+ if config.ALLOW_SITE_ADMIN_COOKIES and sitepass:
+ secret = sitepass
+ key = 'site'
+ else:
+ # BAW: this should probably hand out a site password based
+ # cookie, but that makes me a bit nervous, so just treat site
+ # admin as a list admin since there is currently no site
+ # admin-only functionality.
+ secret = self.password
+ key += 'admin'
+ else:
+ return None, None
+ return key, secret
+
+ def Authenticate(self, authcontexts, response, user=None):
+ # Given a list of authentication contexts, check to see if the
+ # response matches one of the passwords. authcontexts must be a
+ # sequence, and if it contains the context AuthUser, then the user
+ # argument must not be None.
+ #
+ # Return the authcontext from the argument sequence that matches the
+ # response, or UnAuthorized.
+ for ac in authcontexts:
+ if ac == Defaults.AuthCreator:
+ ok = Utils.check_global_password(response, siteadmin=False)
+ if ok:
+ return Defaults.AuthCreator
+ elif ac == Defaults.AuthSiteAdmin:
+ ok = Utils.check_global_password(response)
+ if ok:
+ return Defaults.AuthSiteAdmin
+ elif ac == Defaults.AuthListAdmin:
+ # The password for the list admin and list moderator are not
+ # kept as plain text, but instead as an sha hexdigest. The
+ # response being passed in is plain text, so we need to
+ # digestify it first.
+ key, secret = self.AuthContextInfo(ac)
+ if secret is None:
+ continue
+ if passwords.check_response(secret, response):
+ return ac
+ elif ac == Defaults.AuthListModerator:
+ # The list moderator password must be sha'd
+ key, secret = self.AuthContextInfo(ac)
+ if secret and passwords.check_response(secret, response):
+ return ac
+ elif ac == Defaults.AuthUser:
+ if user is not None:
+ try:
+ if self.authenticateMember(user, response):
+ return ac
+ except Errors.NotAMemberError:
+ pass
+ else:
+ # What is this context???
+ log.error('Bad authcontext: %s', ac)
+ raise ValueError('Bad authcontext: %s' % ac)
+ return Defaults.UnAuthorized
+
+ def WebAuthenticate(self, authcontexts, response, user=None):
+ # Given a list of authentication contexts, check to see if the cookie
+ # contains a matching authorization, falling back to checking whether
+ # the response matches one of the passwords. authcontexts must be a
+ # sequence, and if it contains the context AuthUser, then the user
+ # argument should not be None.
+ #
+ # Returns a flag indicating whether authentication succeeded or not.
+ for ac in authcontexts:
+ ok = self.CheckCookie(ac, user)
+ if ok:
+ return True
+ # Check passwords
+ ac = self.Authenticate(authcontexts, response, user)
+ if ac:
+ print self.MakeCookie(ac, user)
+ return True
+ return False
+
+ def _cookie_path(self):
+ script_name = os.environ.get('SCRIPT_NAME', '')
+ return SLASH.join(script_name.split(SLASH)[:-1]) + SLASH
+
+ def MakeCookie(self, authcontext, user=None):
+ key, secret = self.AuthContextInfo(authcontext, user)
+ if key is None or secret is None or not isinstance(secret, basestring):
+ raise ValueError
+ # Timestamp
+ issued = int(time.time())
+ # Get a digest of the secret, plus other information.
+ mac = sha.new(secret + repr(issued)).hexdigest()
+ # Create the cookie object.
+ c = Cookie.SimpleCookie()
+ c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
+ c[key]['path'] = self._cookie_path()
+ # We use session cookies, so don't set 'expires' or 'max-age' keys.
+ # Set the RFC 2109 required header.
+ c[key]['version'] = 1
+ return c
+
+ def ZapCookie(self, authcontext, user=None):
+ # We can throw away the secret.
+ key, secret = self.AuthContextInfo(authcontext, user)
+ # Logout of the session by zapping the cookie. For safety both set
+ # max-age=0 (as per RFC2109) and set the cookie data to the empty
+ # string.
+ c = Cookie.SimpleCookie()
+ c[key] = ''
+ c[key]['path'] = self._cookie_path()
+ c[key]['max-age'] = 0
+ # Don't set expires=0 here otherwise it'll force a persistent cookie
+ c[key]['version'] = 1
+ return c
+
+ def CheckCookie(self, authcontext, user=None):
+ # Two results can occur: we return 1 meaning the cookie authentication
+ # succeeded for the authorization context, we return 0 meaning the
+ # authentication failed.
+ #
+ # Dig out the cookie data, which better be passed on this cgi
+ # environment variable. If there's no cookie data, we reject the
+ # authentication.
+ cookiedata = os.environ.get('HTTP_COOKIE')
+ if not cookiedata:
+ return False
+ # We can't use the Cookie module here because it isn't liberal in what
+ # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and
+ # you get a CookieError. :(. All we care about is accessing the
+ # cookie data via getitem, so we'll use our own parser, which returns
+ # a dictionary.
+ c = parsecookie(cookiedata)
+ # If the user was not supplied, but the authcontext is AuthUser, we
+ # can try to glean the user address from the cookie key. There may be
+ # more than one matching key (if the user has multiple accounts
+ # subscribed to this list), but any are okay.
+ if authcontext == Defaults.AuthUser:
+ if user:
+ usernames = [user]
+ else:
+ usernames = []
+ prefix = urllib.quote(self.fqdn_listname) + '+user+'
+ for k in c.keys():
+ if k.startswith(prefix):
+ usernames.append(k[len(prefix):])
+ # If any check out, we're golden. Note: '@'s are no longer legal
+ # values in cookie keys.
+ for user in [Utils.UnobscureEmail(u) for u in usernames]:
+ ok = self.__checkone(c, authcontext, user)
+ if ok:
+ return True
+ return False
+ else:
+ return self.__checkone(c, authcontext, user)
+
+ def __checkone(self, c, authcontext, user):
+ # Do the guts of the cookie check, for one authcontext/user
+ # combination.
+ try:
+ key, secret = self.AuthContextInfo(authcontext, user)
+ except Errors.NotAMemberError:
+ return False
+ if key not in c or not isinstance(secret, basestring):
+ return False
+ # Undo the encoding we performed in MakeCookie() above. BAW: I
+ # believe this is safe from exploit because marshal can't be forced to
+ # load recursive data structures, and it can't be forced to execute
+ # any unexpected code. The worst that can happen is that either the
+ # client will have provided us bogus data, in which case we'll get one
+ # of the caught exceptions, or marshal format will have changed, in
+ # which case, the cookie decoding will fail. In either case, we'll
+ # simply request reauthorization, resulting in a new cookie being
+ # returned to the client.
+ try:
+ data = marshal.loads(binascii.unhexlify(c[key]))
+ issued, received_mac = data
+ except (EOFError, ValueError, TypeError, KeyError):
+ return False
+ # Make sure the issued timestamp makes sense
+ now = time.time()
+ if now < issued:
+ return False
+ # Calculate what the mac ought to be based on the cookie's timestamp
+ # and the shared secret.
+ mac = sha.new(secret + repr(issued)).hexdigest()
+ if mac <> received_mac:
+ return False
+ # Authenticated!
+ return True
+
+
+
+splitter = re.compile(';\s*')
+
+def parsecookie(s):
+ c = {}
+ for line in s.splitlines():
+ for p in splitter.split(line):
+ try:
+ k, v = p.split('=', 1)
+ except ValueError:
+ pass
+ else:
+ c[k] = v
+ return c