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