OLD | NEW |
(Empty) | |
| 1 # |
| 2 # Copyright 2015 Google Inc. |
| 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with the License. |
| 6 # You may obtain a copy of the License at |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. |
| 15 |
| 16 """Command-line utility for fetching/inspecting credentials. |
| 17 |
| 18 oauth2l (pronounced "oauthtool") is a small utility for fetching |
| 19 credentials, or inspecting existing credentials. Here we demonstrate |
| 20 some sample use: |
| 21 |
| 22 $ oauth2l fetch userinfo.email bigquery compute |
| 23 Fetched credentials of type: |
| 24 oauth2client.client.OAuth2Credentials |
| 25 Access token: |
| 26 ya29.abcdefghijklmnopqrstuvwxyz123yessirree |
| 27 $ oauth2l header userinfo.email |
| 28 Authorization: Bearer ya29.zyxwvutsrqpnmolkjihgfedcba |
| 29 $ oauth2l validate thisisnotatoken |
| 30 <exit status: 1> |
| 31 $ oauth2l validate ya29.zyxwvutsrqpnmolkjihgfedcba |
| 32 $ oauth2l scopes ya29.abcdefghijklmnopqrstuvwxyz123yessirree |
| 33 https://www.googleapis.com/auth/bigquery |
| 34 https://www.googleapis.com/auth/compute |
| 35 https://www.googleapis.com/auth/userinfo.email |
| 36 |
| 37 The `header` command is designed to be easy to use with `curl`: |
| 38 |
| 39 $ curl "$(oauth2l header bigquery)" \ |
| 40 'https://www.googleapis.com/bigquery/v2/projects' |
| 41 |
| 42 The token can also be printed in other formats, for easy chaining |
| 43 into other programs: |
| 44 |
| 45 $ oauth2l fetch -f json_compact userinfo.email |
| 46 <one-line JSON object with credential information> |
| 47 $ oauth2l fetch -f bare drive |
| 48 ya29.suchT0kenManyCredentialsW0Wokyougetthepoint |
| 49 |
| 50 """ |
| 51 |
| 52 import httplib |
| 53 import json |
| 54 import logging |
| 55 import os |
| 56 import pkgutil |
| 57 import sys |
| 58 import textwrap |
| 59 |
| 60 import gflags as flags |
| 61 from google.apputils import appcommands |
| 62 import oauth2client.client |
| 63 |
| 64 import apitools.base.py as apitools_base |
| 65 from apitools.base.py import cli as apitools_cli |
| 66 |
| 67 FLAGS = flags.FLAGS |
| 68 # We could use a generated client here, but it's used for precisely |
| 69 # one URL, with one parameter and no worries about URL encoding. Let's |
| 70 # go with simple. |
| 71 _OAUTH2_TOKENINFO_TEMPLATE = ( |
| 72 'https://www.googleapis.com/oauth2/v2/tokeninfo' |
| 73 '?access_token={access_token}' |
| 74 ) |
| 75 |
| 76 |
| 77 flags.DEFINE_string( |
| 78 'client_secrets', '', |
| 79 'If specified, use the client ID/secret from the named ' |
| 80 'file, which should be a client_secrets.json file as downloaded ' |
| 81 'from the Developer Console.') |
| 82 flags.DEFINE_string( |
| 83 'credentials_filename', '', |
| 84 '(optional) Filename for fetching/storing credentials.') |
| 85 flags.DEFINE_string( |
| 86 'service_account_json_keyfile', '', |
| 87 'Filename for a JSON service account key downloaded from the Developer ' |
| 88 'Console.') |
| 89 |
| 90 |
| 91 def GetDefaultClientInfo(): |
| 92 client_secrets = json.loads(pkgutil.get_data( |
| 93 'apitools.data', 'apitools_client_secrets.json'))['installed'] |
| 94 return { |
| 95 'client_id': client_secrets['client_id'], |
| 96 'client_secret': client_secrets['client_secret'], |
| 97 'user_agent': 'apitools/0.2 oauth2l/0.1', |
| 98 } |
| 99 |
| 100 |
| 101 def GetClientInfoFromFlags(): |
| 102 """Fetch client info from FLAGS.""" |
| 103 if FLAGS.client_secrets: |
| 104 client_secrets_path = os.path.expanduser(FLAGS.client_secrets) |
| 105 if not os.path.exists(client_secrets_path): |
| 106 raise ValueError('Cannot find file: %s' % FLAGS.client_secrets) |
| 107 with open(client_secrets_path) as client_secrets_file: |
| 108 client_secrets = json.load(client_secrets_file) |
| 109 if 'installed' not in client_secrets: |
| 110 raise ValueError('Provided client ID must be for an installed app') |
| 111 client_secrets = client_secrets['installed'] |
| 112 return { |
| 113 'client_id': client_secrets['client_id'], |
| 114 'client_secret': client_secrets['client_secret'], |
| 115 'user_agent': 'apitools/0.2 oauth2l/0.1', |
| 116 } |
| 117 else: |
| 118 return GetDefaultClientInfo() |
| 119 |
| 120 |
| 121 def _ExpandScopes(scopes): |
| 122 scope_prefix = 'https://www.googleapis.com/auth/' |
| 123 return [s if s.startswith('https://') else scope_prefix + s |
| 124 for s in scopes] |
| 125 |
| 126 |
| 127 def _PrettyJson(data): |
| 128 return json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) |
| 129 |
| 130 |
| 131 def _CompactJson(data): |
| 132 return json.dumps(data, sort_keys=True, separators=(',', ':')) |
| 133 |
| 134 |
| 135 def _Format(fmt, credentials): |
| 136 """Format credentials according to fmt.""" |
| 137 if fmt == 'bare': |
| 138 return credentials.access_token |
| 139 elif fmt == 'header': |
| 140 return 'Authorization: Bearer %s' % credentials.access_token |
| 141 elif fmt == 'json': |
| 142 return _PrettyJson(json.loads(credentials.to_json())) |
| 143 elif fmt == 'json_compact': |
| 144 return _CompactJson(json.loads(credentials.to_json())) |
| 145 elif fmt == 'pretty': |
| 146 format_str = textwrap.dedent('\n'.join([ |
| 147 'Fetched credentials of type:', |
| 148 ' {credentials_type.__module__}.{credentials_type.__name__}', |
| 149 'Access token:', |
| 150 ' {credentials.access_token}', |
| 151 ])) |
| 152 return format_str.format(credentials=credentials, |
| 153 credentials_type=type(credentials)) |
| 154 raise ValueError('Unknown format: {}'.format(fmt)) |
| 155 |
| 156 _FORMATS = set(('bare', 'header', 'json', 'json_compact', 'pretty')) |
| 157 |
| 158 |
| 159 def _GetTokenScopes(access_token): |
| 160 """Return the list of valid scopes for the given token as a list.""" |
| 161 url = _OAUTH2_TOKENINFO_TEMPLATE.format(access_token=access_token) |
| 162 response = apitools_base.MakeRequest( |
| 163 apitools_base.GetHttp(), apitools_base.Request(url)) |
| 164 if response.status_code not in [httplib.OK, httplib.BAD_REQUEST]: |
| 165 raise apitools_base.HttpError.FromResponse(response) |
| 166 if response.status_code == httplib.BAD_REQUEST: |
| 167 return [] |
| 168 return json.loads(response.content)['scope'].split(' ') |
| 169 |
| 170 |
| 171 def _ValidateToken(access_token): |
| 172 """Return True iff the provided access token is valid.""" |
| 173 return bool(_GetTokenScopes(access_token)) |
| 174 |
| 175 |
| 176 def FetchCredentials(scopes, client_info=None, credentials_filename=None): |
| 177 """Fetch a credential for the given client_info and scopes.""" |
| 178 client_info = client_info or GetClientInfoFromFlags() |
| 179 scopes = _ExpandScopes(scopes) |
| 180 if not scopes: |
| 181 raise ValueError('No scopes provided') |
| 182 credentials_filename = credentials_filename or FLAGS.credentials_filename |
| 183 # TODO(craigcitro): Remove this logging nonsense once we quiet the |
| 184 # spurious logging in oauth2client. |
| 185 old_level = logging.getLogger().level |
| 186 logging.getLogger().setLevel(logging.ERROR) |
| 187 credentials = apitools_base.GetCredentials( |
| 188 'oauth2l', scopes, credentials_filename=credentials_filename, |
| 189 service_account_json_keyfile=FLAGS.service_account_json_keyfile, |
| 190 oauth2client_args='', **client_info) |
| 191 logging.getLogger().setLevel(old_level) |
| 192 if not _ValidateToken(credentials.access_token): |
| 193 credentials.refresh(apitools_base.GetHttp()) |
| 194 return credentials |
| 195 |
| 196 |
| 197 class _Email(apitools_cli.NewCmd): |
| 198 |
| 199 """Get user email.""" |
| 200 |
| 201 usage = 'email <access_token>' |
| 202 |
| 203 def RunWithArgs(self, access_token): |
| 204 """Print the email address for this token, if possible.""" |
| 205 userinfo = apitools_base.GetUserinfo( |
| 206 oauth2client.client.AccessTokenCredentials(access_token, |
| 207 'oauth2l/1.0')) |
| 208 user_email = userinfo.get('email') |
| 209 if user_email: |
| 210 print user_email |
| 211 |
| 212 |
| 213 class _Fetch(apitools_cli.NewCmd): |
| 214 |
| 215 """Fetch credentials.""" |
| 216 |
| 217 usage = 'fetch <scope> [<scope> ...]' |
| 218 |
| 219 def __init__(self, name, flag_values): |
| 220 super(_Fetch, self).__init__(name, flag_values) |
| 221 flags.DEFINE_enum( |
| 222 'credentials_format', 'pretty', sorted(_FORMATS), |
| 223 'Output format for token.', |
| 224 short_name='f', flag_values=flag_values) |
| 225 |
| 226 def RunWithArgs(self, *scopes): |
| 227 """Fetch a valid access token and display it.""" |
| 228 credentials = FetchCredentials(scopes) |
| 229 print _Format(FLAGS.credentials_format.lower(), credentials) |
| 230 |
| 231 |
| 232 class _Header(apitools_cli.NewCmd): |
| 233 |
| 234 """Print credentials for a header.""" |
| 235 |
| 236 usage = 'header <scope> [<scope> ...]' |
| 237 |
| 238 def RunWithArgs(self, *scopes): |
| 239 """Fetch a valid access token and display it formatted for a header.""" |
| 240 print _Format('header', FetchCredentials(scopes)) |
| 241 |
| 242 |
| 243 class _Scopes(apitools_cli.NewCmd): |
| 244 |
| 245 """Get the list of scopes for a token.""" |
| 246 |
| 247 usage = 'scopes <access_token>' |
| 248 |
| 249 def RunWithArgs(self, access_token): |
| 250 """Print the list of scopes for a valid token.""" |
| 251 scopes = _GetTokenScopes(access_token) |
| 252 if not scopes: |
| 253 return 1 |
| 254 for scope in sorted(scopes): |
| 255 print scope |
| 256 |
| 257 |
| 258 class _Userinfo(apitools_cli.NewCmd): |
| 259 |
| 260 """Get userinfo.""" |
| 261 |
| 262 usage = 'userinfo <access_token>' |
| 263 |
| 264 def __init__(self, name, flag_values): |
| 265 super(_Userinfo, self).__init__(name, flag_values) |
| 266 flags.DEFINE_enum( |
| 267 'format', 'json', sorted(('json', 'json_compact')), |
| 268 'Output format for userinfo.', |
| 269 short_name='f', flag_values=flag_values) |
| 270 |
| 271 def RunWithArgs(self, access_token): |
| 272 """Print the userinfo for this token (if we have the right scopes).""" |
| 273 userinfo = apitools_base.GetUserinfo( |
| 274 oauth2client.client.AccessTokenCredentials(access_token, |
| 275 'oauth2l/1.0')) |
| 276 if FLAGS.format == 'json': |
| 277 print _PrettyJson(userinfo) |
| 278 else: |
| 279 print _CompactJson(userinfo) |
| 280 |
| 281 |
| 282 class _Validate(apitools_cli.NewCmd): |
| 283 |
| 284 """Validate a token.""" |
| 285 |
| 286 usage = 'validate <access_token>' |
| 287 |
| 288 def RunWithArgs(self, access_token): |
| 289 """Validate an access token. Exits with 0 if valid, 1 otherwise.""" |
| 290 return 1 - (_ValidateToken(access_token)) |
| 291 |
| 292 |
| 293 def run_main(): # pylint:disable=invalid-name |
| 294 """Function to be used as setuptools script entry point.""" |
| 295 # Put the flags for this module somewhere the flags module will look |
| 296 # for them. |
| 297 |
| 298 # pylint:disable=protected-access |
| 299 new_name = flags._GetMainModule() |
| 300 sys.modules[new_name] = sys.modules['__main__'] |
| 301 for flag in FLAGS.FlagsByModuleDict().get(__name__, []): |
| 302 FLAGS._RegisterFlagByModule(new_name, flag) |
| 303 for key_flag in FLAGS.KeyFlagsByModuleDict().get(__name__, []): |
| 304 FLAGS._RegisterKeyFlagForModule(new_name, key_flag) |
| 305 # pylint:enable=protected-access |
| 306 |
| 307 # Now set __main__ appropriately so that appcommands will be |
| 308 # happy. |
| 309 sys.modules['__main__'] = sys.modules[__name__] |
| 310 appcommands.Run() |
| 311 sys.modules['__main__'] = sys.modules.pop(new_name) |
| 312 |
| 313 |
| 314 def main(unused_argv): |
| 315 appcommands.AddCmd('email', _Email) |
| 316 appcommands.AddCmd('fetch', _Fetch) |
| 317 appcommands.AddCmd('header', _Header) |
| 318 appcommands.AddCmd('scopes', _Scopes) |
| 319 appcommands.AddCmd('userinfo', _Userinfo) |
| 320 appcommands.AddCmd('validate', _Validate) |
| 321 |
| 322 |
| 323 if __name__ == '__main__': |
| 324 appcommands.Run() |
OLD | NEW |