You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/third_party/gsutil/plugins/sso_auth.py

106 lines
3.1 KiB
Python

Adds SSO auth to gsutil Code path: 1. plugins.sso_auth is imported, which adds the AuthHandler class to the global state. 2. HasConfiguredCredentials() in gslib/utils.py is called by gsutil, and will return true if "prodaccess" exists on the system, which tells the system that we don't want a no-op auth handler. 3. When a command is called, all the auth handlers are cycled through and sso_auth.SSOAuth is called, which calls a stubby command to emit a gaiamint'ed oauth2 access token, which is then used as the Authorization Header if --bypass_prodaccess is passed in, then: 1. HasConfiguredCredentials() will bypass the check for prodaccess, as if it didn't exist. 2. plugins.sso_auth does not get imported. Which will essentially cause gsutil to behave as if this patch never existed. So the expected behavior is: =.boto file does not exist, prodaccess exists, but unauthenticated= Failure: No handler was ready to authenticate. 3 handlers were checked. ['OAuth2Auth', 'HmacAuthV1Handler', 'SSOAuth'] Check your credentials. =.boto file exists, prodaccess exists, but unauthenticated= sso_auth will raise NotReadyToAuthenticate, and the .boto file will be used instead =.boto file exists, prodaccess exists, authenticated= sso_auth will be run _after_ the default gsutil authenticator, which causes the sso_auth to be used over whatever the default authentication is. bypass_prodaccess is passed in by default to upload_to_google_storage because we expect people who use upload_to_google_storage to not need prodaccess and have their own boto file already. Also the sso_auth plugin will only request a readonlyi token, which will not work for uploading. BUG=258152 Review URL: https://codereview.chromium.org/86123002 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@240266 0039d316-1c4b-4281-b951-d872f2087c98
12 years ago
# Copyright 2013 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.
"""AuthHandler plugin for gsutil's boto to support LOAS based auth."""
import getpass
import json
import os
import re
import subprocess
import time
import urllib2
from boto.auth_handler import AuthHandler
from boto.auth_handler import NotReadyToAuthenticate
CMD = ['stubby', '--proto2', 'call', 'blade:sso', 'CorpLogin.Exchange']
STUBBY_CMD = """target: {
scope: GAIA_USER
name: "%s"
}
target_credential: {
type: OAUTH2_TOKEN
oauth2_attributes: {
scope: 'https://www.googleapis.com/auth/devstorage.read_only'
}
}"""
COOKIE_LOCATION = os.path.expanduser('~/.devstore_token')
TOKEN_EXPIRY = 300
class SSOAuthError(Exception):
pass
class SSOAuth(AuthHandler):
"""SSO based auth handler."""
capability = ['google-oauth2', 's3']
def __init__(self, path, config, provider):
if provider.name == 'google' and self.has_prodaccess():
# If we don't have a loas token, then bypass this auth handler.
if subprocess.call('loas_check',
stdout=subprocess.PIPE,
stderr=subprocess.PIPE):
raise NotReadyToAuthenticate()
else:
raise NotReadyToAuthenticate()
self.token = None
self.expire = 0
def GetAccessToken(self):
"""Returns a valid devstore access token.
This will return from an in-memory cache if the token is there already,
then try a filesystem cache, and then runs a stubby call if none of the
caches have a valid token.
"""
if self.token and self.expire > time.time():
return self.token
# Try to retrieve token from filesystem cache.
if os.path.exists(COOKIE_LOCATION):
last_modified = os.path.getmtime(COOKIE_LOCATION)
if time.time() - last_modified < TOKEN_EXPIRY:
with open(COOKIE_LOCATION, 'rb') as f:
self.token = f.read()
self.expire = last_modified + TOKEN_EXPIRY
return self.token
# If the token is not in either caches, or has expired, then fetch token.
username = '%s@google.com' % getpass.getuser()
proc = subprocess.Popen(CMD, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = proc.communicate(STUBBY_CMD % username)
if proc.returncode:
raise SSOAuthError('Stubby returned %d\n%s' % (proc.returncode, err))
token_match = re.search(r'oauth2_token: "(.*)"$', out)
if not token_match:
raise SSOAuthError('Oauth2 token not found in %s' % out)
token = token_match.group(1)
self.token = token
self.expire = time.time() + TOKEN_EXPIRY
with os.fdopen(os.open(COOKIE_LOCATION,
os.O_WRONLY | os.O_CREAT,
0600), 'wb') as f:
f.write(token)
return token
def add_auth(self, http_request):
http_request.headers['Authorization'] = 'OAuth %s' % self.GetAccessToken()
@staticmethod
def has_prodaccess():
for path in os.environ['PATH'].split(os.pathsep):
exe_file = os.path.join(path, 'prodaccess')
if os.path.exists(exe_file) and os.access(exe_file, os.X_OK):
return True
return False