blob: 7099153f08d84f34a91eff213e052f1d3dc7ab7a [file] [log] [blame]
Vadim Shtayurac4c76b62014-01-13 15:05:41 -08001#!/usr/bin/env python
maruelea586f32016-04-05 11:11:33 -07002# Copyright 2013 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07003# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
Vadim Shtayurac4c76b62014-01-13 15:05:41 -08005
6"""Client tool to perform various authentication related tasks."""
7
Vadim Shtayura36817012015-03-20 19:12:25 -07008__version__ = '0.4'
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
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000014from utils import tools
15tools.force_local_third_party()
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080016
Marc-Antoine Ruel016c7602019-04-02 18:31:13 +000017# third_party/
18import colorama
19from depot_tools import fix_encoding
20from depot_tools import subcommand
21
22# pylint: disable=ungrouped-imports
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040023from utils import logging_utils
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040024from utils import on_error
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080025from utils import net
26from utils import oauth
maruel8e4e40c2016-05-30 06:21:07 -070027from utils import subprocess42
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080028
29
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080030class AuthServiceError(Exception):
31 """Unexpected response from authentication service."""
32
33
Junji Watanabeab2102a2022-01-12 01:44:04 +000034class AuthService:
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080035 """Represents remote Authentication service."""
36
37 def __init__(self, url):
38 self._service = net.get_http_service(url)
39
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080040 def login(self, allow_user_interaction):
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080041 """Refreshes cached access token or creates a new one."""
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080042 return self._service.login(allow_user_interaction)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080043
44 def logout(self):
45 """Purges cached access token."""
46 return self._service.logout()
47
48 def get_current_identity(self):
49 """Returns identity associated with currently used credentials.
50
51 Identity is a string:
52 user:<email> - if using OAuth or cookie based authentication.
53 bot:<id> - if using HMAC based authentication.
54 anonymous:anonymous - if not authenticated.
55 """
Marc-Antoine Ruel0a620612014-08-13 15:47:07 -040056 identity = self._service.json_request('/auth/api/v1/accounts/self')
Vadim Shtayurac4c76b62014-01-13 15:05:41 -080057 if not identity:
58 raise AuthServiceError('Failed to fetch identity')
59 return identity['identity']
60
61
Vadim Shtayura6b555c12014-07-23 16:22:18 -070062def add_auth_options(parser):
63 """Adds command line options related to authentication."""
Vadim Shtayura6b555c12014-07-23 16:22:18 -070064 oauth.add_oauth_options(parser)
65
66
67def process_auth_options(parser, options):
68 """Configures process-wide authentication parameters based on |options|."""
Vadim Shtayura36817012015-03-20 19:12:25 -070069 try:
70 net.set_oauth_config(oauth.extract_oauth_config_from_options(options))
71 except ValueError as exc:
72 parser.error(str(exc))
Vadim Shtayura6b555c12014-07-23 16:22:18 -070073
74
Vadim Shtayura771653f2015-07-31 11:13:09 -070075def normalize_host_url(url):
76 """Makes sure URL starts with http:// or https://."""
77 url = url.lower().rstrip('/')
78 if url.startswith('https://'):
79 return url
80 if url.startswith('http://'):
Marc-Antoine Ruelcd0e0272018-03-13 14:31:45 -040081 allowed = ('http://localhost:', 'http://127.0.0.1:', 'http://[::1]:')
Vadim Shtayura771653f2015-07-31 11:13:09 -070082 if not url.startswith(allowed):
83 raise ValueError(
84 'URL must start with https:// or be on localhost with port number')
85 return url
86 return 'https://' + url
87
88
Vadim Shtayura6b555c12014-07-23 16:22:18 -070089def ensure_logged_in(server_url):
90 """Checks that user is logged in, asking to do it if not.
91
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -050092 Raises:
93 ValueError if the server_url is not acceptable.
Vadim Shtayura6b555c12014-07-23 16:22:18 -070094 """
Vadim Shtayura36817012015-03-20 19:12:25 -070095 # It's just a waste of time on a headless bot (it can't do interactive login).
96 if tools.is_headless() or net.get_oauth_config().disabled:
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -040097 return None
Vadim Shtayura771653f2015-07-31 11:13:09 -070098 server_url = normalize_host_url(server_url)
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):
Lei Leife202df2019-06-11 17:33:34 +0000124 print('Logged in as \'%s\'.' % service.get_current_identity())
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800125 return 0
Lei Leife202df2019-06-11 17:33:34 +0000126 print('Login failed or canceled.')
Marc-Antoine Ruel793bff32019-04-18 17:50:48 +0000127 return 1
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800128
129
130@subcommand.usage('[options]')
131def CMDlogout(parser, args):
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800132 """Purges cached auth token/cookie."""
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800133 (options, args) = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800134 process_auth_options(parser, options)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800135 service = AuthService(options.service)
136 service.logout()
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800137 return 0
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800138
139
140@subcommand.usage('[options]')
141def CMDcheck(parser, args):
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800142 """Shows identity associated with currently cached auth token/cookie."""
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800143 (options, args) = parser.parse_args(args)
Vadim Shtayura5d1efce2014-02-04 10:55:43 -0800144 process_auth_options(parser, options)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800145 service = AuthService(options.service)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800146 service.login(False)
Lei Leife202df2019-06-11 17:33:34 +0000147 print(service.get_current_identity())
Vadim Shtayurae34e13a2014-02-02 11:23:26 -0800148 return 0
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800149
150
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400151class OptionParserAuth(logging_utils.OptionParserWithLogging):
Junji Watanabe38b28b02020-04-23 10:23:30 +0000152
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800153 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400154 logging_utils.OptionParserWithLogging.__init__(
155 self, prog='auth.py', **kwargs)
Vadim Shtayura771653f2015-07-31 11:13:09 -0700156 self.server_group = optparse.OptionGroup(self, 'Server')
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800157 self.server_group.add_option(
Junji Watanabe38b28b02020-04-23 10:23:30 +0000158 '-S', '--service', metavar='URL', default='', help='Service to use')
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800159 self.add_option_group(self.server_group)
160 add_auth_options(self)
161
162 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400163 options, args = logging_utils.OptionParserWithLogging.parse_args(
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800164 self, *args, **kwargs)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800165 if not options.service:
166 self.error('--service is required.')
Vadim Shtayura771653f2015-07-31 11:13:09 -0700167 try:
168 options.service = normalize_host_url(options.service)
169 except ValueError as exc:
170 self.error(str(exc))
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800171 return options, args
172
173
174def main(args):
175 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -0400176 return dispatcher.execute(OptionParserAuth(version=__version__), args)
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800177
178
179if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -0700180 subprocess42.inhibit_os_error_reporting()
Vadim Shtayurac4c76b62014-01-13 15:05:41 -0800181 fix_encoding.fix_encoding()
182 tools.disable_buffering()
183 colorama.init()
184 sys.exit(main(sys.argv[1:]))