| OLD | NEW |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import collections | 5 import collections |
| 6 import logging | 6 import logging |
| 7 import threading | 7 import threading |
| 8 import time | 8 import time |
| 9 | 9 |
| 10 from utils import auth_server | 10 from utils import auth_server |
| 11 | 11 |
| 12 import file_reader | 12 import file_reader |
| 13 | 13 |
| 14 | 14 |
| 15 class AuthSystemError(Exception): | 15 class AuthSystemError(Exception): |
| 16 """Fatal errors raised by AuthSystem class.""" | 16 """Fatal errors raised by AuthSystem class.""" |
| 17 | 17 |
| 18 | 18 |
| 19 # Parsed value of JSON at path specified by --auth-params-file task_runner arg. | 19 # Parsed value of JSON at path specified by --auth-params-file task_runner arg. |
| 20 AuthParams = collections.namedtuple('AuthParams', [ | 20 AuthParams = collections.namedtuple('AuthParams', [ |
| 21 # Dict with HTTP headers to use when calling Swarming backend (specifically). | 21 # Dict with HTTP headers to use when calling Swarming backend (specifically). |
| 22 # They identify the bot to the Swarming backend. Ultimately generated by | 22 # They identify the bot to the Swarming backend. Ultimately generated by |
| 23 # 'get_authentication_headers' in bot_config.py. | 23 # 'get_authentication_headers' in bot_config.py. |
| 24 'swarming_http_headers', | 24 'swarming_http_headers', |
| 25 | 25 |
| 26 # Unix timestamp of when swarming_http_headers expire, or 0 if unknown. | 26 # Unix timestamp of when swarming_http_headers expire, or 0 if unknown. |
| 27 'swarming_http_headers_exp', | 27 'swarming_http_headers_exp', |
| 28 | 28 |
| 29 # Indicates the service account the task runs as. One of: | 29 # Indicates the service account to use for internal bot processes. One of: |
| 30 # - 'none' if the task shouldn't use any authentication at all. | 30 # - 'none' to not use authentication at all. |
| 31 # - 'bot' if the task should use bot's own service account. | 31 # - 'bot' to use whatever bot is using to authenticate itself to Swarming. |
| 32 # - <email> if the task is using service acccount via delegation token. | 32 # - <email> to get tokens through API calls to Swarming. |
| 33 'system_service_account', |
| 34 |
| 35 # Indicates the service account the task runs as. Same range of values as for |
| 36 # 'system_service_account'. |
| 37 # |
| 38 # It is distinct from 'system_service_account' to allow user-supplied payloads |
| 39 # to use a service account also supplied by the user (and not the one used |
| 40 # internally by the bot). |
| 33 'task_service_account', | 41 'task_service_account', |
| 34 ]) | 42 ]) |
| 35 | 43 |
| 36 | 44 |
| 37 def prepare_auth_params_json(bot, manifest): | 45 def prepare_auth_params_json(bot, manifest): |
| 38 """Returns a dict to put into JSON file passed to task_runner. | 46 """Returns a dict to put into JSON file passed to task_runner. |
| 39 | 47 |
| 40 This JSON file contains various tokens and configuration parameters that allow | 48 This JSON file contains various tokens and configuration parameters that allow |
| 41 task_runner to make HTTP calls authenticated by bot's own credentials. | 49 task_runner to make HTTP calls authenticated by bot's own credentials. |
| 42 | 50 |
| 43 The file is managed by bot_main.py (main Swarming bot process) and consumed by | 51 The file is managed by bot_main.py (main Swarming bot process) and consumed by |
| 44 task_runner.py. | 52 task_runner.py. |
| 45 | 53 |
| 46 It lives it the task work directory. | 54 It lives it the task work directory. |
| 47 | 55 |
| 48 Args: | 56 Args: |
| 49 bot: instance of bot.Bot. | 57 bot: instance of bot.Bot. |
| 50 manifest: dict with the task manifest, as generated by the backend in /poll. | 58 manifest: dict with the task manifest, as generated by the backend in /poll. |
| 51 """ | 59 """ |
| 60 def account(acc_id): |
| 61 acc = (manifest.get('service_accounts') or {}).get(acc_id) or {} |
| 62 return acc.get('service_account') or 'none' |
| 52 return { | 63 return { |
| 53 'swarming_http_headers': bot.remote.get_authentication_headers(), | 64 'swarming_http_headers': bot.remote.get_authentication_headers(), |
| 54 'swarming_http_headers_exp': bot.remote.authentication_headers_expiration, | 65 'swarming_http_headers_exp': bot.remote.authentication_headers_expiration, |
| 55 'task_service_account': manifest.get('service_account') or 'none', | 66 'system_service_account': account('system'), |
| 67 'task_service_account': account('task'), |
| 56 } | 68 } |
| 57 | 69 |
| 58 | 70 |
| 59 def process_auth_params_json(val): | 71 def process_auth_params_json(val): |
| 60 """Takes a dict loaded from auth params JSON file and validates it. | 72 """Takes a dict loaded from auth params JSON file and validates it. |
| 61 | 73 |
| 62 Args: | 74 Args: |
| 63 val: decoded JSON value read from auth params JSON file. | 75 val: decoded JSON value read from auth params JSON file. |
| 64 | 76 |
| 65 Returns: | 77 Returns: |
| (...skipping 13 matching lines...) Expand all Loading... |
| 79 exp = val.get('swarming_http_headers_exp') or 0 | 91 exp = val.get('swarming_http_headers_exp') or 0 |
| 80 if not isinstance(exp, (int, long)): | 92 if not isinstance(exp, (int, long)): |
| 81 raise ValueError( | 93 raise ValueError( |
| 82 'Expecting "swarming_http_headers_exp" to be int, got %r' % (exp,)) | 94 'Expecting "swarming_http_headers_exp" to be int, got %r' % (exp,)) |
| 83 | 95 |
| 84 # The headers must be ASCII for sure, so don't bother with picking the | 96 # The headers must be ASCII for sure, so don't bother with picking the |
| 85 # correct unicode encoding, default would work. If not, it'll raise | 97 # correct unicode encoding, default would work. If not, it'll raise |
| 86 # UnicodeEncodeError, which is subclass of ValueError. | 98 # UnicodeEncodeError, which is subclass of ValueError. |
| 87 headers = {str(k): str(v) for k, v in headers.iteritems()} | 99 headers = {str(k): str(v) for k, v in headers.iteritems()} |
| 88 | 100 |
| 89 acc = val.get('task_service_account') or 'none' | 101 def read_account(key): |
| 90 if not isinstance(acc, basestring): | 102 acc = val.get(key) or 'none' |
| 91 raise ValueError( | 103 if not isinstance(acc, basestring): |
| 92 'Expecting "task_service_account" to be a string, got %r' % (acc,)) | 104 raise ValueError('Expecting "%s" to be a string, got %r' % (key, acc)) |
| 105 return str(acc) |
| 93 | 106 |
| 94 return AuthParams(headers, exp, str(acc)) | 107 return AuthParams( |
| 108 swarming_http_headers=headers, |
| 109 swarming_http_headers_exp=exp, |
| 110 system_service_account=read_account('system_service_account'), |
| 111 task_service_account=read_account('task_service_account')) |
| 95 | 112 |
| 96 | 113 |
| 97 class AuthSystem(object): | 114 class AuthSystem(object): |
| 98 """Authentication subsystem used by task_runner. | 115 """Authentication subsystem used by task_runner. |
| 99 | 116 |
| 100 Contains two threads: | 117 Contains two threads: |
| 101 * One thread periodically rereads the file with bots own authentication | 118 * One thread periodically rereads the file with bots own authentication |
| 102 information (auth_params_file). This file is generated by bot_main. | 119 information (auth_params_file). This file is generated by bot_main. |
| 103 * Another thread hosts local HTTP server that servers authentication tokens | 120 * Another thread hosts local HTTP server that servers authentication tokens |
| 104 to local processes. This is enabled only if the task is running in a | 121 to local processes. This is enabled only if the task is running in a |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 147 except file_reader.FatalReadError as e: | 164 except file_reader.FatalReadError as e: |
| 148 raise AuthSystemError('Cannot start FileReaderThread: %s' % e) | 165 raise AuthSystemError('Cannot start FileReaderThread: %s' % e) |
| 149 | 166 |
| 150 # Initial validation. | 167 # Initial validation. |
| 151 try: | 168 try: |
| 152 params = process_auth_params_json(reader.last_value) | 169 params = process_auth_params_json(reader.last_value) |
| 153 except ValueError as e: | 170 except ValueError as e: |
| 154 reader.stop() | 171 reader.stop() |
| 155 raise AuthSystemError('Cannot parse bot_auth_params.json: %s' % e) | 172 raise AuthSystemError('Cannot parse bot_auth_params.json: %s' % e) |
| 156 | 173 |
| 157 # If using task auth, launch local HTTP server that serves tokens (let OS | 174 logging.info('Using following service accounts:') |
| 158 # assign the port). | 175 logging.info(' system: %s', params.system_service_account) |
| 176 logging.info(' task: %s', params.task_service_account) |
| 177 |
| 178 # If using service accounts, launch local HTTP server that serves tokens |
| 179 # (let OS assign the port). |
| 180 # |
| 181 # TODO(vadimsh): Launch local auth server if using 'system' account (or both |
| 182 # 'system' and 'task') too. This can be done only once all processes that |
| 183 # inherit LUCI_CONTEXT know about 'system' and 'task' accounts distinction. |
| 159 server = None | 184 server = None |
| 160 local_auth_context = None | 185 local_auth_context = None |
| 161 if params.task_service_account != 'none': | 186 if params.task_service_account != 'none': |
| 162 try: | 187 try: |
| 163 server = auth_server.LocalAuthServer() | 188 server = auth_server.LocalAuthServer() |
| 164 local_auth_context = server.start(token_provider=self) | 189 local_auth_context = server.start(token_provider=self) |
| 165 except Exception as exc: | 190 except Exception as exc: |
| 166 reader.stop() # cleanup | 191 reader.stop() # cleanup |
| 167 raise AuthSystemError('Failed to start local auth server - %s' % exc) | 192 raise AuthSystemError('Failed to start local auth server - %s' % exc) |
| 168 | 193 |
| (...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 270 # Default to some safe small expiration in case bot_main doesn't report it | 295 # Default to some safe small expiration in case bot_main doesn't report it |
| 271 # to us. This may happen if get_authentication_header bot hook is not | 296 # to us. This may happen if get_authentication_header bot hook is not |
| 272 # reporting expiration time. | 297 # reporting expiration time. |
| 273 exp = auth_params.swarming_http_headers_exp or (time.time() + 4*60) | 298 exp = auth_params.swarming_http_headers_exp or (time.time() + 4*60) |
| 274 logging.info('Bot token expires in %d sec', exp - time.time()) | 299 logging.info('Bot token expires in %d sec', exp - time.time()) |
| 275 | 300 |
| 276 # TODO(vadimsh): For GCE bots specifically we can pass a list of OAuth | 301 # TODO(vadimsh): For GCE bots specifically we can pass a list of OAuth |
| 277 # scopes granted to the GCE token and verify it contains all the requested | 302 # scopes granted to the GCE token and verify it contains all the requested |
| 278 # scopes. | 303 # scopes. |
| 279 return auth_server.AccessToken(tok, exp) | 304 return auth_server.AccessToken(tok, exp) |
| OLD | NEW |