Added: trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth.py (0 => 101695)
--- trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth.py (rev 0)
+++ trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth.py 2011-12-01 19:58:49 UTC (rev 101695)
@@ -0,0 +1,115 @@
+# Copyright (C) 2011 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""An implementation of buildbot.status.web.auth.IAuth for authenticating WebKit committers"""
+
+import ConfigParser
+import buildbot.status.web.auth
+import json
+import zope.interface
+
+from webkitpy.common.net.htdigestparser import HTDigestParser
+
+
+class Error(Exception):
+ pass
+
+
+class CommitterAuth(buildbot.status.web.auth.AuthBase):
+ zope.interface.implements(buildbot.status.web.auth.IAuth)
+
+ def __init__(self, auth_json_filename):
+ self._auth_json_filename = auth_json_filename
+
+ def auth_json(self):
+ try:
+ with self.open_auth_json_file() as f:
+ return json.load(f)
+ except IOError, e:
+ raise Error('Error opening auth.json file: {0}'.format(e.strerror))
+ except ValueError, e:
+ raise Error('Error parsing auth.json file: {0}'.format(e.message))
+
+ def auth_json_filename(self):
+ return self._auth_json_filename
+
+ def authenticate(self, username, password):
+ try:
+ return self.is_webkit_committer(username) and self.is_webkit_trac_user(username, password)
+ except Error, e:
+ self.err = e.message
+ return False
+
+ def is_webkit_committer(self, username):
+ try:
+ if username not in self.webkit_committers():
+ self.err = 'Invalid username/password'
+ return False
+ return True
+ except ConfigParser.Error:
+ raise Error('Error parsing WebKit committers file')
+ except IOError, e:
+ raise Error('Error opening WebKit committers file: {0}'.format(e.strerror))
+
+ def is_webkit_trac_user(self, username, password):
+ try:
+ with self.open_trac_credentials_file() as f:
+ htdigest = HTDigestParser(f)
+ except IOError, e:
+ raise Error('Error opening Trac credentials file: {0}'.format(e.strerror))
+
+ if not htdigest.entries():
+ raise Error('Error parsing Trac credentials file')
+
+ if not htdigest.authenticate(username, 'Mac OS Forge', password):
+ self.err = 'Invalid username/password'
+ return False
+
+ return True
+
+ # These three methods exist for ease of testing.
+ def open_auth_json_file(self):
+ return open(self.auth_json_filename())
+
+ def open_trac_credentials_file(self):
+ return open(self.trac_credentials_filename())
+
+ def open_webkit_committers_file(self):
+ return open(self.webkit_committers_filename())
+
+ def trac_credentials_filename(self):
+ try:
+ return self.auth_json()['trac_credentials']
+ except KeyError:
+ raise Error('auth.json file is missing "trac_credentials" key')
+
+ def webkit_committers(self):
+ config = ConfigParser.RawConfigParser()
+ with self.open_webkit_committers_file() as f:
+ config.readfp(f)
+ return config.get('groups', 'webkit').split(',')
+
+ def webkit_committers_filename(self):
+ try:
+ return self.auth_json()['webkit_committers']
+ except KeyError:
+ raise Error('auth.json file is missing "webkit_committers" key')
Added: trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth_unittest.py (0 => 101695)
--- trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth_unittest.py (rev 0)
+++ trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth_unittest.py 2011-12-01 19:58:49 UTC (rev 101695)
@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2011 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import StringIO
+import __builtin__
+import buildbot.status.web.auth
+import contextlib
+import os
+import unittest
+
+from committer_auth import CommitterAuth
+
+
+# This subclass of StringIO supports the context manager protocol so it works
+# with "with" statements, just like real files.
+class CMStringIO(StringIO.StringIO):
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exception, value, traceback):
+ self.close()
+
+
+@contextlib.contextmanager
+def open_override(func):
+ original_open = __builtin__.open
+ __builtin__.open = func
+ yield
+ __builtin__.open = original_open
+
+
+class CommitterAuthTest(unittest.TestCase):
+ def setUp(self):
+ self.auth = CommitterAuth('path/to/auth.json')
+ self.auth.open_auth_json_file = self.fake_auth_json_file
+ self.auth.open_webkit_committers_file = self.fake_committers_file
+ self.auth.open_trac_credentials_file = self.fake_htdigest_file
+
+ def fake_open_function(self, expected_filename):
+ def fake_open(name, mode='r'):
+ self.fake_open_was_called = True
+ self.assertEqual(expected_filename, name)
+ return fake_open
+
+ def test_authentication_success(self):
+ self.assertTrue(self.auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('', self.auth.errmsg())
+ self.assertTrue(self.auth.authenticate('committ...@example.com', 'committer2password'))
+ self.assertEqual('', self.auth.errmsg())
+
+ def test_committer_without_trac_credentials_fails(self):
+ self.assertFalse(self.auth.authenticate('committ...@webkit.org', 'committer3password'))
+ self.assertEqual('Invalid username/password', self.auth.errmsg())
+
+ def test_fail_to_open_auth_json_file(self):
+ def raise_IOError():
+ raise IOError(2, 'No such file or directory', 'path/to/auth.json')
+ auth = CommitterAuth('path/to/auth.json')
+ auth.open_auth_json_file = raise_IOError
+ self.assertFalse(auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('Error opening auth.json file: No such file or directory', auth.errmsg())
+
+ def test_fail_to_open_trac_credentials_file(self):
+ def raise_IOError():
+ raise IOError(2, 'No such file or directory', 'path/to/trac/credentials')
+ self.auth.open_trac_credentials_file = raise_IOError
+ self.assertFalse(self.auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('Error opening Trac credentials file: No such file or directory', self.auth.errmsg())
+
+ def test_fail_to_open_webkit_committers_file(self):
+ def raise_IOError():
+ raise IOError(2, 'No such file or directory', 'path/to/webkit/committers')
+ self.auth.open_webkit_committers_file = raise_IOError
+ self.assertFalse(self.auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('Error opening WebKit committers file: No such file or directory', self.auth.errmsg())
+
+ def test_implements_IAuth(self):
+ self.assertTrue(buildbot.status.web.auth.IAuth.implementedBy(CommitterAuth))
+
+ def test_invalid_auth_json_file(self):
+ auth = CommitterAuth('path/to/auth.json')
+ auth.open_auth_json_file = self.invalid_auth_json_file
+ self.assertFalse(auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('Error parsing auth.json file: No JSON object could be decoded', auth.errmsg())
+
+ def test_invalid_committers_file(self):
+ self.auth.open_webkit_committers_file = self.invalid_committers_file
+ self.assertFalse(self.auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('Error parsing WebKit committers file', self.auth.errmsg())
+
+ def test_invalid_trac_credentials_file(self):
+ self.auth.open_trac_credentials_file = self.invalid_htdigest_file
+ self.assertFalse(self.auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('Error parsing Trac credentials file', self.auth.errmsg())
+
+ def test_missing_auth_json_keys(self):
+ auth = CommitterAuth('path/to/auth.json')
+ auth.open_auth_json_file = lambda: CMStringIO('{ "trac_credentials": "path/to/trac/credentials" }')
+ self.assertFalse(auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('auth.json file is missing "webkit_committers" key', auth.errmsg())
+
+ auth.open_auth_json_file = lambda: CMStringIO('{ "webkit_committers": "path/to/webkit/committers" }')
+ auth.open_webkit_committers_file = self.fake_committers_file
+ self.assertFalse(auth.authenticate('commit...@webkit.org', 'committerpassword'))
+ self.assertEqual('auth.json file is missing "trac_credentials" key', auth.errmsg())
+
+ def test_open_auth_json_file(self):
+ auth = CommitterAuth('path/to/auth.json')
+ self.fake_open_was_called = False
+ with open_override(self.fake_open_function(auth.auth_json_filename())):
+ auth.open_auth_json_file()
+ self.assertTrue(self.fake_open_was_called)
+
+ def test_open_trac_credentials_file(self):
+ auth = CommitterAuth('path/to/auth.json')
+ auth.trac_credentials_filename = lambda: 'trac credentials filename'
+ self.fake_open_was_called = False
+ with open_override(self.fake_open_function(auth.trac_credentials_filename())):
+ auth.open_trac_credentials_file()
+ self.assertTrue(self.fake_open_was_called)
+
+ def test_open_webkit_committers_file(self):
+ auth = CommitterAuth('path/to/auth.json')
+ auth.webkit_committers_filename = lambda: 'webkit committers filename'
+ self.fake_open_was_called = False
+ with open_override(self.fake_open_function(auth.webkit_committers_filename())):
+ auth.open_webkit_committers_file()
+ self.assertTrue(self.fake_open_was_called)
+
+ def test_non_committer_fails(self):
+ self.assertFalse(self.auth.authenticate('noncommit...@example.com', 'noncommitterpassword'))
+ self.assertEqual('Invalid username/password', self.auth.errmsg())
+
+ def test_trac_credentials_filename(self):
+ self.assertEqual('path/to/trac/credentials', self.auth.trac_credentials_filename())
+
+ def test_unknown_user_fails(self):
+ self.assertFalse(self.auth.authenticate('nob...@example.com', 'nobodypassword'))
+ self.assertEqual('Invalid username/password', self.auth.errmsg())
+
+ def test_username_is_prefix_of_valid_user(self):
+ self.assertFalse(self.auth.authenticate('commit...@webkit.orgg', 'committerpassword'))
+ self.assertEqual('Invalid username/password', self.auth.errmsg())
+
+ def test_webkit_committers(self):
+ self.assertEqual(['commit...@webkit.org', 'committ...@example.com', 'committ...@webkit.org'], self.auth.webkit_committers())
+
+ def test_webkit_committers_filename(self):
+ self.assertEqual('path/to/webkit/committers', self.auth.webkit_committers_filename())
+
+ def test_wrong_password_fails(self):
+ self.assertFalse(self.auth.authenticate('commit...@webkit.org', 'wrongpassword'))
+ self.assertEqual('Invalid username/password', self.auth.errmsg())
+
+ def fake_auth_json_file(self):
+ return CMStringIO("""{
+ "trac_credentials": "path/to/trac/credentials",
+ "webkit_committers": "path/to/webkit/committers"
+}""")
+
+ def invalid_auth_json_file(self):
+ return CMStringIO('~!@#$%^&*()_+')
+
+ def fake_committers_file(self):
+ return CMStringIO("""[groups]
+group1 = u...@example.com,us...@example.com
+group2 = us...@example.com
+
+group3 =
+group4 =
+
+webkit = commit...@webkit.org,committ...@example.com,committ...@webkit.org
+
+[service:/]
+* = r
+""")
+
+ def invalid_committers_file(self):
+ return CMStringIO("""[groups]
+
+[[groups2]
+""")
+
+ def fake_htdigest_file(self):
+ return CMStringIO("""commit...@webkit.org:Mac OS Forge:761c8dcb7d9b5908007ed142f62fe73a
+committ...@example.com:Mac OS Forge:faeee69acc2e49af3a0dbb15bd593ef4
+noncommit...@example.com:Mac OS Forge:b99aa7ad32306a654ca4d57839fde9c1
+""")
+
+ def invalid_htdigest_file(self):
+ return CMStringIO("""commit...@webkit.org:Mac OS Forge:761c8dcb7d9b5908007ed142f62fe73a
+committ...@example.com:Mac OS Forge:faeee69acc2e49af3a0dbb15bd593ef4
+noncommit...@example.com:Mac OS Forge:b99aa7ad32306a654ca4d57839fde9c1
+committ...@example.com:Mac OS Forge:::
+""")
+
+
+if __name__ == '__main__':
+ unittest.main()
Property changes on: trunk/Tools/BuildSlaveSupport/build.webkit.org-config/committer_auth_unittest.py
___________________________________________________________________
Modified: trunk/Tools/BuildSlaveSupport/build.webkit.org-config/master.cfg (101694 => 101695)
--- trunk/Tools/BuildSlaveSupport/build.webkit.org-config/master.cfg 2011-12-01 19:58:38 UTC (rev 101694)
+++ trunk/Tools/BuildSlaveSupport/build.webkit.org-config/master.cfg 2011-12-01 19:58:49 UTC (rev 101695)
@@ -18,6 +18,7 @@
import simplejson
import urllib
+from committer_auth import CommitterAuth
from webkitpy.common.config import build as wkbuild
from webkitpy.common.net.buildbot import BuildBot as wkbuildbot
@@ -28,8 +29,9 @@
# permissions for WebStatus
authz = Authz(
- forceBuild=False,
- forceAllBuilds=False,
+ auth=CommitterAuth('auth.json'),
+ forceBuild='auth',
+ forceAllBuilds='auth',
pingBuilder=True,
gracefulShutdown=False,
stopBuild=True,
Modified: trunk/Tools/ChangeLog (101694 => 101695)
--- trunk/Tools/ChangeLog 2011-12-01 19:58:38 UTC (rev 101694)
+++ trunk/Tools/ChangeLog 2011-12-01 19:58:49 UTC (rev 101695)
@@ -1,5 +1,105 @@
2011-12-01 Adam Roben <aro...@apple.com>
+ Allow committers to use their Trac credentials to force builds on the buildbots
+
+ Fixes <http://webkit.org/b/73353>
+
+ A new class, CommitterAuth, handles authentication of WebKit committers. CommitterAuth uses
+ three files to do its job: a config file that contains a list of WebKit committer usernames,
+ an htdigest file that contains Trac credentials, and JSON file that gives the paths for
+ those two files.
+
+ Reviewed by Darin Adler.
+
+ * BuildSlaveSupport/build.webkit.org-config/committer_auth.py: Added.
+ (Error): Basic wrapper around Exception that we use for cases where we couldn't even check
+ whether credentials were valid or not.
+ (CommitterAuth.__init__): Just store the path to auth.json.
+ (CommitterAuth.auth_json): Load, parse, and return auth.json.
+ (CommitterAuth.auth_json_filename): Return the path to auth.json.
+ (CommitterAuth.authenticate): Return true if the user is a WebKit committer and their
+ credentials are valid Trac credentials. Return false otherwise or if an error occurred while
+ checking those conditions.
+ (CommitterAuth.is_webkit_committer): Return true if the user is a WebKit committer. Return
+ false otherwise or if an exception was thrown.
+ (CommitterAuth.is_webkit_trac_user): Return true if the username/password are present in the
+ Trac credentials htdigest file. Return false otherwise or if an exception was thrown.
+
+ (CommitterAuth.open_auth_json_file):
+ (CommitterAuth.open_trac_credentials_file):
+ (CommitterAuth.open_webkit_committers_file):
+ Open the specified file. These are mostly useful for testing purposes.
+
+ (CommitterAuth.trac_credentials_filename):
+ (CommitterAuth.webkit_committers_filename):
+ Return the path to the specified file by retrieving it from auth.json.
+
+ (CommitterAuth.webkit_committers): Load and parse the committers file and extract the list
+ of WebKit committers from it.
+
+ * BuildSlaveSupport/build.webkit.org-config/committer_auth_unittest.py: Added.
+ (CMStringIO.__enter__):
+ (CMStringIO.__exit__):
+ Helper class that makes it possible to use StringIO with the "with" statement.
+
+ (open_override): Helper context manager for overriding the global "open" function
+ temporarily.
+
+ (CommitterAuthTest.setUp): Set up a somewhat-mocked CommitterAuth that is used by most
+ tests.
+ (CommitterAuthTest.fake_open_function): Returns a function that can be used in place of
+ "open" to test that the expected path was opened.
+ (CommitterAuthTest.test_authentication_success): Test that committers can authenticate
+ successfully.
+ (CommitterAuthTest.test_committer_without_trac_credentials_fails): Test that committers who
+ somehow have no Trac account can't authenticate.
+
+ (CommitterAuthTest.test_fail_to_open_auth_json_file):
+ (CommitterAuthTest.test_fail_to_open_trac_credentials_file):
+ (CommitterAuthTest.test_fail_to_open_webkit_committers_file):
+ Test what happens when we can't open the three files we depend upon.
+
+ (CommitterAuthTest.test_implements_IAuth): Test that we fulfill buildbot's expectations for
+ an authentication class.
+
+ (CommitterAuthTest.test_invalid_auth_json_file):
+ (CommitterAuthTest.test_invalid_committers_file):
+ (CommitterAuthTest.test_invalid_trac_credentials_file):
+ (CommitterAuthTest.test_missing_auth_json_keys):
+ Test what happens when the three files we depend upon are invalid in some way.
+
+ (CommitterAuthTest.test_open_auth_json_file):
+ (CommitterAuthTest.test_open_trac_credentials_file):
+ (CommitterAuthTest.test_open_webkit_committers_file):
+ Test that we open the expected paths.
+
+ (CommitterAuthTest.test_trac_credentials_filename):
+ (CommitterAuthTest.test_webkit_committers_filename):
+ Test that we extract filenames out of auth.json correctly.
+
+ (CommitterAuthTest.test_non_committer_fails):
+ (CommitterAuthTest.test_unknown_user_fails):
+ (CommitterAuthTest.test_username_is_prefix_of_valid_user):
+ (CommitterAuthTest.test_wrong_password_fails):
+ Test various failed authentication attempts.
+
+ (CommitterAuthTest.test_webkit_committers): Test that we can parse the list of WebKit
+ committers out of the committers file correctly.
+
+ (CommitterAuthTest.fake_auth_json_file):
+ (CommitterAuthTest.invalid_auth_json_file):
+ (CommitterAuthTest.fake_committers_file):
+ (CommitterAuthTest.invalid_committers_file):
+ (CommitterAuthTest.fake_htdigest_file):
+ (CommitterAuthTest.invalid_htdigest_file):
+ Return various fake files for testing.
+
+ * BuildSlaveSupport/build.webkit.org-config/master.cfg: Specify an instance of CommitterAuth
+ to be used for authentication in the web interface, and specify that only authenticated
+ users may force builds.
+
+2011-12-01 Adam Roben <aro...@apple.com>
+
Add an HTDigestParser class to webkitpy
Fixes <http://webkit.org/b/73575> webkitpy doesn't provide a way to parse htdigest files