blob: 5ae01ae4349a1e6cb36ae69c8a4389dfbe95576a [file] [log] [blame]
Vadim Shtayurac4c76b62014-01-13 15:05:41 -08001#!/usr/bin/env python
2# Copyright 2013 The Swarming Authors. All rights reserved.
3# Use of this source code is governed under the Apache License, Version 2.0 that
4# can be found in the LICENSE file.
5
6"""Client tool to perform various authentication related tasks."""
7
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04008__version__ = '0.3.2'
Vadim Shtayurac4c76b62014-01-13 15:05:41 -08009
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -040010import logging
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080011import optparse
12import sys
13
14from third_party import colorama
15from third_party.depot_tools import fix_encoding
16from third_party.depot_tools import subcommand
17
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040018from utils import on_error
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080019from utils import net
20from utils import oauth
21from utils import tools
22
23
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080024class AuthServiceError(Exception):
25 """Unexpected response from authentication service."""
26
27
28class AuthService(object):
29 """Represents remote Authentication service."""
30
31 def __init__(self, url):
32 self._service = net.get_http_service(url)
33
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080034 def login(self, allow_user_interaction):
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080035 """Refreshes cached access token or creates a new one."""
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080036 return self._service.login(allow_user_interaction)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080037
38 def logout(self):
39 """Purges cached access token."""
40 return self._service.logout()
41
42 def get_current_identity(self):
43 """Returns identity associated with currently used credentials.
44
45 Identity is a string:
46 user:<email> - if using OAuth or cookie based authentication.
47 bot:<id> - if using HMAC based authentication.
48 anonymous:anonymous - if not authenticated.
49 """
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -040050 identity = self._service.json_request('/auth/api/v1/accounts/self')
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080051 if not identity:
52 raise AuthServiceError('Failed to fetch identity')
53 return identity['identity']
54
55
Vadim Shtayura6b555c12014-07-23 16:22:18 -070056def add_auth_options(parser):
57 """Adds command line options related to authentication."""
58 parser.auth_group = optparse.OptionGroup(parser, 'Authentication')
59 parser.auth_group.add_option(
60 '--auth-method',
61 metavar='METHOD',
62 default=net.get_default_auth_config()[0],
63 help='Authentication method to use: %s. [default: %%default]' %
64 ', '.join(name for name, _ in net.AUTH_METHODS))
65 parser.add_option_group(parser.auth_group)
66 oauth.add_oauth_options(parser)
67
68
69def process_auth_options(parser, options):
70 """Configures process-wide authentication parameters based on |options|."""
71 # Validate that authentication method is known.
72 if options.auth_method not in dict(net.AUTH_METHODS):
73 parser.error('Invalid --auth-method value: %s' % options.auth_method)
74
75 # Process the rest of the flags based on actual method used.
76 # Only oauth is configurable now.
77 config = None
78 if options.auth_method == 'oauth':
79 config = oauth.extract_oauth_config_from_options(options)
80
81 # Now configure 'net' globally to use this for every request.
82 net.configure_auth(options.auth_method, config)
83
84
85def ensure_logged_in(server_url):
86 """Checks that user is logged in, asking to do it if not.
87
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -050088 Raises:
89 ValueError if the server_url is not acceptable.
Vadim Shtayura6b555c12014-07-23 16:22:18 -070090 """
91 if net.get_auth_method() not in ('cookie', 'oauth'):
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -040092 return None
Vadim Shtayura6b555c12014-07-23 16:22:18 -070093 server_url = server_url.lower().rstrip('/')
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -050094 allowed = (
95 'https://',
96 'http://localhost:', 'http://127.0.0.1:', 'http://::1:')
97 if not server_url.startswith(allowed):
98 raise ValueError('URL must start with https:// or be http:// to localhost')
Vadim Shtayura6b555c12014-07-23 16:22:18 -070099 service = AuthService(server_url)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500100 try:
101 service.login(False)
102 except IOError:
103 raise ValueError('Failed to contact %s' % server_url)
104 try:
105 identity = service.get_current_identity()
106 except AuthServiceError:
107 raise ValueError('Failed to fetch identify from %s' % server_url)
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700108 if identity == 'anonymous:anonymous':
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500109 raise ValueError(
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700110 'Please login to %s: \n'
111 ' python auth.py login --service=%s' % (server_url, server_url))
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700112 email = identity.split(':')[1]
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -0400113 logging.info('Logged in to %s: %s', server_url, email)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400114 return email
Vadim Shtayura6b555c12014-07-23 16:22:18 -0700115
116
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800117@subcommand.usage('[options]')
118def CMDlogin(parser, args):
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800119 """Runs interactive login flow and stores auth token/cookie on disk."""
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800120 (options, args) = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800121 process_auth_options(parser, options)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800122 service = AuthService(options.service)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800123 if service.login(True):
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800124 print 'Logged in as \'%s\'.' % service.get_current_identity()
125 return 0
126 else:
127 print 'Login failed or canceled.'
128 return 1
129
130
131@subcommand.usage('[options]')
132def CMDlogout(parser, args):
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800133 """Purges cached auth token/cookie."""
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800134 (options, args) = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800135 process_auth_options(parser, options)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800136 service = AuthService(options.service)
137 service.logout()
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800138 return 0
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800139
140
141@subcommand.usage('[options]')
142def CMDcheck(parser, args):
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800143 """Shows identity associated with currently cached auth token/cookie."""
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800144 (options, args) = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800145 process_auth_options(parser, options)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800146 service = AuthService(options.service)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800147 service.login(False)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800148 print service.get_current_identity()
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800149 return 0
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800150
151
152class OptionParserAuth(tools.OptionParserWithLogging):
153 def __init__(self, **kwargs):
154 tools.OptionParserWithLogging.__init__(self, prog='auth.py', **kwargs)
155 self.server_group = tools.optparse.OptionGroup(self, 'Server')
156 self.server_group.add_option(
157 '-S', '--service',
158 metavar='URL', default='',
159 help='Service to use')
160 self.add_option_group(self.server_group)
161 add_auth_options(self)
162
163 def parse_args(self, *args, **kwargs):
164 options, args = tools.OptionParserWithLogging.parse_args(
165 self, *args, **kwargs)
166 options.service = options.service.rstrip('/')
167 if not options.service:
168 self.error('--service is required.')
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400169 on_error.report_on_exception_exit(options.service)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800170 return options, args
171
172
173def main(args):
174 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400175 return dispatcher.execute(OptionParserAuth(version=__version__), args)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800176
177
178if __name__ == '__main__':
179 fix_encoding.fix_encoding()
180 tools.disable_buffering()
181 colorama.init()
182 sys.exit(main(sys.argv[1:]))