X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/8a088b79fd80e96d9e15e38d98ff4acc558e2c54..c4929a8bcca4a43dc6434394a91a8ea67d854844:/lib/http/auth.py diff --git a/lib/http/auth.py b/lib/http/auth.py index b9a66a5..5a7bfac 100644 --- a/lib/http/auth.py +++ b/lib/http/auth.py @@ -23,18 +23,15 @@ """ import logging -import time import re import base64 import binascii -from ganeti import constants -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" @@ -75,23 +72,43 @@ def _FormatAuthHeader(scheme, params): 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 overriden 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=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=W0613,R0201 + return False + def PreHandleRequest(self, req): """Called before a request is handled. @@ -99,11 +116,16 @@ class HttpServerRequestAuthentication(object): @param req: HTTP request context """ + # Authentication not required, and no credentials given? + if not (self.AuthenticationRequired(req) or + (req.request_headers and + http.HTTP_AUTHORIZATION in req.request_headers)): + return + realm = self.GetAuthRealm(req) - # Authentication required? - if realm is None: - return + if not realm: + raise AssertionError("No authentication realm") # Check "Authorization" header if self._CheckAuthorization(req): @@ -176,7 +198,7 @@ class HttpServerRequestAuthentication(object): """ try: - creds = base64.b64decode(in_data.encode('ascii')).decode('ascii') + creds = base64.b64decode(in_data.encode("ascii")).decode("ascii") except (TypeError, binascii.Error, UnicodeError): logging.exception("Error when decoding Basic authentication credentials") return False @@ -191,11 +213,68 @@ class HttpServerRequestAuthentication(object): def Authenticate(self, req, user, password): """Checks the password for a user. - This function MUST be overriden by a subclass. + This function MUST be overridden by a subclass. """ 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. @@ -207,8 +286,8 @@ class PasswordFileUser(object): 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:: @@ -218,15 +297,15 @@ def ReadPasswordFile(file_name): 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