Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(16)

Unified Diff: third_party/upload.py

Issue 14878007: Update update.py from rietveld/chromium@r1052. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/upload.py
diff --git a/third_party/upload.py b/third_party/upload.py
index c7899c70a93dbe87f48f20286434e1910e2d773a..895c8f59442e260f9944c5ed8e77f37347e385e8 100755
--- a/third_party/upload.py
+++ b/third_party/upload.py
@@ -34,6 +34,7 @@ against by using the '--rev' option.
# This code is derived from appcfg.py in the App Engine SDK (open source),
# and from ASPN recipe #146306.
+import BaseHTTPServer
import ConfigParser
import cookielib
import errno
@@ -51,6 +52,7 @@ import sys
import urllib
import urllib2
import urlparse
+import webbrowser
# The md5 module was deprecated in Python 2.5.
try:
@@ -106,6 +108,41 @@ VCS_ABBREVIATIONS = {
VCS_CVS.lower(): VCS_CVS,
}
+# OAuth 2.0-Related Constants
+LOCALHOST_IP = '127.0.0.1'
+DEFAULT_OAUTH2_PORT = 8001
+ACCESS_TOKEN_PARAM = 'access_token'
+OAUTH_PATH = '/get-access-token'
+OAUTH_PATH_PORT_TEMPLATE = OAUTH_PATH + '?port=%(port)d'
+AUTH_HANDLER_RESPONSE = """\
+<html>
+ <head>
+ <title>Authentication Status</title>
+ </head>
+ <body>
+ <p>The authentication flow has completed.</p>
+ </body>
+</html>
+"""
+# Borrowed from google-api-python-client
+OPEN_LOCAL_MESSAGE_TEMPLATE = """\
+Your browser has been opened to visit:
+
+ %s
+
+If your browser is on a different machine then exit and re-run
+upload.py with the command-line parameter
+
+ --no_oauth2_webbrowser
+"""
+NO_OPEN_LOCAL_MESSAGE_TEMPLATE = """\
+Go to the following link in your browser:
+
+ %s
+
+and copy the access token.
+"""
+
# The result of parsing Subversion's [auto-props] setting.
svn_auto_props_map = None
@@ -179,8 +216,9 @@ class ClientLoginError(urllib2.HTTPError):
class AbstractRpcServer(object):
"""Provides a common interface for a simple RPC server."""
- def __init__(self, host, auth_function, host_override=None, extra_headers={},
- save_cookies=False, account_type=AUTH_ACCOUNT_TYPE):
+ def __init__(self, host, auth_function, host_override=None,
+ extra_headers=None, save_cookies=False,
+ account_type=AUTH_ACCOUNT_TYPE):
"""Creates a new AbstractRpcServer.
Args:
@@ -203,7 +241,7 @@ class AbstractRpcServer(object):
self.host_override = host_override
self.auth_function = auth_function
self.authenticated = False
- self.extra_headers = extra_headers
+ self.extra_headers = extra_headers or {}
self.save_cookies = save_cookies
self.account_type = account_type
self.opener = self._GetOpener()
@@ -425,10 +463,16 @@ class HttpRpcServer(AbstractRpcServer):
def _Authenticate(self):
"""Save the cookie jar after authentication."""
- super(HttpRpcServer, self)._Authenticate()
- if self.save_cookies:
- StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
- self.cookie_jar.save()
+ if isinstance(self.auth_function, OAuth2Creds):
+ access_token = self.auth_function()
+ if access_token is not None:
+ self.extra_headers['Authorization'] = 'OAuth %s' % (access_token,)
+ self.authenticated = True
+ else:
+ super(HttpRpcServer, self)._Authenticate()
+ if self.save_cookies:
+ StatusUpdate("Saving authentication cookies to %s" % self.cookie_file)
+ self.cookie_jar.save()
def _GetOpener(self):
"""Returns an OpenerDirector that supports cookies and ignores redirects.
@@ -495,7 +539,8 @@ class CondensedHelpFormatter(optparse.IndentedHelpFormatter):
parser = optparse.OptionParser(
- usage="%prog [options] [-- diff_options] [path...]",
+ usage=("%prog [options] [-- diff_options] [path...]\n"
+ "See also: http://code.google.com/p/rietveld/wiki/UploadPyUsage"),
add_help_option=False,
formatter=CondensedHelpFormatter()
)
@@ -531,6 +576,17 @@ group.add_option("-H", "--host", action="store", dest="host",
group.add_option("--no_cookies", action="store_false",
dest="save_cookies", default=True,
help="Do not save authentication cookies to local disk.")
+group.add_option("--oauth2", action="store_true",
+ dest="use_oauth2", default=False,
+ help="Use OAuth 2.0 instead of a password.")
+group.add_option("--oauth2_port", action="store", type="int",
+ dest="oauth2_port", default=DEFAULT_OAUTH2_PORT,
+ help=("Port to use to handle OAuth 2.0 redirect. Must be an "
+ "integer in the range 1024-49151, defaults to "
+ "'%default'."))
+group.add_option("--no_oauth2_webbrowser", action="store_false",
+ dest="open_oauth2_local_webbrowser", default=True,
+ help="Don't open a browser window to get an access token.")
group.add_option("--account_type", action="store", dest="account_type",
metavar="TYPE", default=AUTH_ACCOUNT_TYPE,
choices=["GOOGLE", "HOSTED"],
@@ -612,10 +668,137 @@ group.add_option("--p4_user", action="store", dest="p4_user",
help=("Perforce user"))
+# OAuth 2.0 Methods and Helpers
+class ClientRedirectServer(BaseHTTPServer.HTTPServer):
+ """A server for redirects back to localhost from the associated server.
+
+ Waits for a single request and parses the query parameters for an access token
+ and then stops serving.
+ """
+ access_token = None
+
+
+class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """A handler for redirects back to localhost from the associated server.
+
+ Waits for a single request and parses the query parameters into the server's
+ access_token and then stops serving.
+ """
+
+ def SetAccessToken(self):
+ """Stores the access token from the request on the server.
+
+ Will only do this if exactly one query parameter was passed in to the
+ request and that query parameter used 'access_token' as the key.
+ """
+ query_string = urlparse.urlparse(self.path).query
+ query_params = urlparse.parse_qs(query_string)
+
+ if len(query_params) == 1:
+ access_token_list = query_params.get(ACCESS_TOKEN_PARAM, [])
+ if len(access_token_list) == 1:
+ self.server.access_token = access_token_list[0]
+
+ def do_GET(self):
+ """Handle a GET request.
+
+ Parses and saves the query parameters and prints a message that the server
+ has completed its lone task (handling a redirect).
+
+ Note that we can't detect if an error occurred.
+ """
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ self.SetAccessToken()
+ self.wfile.write(AUTH_HANDLER_RESPONSE)
+
+ def log_message(self, format, *args):
+ """Do not log messages to stdout while running as command line program."""
+ pass
+
+
+def OpenOAuth2ConsentPage(server=DEFAULT_REVIEW_SERVER,
+ port=DEFAULT_OAUTH2_PORT):
+ """Opens the OAuth 2.0 consent page or prints instructions how to.
+
+ Uses the webbrowser module to open the OAuth server side page in a browser.
+
+ Args:
+ server: String containing the review server URL. Defaults to
+ DEFAULT_REVIEW_SERVER.
+ port: Integer, the port where the localhost server receiving the redirect
+ is serving. Defaults to DEFAULT_OAUTH2_PORT.
+ """
+ path = OAUTH_PATH_PORT_TEMPLATE % {'port': port}
+ page = 'https://%s%s' % (server, path)
+ webbrowser.open(page, new=1, autoraise=True)
+ print OPEN_LOCAL_MESSAGE_TEMPLATE % (page,)
+
+
+def WaitForAccessToken(port=DEFAULT_OAUTH2_PORT):
+ """Spins up a simple HTTP Server to handle a single request.
+
+ Intended to handle a single redirect from the production server after the
+ user authenticated via OAuth 2.0 with the server.
+
+ Args:
+ port: Integer, the port where the localhost server receiving the redirect
+ is serving. Defaults to DEFAULT_OAUTH2_PORT.
+
+ Returns:
+ The access token passed to the localhost server, or None if no access token
+ was passed.
+ """
+ httpd = ClientRedirectServer((LOCALHOST_IP, port), ClientRedirectHandler)
+ # Wait to serve just one request before deferring control back
+ # to the caller of wait_for_refresh_token
+ httpd.handle_request()
+ return httpd.access_token
+
+
+def GetAccessToken(server=DEFAULT_REVIEW_SERVER, port=DEFAULT_OAUTH2_PORT,
+ open_local_webbrowser=True):
+ """Gets an Access Token for the current user.
+
+ Args:
+ server: String containing the review server URL. Defaults to
+ DEFAULT_REVIEW_SERVER.
+ port: Integer, the port where the localhost server receiving the redirect
+ is serving. Defaults to DEFAULT_OAUTH2_PORT.
+ open_local_webbrowser: Boolean, defaults to True. If set, opens a page in
+ the user's browser.
+
+ Returns:
+ A string access token that was sent to the local server. If the serving page
+ via WaitForAccessToken does not receive an access token, this method
+ returns None.
+ """
+ access_token = None
+ if open_local_webbrowser:
+ OpenOAuth2ConsentPage(server=server, port=port)
+ try:
+ access_token = WaitForAccessToken(port=port)
+ except socket.error, e:
+ print 'Can\'t start local webserver. Socket Error: %s\n' % (e.strerror,)
+
+ if access_token is None:
+ # TODO(dhermes): Offer to add to clipboard using xsel, xclip, pbcopy, etc.
+ page = 'https://%s%s' % (server, OAUTH_PATH)
+ print NO_OPEN_LOCAL_MESSAGE_TEMPLATE % (page,)
+ access_token = raw_input('Enter access token: ').strip()
+
+ return access_token
+
+
class KeyringCreds(object):
def __init__(self, server, host, email):
self.server = server
- self.host = host
+ # Explicitly cast host to str to work around bug in old versions of Keyring
+ # (versions before 0.10). Even though newer versions of Keyring fix this,
+ # some modern linuxes (such as Ubuntu 12.04) still bundle a version with
+ # the bug.
+ self.host = str(host)
self.email = email
self.accounts_seen = set()
@@ -653,8 +836,24 @@ class KeyringCreds(object):
return (email, password)
+class OAuth2Creds(object):
+ """Simple object to hold server and port to be passed to GetAccessToken."""
+
+ def __init__(self, server, port, open_local_webbrowser=True):
+ self.server = server
+ self.port = port
+ self.open_local_webbrowser = open_local_webbrowser
+
+ def __call__(self):
+ """Uses stored server and port to retrieve OAuth 2.0 access token."""
+ return GetAccessToken(server=self.server, port=self.port,
+ open_local_webbrowser=self.open_local_webbrowser)
+
+
def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
- account_type=AUTH_ACCOUNT_TYPE):
+ account_type=AUTH_ACCOUNT_TYPE, use_oauth2=False,
+ oauth2_port=DEFAULT_OAUTH2_PORT,
+ open_oauth2_local_webbrowser=True):
"""Returns an instance of an AbstractRpcServer.
Args:
@@ -665,11 +864,16 @@ def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
save_cookies: Whether authentication cookies should be saved to disk.
account_type: Account type for authentication, either 'GOOGLE'
or 'HOSTED'. Defaults to AUTH_ACCOUNT_TYPE.
+ use_oauth2: Boolean indicating whether OAuth 2.0 should be used for
+ authentication.
+ oauth2_port: Integer, the port where the localhost server receiving the
+ redirect is serving. Defaults to DEFAULT_OAUTH2_PORT.
+ open_oauth2_local_webbrowser: Boolean, defaults to True. If True and using
+ OAuth, this opens a page in the user's browser to obtain a token.
Returns:
A new HttpRpcServer, on which RPC calls can be made.
"""
-
# If this is the dev_appserver, use fake authentication.
host = (host_override or server).lower()
if re.match(r'(http://)?localhost([:/]|$)', host):
@@ -688,10 +892,16 @@ def GetRpcServer(server, email=None, host_override=None, save_cookies=True,
server.authenticated = True
return server
- return HttpRpcServer(server,
- KeyringCreds(server, host, email).GetUserCredentials,
+ positional_args = [server]
+ if use_oauth2:
+ positional_args.append(
+ OAuth2Creds(server, oauth2_port, open_oauth2_local_webbrowser))
+ else:
+ positional_args.append(KeyringCreds(server, host, email).GetUserCredentials)
+ return HttpRpcServer(*positional_args,
host_override=host_override,
- save_cookies=save_cookies)
+ save_cookies=save_cookies,
+ account_type=account_type)
def EncodeMultipartFormData(fields, files):
@@ -2209,7 +2419,11 @@ def RealMain(argv, data=None):
if options.help:
if options.verbose < 2:
# hide Perforce options
- parser.epilog = "Use '--help -v' to show additional Perforce options."
+ parser.epilog = (
+ "Use '--help -v' to show additional Perforce options. "
+ "For more help, see "
+ "http://code.google.com/p/rietveld/wiki/CodeReviewHelp"
+ )
parser.option_groups.remove(parser.get_option_group('--p4_port'))
parser.print_help()
sys.exit(0)
@@ -2250,11 +2464,16 @@ def RealMain(argv, data=None):
files = vcs.GetBaseFiles(data)
if verbosity >= 1:
print "Upload server:", options.server, "(change with -s/--server)"
+ if options.use_oauth2:
+ options.save_cookies = False
rpc_server = GetRpcServer(options.server,
options.email,
options.host,
options.save_cookies,
- options.account_type)
+ options.account_type,
+ options.use_oauth2,
+ options.oauth2_port,
+ options.open_oauth2_local_webbrowser)
form_fields = []
repo_guid = vcs.GetGUID()
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698