diff --git a/auth.py b/auth.py index 0da997c7a..f717be15d 100644 --- a/auth.py +++ b/auth.py @@ -50,13 +50,6 @@ OAUTH_SCOPE_GERRIT = 'https://www.googleapis.com/auth/gerritcodereview' # Deprecated. Use OAUTH_SCOPE_EMAIL instead. OAUTH_SCOPES = OAUTH_SCOPE_EMAIL -# Path to a file with cached OAuth2 credentials used by default relative to the -# home dir (see _get_token_cache_path). It should be a safe location accessible -# only to a current user: knowing content of this file is roughly equivalent to -# knowing account password. Single file can hold multiple independent tokens -# identified by token_cache_key (see Authenticator). -OAUTH_TOKENS_CACHE = '.depot_tools_oauth2_tokens' - # Authentication configuration extracted from command line options. # See doc string for 'make_auth_config' for meaning of fields. @@ -381,79 +374,38 @@ def auth_config_to_command_options(auth_config): return opts -def get_authenticator_for_host(hostname, config, scopes=OAUTH_SCOPE_EMAIL): +def get_authenticator(config, scopes=OAUTH_SCOPE_EMAIL): """Returns Authenticator instance to access given host. Args: - hostname: a naked hostname or http(s)://[/] URL. Used to derive - a cache key for token cache. config: AuthConfig instance. scopes: space separated oauth scopes. Defaults to OAUTH_SCOPE_EMAIL. Returns: Authenticator object. - - Raises: - AuthenticationError if hostname is invalid. """ - hostname = hostname.lower().rstrip('/') - # Append some scheme, otherwise urlparse puts hostname into parsed.path. - if '://' not in hostname: - hostname = 'https://' + hostname - parsed = urlparse.urlparse(hostname) - - if parsed.path or parsed.params or parsed.query or parsed.fragment: - raise AuthenticationError( - 'Expecting a hostname or root host URL, got %s instead' % hostname) - return Authenticator(parsed.netloc, config, scopes) + return Authenticator(config, scopes) class Authenticator(object): """Object that knows how to refresh access tokens when needed. Args: - token_cache_key: string key of a section of the token cache file to use - to keep the tokens. See hostname_to_token_cache_key. config: AuthConfig object that holds authentication configuration. """ - def __init__(self, token_cache_key, config, scopes): + def __init__(self, config, scopes): assert isinstance(config, AuthConfig) assert config.use_oauth2 self._access_token = None self._config = config self._lock = threading.Lock() - self._token_cache_key = token_cache_key self._external_token = None self._scopes = scopes if config.refresh_token_json: self._external_token = _read_refresh_token_json(config.refresh_token_json) logging.debug('Using auth config %r', config) - def login(self): - """Performs interactive login flow if necessary. - - Raises: - AuthenticationError on error or if interrupted. - """ - if self._external_token: - raise AuthenticationError( - 'Can\'t run login flow when using --auth-refresh-token-json.') - return self.get_access_token( - force_refresh=True, allow_user_interaction=True) - - def logout(self): - """Revokes the refresh token and deletes it from the cache. - - Returns True if had some credentials cached. - """ - with self._lock: - self._access_token = None - had_creds = bool(_get_luci_auth_credentials(self._scopes)) - subprocess2.check_call( - ['luci-auth', 'logout', '-scopes', self._scopes]) - return had_creds - def has_cached_credentials(self): """Returns True if long term credentials (refresh token) are in cache. @@ -526,16 +478,6 @@ class Authenticator(object): return self._access_token - def get_token_info(self): - """Returns a result of /oauth2/v2/tokeninfo call with token info.""" - access_token = self.get_access_token() - resp, content = httplib2.Http().request( - uri='https://www.googleapis.com/oauth2/v2/tokeninfo?%s' % ( - urllib.urlencode({'access_token': access_token.token}))) - if resp.status == 200: - return json.loads(content) - raise AuthenticationError('Failed to fetch the token info: %r' % content) - def authorize(self, http): """Monkey patches authentication logic of httplib2.Http instance. @@ -684,20 +626,6 @@ class Authenticator(object): ## Private functions. -def _get_token_cache_path(): - # On non Win just use HOME. - if sys.platform != 'win32': - return os.path.join(os.path.expanduser('~'), OAUTH_TOKENS_CACHE) - # Prefer USERPROFILE over HOME, since HOME is overridden in - # git-..._bin/cmd/git.cmd to point to depot_tools. depot-tools-auth.py script - # (and all other scripts) doesn't use this override and thus uses another - # value for HOME. git.cmd doesn't touch USERPROFILE though, and usually - # USERPROFILE == HOME on Windows. - if 'USERPROFILE' in os.environ: - return os.path.join(os.environ['USERPROFILE'], OAUTH_TOKENS_CACHE) - return os.path.join(os.path.expanduser('~'), OAUTH_TOKENS_CACHE) - - def _is_headless(): """True if machine doesn't seem to have a display.""" return sys.platform == 'linux2' and not os.environ.get('DISPLAY') @@ -750,6 +678,7 @@ def _get_luci_auth_credentials(scopes): user_agent=None, revoke_uri=None) + def _run_oauth_dance(scopes): """Perform full 3-legged OAuth2 flow with the browser. diff --git a/depot-tools-auth b/depot-tools-auth deleted file mode 100755 index 9233c92ed..000000000 --- a/depot-tools-auth +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2015 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -base_dir=$(dirname "$0") - -PYTHONDONTWRITEBYTECODE=1 exec python "$base_dir/depot-tools-auth.py" "$@" diff --git a/depot-tools-auth.bat b/depot-tools-auth.bat deleted file mode 100644 index 37280b765..000000000 --- a/depot-tools-auth.bat +++ /dev/null @@ -1,12 +0,0 @@ -@echo off -:: Copyright 2015 The Chromium Authors. All rights reserved. -:: Use of this source code is governed by a BSD-style license that can be -:: found in the LICENSE file. -setlocal - -:: Ensure that "depot_tools" is somewhere in PATH so this tool can be used -:: standalone, but allow other PATH manipulations to take priority. -set PATH=%PATH%;%~dp0 - -:: Defer control. -python "%~dp0\depot-tools-auth.py" %* diff --git a/depot-tools-auth.py b/depot-tools-auth.py deleted file mode 100755 index 17be1e428..000000000 --- a/depot-tools-auth.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python -# Copyright 2015 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Manages cached OAuth2 tokens used by other depot_tools scripts. - -Usage: - depot-tools-auth login codereview.chromium.org - depot-tools-auth info codereview.chromium.org - depot-tools-auth logout codereview.chromium.org -""" - -from __future__ import print_function - -import logging -import optparse -import sys -import os - -import auth -import setup_color -import subcommand - -__version__ = '1.0' - - -@subcommand.usage('') -def CMDlogin(parser, args): - """Performs interactive login and caches authentication token.""" - # Forcefully relogin, revoking previous token. - hostname, authenticator = parser.parse_args(args) - authenticator.logout() - authenticator.login() - print_token_info(hostname, authenticator) - return 0 - - -@subcommand.usage('') -def CMDlogout(parser, args): - """Revokes cached authentication token and removes it from disk.""" - _, authenticator = parser.parse_args(args) - done = authenticator.logout() - print('Done.' if done else 'Already logged out.') - return 0 - - -@subcommand.usage('') -def CMDinfo(parser, args): - """Shows email associated with a cached authentication token.""" - # If no token is cached, AuthenticationError will be caught in 'main'. - hostname, authenticator = parser.parse_args(args) - print_token_info(hostname, authenticator) - return 0 - - -def print_token_info(hostname, authenticator): - token_info = authenticator.get_token_info() - print('Logged in to %s as %s.' % (hostname, token_info['email'])) - print('') - print('To login with a different email run:') - print(' depot-tools-auth login %s' % hostname) - print('To logout and purge the authentication token run:') - print(' depot-tools-auth logout %s' % hostname) - - -class OptionParser(optparse.OptionParser): - def __init__(self, *args, **kwargs): - optparse.OptionParser.__init__( - self, *args, prog='depot-tools-auth', version=__version__, **kwargs) - self.add_option( - '-v', '--verbose', action='count', default=0, - help='Use 2 times for more debugging info') - auth.add_auth_options(self, auth.make_auth_config(use_oauth2=True)) - - def parse_args(self, args=None, values=None): - """Parses options and returns (hostname, auth.Authenticator object).""" - options, args = optparse.OptionParser.parse_args(self, args, values) - levels = [logging.WARNING, logging.INFO, logging.DEBUG] - logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)]) - auth_config = auth.extract_auth_config_from_options(options) - if len(args) != 1: - self.error('Expecting single argument (hostname).') - if not auth_config.use_oauth2: - self.error('This command is only usable with OAuth2 authentication') - return args[0], auth.get_authenticator_for_host(args[0], auth_config) - - -def main(argv): - dispatcher = subcommand.CommandDispatcher(__name__) - try: - return dispatcher.execute(OptionParser(), argv) - except auth.AuthenticationError as e: - print(e, file=sys.stderr) - return 1 - - -if __name__ == '__main__': - setup_color.init() - try: - sys.exit(main(sys.argv[1:])) - except KeyboardInterrupt: - sys.stderr.write('interrupted\n') - sys.exit(1) diff --git a/git_cl.py b/git_cl.py index 0b1d91507..ff6d5b324 100755 --- a/git_cl.py +++ b/git_cl.py @@ -465,11 +465,7 @@ def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset): if not requests: return - codereview_url = changelist.GetCodereviewServer() - codereview_host = urlparse.urlparse(codereview_url).hostname - - authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) - http = authenticator.authorize(httplib2.Http()) + http = auth.get_authenticator(auth_config).authorize(httplib2.Http()) http.force_exception_to_status_code = True batch_request = {'requests': requests} @@ -544,10 +540,7 @@ def fetch_try_jobs(auth_config, changelist, buildbucket_host, 'fields': ','.join('builds.*.' + field for field in fields), } - codereview_url = changelist.GetCodereviewServer() - codereview_host = urlparse.urlparse(codereview_url).hostname - - authenticator = auth.get_authenticator_for_host(codereview_host, auth_config) + authenticator = auth.get_authenticator(auth_config) if authenticator.has_cached_credentials(): http = authenticator.authorize(httplib2.Http()) else: diff --git a/my_activity.py b/my_activity.py index 5e76cc15f..81b901d0a 100755 --- a/my_activity.py +++ b/my_activity.py @@ -293,8 +293,7 @@ class MyActivity(object): def monorail_get_auth_http(self): auth_config = auth.extract_auth_config_from_options(self.options) - authenticator = auth.get_authenticator_for_host( - 'bugs.chromium.org', auth_config) + authenticator = auth.get_authenticator(auth_config) # Manually use a long timeout (10m); for some users who have a # long history on the issue tracker, whatever the default timeout # is is reached. diff --git a/presubmit_canned_checks.py b/presubmit_canned_checks.py index 4cd3a8450..2b559af7e 100644 --- a/presubmit_canned_checks.py +++ b/presubmit_canned_checks.py @@ -1417,8 +1417,7 @@ def CheckChangedLUCIConfigs(input_api, output_api): # authentication try: - authenticator = auth.get_authenticator_for_host( - LUCI_CONFIG_HOST_NAME, auth.make_auth_config()) + authenticator = auth.get_authenticator(auth.make_auth_config()) acc_tkn = authenticator.get_access_token() except auth.AuthenticationError as e: return [output_api.PresubmitError( diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py index 1a2612f77..917024a14 100755 --- a/tests/git_cl_test.py +++ b/tests/git_cl_test.py @@ -636,7 +636,7 @@ class TestGitCl(TestCase): self._mocked_call('write_json', path, contents)) self.mock(git_cl.presubmit_support, 'DoPresubmitChecks', PresubmitMock) self.mock(git_cl.watchlists, 'Watchlists', WatchlistsMock) - self.mock(git_cl.auth, 'get_authenticator_for_host', AuthenticatorMock) + self.mock(git_cl.auth, 'get_authenticator', AuthenticatorMock) self.mock(git_cl.gerrit_util, 'GetChangeDetail', lambda *args, **kwargs: self._mocked_call( 'GetChangeDetail', *args, **kwargs)) @@ -3021,7 +3021,8 @@ class CMDTestCaseBase(unittest.TestCase): return_value='https://chromium-review.googlesource.com').start() mock.patch('git_cl.Changelist.GetMostRecentPatchset', return_value=7).start() - mock.patch('git_cl.auth.get_authenticator_for_host', AuthenticatorMock()) + mock.patch('git_cl.auth.get_authenticator', + return_value=AuthenticatorMock()).start() mock.patch('git_cl.Changelist._GetChangeDetail', return_value=self._CHANGE_DETAIL).start() mock.patch('git_cl._call_buildbucket', diff --git a/tests/presubmit_unittest.py b/tests/presubmit_unittest.py index 8c476dbac..af7acd3cf 100755 --- a/tests/presubmit_unittest.py +++ b/tests/presubmit_unittest.py @@ -1642,8 +1642,8 @@ class CannedChecksUnittest(PresubmitTestsBase): presubmit.OutputApi.PresubmitPromptWarning) @mock.patch('git_cl.Changelist') - @mock.patch('auth.get_authenticator_for_host') - def testCannedCheckChangedLUCIConfigs(self, mockGAFH, mockChangelist): + @mock.patch('auth.get_authenticator') + def testCannedCheckChangedLUCIConfigs(self, mockGetAuth, mockChangelist): affected_file1 = mock.MagicMock(presubmit.GitAffectedFile) affected_file1.LocalPath.return_value = 'foo.cfg' affected_file1.NewContents.return_value = ['test', 'foo'] @@ -1651,7 +1651,7 @@ class CannedChecksUnittest(PresubmitTestsBase): affected_file2.LocalPath.return_value = 'bar.cfg' affected_file2.NewContents.return_value = ['test', 'bar'] - mockGAFH().get_access_token().token = 123 + mockGetAuth().get_access_token().token = 123 host = 'https://host.com' branch = 'branch'