Commit 55e5853e authored by Edward Lemur's avatar Edward Lemur Committed by Commit Bot

depot_tools: Stop using oauth2client

Bug: 1001756
Change-Id: I8a0ca2b0f44b20564a9d3192543a7a69788d8d87
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1854898
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
Reviewed-by: 's avatarVadim Shtayura <vadimsh@chromium.org>
parent 0cb3e4bf
......@@ -21,7 +21,6 @@ import urlparse
import subprocess2
from third_party import httplib2
from third_party.oauth2client import client
# depot_tools/.
......@@ -55,8 +54,8 @@ class AccessToken(collections.namedtuple('AccessToken', [
"""True if this AccessToken should be refreshed."""
if self.expires_at is not None:
now = now or datetime.datetime.utcnow()
# Allow 3 min of clock skew between client and backend.
now += datetime.timedelta(seconds=180)
# Allow 30s of clock skew between client and backend.
now += datetime.timedelta(seconds=30)
return now >= self.expires_at
# Token without expiration time never expires.
return False
......@@ -100,6 +99,8 @@ def has_luci_context_local_auth():
return bool(params.default_account_id)
# TODO(crbug.com/1001756): Remove. luci-auth uses local auth if available,
# making this unnecessary.
def get_luci_context_access_token(scopes=OAUTH_SCOPE_EMAIL):
"""Returns a valid AccessToken from the local LUCI context auth server.
......@@ -291,18 +292,18 @@ def add_auth_options(parser, default_config=None):
help='Do not save authentication cookies to local disk.')
# OAuth2 related options.
# TODO(crbug.com/1001756): Remove. No longer supported.
parser.auth_group.add_option(
'--auth-no-local-webserver',
action='store_false',
dest='use_local_webserver',
default=default_config.use_local_webserver,
help='Do not run a local web server when performing OAuth2 login flow.')
help='DEPRECATED. Do not use')
parser.auth_group.add_option(
'--auth-host-port',
type=int,
default=default_config.webserver_port,
help='Port a local web server should listen on. Used only if '
'--auth-no-local-webserver is not set. [default: %default]')
help='DEPRECATED. Do not use')
parser.auth_group.add_option(
'--auth-refresh-token-json',
help='DEPRECATED. Do not use')
......@@ -372,27 +373,25 @@ class Authenticator(object):
logging.debug('Using auth config %r', config)
def has_cached_credentials(self):
"""Returns True if long term credentials (refresh token) are in cache.
Doesn't make network calls.
"""Returns True if credentials can be obtained.
If returns False, get_access_token() later will ask for interactive login by
raising LoginRequiredError.
If returns False, get_access_token() later will probably ask for interactive
login by raising LoginRequiredError, unless local auth in configured.
If returns True, most probably get_access_token() won't ask for interactive
login, though it is not guaranteed, since cached token can be already
revoked and there's no way to figure this out without actually trying to use
it.
login, unless an external token is provided that has been revoked.
"""
with self._lock:
return bool(self._get_cached_credentials())
return bool(self._get_luci_auth_token())
def get_access_token(self, force_refresh=False, allow_user_interaction=False,
use_local_auth=True):
"""Returns AccessToken, refreshing it if necessary.
Args:
force_refresh: forcefully refresh access token even if it is not expired.
TODO(crbug.com/1001756): Remove. luci-auth doesn't support
force-refreshing tokens.
force_refresh: Ignored,
allow_user_interaction: True to enable blocking for user input if needed.
use_local_auth: default to local auth if needed.
......@@ -401,53 +400,41 @@ class Authenticator(object):
LoginRequiredError if user interaction is required, but
allow_user_interaction is False.
"""
def get_loc_auth_tkn():
exi = sys.exc_info()
if not use_local_auth:
logging.error('Failed to create access token')
raise
try:
self._access_token = get_luci_context_access_token()
if not self._access_token:
logging.error('Failed to create access token')
raise
with self._lock:
if self._access_token and not self._access_token.needs_refresh():
return self._access_token
except LuciContextAuthError:
logging.exception('Failed to use local auth')
raise exi[0], exi[1], exi[2]
with self._lock:
if force_refresh:
logging.debug('Forcing access token refresh')
# Token expired or missing. Maybe some other process already updated it,
# reload from the cache.
self._access_token = self._get_luci_auth_token()
if self._access_token and not self._access_token.needs_refresh():
return self._access_token
# Nope, still expired, need to run the refresh flow.
if not self._external_token and allow_user_interaction:
logging.debug('Launching luci-auth login')
self._access_token = self._run_oauth_dance()
if self._access_token and not self._access_token.needs_refresh():
return self._access_token
# TODO(crbug.com/1001756): Remove. luci-auth uses local auth if it exists.
# Refresh flow failed. Try local auth.
if use_local_auth:
try:
self._access_token = self._create_access_token(allow_user_interaction)
return self._access_token
except LoginRequiredError:
return get_loc_auth_tkn()
# Load from on-disk cache on a first access.
if not self._access_token:
self._access_token = self._load_access_token()
# Refresh if expired or missing.
if not self._access_token or self._access_token.needs_refresh():
# Maybe some other process already updated it, reload from the cache.
self._access_token = self._load_access_token()
# Nope, still expired, need to run the refresh flow.
if not self._access_token or self._access_token.needs_refresh():
try:
self._access_token = self._create_access_token(
allow_user_interaction)
except LoginRequiredError:
get_loc_auth_tkn()
return self._access_token
self._access_token = get_luci_context_access_token()
except LuciContextAuthError:
logging.exception('Failed to use local auth')
if self._access_token and not self._access_token.needs_refresh():
return self._access_token
# Give up.
logging.error('Failed to create access token')
raise LoginRequiredError(self._scopes)
def authorize(self, http):
"""Monkey patches authentication logic of httplib2.Http instance.
The modified http.request method will add authentication headers to each
request and will refresh access_tokens when a 401 is received on a
request.
Args:
......@@ -457,7 +444,6 @@ class Authenticator(object):
A modified instance of http that was passed in.
"""
# Adapted from oauth2client.OAuth2Credentials.authorize.
request_orig = http.request
@functools.wraps(request_orig)
......@@ -467,92 +453,37 @@ class Authenticator(object):
connection_type=None):
headers = (headers or {}).copy()
headers['Authorization'] = 'Bearer %s' % self.get_access_token().token
resp, content = request_orig(
return request_orig(
uri, method, body, headers, redirections, connection_type)
if resp.status in client.REFRESH_STATUS_CODES:
logging.info('Refreshing due to a %s', resp.status)
access_token = self.get_access_token(force_refresh=True)
headers['Authorization'] = 'Bearer %s' % access_token.token
return request_orig(
uri, method, body, headers, redirections, connection_type)
else:
return (resp, content)
http.request = new_request
return http
## Private methods.
def _get_cached_credentials(self):
"""Returns oauth2client.Credentials loaded from luci-auth."""
credentials = _get_luci_auth_credentials(self._scopes)
if not credentials:
logging.debug('No cached token')
else:
_log_credentials_info('cached token', credentials)
return credentials if (credentials and not credentials.invalid) else None
def _load_access_token(self):
"""Returns cached AccessToken if it is not expired yet."""
logging.debug('Reloading access token from cache')
creds = self._get_cached_credentials()
if not creds or not creds.access_token or creds.access_token_expired:
logging.debug('Access token is missing or expired')
return None
return AccessToken(str(creds.access_token), creds.token_expiry)
def _create_access_token(self, allow_user_interaction=False):
"""Mints and caches a new access token, launching OAuth2 dance if necessary.
Uses cached refresh token, if present. In that case user interaction is not
required and function will finish quietly. Otherwise it will launch 3-legged
OAuth2 flow, that needs user interaction.
Args:
allow_user_interaction: if True, allow interaction with the user (e.g.
reading standard input, or launching a browser).
def _run_luci_auth_login(self):
"""Run luci-auth login.
Returns:
AccessToken.
Raises:
AuthenticationError on error or if authentication flow was interrupted.
LoginRequiredError if user interaction is required, but
allow_user_interaction is False.
AccessToken with credentials.
"""
logging.debug(
'Making new access token (allow_user_interaction=%r)',
allow_user_interaction)
credentials = self._get_cached_credentials()
# 3-legged flow with (perhaps cached) refresh token.
refreshed = False
if credentials and not credentials.invalid:
try:
logging.debug('Attempting to refresh access_token')
credentials.refresh(httplib2.Http())
_log_credentials_info('refreshed token', credentials)
refreshed = True
except client.Error as err:
logging.warning(
'OAuth error during access token refresh (%s). '
'Attempting a full authentication flow.', err)
# Refresh token is missing or invalid, go through the full flow.
if not refreshed:
if not allow_user_interaction:
logging.debug('Requesting user to login')
raise LoginRequiredError(self._scopes)
logging.debug('Launching OAuth browser flow')
credentials = _run_oauth_dance(self._scopes)
_log_credentials_info('new token', credentials)
logging.info(
'OAuth access_token refreshed. Expires in %s.',
credentials.token_expiry - datetime.datetime.utcnow())
return AccessToken(str(credentials.access_token), credentials.token_expiry)
logging.debug('Running luci-auth login')
subprocess2.check_call(['luci-auth', 'login', '-scopes', self._scopes])
return self._get_luci_auth_token()
def _get_luci_auth_token(self):
logging.debug('Running luci-auth token')
try:
out, err = subprocess2.check_call_out(
['luci-auth', 'token', '-scopes', self._scopes, '-json-output', '-'],
stdout=subprocess2.PIPE, stderr=subprocess2.PIPE)
logging.debug('luci-auth token stderr:\n%s', err)
token_info = json.loads(out)
return AccessToken(
token_info['token'],
datetime.datetime.utcfromtimestamp(token_info['expiry']))
except subprocess2.CalledProcessError:
return None
## Private functions.
......@@ -561,44 +492,3 @@ class Authenticator(object):
def _is_headless():
"""True if machine doesn't seem to have a display."""
return sys.platform == 'linux2' and not os.environ.get('DISPLAY')
def _log_credentials_info(title, credentials):
"""Dumps (non sensitive) part of client.Credentials object to debug log."""
if credentials:
logging.debug('%s info: %r', title, {
'access_token_expired': credentials.access_token_expired,
'has_access_token': bool(credentials.access_token),
'invalid': credentials.invalid,
'utcnow': datetime.datetime.utcnow(),
'token_expiry': credentials.token_expiry,
})
def _get_luci_auth_credentials(scopes):
try:
token_info = json.loads(subprocess2.check_output(
['luci-auth', 'token', '-scopes', scopes, '-json-output', '-'],
stderr=subprocess2.VOID))
except subprocess2.CalledProcessError:
return None
return client.OAuth2Credentials(
access_token=token_info['token'],
client_id=None,
client_secret=None,
refresh_token=None,
token_expiry=datetime.datetime.utcfromtimestamp(token_info['expiry']),
token_uri=None,
user_agent=None,
revoke_uri=None)
def _run_oauth_dance(scopes):
"""Perform full 3-legged OAuth2 flow with the browser.
Returns:
oauth2client.Credentials.
"""
subprocess2.check_call(['luci-auth', 'login', '-scopes', scopes])
return _get_luci_auth_credentials(scopes)
......@@ -5,7 +5,7 @@
"""Unit Tests for auth.py"""
import __builtin__
import contextlib
import datetime
import json
import logging
......@@ -16,37 +16,35 @@ import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from testing_support import auto_stub
from third_party import httplib2
from third_party import mock
from third_party import httplib2
import auth
class TestLuciContext(auto_stub.TestCase):
def setUp(self):
auth._get_luci_context_local_auth_params.clear_cache()
def _mock_local_auth(self, account_id, secret, rpc_port):
self.mock(os, 'environ', {'LUCI_CONTEXT': 'default/test/path'})
self.mock(auth, '_load_luci_context', mock.Mock())
auth._load_luci_context.return_value = {
def _mockLocalAuth(account_id, secret, rpc_port):
mock_luci_context = {
'local_auth': {
'default_account_id': account_id,
'secret': secret,
'rpc_port': rpc_port,
'default_account_id': account_id,
'secret': secret,
'rpc_port': rpc_port,
}
}
}
mock.patch('auth._load_luci_context', return_value=mock_luci_context).start()
mock.patch('os.environ', {'LUCI_CONTEXT': 'default/test/path'}).start()
def _mockResponse(status, content):
mock_response = (mock.Mock(status=status), content)
mock.patch('auth.httplib2.Http.request', return_value=mock_response).start()
def _mock_loc_server_resp(self, status, content):
mock_resp = mock.Mock()
mock_resp.status = status
self.mock(httplib2.Http, 'request', mock.Mock())
httplib2.Http.request.return_value = (mock_resp, content)
class TestLuciContext(unittest.TestCase):
def setUp(self):
auth._get_luci_context_local_auth_params.clear_cache()
def test_all_good(self):
self._mock_local_auth('account', 'secret', 8080)
_mockLocalAuth('account', 'secret', 8080)
self.assertTrue(auth.has_luci_context_local_auth())
expiry_time = datetime.datetime.min + datetime.timedelta(hours=1)
......@@ -57,18 +55,18 @@ class TestLuciContext(auto_stub.TestCase):
'expiry': (expiry_time
- datetime.datetime.utcfromtimestamp(0)).total_seconds(),
}
self._mock_loc_server_resp(200, json.dumps(resp_content))
_mockResponse(200, json.dumps(resp_content))
params = auth._get_luci_context_local_auth_params()
token = auth._get_luci_context_access_token(params, datetime.datetime.min)
self.assertEqual(token.token, 'token')
def test_no_account_id(self):
self._mock_local_auth(None, 'secret', 8080)
_mockLocalAuth(None, 'secret', 8080)
self.assertFalse(auth.has_luci_context_local_auth())
self.assertIsNone(auth.get_luci_context_access_token())
def test_incorrect_port_format(self):
self._mock_local_auth('account', 'secret', 'port')
_mockLocalAuth('account', 'secret', 'port')
self.assertFalse(auth.has_luci_context_local_auth())
with self.assertRaises(auth.LuciContextAuthError):
auth.get_luci_context_access_token()
......@@ -81,7 +79,7 @@ class TestLuciContext(auto_stub.TestCase):
'access_token': 'token',
'expiry': 1,
}
self._mock_loc_server_resp(200, json.dumps(resp_content))
_mockResponse(200, json.dumps(resp_content))
with self.assertRaises(auth.LuciContextAuthError):
auth._get_luci_context_access_token(
params, datetime.datetime.utcfromtimestamp(1))
......@@ -94,13 +92,13 @@ class TestLuciContext(auto_stub.TestCase):
'access_token': 'token',
'expiry': 'dead',
}
self._mock_loc_server_resp(200, json.dumps(resp_content))
_mockResponse(200, json.dumps(resp_content))
with self.assertRaises(auth.LuciContextAuthError):
auth._get_luci_context_access_token(params, datetime.datetime.min)
def test_incorrect_response_content_format(self):
params = auth._LuciContextLocalAuthParams('account', 'secret', 8080)
self._mock_loc_server_resp(200, '5')
_mockResponse(200, '5')
with self.assertRaises(auth.LuciContextAuthError):
auth._get_luci_context_access_token(params, datetime.datetime.min)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment