diff options
| author | bwarsaw | 2006-10-30 02:56:10 +0000 |
|---|---|---|
| committer | bwarsaw | 2006-10-30 02:56:10 +0000 |
| commit | 0e306bb5b88fe02d83f9964c90177491602fa158 (patch) | |
| tree | 71244e23072953105f7b21689d98c6214d0b3b84 /Mailman/SecurityManager.py | |
| parent | 3256c431e7bf966d3de49e4dc31dd01d57ffb02f (diff) | |
| download | mailman-0e306bb5b88fe02d83f9964c90177491602fa158.tar.gz mailman-0e306bb5b88fe02d83f9964c90177491602fa158.tar.zst mailman-0e306bb5b88fe02d83f9964c90177491602fa158.zip | |
Repair a problem with cookie paths reported by Tokio when HTTPRunner is used
but a reverse proxy maps our wsgiref server into an upstream server's web
space.
Here's the basic problem: the Set-Cookie header we return has a Path attribute
which must match subsequent request uri's in order for the client to send the
cookie data back to us later in a Cookie header of that subsequent request.
The problem though is that we cannot guarantee that we know how our wsgi
server is mapped into the upstream proxy. It's probably '/mailman/' for
backward compatibility, but there's no way for us to tell, because there's
nothing specifically included in the request that tells us what the originally
requested uri is.
If we get the cookie path wrong, the effect is to require a login every time
an admin page is hit, because the client will not see a matching path prefix
and will not send us the cookie data.
We solve this (albeit, by hack) by looking at the HTTP_REFERER environment
variable we see. This will point to the admin login page on which the admin
password was entered. We'll pick this uri apart to attempt to find the prefix
at which our wsgi server was mapped. If we find this, we'll use it to craft
an appropriate cookie path. Hopefully. If that cgi environment variable is
not available, we just return the path as we've seen it.
This approach allows for accessing the admin pages either through a reverse
proxy or directly, with no additional configuration necessary. In fact, both
access mechanisms can work at the same time; try hitting these two uri's with
different browsers:
http://example.com/mailman/admin/mylist@example.com/general
http://example.com:2580/admin/mylist@example.com/general
The first will hit our reverse proxy, and the second will hit our wsgi server
directly. The responses will included two different cookie paths, but both
will work! This is an important principle to uphold, especially for debugging
purposes. Note that I could have added a configuration variable to handle the
cookie path remapping, but then we'd have to support either the reverse proxy
or the wsgi server, but not (easily) both. Plus, it's another configuration
variable. Yuck.
Note that Apache 2.2 has something called the ProxyPassReverseCookiePath
directive, which should probably be used if available. It's not in Apache
2.0, which is probably the most prevalent server in use with Mailman right
now, so we can't count on it. Plus, if ProxyPassReverseCookiePath is
available, our algorithm should still work.
What about other proxy servers? Dunno. We'll have to wait for feedback from
users.
This change also fixes a buglet with MTA/Postfix.py when
POSTFIX_STYLE_VIRTUAL_DOMAINS is used. You can end up trying to call
_update_maps() before data/aliases exist. This just defers that call until
it's guaranteed both the transport and the alias files have been created.
Also, finish converting SecurityManager.py to use the configuration object
(and the Defaults module where appropriate) instead of mm_cfg.py.
Diffstat (limited to 'Mailman/SecurityManager.py')
| -rw-r--r-- | Mailman/SecurityManager.py | 54 |
1 files changed, 38 insertions, 16 deletions
diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py index 8c55d2863..eb7676835 100644 --- a/Mailman/SecurityManager.py +++ b/Mailman/SecurityManager.py @@ -57,9 +57,12 @@ import logging import marshal import binascii +from urlparse import urlparse + +from Mailman import Defaults from Mailman import Errors -from Mailman import mm_cfg from Mailman import Utils +from Mailman.configuration import config try: import crypt @@ -93,23 +96,23 @@ class SecurityManager: # NotAMemberError will be raised. If the user's secret is None, raise # a MMBadUserError. key = self.internal_name() + '+' - if authcontext == mm_cfg.AuthUser: + 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 == mm_cfg.AuthListModerator: + elif authcontext == Defaults.AuthListModerator: secret = self.mod_password key += 'moderator' - elif authcontext == mm_cfg.AuthListAdmin: + elif authcontext == Defaults.AuthListAdmin: secret = self.password key += 'admin' # BAW: AuthCreator - elif authcontext == mm_cfg.AuthSiteAdmin: + elif authcontext == Defaults.AuthSiteAdmin: sitepass = Utils.get_global_password() - if mm_cfg.ALLOW_SITE_ADMIN_COOKIES and sitepass: + if config.ALLOW_SITE_ADMIN_COOKIES and sitepass: secret = sitepass key = 'site' else: @@ -132,15 +135,15 @@ class SecurityManager: # Return the authcontext from the argument sequence that matches the # response, or UnAuthorized. for ac in authcontexts: - if ac == mm_cfg.AuthCreator: + if ac == Defaults.AuthCreator: ok = Utils.check_global_password(response, siteadmin=0) if ok: - return mm_cfg.AuthCreator - elif ac == mm_cfg.AuthSiteAdmin: + return Defaults.AuthCreator + elif ac == Defaults.AuthSiteAdmin: ok = Utils.check_global_password(response) if ok: - return mm_cfg.AuthSiteAdmin - elif ac == mm_cfg.AuthListAdmin: + return Defaults.AuthSiteAdmin + elif ac == Defaults.AuthListAdmin: def cryptmatchp(response, secret): try: salt = secret[:2] @@ -186,12 +189,12 @@ class SecurityManager: self.Unlock() if ok: return ac - elif ac == mm_cfg.AuthListModerator: + elif ac == Defaults.AuthListModerator: # The list moderator password must be sha'd key, secret = self.AuthContextInfo(ac) if secret and sha.new(response).hexdigest() == secret: return ac - elif ac == mm_cfg.AuthUser: + elif ac == Defaults.AuthUser: if user is not None: try: if self.authenticateMember(user, response): @@ -202,7 +205,7 @@ class SecurityManager: # What is this context??? log.error('Bad authcontext: %s', ac) raise ValueError('Bad authcontext: %s' % ac) - return mm_cfg.UnAuthorized + return Defaults.UnAuthorized def WebAuthenticate(self, authcontexts, response, user=None): # Given a list of authentication contexts, check to see if the cookie @@ -224,7 +227,26 @@ class SecurityManager: return False def _cookie_path(self): - return '/%s/%s' % (os.environ['SCRIPT_NAME'], self.fqdn_listname) + # We could be reverse proxied, in which case our cookie path must + # match the path as seen by the upstream server, otherwise the client + # won't send us our cookie data. We try to figure this out by looking + # at the HTTP_REFERER header, which should include the uri of the + # admin login screen as seen by the client. This is a hack because + # we're not guaranteed to see that envar, but there's really no other + # way to do it -- the original uri that's proxied to us is not + # included in the backend request. XXX what happens when Apache 2.2's + # ProxyPassReverseCookiePath is set? + target = '/%s/%s' % (os.environ['SCRIPT_NAME'], self.fqdn_listname) + referer = os.environ.get('HTTP_REFERER') + if not referer: + return target + # Python 2.5 XXX urlparse(referer).path + path = urlparse(referer)[2] + i = path.find(target) + if i < 0: + return target + prefix = path[:i] + return prefix + target def MakeCookie(self, authcontext, user=None): key, secret = self.AuthContextInfo(authcontext, user) @@ -278,7 +300,7 @@ class SecurityManager: # 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 == mm_cfg.AuthUser: + if authcontext == Defaults.AuthUser: if user: usernames = [user] else: |
