Update of /cvsroot/mailman/mailman/Mailman
In directory usw-pr-cvs1:/tmp/cvs-serv9347

Modified Files:
        SecurityManager.py 
Log Message:
Changes to support the new world order of authentication, using
authorization contexts and the roles of User, List Owner, List
Moderator, (List) Creator/Destroyer, Site Administrator.
Specifically,

InitVars(): Add a mod_password attribute that can contain the sha
hashed list moderator's password.

ValidAdminPassword(), ConfirmAdminPassword(): Removed as obsolete.

AuthContextInfo(): Given an authorization context, and optionally a
user (if authcontext == AuthUser), return the context's secret and
cookie key.  The tuple (None, None) is returned if the authcontext is
bogus.  MMNotAMemberError is raised if the user isn't a member of the
list, and MMBadUserError is raised if the user's secret is None.

Authenticate(): The non-web way of doing authentication.  Takes a list
of allowed authcontexts (and optionally a user name if AuthUser is one
of those contexts), and a response string (i.e. password).  Returns
the authcontext from the argument sequence that matches the response,
or UnAuthorized if none of them did.

WebAuthenticate(): The web way of doing authentication.  The arguments
are the same as Authenticate(), but first the cookie data is checked.
If that fails, then Authenticate() is used.  Returns a flag indicating
whether authentication succeeded or not.

MakeCookie(): Now takes an authcontext and optionally a user (required
if authcontext is AuthUser).  Generates a cookie item for this
context.

ZapCookie(): Now takes an authcontext and optionally a user (required
if authcontext is AuthUser).  Generates an empty cookie item for this
context, effectively logging out that authcontext.

CheckCookie(): Now takes an authcontext and optionally a user (required
if authcontext is AuthUser).  Returns a flag indicating whether the
authcontext's cookie matches the expected value, i.e. whether they are
cookie authenticated or not.

ChangeUserPassword(): Remove the test for IsListInitialized(), and
removed the Save() call, since all paths to this method should be
wrapped in the standard lock-modify-save-unlock fence.


