[Buildbot-devel] [PATCH 11/11] Allow authentication to work with .htpasswd-style files.

Benoit Sigoure tsuna at lrde.epita.fr
Fri Nov 16 17:13:01 UTC 2007


	* NEWS: Mention the change.
	* buildbot/status/web/baseweb.py (WebStatus.__init__): Adjust to
	also accept a string as `userpass' argument.
	(WebStatus._auth_htpasswd): New.
	(WebStatus.authUser): Use the new method when htpasswd-style
	authentication is used.
	* docs/buildbot.texinfo (WebStatus Configuration Parameters):
	Document the new feature.

Signed-off-by: Benoit Sigoure <tsuna at lrde.epita.fr>
---
 NEWS                           |    3 +-
 buildbot/status/web/baseweb.py |   62 +++++++++++++++++++++++++++++++--------
 docs/buildbot.texinfo          |    3 +-
 3 files changed, 53 insertions(+), 15 deletions(-)

diff --git a/NEWS b/NEWS
index d136cf7..7d4496b 100644
--- a/NEWS
+++ b/NEWS
@@ -11,7 +11,8 @@ User visible changes in Buildbot.             -*- outline -*-
 The WebStatus constructor can take a list of login/password as a (optional)
 `userpass' argument in pretty much the same way as Try_Userpass does.
 Only users with a valid login/password can then force/stop builds from the
-WebStatus.
+WebStatus.  Alternatively, the `userpass' argument can be a string that
+contains a path to a .htpasswd-style file to use to authenticate users.
 
 * Release 0.7.6 (30 Sep 2007)
 
diff --git a/buildbot/status/web/baseweb.py b/buildbot/status/web/baseweb.py
index 5755140..d8a8448 100644
--- a/buildbot/status/web/baseweb.py
+++ b/buildbot/status/web/baseweb.py
@@ -353,7 +353,7 @@ class WebStatus(service.MultiService):
     # all the changes).
 
     def __init__(self, http_port=None, distrib_port=None, allowForce=False,
-                 userpass=[]):
+                 userpass=None):
         """Run a web server that provides Buildbot status.
 
         @type  http_port: int or L{twisted.application.strports} string
@@ -388,12 +388,15 @@ class WebStatus(service.MultiService):
                              the strports parser.
         @param allowForce: boolean, if True then the webserver will allow
                            visitors to trigger and cancel builds
-        @rtpw  userpass: list of (string, string) tuples
+        @rtpw  userpass: list of (string, string) tuples or a string
         @param userpass: a list of login/password pairs to restrain access to
                          the C{allowForce} features.  Ignored if C{allowForce}
                          is not True.  If you don't pass a list of
                          user/passwords, the legacy behavior is used: people
                          can force/stop builds without auth.
+                         If C{userpass} is a string, it is assumed to be a
+                         path to a .htpasswd-style file which will be used to
+                         authenticate users.
         """
 
         service.MultiService.__init__(self)
@@ -408,17 +411,25 @@ class WebStatus(service.MultiService):
         self.distrib_port = distrib_port
         self.allowForce = allowForce
         if allowForce and userpass:
-            for user_pass_pair in userpass:
-                assert isinstance(user_pass_pair, tuple)
-                login, password = user_pass_pair
-                assert isinstance(login, str)
-                assert isinstance(password, str)
+            if isinstance(userpass, str):
+                self.auth_type = 'htpasswd'
+                import os
+                assert os.path.exists(userpass)
+            elif isinstance(userpass, list):
+                self.auth_type = 'simple'
+                for user_pass_pair in userpass:
+                    assert isinstance(user_pass_pair, tuple)
+                    login, password = user_pass_pair
+                    assert isinstance(login, str)
+                    assert isinstance(password, str)
+            else:
+                raise TypeError("userpass has an invalid type")
             self.userpass = userpass
         else:
             if userpass:
                 log.msg("warning: discarding the list of user/passwords, you"
                         "must also set allowForce to True to use it.")
-            self.userpass = []
+            self.userpass = None
 
         # this will be replaced once we've been attached to a parent (and
         # thus have a basedir and can reference BASEDIR/public_html/)
@@ -527,6 +538,23 @@ class WebStatus(service.MultiService):
             return True
         return False
 
+    def _auth_htpasswd(self, login, password):
+        """Authenticate C{login} and C{password} against a .htpasswd-style
+        file"""
+        # Fetch the lines from the .htpasswd file and split them in a
+        # [login, password] array.
+        lines = [l.rstrip().split(':', 1)
+                 for l in file(self.userpass).readlines()]
+        # Keep only the line for this login
+        lines = [l for l in lines if l[0] == login]
+        if not lines:
+            return False
+        # This is the DES-hash of the password.  The first two characters are
+        # the salt used to introduce disorder in the DES algorithm.
+        hash = lines[0][1]
+        from crypt import crypt
+        return hash == crypt(password, hash[0:2])
+
     def authUser(self, login, password):
         """Check that login/password is a valid user/password pair and can be
            allowed to perform a privileged action.  If this WebStatus is not
@@ -534,11 +562,19 @@ class WebStatus(service.MultiService):
            approach)."""
         if not self.userpass:
             return False
-        for l, p in self.userpass:
-            if login == l and password == p:
-                return True
-        log.msg("Authentication failed (%s/%s)" % (login, password))
-        return False
+
+        res = False
+        if self.auth_type == 'htpasswd':
+            res = self._auth_htpasswd(login, password)
+        else:
+            # Basic authentication against a list of login/password.
+            for l, p in self.userpass:
+                if login == l and password == p:
+                    res = True
+
+        if not res:
+            log.msg("Authentication failed (%s/%s)" % (login, password))
+        return res
 
     def getPortnum(self):
         # this is for the benefit of unit tests
diff --git a/docs/buildbot.texinfo b/docs/buildbot.texinfo
index 7086f96..5cf9d17 100644
--- a/docs/buildbot.texinfo
+++ b/docs/buildbot.texinfo
@@ -6067,7 +6067,8 @@ installed at the time of the previous build.  The default value is False.
 
 You may not wish to allow strangers to cause a build to run or to stop current
 builds, in that case you can pass a list of pair of login/password as a
- at code{userpass} argument.
+ at code{userpass} argument.  Alternatively, this argument can be a string that
+contains a path to a @file{.htpasswd}-style file to use to authenticate users.
 
 
 @node Buildbot Web Resources, XMLRPC server, WebStatus Configuration Parameters, WebStatus
-- 
1.5.3.5.654.gdd5ec





More information about the devel mailing list