• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Command-line utility for fetching/inspecting credentials.
2
3oauth2l (pronounced "oauthtool") is a small utility for fetching
4credentials, or inspecting existing credentials. Here we demonstrate
5some 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
22The `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
27The token can also be printed in other formats, for easy chaining
28into 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
37import httplib
38import json
39import logging
40import os
41import pkgutil
42import sys
43import textwrap
44
45import gflags as flags
46from google.apputils import appcommands
47import oauth2client.client
48
49import apitools.base.py as apitools_base
50from apitools.base.py import cli as apitools_cli
51
52FLAGS = 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
62flags.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.')
67flags.DEFINE_string(
68    'credentials_filename', '',
69    '(optional) Filename for fetching/storing credentials.')
70flags.DEFINE_string(
71    'service_account_json_keyfile', '',
72    'Filename for a JSON service account key downloaded from the Developer '
73    'Console.')
74
75
76def 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
86def 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
106def _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
112def _PrettyJson(data):
113    return json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))
114
115
116def _CompactJson(data):
117    return json.dumps(data, sort_keys=True, separators=(',', ':'))
118
119
120def _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
144def _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
156def _ValidateToken(access_token):
157    """Return True iff the provided access token is valid."""
158    return bool(_GetTokenScopes(access_token))
159
160
161def 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
182class _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
198class _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
217class _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
228class _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
243class _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
267class _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
278def 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
299def 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
308if __name__ == '__main__':
309    appcommands.Run()
310