| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2014 Google Inc. All Rights Reserved. |
| 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at |
| 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. |
| 14 """Common credentials classes and constructors.""" |
| 15 |
| 16 import json |
| 17 import os |
| 18 import urllib2 |
| 19 |
| 20 |
| 21 import httplib2 |
| 22 import oauth2client.client |
| 23 import oauth2client.gce |
| 24 import oauth2client.multistore_file |
| 25 |
| 26 from gslib.third_party.storage_apitools import exceptions |
| 27 from gslib.third_party.storage_apitools import util |
| 28 |
| 29 __all__ = [ |
| 30 'CredentialsFromFile', |
| 31 'GaeAssertionCredentials', |
| 32 'GceAssertionCredentials', |
| 33 'GetCredentials', |
| 34 'ServiceAccountCredentials', |
| 35 'ServiceAccountCredentialsFromFile', |
| 36 ] |
| 37 |
| 38 |
| 39 # TODO: Expose the extra args here somewhere higher up, |
| 40 # possibly as flags in the generated CLI. |
| 41 def GetCredentials(package_name, scopes, client_id, client_secret, user_agent, |
| 42 credentials_filename=None, |
| 43 service_account_name=None, service_account_keyfile=None, |
| 44 api_key=None, client=None): |
| 45 """Attempt to get credentials, using an oauth dance as the last resort.""" |
| 46 scopes = util.NormalizeScopes(scopes) |
| 47 # TODO: Error checking. |
| 48 client_info = { |
| 49 'client_id': client_id, |
| 50 'client_secret': client_secret, |
| 51 'scope': ' '.join(sorted(util.NormalizeScopes(scopes))), |
| 52 'user_agent': user_agent or '%s-generated/0.1' % package_name, |
| 53 } |
| 54 if service_account_name is not None: |
| 55 credentials = ServiceAccountCredentialsFromFile( |
| 56 service_account_name, service_account_keyfile, scopes) |
| 57 if credentials is not None: |
| 58 return credentials |
| 59 credentials = GaeAssertionCredentials.Get(scopes) |
| 60 if credentials is not None: |
| 61 return credentials |
| 62 credentials = GceAssertionCredentials.Get(scopes) |
| 63 if credentials is not None: |
| 64 return credentials |
| 65 credentials_filename = credentials_filename or os.path.expanduser( |
| 66 '~/.apitools.token') |
| 67 credentials = CredentialsFromFile(credentials_filename, client_info) |
| 68 if credentials is not None: |
| 69 return credentials |
| 70 raise exceptions.CredentialsError('Could not create valid credentials') |
| 71 |
| 72 |
| 73 def ServiceAccountCredentialsFromFile( |
| 74 service_account_name, private_key_filename, scopes): |
| 75 with open(private_key_filename) as key_file: |
| 76 return ServiceAccountCredentials( |
| 77 service_account_name, key_file.read(), scopes) |
| 78 |
| 79 |
| 80 def ServiceAccountCredentials(service_account_name, private_key, scopes): |
| 81 scopes = util.NormalizeScopes(scopes) |
| 82 return oauth2client.client.SignedJwtAssertionCredentials( |
| 83 service_account_name, private_key, scopes) |
| 84 |
| 85 |
| 86 # TODO: We override to add some utility code, and to |
| 87 # update the old refresh implementation. Either push this code into |
| 88 # oauth2client or drop oauth2client. |
| 89 class GceAssertionCredentials(oauth2client.gce.AppAssertionCredentials): |
| 90 """Assertion credentials for GCE instances.""" |
| 91 |
| 92 def __init__(self, scopes=None, service_account_name='default', **kwds): |
| 93 """Initializes the credentials instance. |
| 94 |
| 95 Args: |
| 96 scopes: The scopes to get. If None, whatever scopes that are available |
| 97 to the instance are used. |
| 98 service_account_name: The service account to retrieve the scopes from. |
| 99 **kwds: Additional keyword args. |
| 100 """ |
| 101 if not util.DetectGce(): |
| 102 raise exceptions.ResourceUnavailableError( |
| 103 'GCE credentials requested outside a GCE instance') |
| 104 if not self.GetServiceAccount(service_account_name): |
| 105 raise exceptions.ResourceUnavailableError( |
| 106 'GCE credentials requested but service account %s does not exist.' % |
| 107 service_account_name) |
| 108 self.__service_account_name = service_account_name |
| 109 if scopes: |
| 110 scope_ls = util.NormalizeScopes(scopes) |
| 111 instance_scopes = self.GetInstanceScopes() |
| 112 if scope_ls > instance_scopes: |
| 113 raise exceptions.CredentialsError( |
| 114 'Instance did not have access to scopes %s' % ( |
| 115 sorted(list(scope_ls - instance_scopes)),)) |
| 116 else: |
| 117 scopes = self.GetInstanceScopes() |
| 118 super(GceAssertionCredentials, self).__init__(scopes, **kwds) |
| 119 |
| 120 @classmethod |
| 121 def Get(cls, *args, **kwds): |
| 122 try: |
| 123 return cls(*args, **kwds) |
| 124 except exceptions.Error: |
| 125 return None |
| 126 |
| 127 def GetServiceAccount(self, account): |
| 128 account_uri = ( |
| 129 'http://metadata.google.internal/computeMetadata/' |
| 130 'v1/instance/service-accounts') |
| 131 additional_headers = {'X-Google-Metadata-Request': 'True'} |
| 132 request = urllib2.Request(account_uri, headers=additional_headers) |
| 133 try: |
| 134 response = urllib2.urlopen(request) |
| 135 except urllib2.URLError as e: |
| 136 raise exceptions.CommunicationError( |
| 137 'Could not reach metadata service: %s' % e.reason) |
| 138 response_lines = [line.rstrip('/\n\r') for line in response.readlines()] |
| 139 return account in response_lines |
| 140 |
| 141 def GetInstanceScopes(self): |
| 142 # Extra header requirement can be found here: |
| 143 # https://developers.google.com/compute/docs/metadata |
| 144 scopes_uri = ( |
| 145 'http://metadata.google.internal/computeMetadata/v1/instance/' |
| 146 'service-accounts/%s/scopes') % self.__service_account_name |
| 147 additional_headers = {'X-Google-Metadata-Request': 'True'} |
| 148 request = urllib2.Request(scopes_uri, headers=additional_headers) |
| 149 try: |
| 150 response = urllib2.urlopen(request) |
| 151 except urllib2.URLError as e: |
| 152 raise exceptions.CommunicationError( |
| 153 'Could not reach metadata service: %s' % e.reason) |
| 154 return util.NormalizeScopes(scope.strip() for scope in response.readlines()) |
| 155 |
| 156 def _refresh(self, do_request): # pylint: disable=g-bad-name |
| 157 """Refresh self.access_token. |
| 158 |
| 159 Args: |
| 160 do_request: A function matching httplib2.Http.request's signature. |
| 161 """ |
| 162 token_uri = ( |
| 163 'http://metadata.google.internal/computeMetadata/v1/instance/' |
| 164 'service-accounts/%s/token') % self.__service_account_name |
| 165 extra_headers = {'X-Google-Metadata-Request': 'True'} |
| 166 request = urllib2.Request(token_uri, headers=extra_headers) |
| 167 try: |
| 168 content = urllib2.urlopen(request).read() |
| 169 except urllib2.URLError as e: |
| 170 raise exceptions.CommunicationError( |
| 171 'Could not reach metadata service: %s' % e.reason) |
| 172 try: |
| 173 credential_info = json.loads(content) |
| 174 except ValueError: |
| 175 raise exceptions.CredentialsError( |
| 176 'Invalid credentials response: uri %s' % token_uri) |
| 177 |
| 178 self.access_token = credential_info['access_token'] |
| 179 |
| 180 |
| 181 # TODO: Currently, we can't even *load* |
| 182 # `oauth2client.appengine` without being on appengine, because of how |
| 183 # it handles imports. Fix that by splitting that module into |
| 184 # GAE-specific and GAE-independent bits, and guarding imports. |
| 185 class GaeAssertionCredentials(oauth2client.client.AssertionCredentials): |
| 186 """Assertion credentials for Google App Engine apps.""" |
| 187 |
| 188 def __init__(self, scopes, **kwds): |
| 189 if not util.DetectGae(): |
| 190 raise exceptions.ResourceUnavailableError( |
| 191 'GCE credentials requested outside a GCE instance') |
| 192 self._scopes = list(util.NormalizeScopes(scopes)) |
| 193 super(GaeAssertionCredentials, self).__init__(None, **kwds) |
| 194 |
| 195 @classmethod |
| 196 def Get(cls, *args, **kwds): |
| 197 try: |
| 198 return cls(*args, **kwds) |
| 199 except exceptions.Error: |
| 200 return None |
| 201 |
| 202 @classmethod |
| 203 def from_json(cls, json_data): # pylint: disable=g-bad-name |
| 204 data = json.loads(json_data) |
| 205 return GaeAssertionCredentials(data['_scopes']) |
| 206 |
| 207 def _refresh(self, _): # pylint: disable=g-bad-name |
| 208 """Refresh self.access_token. |
| 209 |
| 210 Args: |
| 211 _: (ignored) A function matching httplib2.Http.request's signature. |
| 212 """ |
| 213 # pylint: disable=g-import-not-at-top |
| 214 from google.appengine.api import app_identity |
| 215 try: |
| 216 token, _ = app_identity.get_access_token(self._scopes) |
| 217 except app_identity.Error as e: |
| 218 raise exceptions.CredentialsError(str(e)) |
| 219 self.access_token = token |
| 220 |
| 221 |
| 222 # TODO: Switch this from taking a path to taking a stream. |
| 223 def CredentialsFromFile(path, client_info): |
| 224 """Read credentials from a file.""" |
| 225 credential_store = oauth2client.multistore_file.get_credential_storage( |
| 226 path, |
| 227 client_info['client_id'], |
| 228 client_info['user_agent'], |
| 229 client_info['scope']) |
| 230 credentials = credential_store.get() |
| 231 if credentials is None or credentials.invalid: |
| 232 print 'Generating new OAuth credentials ...' |
| 233 while True: |
| 234 # If authorization fails, we want to retry, rather than let this |
| 235 # cascade up and get caught elsewhere. If users want out of the |
| 236 # retry loop, they can ^C. |
| 237 try: |
| 238 flow = oauth2client.client.OAuth2WebServerFlow(**client_info) |
| 239 flow.redirect_uri = oauth2client.client.OOB_CALLBACK_URN |
| 240 authorize_url = flow.step1_get_authorize_url() |
| 241 print 'Go to the following link in your browser:' |
| 242 print |
| 243 print ' ' + authorize_url |
| 244 print |
| 245 code = raw_input('Enter verification code: ').strip() |
| 246 credential = flow.step2_exchange(code) |
| 247 credential_store.put(credential) |
| 248 credential.set_store(credential_store) |
| 249 break |
| 250 except (oauth2client.client.FlowExchangeError, SystemExit) as e: |
| 251 # Here SystemExit is "no credential at all", and the |
| 252 # FlowExchangeError is "invalid" -- usually because you reused |
| 253 # a token. |
| 254 print 'Invalid authorization: %s' % (e,) |
| 255 except httplib2.HttpLib2Error as e: |
| 256 print 'Communication error: %s' % (e,) |
| 257 raise exceptions.CredentialsError( |
| 258 'Communication error creating credentials: %s' % e) |
| 259 return credentials |
| OLD | NEW |