blob: 4514fbfff870d560b16614fac4538a7dcf613ee6 [file] [log] [blame]
Ben Murdochf91f0612016-11-29 16:50:11 +00001#!/usr/bin/env python
2# Copyright 2013 The LUCI Authors. All rights reserved.
3# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
5
6"""Client tool to perform various authentication related tasks."""
7
8__version__ = '0.4'
9
10import logging
11import 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
18from utils import logging_utils
19from utils import on_error
20from utils import net
21from utils import oauth
22from utils import subprocess42
23from utils import tools
24
25
26class AuthServiceError(Exception):
27 """Unexpected response from authentication service."""
28
29
30class AuthService(object):
31 """Represents remote Authentication service."""
32
33 def __init__(self, url):
34 self._service = net.get_http_service(url)
35
36 def login(self, allow_user_interaction):
37 """Refreshes cached access token or creates a new one."""
38 return self._service.login(allow_user_interaction)
39
40 def logout(self):
41 """Purges cached access token."""
42 return self._service.logout()
43
44 def get_current_identity(self):
45 """Returns identity associated with currently used credentials.
46
47 Identity is a string:
48 user:<email> - if using OAuth or cookie based authentication.
49 bot:<id> - if using HMAC based authentication.
50 anonymous:anonymous - if not authenticated.
51 """
52 identity = self._service.json_request('/auth/api/v1/accounts/self')
53 if not identity:
54 raise AuthServiceError('Failed to fetch identity')
55 return identity['identity']
56
57
58def add_auth_options(parser):
59 """Adds command line options related to authentication."""
60 oauth.add_oauth_options(parser)
61
62
63def process_auth_options(parser, options):
64 """Configures process-wide authentication parameters based on |options|."""
65 try:
66 net.set_oauth_config(oauth.extract_oauth_config_from_options(options))
67 except ValueError as exc:
68 parser.error(str(exc))
69
70
71def normalize_host_url(url):
72 """Makes sure URL starts with http:// or https://."""
73 url = url.lower().rstrip('/')
74 if url.startswith('https://'):
75 return url
76 if url.startswith('http://'):
77 allowed = ('http://localhost:', 'http://127.0.0.1:', 'http://::1:')
78 if not url.startswith(allowed):
79 raise ValueError(
80 'URL must start with https:// or be on localhost with port number')
81 return url
82 return 'https://' + url
83
84
85def ensure_logged_in(server_url):
86 """Checks that user is logged in, asking to do it if not.
87
88 Raises:
89 ValueError if the server_url is not acceptable.
90 """
91 # It's just a waste of time on a headless bot (it can't do interactive login).
92 if tools.is_headless() or net.get_oauth_config().disabled:
93 return None
94 server_url = normalize_host_url(server_url)
95 service = AuthService(server_url)
96 try:
97 service.login(False)
98 except IOError:
99 raise ValueError('Failed to contact %s' % server_url)
100 try:
101 identity = service.get_current_identity()
102 except AuthServiceError:
103 raise ValueError('Failed to fetch identify from %s' % server_url)
104 if identity == 'anonymous:anonymous':
105 raise ValueError(
106 'Please login to %s: \n'
107 ' python auth.py login --service=%s' % (server_url, server_url))
108 email = identity.split(':')[1]
109 logging.info('Logged in to %s: %s', server_url, email)
110 return email
111
112
113@subcommand.usage('[options]')
114def CMDlogin(parser, args):
115 """Runs interactive login flow and stores auth token/cookie on disk."""
116 (options, args) = parser.parse_args(args)
117 process_auth_options(parser, options)
118 service = AuthService(options.service)
119 if service.login(True):
120 print 'Logged in as \'%s\'.' % service.get_current_identity()
121 return 0
122 else:
123 print 'Login failed or canceled.'
124 return 1
125
126
127@subcommand.usage('[options]')
128def CMDlogout(parser, args):
129 """Purges cached auth token/cookie."""
130 (options, args) = parser.parse_args(args)
131 process_auth_options(parser, options)
132 service = AuthService(options.service)
133 service.logout()
134 return 0
135
136
137@subcommand.usage('[options]')
138def CMDcheck(parser, args):
139 """Shows identity associated with currently cached auth token/cookie."""
140 (options, args) = parser.parse_args(args)
141 process_auth_options(parser, options)
142 service = AuthService(options.service)
143 service.login(False)
144 print service.get_current_identity()
145 return 0
146
147
148class OptionParserAuth(logging_utils.OptionParserWithLogging):
149 def __init__(self, **kwargs):
150 logging_utils.OptionParserWithLogging.__init__(
151 self, prog='auth.py', **kwargs)
152 self.server_group = optparse.OptionGroup(self, 'Server')
153 self.server_group.add_option(
154 '-S', '--service',
155 metavar='URL', default='',
156 help='Service to use')
157 self.add_option_group(self.server_group)
158 add_auth_options(self)
159
160 def parse_args(self, *args, **kwargs):
161 options, args = logging_utils.OptionParserWithLogging.parse_args(
162 self, *args, **kwargs)
163 if not options.service:
164 self.error('--service is required.')
165 try:
166 options.service = normalize_host_url(options.service)
167 except ValueError as exc:
168 self.error(str(exc))
169 on_error.report_on_exception_exit(options.service)
170 return options, args
171
172
173def main(args):
174 dispatcher = subcommand.CommandDispatcher(__name__)
175 return dispatcher.execute(OptionParserAuth(version=__version__), args)
176
177
178if __name__ == '__main__':
179 subprocess42.inhibit_os_error_reporting()
180 fix_encoding.fix_encoding()
181 tools.disable_buffering()
182 colorama.init()
183 sys.exit(main(sys.argv[1:]))