diff options
| -rw-r--r-- | Mailman/Cgi/admin.py | 79 | ||||
| -rw-r--r-- | Mailman/Cgi/admindb.py | 56 | ||||
| -rw-r--r-- | Mailman/Cgi/private.py | 148 | ||||
| -rw-r--r-- | Mailman/SecurityManager.py | 54 |
4 files changed, 171 insertions, 166 deletions
diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py index 73a7b2cd9..a1602bc3c 100644 --- a/Mailman/Cgi/admin.py +++ b/Mailman/Cgi/admin.py @@ -26,7 +26,6 @@ import sys import os, cgi, string, types, time import paths # path hacking from Mailman import Utils, MailList, Errors, MailCommandHandler -from Mailman import Cookie from Mailman.htmlformat import * from Mailman.Crypt import crypt from Mailman import mm_cfg @@ -41,33 +40,6 @@ CATEGORIES = [('general', "General Options"), ('gateway', "Mail-News and News-Mail gateways")] - -def isAuthenticated(list, password=None, SECRET="SECRET"): - if password is not None: # explicit login - try: - list.ConfirmAdminPassword(password) - except Errors.MMBadPasswordError: - AddErrorMessage(doc, 'Error: Incorrect admin password.') - return 0 - - token = list.MakeCookie() - c = Cookie.Cookie() - cookie_key = list_name + "-admin" - c[cookie_key] = token - c[cookie_key]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE - print c # Output the cookie - return 1 - if os.environ.has_key('HTTP_COOKIE'): - c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) - if c.has_key(list_name + "-admin"): - if list.CheckCookie(c[list_name + "-admin"].value): - return 1 - else: - AddErrorMessage(doc, "error decoding authorization cookie") - return 0 - return 0 - - def main(): """Process and produce list options form. @@ -110,26 +82,51 @@ def main(): category = 'general' global cgi_data cgi_data = cgi.FieldStorage() + + # Authenticate. is_auth = 0 - if cgi_data.has_key("adminpw"): - is_auth = isAuthenticated(lst, cgi_data["adminpw"].value) - message = FontAttr("Sorry, wrong password. Try again.", - color="ff5060", size="+1").Format() - else: - is_auth = isAuthenticated(lst) - message = "" + adminpw = None + message = "" + + # If we get a password change request, we first authenticate + # by cookie here, and issue a new cookie later on iff the + # password change worked out. + # + # The idea is to set only _one_ cookie when the admin password + # changes. The new cookie is needed, because the checksum part + # of the cookie is based on (among other things) the list's + # admin password. + if cgi_data.has_key('adminpw') and not cgi_data.has_key('newpw'): + adminpw = cgi_data['adminpw'].value + try: + # admin uses cookies with -admin name suffix + is_auth = lst.WebAuthenticate(password=adminpw, + cookie='admin') + except Errors.MMBadPasswordError: + message = 'Sorry, wrong password. Try again.' + except Errors.MMExpiredCookieError: + message = 'Your cookie has gone stale, ' \ + 'enter password to get a new one.', + except Errors.MMInvalidCookieError: + message = 'Error decoding authorization cookie.' + except Errors.MMAuthenticationError: + message = 'Authentication error.' + if not is_auth: defaulturi = '/mailman/admin%s/%s' % (mm_cfg.CGIEXT, list_name) - print "Content-type: text/html\n\n" + if message: + message = FontAttr( + message, color='FF5060', size='+1').Format() + print 'Content-type: text/html\n\n' text = Utils.maketext( 'admlogin.txt', - {"listname": list_name, - "path" : Utils.GetRequestURI(defaulturi), - "message" : message, + {'listname': list_name, + 'path' : Utils.GetRequestURI(defaulturi), + 'message' : message, }) print text return - + # is the request for variable details? varhelp = None if cgi_data.has_key('VARHELP'): @@ -743,6 +740,8 @@ def ChangeOptions(lst, category, cgi_info, document): if new == confirm: lst.password = crypt(new, Utils.GetRandomSeed()) dirty = 1 + # Re-authenticate (to set new cookie) + lst.WebAuthenticate(password=new, cookie='admin') else: m = 'Error: Passwords did not match.' document.AddItem(Header(3, Italic(FontAttr(m, color="ff5060")))) diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py index 0f76c032c..ac979a62b 100644 --- a/Mailman/Cgi/admindb.py +++ b/Mailman/Cgi/admindb.py @@ -22,35 +22,8 @@ import sys import os, cgi, string, types from Mailman import Utils, MailList, Errors from Mailman.htmlformat import * -from Mailman import Cookie from Mailman import mm_cfg -# copied from admin.py -def isAuthenticated(mlist, password=None, SECRET="SECRET"): - if password is not None: # explicit login - try: - mlist.ConfirmAdminPassword(password) - except Errors.MMBadPasswordError: - AddErrorMessage(doc, 'Error: Incorrect admin password.') - return 0 - - token = list.MakeCookie() - c = Cookie.Cookie() - cookie_key = list_name + "-admin" - c[cookie_key] = token - c[cookie_key]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE - print c # Output the cookie - return 1 - if os.environ.has_key('HTTP_COOKIE'): - c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) - if c.has_key(list_name + "-admin"): - if list.CheckCookie(c[list_name + "-admin"].value): - return 1 - else: - AddErrorMessage(doc, "error decoding authorization cookie") - return 0 - return 0 - def main(): # XXX: Yuk, blech, ick @@ -98,17 +71,32 @@ def main(): try: form = cgi.FieldStorage() - # authenticate. all copied from admin.py + # Authenticate. is_auth = 0 + adminpw = None + message = "" + if form.has_key('adminpw'): - is_auth = isAuthenticated(list, form['adminpw'].value) - message = FontAttr('Sorry, wrong password. Try again.', - color='ff5060', size='+1').Format() - else: - is_auth = isAuthenticated(list) - message = '' + adminpw = form['adminpw'].value + try: + # admindb uses the same cookie as admin + is_auth = list.WebAuthenticate(password=adminpw, + cookie='admin') + except Errors.MMBadPasswordError: + message = 'Sorry, wrong password. Try again.' + except Errors.MMExpiredCookieError: + message = 'Your cookie has gone stale, ' \ + 'enter password to get a new one.', + except Errors.MMInvalidCookieError: + message = 'Error decoding authorization cookie.' + except Errors.MMAuthenticationError: + message = 'Authentication error.' + if not is_auth: defaulturi = '/mailman/admindb%s/%s' % (mm_cfg.CGIEXT, list_name) + if message: + message = FontAttr( + message, color='FF5060', size='+1').Format() print 'Content-type: text/html\n\n' text = Utils.maketext( 'admlogin.txt', diff --git a/Mailman/Cgi/private.py b/Mailman/Cgi/private.py index e46879371..24eff56e7 100644 --- a/Mailman/Cgi/private.py +++ b/Mailman/Cgi/private.py @@ -22,12 +22,11 @@ Currently this is organized to obtain passwords for Mailman mailing list subscribers. """ -import sys, os, string -from Mailman import MailList, Errors -from Mailman import Cookie +import sys, os, string, cgi +from Mailman import Utils, MailList, Errors +from Mailman.htmlformat import * from Mailman.Logging.Utils import LogStdErr -from Mailman import Utils -import Mailman.mm_cfg +from Mailman import mm_cfg LogStdErr("error", "private") @@ -70,58 +69,6 @@ PAGE = ''' login_attempted = 0 _list = None -def GetListobj(list_name): - """Return an unlocked instance of the named mailing list, if found.""" - global _list - if _list: - return _list - _list = MailList.MailList(list_name, lock=0) - return _list - -def isAuthenticated(list_name): - try: - listobj = GetListobj(list_name) - except Errors.MMUnknownListError: - print "\n<H3>List", repr(list_name), "not found.</h3>" - raise SystemExit - if os.environ.has_key('HTTP_COOKIE'): - c = Cookie.Cookie( os.environ['HTTP_COOKIE'] ) - if c.has_key(list_name): - if listobj.CheckCookie(c[list_name].value): - return 1 - # No corresponding cookie. OK, then check for username, password - # CGI variables - import cgi - v = cgi.FieldStorage() - username = password = None - if v.has_key('username'): - username = v['username'] - if type(username) == type([]): username = username[0] - username = username.value - if v.has_key('password'): - password = v['password'] - if type(password) == type([]): password = password[0] - password = password.value - - if username is None or password is None: return 0 - - # Record that this is a login attempt, so if it fails the form can - # be displayed with an appropriate message. - global login_attempted - login_attempted=1 - try: - listobj.ConfirmUserPassword( username, password) - except (Errors.MMBadUserError, Errors.MMBadPasswordError, - Errors.MMNotAMemberError): - return 0 - - token = listobj.MakeCookie() - c = Cookie.Cookie() - c[list_name] = token - print c # Output the cookie - return 1 - - def true_path(path): "Ensure that the path is safe by removing .." path = string.replace(path, "../", "") @@ -138,15 +85,28 @@ def content_type(path): def main(): - path = os.environ.get('PATH_INFO', "/index.html") + doc = Document() + + try: + path = os.environ['PATH_INFO'] + except KeyError: + doc.SetTitle("Private Archive Error") + doc.AddItem(Header(3, "You must specify a list.")) + print doc.Format(bgcolor="#FFFFFF") + sys.exit(0) + true_filename = os.path.join( - Mailman.mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, + mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, true_path(path)) + list_info = Utils.GetPathPieces(path) - if len(list_info) == 0: - list_name = None - else: - list_name = string.lower(list_info[0]) + + if len(list_info) < 1: + doc.SetTitle("Private Archive Error") + doc.AddItem(Header(3, "You must specify a list.")) + print doc.Format(bgcolor="#FFFFFF") + sys.exit(0) + list_name = string.lower(list_info[0]) # If it's a directory, we have to append index.html in this script. We # must also check for a gzipped file, because the text archives are @@ -158,29 +118,55 @@ def main(): # then true_filename = true_filename + '.gz' - if not list_name or not isAuthenticated(list_name): + try: + listobj = MailList.MailList(list_name, lock=0) + except Errors.MMUnknownListError: + listobj = None + if not (listobj and listobj._ready): + msg = "%s: No such list." % list_name + doc.SetTitle("Private Archive Error - %s" % msg) + doc.AddItem(Header(2, msg)) + print doc.Format(bgcolor="#FFFFFF") + sys.exit(0) + + form = cgi.FieldStorage() + user = password = None + if form.has_key('username'): + user = form['username'] + if type(user) == type([]): user = user[0] + user = user.value + if form.has_key('password'): + password = form['password'] + if type(password) == type([]): password = password[0] + password = password.value + + is_auth = 0 + message = ("Please enter your %s subscription email address " + "and password." % listobj.real_name) + try: + is_auth = listobj.WebAuthenticate(user=user, + password=password, + cookie='archive') + except (Errors.MMBadUserError, Errors.MMBadPasswordError, + Errors.MMNotAMemberError): + message = ('Your email address or password were incorrect. ' + 'Please try again.') + except Errors.MMExpiredCookieError: + message = 'Your cookie has gone stale, ' \ + 'enter password to get a new one.', + except Errors.MMInvalidCookieError: + message = 'Error decoding authorization cookie.' + except Errors.MMAuthenticationError: + message = 'Authentication error.' + + if not is_auth: # Output the password form - print 'Content-type: text/html\n' + print 'Content-type: text/html\n\n' page = PAGE - - if not list_name: - print '\n<h3>No list name found.</h3>' - raise SystemExit - try: - listobj = GetListobj(list_name) - except Errors.MMUnknownListError: - print "\n<H3>List", repr(list_name), "not found.</h3>" - raise SystemExit - if login_attempted: - message = ("Your email address or password were incorrect." - " Please try again.") - else: - message = ("Please enter your %s subscription email address" - " and password." % listobj.real_name) while path and path[0] == '/': path=path[1:] # Remove leading /'s basepath = os.path.split(listobj.GetBaseArchiveURL())[0] listname = listobj.real_name - print '\n\n', page % vars() + print page % vars() sys.exit(0) # Authorization confirmed... output the desired file diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py index d60a93179..289762834 100644 --- a/Mailman/SecurityManager.py +++ b/Mailman/SecurityManager.py @@ -25,6 +25,8 @@ import types import Crypt import Errors import Utils +import Cookie +from urlparse import urlparse import mm_cfg # TBD: is this the best location for the site password? @@ -68,30 +70,60 @@ class SecurityManager: raise Errors.MMBadPasswordError return 1 - def MakeCookie(self): + def WebAuthenticate(self, user=None, password=None, cookie=None): + if cookie: + cookie_key = self._internal_name + ':' + cookie + else: + cookie_key = self._internal_name + if password is not None: # explicit login + if user: + self.ConfirmUserPassword(user, password) + else: + self.ConfirmAdminPassword(password) + print self.MakeCookie(cookie_key) + return 1 + else: + return self.CheckCookie(cookie_key) + + def MakeCookie(self, key): + # Make sure we have the necessary ingredients for our cookie client_ip = os.environ.get('REMOTE_ADDR') or '0.0.0.0' issued = int(time.time()) expires = issued + mm_cfg.ADMIN_COOKIE_LIFE + # ... including the secret ingredient :) secret = self.password mac = hash(secret + client_ip + `issued` + `expires`) - return [client_ip, issued, expires, mac] + # Mix all ingredients gently together, + c = Cookie.Cookie() + c[key] = [client_ip, issued, expires, mac] + # place in oven, + path = urlparse(mm_cfg.DEFAULT_URL)[2] # '/mailman' + c[key]['path'] = path + # and bake until golden brown + c[key]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE + return c - def CheckCookie(self, cookie): - if type(cookie) <> type([]): + def CheckCookie(self, key): + if not os.environ.has_key('HTTP_COOKIE'): return 0 - if len(cookie) <> 4: + c = Cookie.Cookie(os.environ['HTTP_COOKIE']) + if not c.has_key(key): return 0 + cookie = c[key].value + if (type(cookie) <> type([]) or + len(cookie) <> 4): + raise Errors.MMInvalidCookieError client_ip = os.environ.get('REMOTE_ADDR') or '0.0.0.0' - [for_ip, issued, expires, received_mac] = cookie - if for_ip <> client_ip: - return 0 now = time.time() - if not issued < now < expires: - return 0 + [for_ip, issued, expires, received_mac] = cookie + if (for_ip <> client_ip or now < issued): + raise Errors.MMInvalidCookieError + if now > expires: + raise Errors.MMExpiredCookieError secret = self.password mac = hash(secret + client_ip + `issued` + `expires`) if mac <> received_mac: - return 0 + raise Errors.MMInvalidCookieError return 1 def ConfirmUserPassword(self, user, pw): |