Index: SecurityManager.py
===================================================================
RCS file: /cvsroot/mailman/mailman/Mailman/SecurityManager.py,v
retrieving revision 2.3
retrieving revision 2.4
diff -C2 -r2.3 -r2.4
*** SecurityManager.py  2001/05/16 05:24:41     2.3
--- SecurityManager.py  2001/05/31 17:46:11     2.4
***************
*** 18,22 ****
--- 18,51 ----
  """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 time
***************
*** 41,94 ****
  class SecurityManager:
      def InitVars(self, crypted_password):
!       # Configurable, however, we don't pass this back in GetConfigInfo
        # because it's a special case as it requires confirmation to change.
!       self.password = crypted_password      
        # Non configurable
        self.passwords = {}
  
!     def ValidAdminPassword(self, response):
!         if Utils.check_global_password(response):
!             return 1
!         # Old passwords may have been encrypted w/crypt.  First try comparing
!         # challenge with sha hashed response and if that fails, use crypt and
!         # upgrade.
!         challenge = self.password
!         sha_response = sha.new(response).hexdigest()
!         if challenge == sha_response:
!             return 1
!         if crypt and challenge == crypt.crypt(response, challenge[:2]):
!             # Upgrade the password hash to SHA1.   BAW: should we store the
!             # plaintext password as well?  We could implement a mail-back
!             # option for list owners that way.
!             self.password = sha_response
!             return 1
!         # Didn't match.
!         return 0
! 
!     def ConfirmAdminPassword(self, pw):
!         if not self.ValidAdminPassword(pw):
!           raise Errors.MMBadPasswordError
!       return 1
! 
!     def WebAuthenticate(self, user=None, password=None, cookie=None):
!         key = self.internal_name()
!         if cookie:
!             key = key + ':' + cookie
!         # password will be None for explicit login
!         if password is None:
!             return self.CheckCookie(key)
          else:
!             if user:
!                 self.ConfirmUserPassword(user, password)
              else:
!                 self.ConfirmAdminPassword(password)
!             print self.MakeCookie(key)
!             return 1
  
!     def MakeCookie(self, key):
!         # Ingredients for our cookie: our `secret' which is the list's admin
!         # password (never sent in the clear) and the time right now in seconds
!         # since the epoch.
!         secret = self.password
          issued = int(time.time())
          # Get a digest of the secret, plus other information.
--- 70,179 ----
  class SecurityManager:
      def InitVars(self, crypted_password):
!       # Configurable, however we don't pass these back in GetConfigInfo
        # because it's a special case as it requires confirmation to change.
!       self.password = crypted_password
!         self.mod_password = None
        # Non configurable
        self.passwords = {}
  
!     def AuthContextInfo(self, authcontext, user):
!         # 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, raise a
!         # MMNotAMemberError error.  If the user's secret is None, raise a
!         # MMBadUserError.
!         key = self.internal_name() + ':'
!         if authcontext == mm_cfg.AuthUser:
!             if user is None:
!                 # A bad system error
!                 raise TypeError, 'No user supplied for AuthUser context'
!             addr = self.FindUser(user)
!             if addr is None:
!                 raise Errors.MMNotAMemberError
!             secret = self.passwords.get(addr)
!             if secret is None:
!                 raise Errors.MMBadUserError
!             key += 'user:%s' % addr
!         elif authcontext == mm_cfg.AuthListModerator:
!             secret = self.mod_password
!             key += 'moderator'
!         elif authcontext == mm_cfg.AuthListAdmin:
!             secret = self.password
!             key += 'admin'
!         # BAW: AuthCreator
!         elif authcontext == mm_cfg.AuthSiteAdmin:
!             # 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 == mm_cfg.AuthCreator:
!                 ok = Utils.check_global_password(response, siteadmin=0)
!                 if ok:
!                     return mm_cfg.AuthCreator
!             elif ac == mm_cfg.AuthSiteAdmin:
!                 ok = Utils.check_global_password(response)
!                 if ok:
!                     return mm_cfg.AuthSiteAdmin
              else:
!                 # 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.
!                 if ac in (mm_cfg.AuthListAdmin, mm_cfg.AuthListModerator):
!                     response = sha.new(response).hexdigest()
! 
!                 key, secret = self.AuthContextInfo(ac, user)
!                 if secret is not None and response == secret:
!                     return ac
!         return mm_cfg.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 must not be None.
!         #
!         # Returns a flag indicating whether authentication succeeded or not.
!         try:
!             for ac in authcontexts:
!                 ok = self.CheckCookie(ac, user)
!                 if ok:
!                     return 1
!             # Check passwords
!             ac = self.Authenticate(authcontexts, response, user)
!             if ac:
!                 print self.MakeCookie(ac, user)
!                 return 1
!         except Errors.MMNotAMemberError:
!             pass
!         return 0
  
!     def MakeCookie(self, authcontext, user=None):
!         key, secret = self.AuthContextInfo(authcontext, user)
!         if key is None or secret is None:
!             raise MMBadUserError
!         # Timestamp
          issued = int(time.time())
          # Get a digest of the secret, plus other information.
***************
*** 103,118 ****
          path = urlparse(self.web_page_url)[2]
          c[key]['path'] = path
!         # Should we use session or persistent cookies?
!         if mm_cfg.ADMIN_COOKIE_LIFE > 0:
!             c[key]['expires'] = mm_cfg.ADMIN_COOKIE_LIFE
!             c[key]['max-age'] = mm_cfg.ADMIN_COOKIE_LIFE
!         # Set the RFC 2109 required header
          c[key]['version'] = 1
          return c
  
!     def ZapCookie(self, cookie=None):
!         key = self.internal_name()
!         if cookie:
!             key = key + ':' + cookie
          # 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
--- 188,199 ----
          path = urlparse(self.web_page_url)[2]
          c[key]['path'] = 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
***************
*** 129,133 ****
          return c
  
!     def CheckCookie(self, key):
          cookiedata = os.environ.get('HTTP_COOKIE')
          if not cookiedata:
--- 210,221 ----
          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.
!         key, secret = self.AuthContextInfo(authcontext, user)
!         # 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:
***************
*** 138,152 ****
          # Undo the encoding we performed in MakeCookie() above
          try:
!             cookie = marshal.loads(binascii.unhexlify(c[key].value))
!             issued, received_mac = cookie
          except (EOFError, ValueError, TypeError):
!             raise Errors.MMInvalidCookieError
          now = time.time()
          if now < issued:
!             raise Errors.MMInvalidCookieError
!         secret = self.password
          mac = sha.new(secret + `issued`).hexdigest()
          if mac <> received_mac:
!             raise Errors.MMInvalidCookieError
          return 1
  
--- 226,243 ----
          # Undo the encoding we performed in MakeCookie() above
          try:
!             data = marshal.loads(binascii.unhexlify(c[key].value))
!             issued, received_mac = data
          except (EOFError, ValueError, TypeError):
!             return 0
!         # Make sure the issued timestamp makes sense
          now = time.time()
          if now < issued:
!             return 0
!         # Calculate what the mac ought to be based on the cookie's timestamp
!         # and the shared secret.
          mac = sha.new(secret + `issued`).hexdigest()
          if mac <> received_mac:
!             return 0
!         # Authenticated!
          return 1
  
***************
*** 168,172 ****
  
      def ChangeUserPassword(self, user, newpw, confirm):
-       self.IsListInitialized()
        addr = self.FindUser(user)
        if not addr:
--- 259,262 ----
***************
*** 175,177 ****
            raise Errors.MMPasswordsMustMatch
        self.passwords[addr] = newpw
-       self.Save()
--- 265,266 ----


_______________________________________________
Mailman-checkins mailing list
[EMAIL PROTECTED]
http://mail.python.org/mailman/listinfo/mailman-checkins

Reply via email to