auth: Use luci-auth to get credentials.

Bug: 1001756
Change-Id: Ieab5391662e92ec9e2715a81fce2cef41717c2e3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1790607
Commit-Queue: Edward Lesmes <ehmaldonado@chromium.org>
Reviewed-by: Andrii Shyshkalov <tandrii@google.com>
changes/07/1790607/8
Edward Lemur 6 years ago committed by Commit Bot
parent 3841eaa164
commit ba5bc99b6a

@ -6,26 +6,22 @@
from __future__ import print_function from __future__ import print_function
import BaseHTTPServer
import collections import collections
import datetime import datetime
import functools import functools
import hashlib
import json import json
import logging import logging
import optparse import optparse
import os import os
import socket
import sys import sys
import threading import threading
import time
import urllib import urllib
import urlparse import urlparse
import webbrowser
import subprocess2
from third_party import httplib2 from third_party import httplib2
from third_party.oauth2client import client from third_party.oauth2client import client
from third_party.oauth2client import multistore_file
# depot_tools/. # depot_tools/.
@ -105,11 +101,10 @@ class AuthenticationError(Exception):
class LoginRequiredError(AuthenticationError): class LoginRequiredError(AuthenticationError):
"""Interaction with the user is required to authenticate.""" """Interaction with the user is required to authenticate."""
def __init__(self, token_cache_key): def __init__(self, scopes=OAUTH_SCOPE_EMAIL):
# HACK(vadimsh): It is assumed here that the token cache key is a hostname.
msg = ( msg = (
'You are not logged in. Please login first by running:\n' 'You are not logged in. Please login first by running:\n'
' depot-tools-auth login %s' % token_cache_key) ' luci-auth login -scopes %s' % scopes)
super(LoginRequiredError, self).__init__(msg) super(LoginRequiredError, self).__init__(msg)
@ -454,15 +449,9 @@ class Authenticator(object):
""" """
with self._lock: with self._lock:
self._access_token = None self._access_token = None
storage = self._get_storage() had_creds = bool(_get_luci_auth_credentials(self._scopes))
credentials = storage.get() subprocess2.check_call(
had_creds = bool(credentials) ['luci-auth', 'logout', '-scopes', self._scopes])
if credentials and credentials.refresh_token and credentials.revoke_uri:
try:
credentials.revoke(httplib2.Http())
except client.TokenRevokeError as e:
logging.warning('Failed to revoke refresh token: %s', e)
storage.delete()
return had_creds return had_creds
def has_cached_credentials(self): def has_cached_credentials(self):
@ -587,23 +576,9 @@ class Authenticator(object):
## Private methods. ## Private methods.
def _get_storage(self):
"""Returns oauth2client.Storage with cached tokens."""
# Do not mix cache keys for different externally provided tokens.
if self._external_token:
token_hash = hashlib.sha1(self._external_token.refresh_token).hexdigest()
cache_key = '%s:refresh_tok:%s' % (self._token_cache_key, token_hash)
else:
cache_key = self._token_cache_key
path = _get_token_cache_path()
logging.debug('Using token storage %r (cache key %r)', path, cache_key)
return multistore_file.get_credential_storage_custom_string_key(
path, cache_key)
def _get_cached_credentials(self): def _get_cached_credentials(self):
"""Returns oauth2client.Credentials loaded from storage.""" """Returns oauth2client.Credentials loaded from luci-auth."""
storage = self._get_storage() credentials = _get_luci_auth_credentials(self._scopes)
credentials = storage.get()
if not credentials: if not credentials:
logging.debug('No cached token') logging.debug('No cached token')
@ -636,8 +611,6 @@ class Authenticator(object):
token_uri='https://accounts.google.com/o/oauth2/token', token_uri='https://accounts.google.com/o/oauth2/token',
user_agent=None, user_agent=None,
revoke_uri=None) revoke_uri=None)
credentials.set_store(storage)
storage.put(credentials)
return credentials return credentials
# Not using external refresh token -> return whatever is cached. # Not using external refresh token -> return whatever is cached.
@ -697,17 +670,14 @@ class Authenticator(object):
'Token provided via --auth-refresh-token-json is no longer valid.') 'Token provided via --auth-refresh-token-json is no longer valid.')
if not allow_user_interaction: if not allow_user_interaction:
logging.debug('Requesting user to login') logging.debug('Requesting user to login')
raise LoginRequiredError(self._token_cache_key) raise LoginRequiredError(self._scopes)
logging.debug('Launching OAuth browser flow') logging.debug('Launching OAuth browser flow')
credentials = _run_oauth_dance(self._config, self._scopes) credentials = _run_oauth_dance(self._scopes)
_log_credentials_info('new token', credentials) _log_credentials_info('new token', credentials)
logging.info( logging.info(
'OAuth access_token refreshed. Expires in %s.', 'OAuth access_token refreshed. Expires in %s.',
credentials.token_expiry - datetime.datetime.utcnow()) credentials.token_expiry - datetime.datetime.utcnow())
storage = self._get_storage()
credentials.set_store(storage)
storage.put(credentials)
return AccessToken(str(credentials.access_token), credentials.token_expiry) return AccessToken(str(credentials.access_token), credentials.token_expiry)
@ -762,116 +732,29 @@ def _log_credentials_info(title, credentials):
}) })
def _run_oauth_dance(config, scopes): 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=OAUTH_CLIENT_ID,
client_secret=OAUTH_CLIENT_SECRET,
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. """Perform full 3-legged OAuth2 flow with the browser.
Returns: Returns:
oauth2client.Credentials. oauth2client.Credentials.
Raises:
AuthenticationError on errors.
"""
flow = client.OAuth2WebServerFlow(
OAUTH_CLIENT_ID,
OAUTH_CLIENT_SECRET,
scopes,
approval_prompt='force')
use_local_webserver = config.use_local_webserver
port = config.webserver_port
if config.use_local_webserver:
success = False
try:
httpd = _ClientRedirectServer(('localhost', port), _ClientRedirectHandler)
except socket.error:
pass
else:
success = True
use_local_webserver = success
if not success:
print(
'Failed to start a local webserver listening on port %d.\n'
'Please check your firewall settings and locally running programs that '
'may be blocking or using those ports.\n\n'
'Falling back to --auth-no-local-webserver and continuing with '
'authentication.\n' % port)
if use_local_webserver:
oauth_callback = 'http://localhost:%s/' % port
else:
oauth_callback = client.OOB_CALLBACK_URN
flow.redirect_uri = oauth_callback
authorize_url = flow.step1_get_authorize_url()
if use_local_webserver:
webbrowser.open(authorize_url, new=1, autoraise=True)
print(
'Your browser has been opened to visit:\n\n'
' %s\n\n'
'If your browser is on a different machine then exit and re-run this '
'application with the command-line parameter\n\n'
' --auth-no-local-webserver\n' % authorize_url)
else:
print(
'Go to the following link in your browser:\n\n'
' %s\n' % authorize_url)
try:
code = None
if use_local_webserver:
httpd.handle_request()
if 'error' in httpd.query_params:
raise AuthenticationError(
'Authentication request was rejected: %s' %
httpd.query_params['error'])
if 'code' not in httpd.query_params:
raise AuthenticationError(
'Failed to find "code" in the query parameters of the redirect.\n'
'Try running with --auth-no-local-webserver.')
code = httpd.query_params['code']
else:
code = raw_input('Enter verification code: ').strip()
except KeyboardInterrupt:
raise AuthenticationError('Authentication was canceled.')
try:
return flow.step2_exchange(code)
except client.FlowExchangeError as e:
raise AuthenticationError('Authentication has failed: %s' % e)
class _ClientRedirectServer(BaseHTTPServer.HTTPServer):
"""A server to handle OAuth 2.0 redirects back to localhost.
Waits for a single request and parses the query parameters
into query_params and then stops serving.
"""
query_params = {}
class _ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""A handler for OAuth 2.0 redirects back to localhost.
Waits for a single request and parses the query parameters
into the servers query_params and then stops serving.
""" """
subprocess2.check_call(['luci-auth', 'login', '-scopes', scopes])
def do_GET(self): return _get_luci_auth_credentials(scopes)
"""Handle a GET request.
Parses the query parameters and prints a message
if the flow has completed. Note that we can't detect
if an error occurred.
"""
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
query = self.path.split('?', 1)[-1]
query = dict(urlparse.parse_qsl(query))
self.server.query_params = query
self.wfile.write('<html><head><title>Authentication Status</title></head>')
self.wfile.write('<body><p>The authentication flow has completed.</p>')
self.wfile.write('</body></html>')
def log_message(self, _format, *args):
"""Do not log messages to stdout while running as command line program."""

@ -538,7 +538,7 @@ def fetch_try_jobs(auth_config, changelist, buildbucket_host,
else: else:
print('Warning: Some results might be missing because %s' % print('Warning: Some results might be missing because %s' %
# Get the message on how to login. # Get the message on how to login.
(auth.LoginRequiredError(codereview_host).message,)) (auth.LoginRequiredError().message,))
http = httplib2.Http() http = httplib2.Http()
http.force_exception_to_status_code = True http.force_exception_to_status_code = True

Loading…
Cancel
Save