Commit bf9bd8dd authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

http.auth: Add new function to verify passwords



This new function supports two schemes for passwords:
- Old-style cleartext passwords
- Hashed passwords according to RFC2617 (H(A1))

Schemes are differentiated by their prefix, a concept also
used in OpenLDAP. Cleartext passwords can no longer start
with an opening brace ("{") unless they're prefixed with
"{cleartext}" (case insensitive).

Currently there's no documentation for rapi_users at all.
It'll be in a consecutive patch.
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent c6aa0c42
......@@ -32,6 +32,11 @@ from ganeti import http
from cStringIO import StringIO
try:
from hashlib import md5
except ImportError:
from md5 import new as md5
# Digest types from RFC2617
HTTP_BASIC_AUTH = "Basic"
......@@ -75,6 +80,10 @@ class HttpServerRequestAuthentication(object):
# Default authentication realm
AUTH_REALM = None
# Schemes for passwords
_CLEARTEXT_SCHEME = "{CLEARTEXT}"
_HA1_SCHEME = "{HA1}"
def GetAuthRealm(self, req):
"""Returns the authentication realm for a request.
......@@ -198,6 +207,63 @@ class HttpServerRequestAuthentication(object):
"""
raise NotImplementedError()
def VerifyBasicAuthPassword(self, req, username, password, expected):
"""Checks the password for basic authentication.
As long as they don't start with an opening brace ("{"), old passwords are
supported. A new scheme uses H(A1) from RFC2617, where H is MD5 and A1
consists of the username, the authentication realm and the actual password.
@type req: L{http.server._HttpServerRequest}
@param req: HTTP request context
@type username: string
@param username: Username from HTTP headers
@type password: string
@param password: Password from HTTP headers
@type expected: string
@param expected: Expected password with optional scheme prefix (e.g. from
users file)
"""
# Backwards compatibility for old-style passwords without a scheme
if not expected.startswith("{"):
expected = self._CLEARTEXT_SCHEME + expected
# Check again, just to be sure
if not expected.startswith("{"):
raise AssertionError("Invalid scheme")
scheme_end_idx = expected.find("}", 1)
# Ensure scheme has a length of at least one character
if scheme_end_idx <= 1:
logging.warning("Invalid scheme in password for user '%s'", username)
return False
scheme = expected[:scheme_end_idx + 1].upper()
expected_password = expected[scheme_end_idx + 1:]
# Good old plain text password
if scheme == self._CLEARTEXT_SCHEME:
return password == expected_password
# H(A1) as described in RFC2617
if scheme == self._HA1_SCHEME:
realm = self.GetAuthRealm(req)
if not realm:
# There can not be a valid password for this case
return False
expha1 = md5()
expha1.update("%s:%s:%s" % (username, realm, password))
return (expected_password.lower() == expha1.hexdigest().lower())
logging.warning("Unknown scheme '%s' in password for user '%s'",
scheme, username)
return False
class PasswordFileUser(object):
"""Data structure for users from password file.
......
......@@ -30,6 +30,7 @@ from ganeti import http
import ganeti.http.server
import ganeti.http.client
import ganeti.http.auth
class TestStartLines(unittest.TestCase):
......@@ -93,5 +94,73 @@ class TestMisc(unittest.TestCase):
self.assert_(message_reader_class.HEADER_LENGTH_MAX > 0)
class _FakeRequestAuth(http.auth.HttpServerRequestAuthentication):
def __init__(self, realm):
http.auth.HttpServerRequestAuthentication.__init__(self)
self.realm = realm
def GetAuthRealm(self, req):
return self.realm
class TestAuth(unittest.TestCase):
"""Authentication tests"""
hsra = http.auth.HttpServerRequestAuthentication
def testConstants(self):
self.assertEqual(self.hsra._CLEARTEXT_SCHEME,
self.hsra._CLEARTEXT_SCHEME.upper())
self.assertEqual(self.hsra._HA1_SCHEME,
self.hsra._HA1_SCHEME.upper())
def _testVerifyBasicAuthPassword(self, realm, user, password, expected):
ra = _FakeRequestAuth(realm)
return ra.VerifyBasicAuthPassword(None, user, password, expected)
def testVerifyBasicAuthPassword(self):
tvbap = self._testVerifyBasicAuthPassword
good_pws = ["pw", "pw{", "pw}", "pw{}", "pw{x}y", "}pw",
"0", "123", "foo...:xyz", "TeST"]
for pw in good_pws:
# Try cleartext passwords
self.assert_(tvbap("abc", "user", pw, pw))
self.assert_(tvbap("abc", "user", pw, "{cleartext}" + pw))
self.assert_(tvbap("abc", "user", pw, "{ClearText}" + pw))
self.assert_(tvbap("abc", "user", pw, "{CLEARTEXT}" + pw))
# Try with invalid password
self.failIf(tvbap("abc", "user", pw, "something"))
# Try with invalid scheme
self.failIf(tvbap("abc", "user", pw, "{000}" + pw))
self.failIf(tvbap("abc", "user", pw, "{unk}" + pw))
self.failIf(tvbap("abc", "user", pw, "{Unk}" + pw))
self.failIf(tvbap("abc", "user", pw, "{UNK}" + pw))
# Try with invalid scheme format
self.failIf(tvbap("abc", "user", "pw", "{something"))
# Hash is MD5("user:This is only a test:pw")
self.assert_(tvbap("This is only a test", "user", "pw",
"{ha1}92ea58ae804481498c257b2f65561a17"))
self.assert_(tvbap("This is only a test", "user", "pw",
"{HA1}92ea58ae804481498c257b2f65561a17"))
self.failIf(tvbap(None, "user", "pw",
"{HA1}92ea58ae804481498c257b2f65561a17"))
self.failIf(tvbap("Admin area", "user", "pw",
"{HA1}92ea58ae804481498c257b2f65561a17"))
self.failIf(tvbap("This is only a test", "someone", "pw",
"{HA1}92ea58ae804481498c257b2f65561a17"))
self.failIf(tvbap("This is only a test", "user", "something",
"{HA1}92ea58ae804481498c257b2f65561a17"))
if __name__ == '__main__':
unittest.main()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment