Reland "depot_tools: Stop using oauth2client"
This is a reland of 55e5853e5ce49fda2589948c5a0fa6a56d4b3015
Original change's description:
> 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: Vadim Shtayura <vadimsh@chromium.org>
Bug: 1001756
Recipe-Nontrivial-Roll: chromiumos
Recipe-Nontrivial-Roll: skia
Change-Id: If2f584ce0b327324cfb67ce5f29d80986260bd61
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1867109
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
Reviewed-by: Vadim Shtayura <vadimsh@chromium.org>
diff --git a/auth.py b/auth.py
index e0afec9..7f15fe9 100644
--- a/auth.py
+++ b/auth.py
@@ -21,7 +21,6 @@
import subprocess2
from third_party import httplib2
-from third_party.oauth2client import client
# depot_tools/.
@@ -35,6 +34,11 @@
OAUTH_SCOPES = OAUTH_SCOPE_EMAIL
+# Mockable datetime.datetime.utcnow for testing.
+def datetime_now():
+ return datetime.datetime.utcnow()
+
+
# Authentication configuration extracted from command line options.
# See doc string for 'make_auth_config' for meaning of fields.
AuthConfig = collections.namedtuple('AuthConfig', [
@@ -54,9 +58,9 @@
def needs_refresh(self, now=None):
"""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)
+ now = now or datetime_now()
+ # 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 +104,8 @@
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 +297,18 @@
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,82 +378,49 @@
logging.debug('Using auth config %r', config)
def has_cached_credentials(self):
- """Returns True if long term credentials (refresh token) are in cache.
+ """Returns True if credentials can be obtained.
- Doesn't make network calls.
+ If returns False, get_access_token() later will probably ask for interactive
+ login by raising LoginRequiredError, unless local auth is configured.
- If returns False, get_access_token() later will ask for interactive login by
- raising LoginRequiredError.
-
- 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.
+ If returns True, get_access_token() won't ask for interactive login.
"""
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.
- allow_user_interaction: True to enable blocking for user input if needed.
- use_local_auth: default to local auth if needed.
+ TODO(crbug.com/1001756): Remove.
+ force_refresh: Ignored, luci-auth doesn't support force-refreshing tokens.
+ allow_user_interaction: Ignored. allow_user_interaction is always False.
+ use_local_auth: Ignored. luci-auth already covers local_auth.
Raises:
AuthenticationError on error or if authentication flow was interrupted.
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
- 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')
- try:
- self._access_token = self._create_access_token(allow_user_interaction)
- return self._access_token
- except LoginRequiredError:
- return get_loc_auth_tkn()
+ if self._access_token and not self._access_token.needs_refresh():
+ return self._access_token
- # Load from on-disk cache on a first access.
- if not self._access_token:
- self._access_token = self._load_access_token()
+ # 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
- # 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
+ # Nope, still expired, need to run the refresh flow.
+ 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 +430,6 @@
A modified instance of http that was passed in.
"""
# Adapted from oauth2client.OAuth2Credentials.authorize.
-
request_orig = http.request
@functools.wraps(request_orig)
@@ -467,92 +439,37 @@
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()
+ logging.debug('Running luci-auth login')
+ subprocess2.check_call(['luci-auth', 'login', '-scopes', self._scopes])
+ return self._get_luci_auth_token()
- # 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)
+ 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 +478,3 @@
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)