Merge branch 'devel-2.7'
[ganeti-local] / lib / http / auth.py
index a13c60f..5dec95b 100644 (file)
@@ -27,17 +27,12 @@ import re
 import base64
 import binascii
 
-from ganeti import utils
+from ganeti import compat
 from ganeti import http
+from ganeti import utils
 
 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"
 HTTP_DIGEST_AUTH = "Digest"
@@ -78,7 +73,7 @@ def _FormatAuthHeader(scheme, params):
 
 class HttpServerRequestAuthentication(object):
   # Default authentication realm
-  AUTH_REALM = None
+  AUTH_REALM = "Unspecified"
 
   # Schemes for passwords
   _CLEARTEXT_SCHEME = "{CLEARTEXT}"
@@ -87,21 +82,34 @@ class HttpServerRequestAuthentication(object):
   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
+    # 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.
 
@@ -109,15 +117,16 @@ class HttpServerRequestAuthentication(object):
     @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):
@@ -190,7 +199,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
@@ -255,9 +264,9 @@ class HttpServerRequestAuthentication(object):
       realm = self.GetAuthRealm(req)
       if not realm:
         # There can not be a valid password for this case
-        return False
+        raise AssertionError("No authentication realm")
 
-      expha1 = md5()
+      expha1 = compat.md5_hash()
       expha1.update("%s:%s:%s" % (username, realm, password))
 
       return (expected_password.lower() == expha1.hexdigest().lower())
@@ -278,8 +287,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::
 
@@ -289,24 +298,20 @@ 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():
-    line = line.strip()
-
-    # Ignore empty lines and comments
-    if not line or line.startswith("#"):
-      continue
-
+  for line in utils.FilterEmptyLinesAndComments(contents):
     parts = line.split(None, 2)
     if len(parts) < 2:
       # Invalid line
+      # TODO: Return line number from FilterEmptyLinesAndComments
+      logging.warning("Ignoring non-comment line with less than two fields")
       continue
 
     name = parts[0]
@@ -317,6 +322,8 @@ def ReadPasswordFile(file_name):
     if len(parts) >= 3:
       for part in parts[2].split(","):
         options.append(part.strip())
+    else:
+      logging.warning("Ignoring values for user '%s': %s", name, parts[3:])
 
     users[name] = PasswordFileUser(name, password, options)