import base64
import binascii
-from ganeti import utils
+from ganeti import compat
from ganeti import http
from cStringIO import StringIO
-
# Digest types from RFC2617
HTTP_BASIC_AUTH = "Basic"
HTTP_DIGEST_AUTH = "Digest"
class HttpServerRequestAuthentication(object):
# Default authentication realm
- AUTH_REALM = None
+ AUTH_REALM = "Unspecified"
+
+ # Schemes for passwords
+ _CLEARTEXT_SCHEME = "{CLEARTEXT}"
+ _HA1_SCHEME = "{HA1}"
def GetAuthRealm(self, req):
"""Returns the authentication realm for a request.
- MAY be overridden by a subclass, which then can return different realms for
- different paths. Returning "None" means no authentication is needed for a
- request.
+ May be overridden by a subclass, which then can return different realms for
+ different paths.
@type req: L{http.server._HttpServerRequest}
@param req: HTTP request context
- @rtype: str or None
+ @rtype: string
@return: Authentication realm
"""
+ # today we don't have per-request filtering, but we might want to
+ # add it in the future
+ # pylint: disable-msg=W0613
return self.AUTH_REALM
+ def AuthenticationRequired(self, req):
+ """Determines whether authentication is required for a request.
+
+ To enable authentication, override this function in a subclass and return
+ C{True}. L{AUTH_REALM} must be set.
+
+ @type req: L{http.server._HttpServerRequest}
+ @param req: HTTP request context
+
+ """
+ # Unused argument, method could be a function
+ # pylint: disable-msg=W0613,R0201
+ return False
+
def PreHandleRequest(self, req):
"""Called before a request is handled.
@param req: HTTP request context
"""
- realm = self.GetAuthRealm(req)
-
# Authentication not required, and no credentials given?
- if realm is None and http.HTTP_AUTHORIZATION not in req.request_headers:
+ if not (self.AuthenticationRequired(req) or
+ (req.request_headers and
+ http.HTTP_AUTHORIZATION in req.request_headers)):
return
- if realm is None: # in case we don't require auth but someone
- # passed the crendentials anyway
- realm = "Unspecified"
+ realm = self.GetAuthRealm(req)
+
+ if not realm:
+ raise AssertionError("No authentication realm")
# Check "Authorization" header
if self._CheckAuthorization(req):
"""
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 ("E{lb}"), 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
+ raise AssertionError("No authentication realm")
+
+ expha1 = compat.md5_hash()
+ 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.
self.options = options
-def ReadPasswordFile(file_name):
- """Reads a password file.
+def ParsePasswordFile(contents):
+ """Parses the contents of a password file.
Lines in the password file are of the following format::
options are optional and separated by comma (','). Empty lines and comments
('#') are ignored.
- @type file_name: str
- @param file_name: Path to password file
+ @type contents: str
+ @param contents: Contents of password file
@rtype: dict
@return: Dictionary containing L{PasswordFileUser} instances
"""
users = {}
- for line in utils.ReadFile(file_name).splitlines():
+ for line in contents.splitlines():
line = line.strip()
# Ignore empty lines and comments