| 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 threading |
| 7 |
| 8 import file_reader |
| 9 |
| 10 |
| 11 class AuthSystemError(Exception): |
| 12 """Fatal errors raised by AuthSystem class.""" |
| 6 | 13 |
| 7 | 14 |
| 8 # Parsed value of JSON at path specified by --auth-params-file task_runner arg. | 15 # Parsed value of JSON at path specified by --auth-params-file task_runner arg. |
| 9 AuthParams = collections.namedtuple('AuthParams', [ | 16 AuthParams = collections.namedtuple('AuthParams', [ |
| 10 # Dict with HTTP headers to use when calling Swarming backend (specifically). | 17 # Dict with HTTP headers to use when calling Swarming backend (specifically). |
| 11 # They identify the bot to the Swarming backend. Ultimately generated by | 18 # They identify the bot to the Swarming backend. Ultimately generated by |
| 12 # 'get_authentication_headers' in bot_config.py. | 19 # 'get_authentication_headers' in bot_config.py. |
| 13 'swarming_http_headers', | 20 'swarming_http_headers', |
| 14 ]) | 21 ]) |
| 15 | 22 |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 52 if not isinstance(headers, dict): | 59 if not isinstance(headers, dict): |
| 53 raise ValueError( | 60 raise ValueError( |
| 54 'Expecting "swarming_http_headers" to be dict, got %r' % (headers,)) | 61 'Expecting "swarming_http_headers" to be dict, got %r' % (headers,)) |
| 55 | 62 |
| 56 # The headers must be ASCII for sure, so don't bother with picking the | 63 # The headers must be ASCII for sure, so don't bother with picking the |
| 57 # correct unicode encoding, default would work. If not, it'll raise | 64 # correct unicode encoding, default would work. If not, it'll raise |
| 58 # UnicodeEncodeError, which is subclass of ValueError. | 65 # UnicodeEncodeError, which is subclass of ValueError. |
| 59 headers = {str(k): str(v) for k, v in headers.iteritems()} | 66 headers = {str(k): str(v) for k, v in headers.iteritems()} |
| 60 | 67 |
| 61 return AuthParams(headers) | 68 return AuthParams(headers) |
| 69 |
| 70 |
| 71 class AuthSystem(object): |
| 72 """Authentication subsystem used by task_runner. |
| 73 |
| 74 Contains two threads: |
| 75 * One thread periodically rereads the file with bots own authentication |
| 76 information (auth_params_file). This file is generated by bot_main. |
| 77 * Another thread hosts local HTTP server that servers authentication tokens |
| 78 to local processes. |
| 79 |
| 80 The local HTTP server exposes /prpc/LuciLocalAuthService.GetOAuthToken |
| 81 endpoint that the processes running inside Swarming tasks can use to request |
| 82 an OAuth access token associated with the task. |
| 83 |
| 84 They can discover the port to connect to by looking at LUCI_CONTEXT |
| 85 environment variable. |
| 86 |
| 87 TODO(vadimsh): Actually implement the second thread and LUCI_CONTEXT. |
| 88 """ |
| 89 |
| 90 def __init__(self): |
| 91 self._auth_params_reader = None |
| 92 self._lock = threading.Lock() |
| 93 |
| 94 def start(self, auth_params_file): |
| 95 """Grabs initial bot auth headers and starts all auth related threads. |
| 96 |
| 97 Args: |
| 98 auth_params_file: path to a file with AuthParams dict, update by bot_main. |
| 99 |
| 100 Raises: |
| 101 AuthSystemError on fatal errors. |
| 102 """ |
| 103 assert not self._auth_params_reader, 'already running' |
| 104 try: |
| 105 # Read headers more often than bot_main writes them (which is 60 sec), to |
| 106 # reduce maximum possible latency between header updates and reads. Use |
| 107 # interval that isn't a divisor of 60 to avoid reads and writes happening |
| 108 # at the same moment in time. |
| 109 reader = file_reader.FileReaderThread(auth_params_file, interval_sec=53) |
| 110 reader.start() |
| 111 except file_reader.FatalReadError as e: |
| 112 raise AuthSystemError('Cannot start FileReaderThread: %s' % e) |
| 113 |
| 114 # Initial validation. |
| 115 try: |
| 116 process_auth_params_json(reader.last_value) |
| 117 except ValueError as e: |
| 118 reader.stop() |
| 119 raise AuthSystemError('Cannot parse bot_auth_params.json: %s' % e) |
| 120 |
| 121 # Good to go. |
| 122 with self._lock: |
| 123 self._auth_params_reader = reader |
| 124 |
| 125 def stop(self): |
| 126 """Shuts down all the threads if they are running.""" |
| 127 with self._lock: |
| 128 reader = self._auth_params_reader |
| 129 self._auth_params_reader = None |
| 130 if reader: |
| 131 reader.stop() |
| 132 |
| 133 @property |
| 134 def bot_headers(self): |
| 135 """A dict with HTTP headers that contain bots own credentials. |
| 136 |
| 137 Such headers can be sent to Swarming server's /bot/* API. Must be used only |
| 138 after 'start' and before 'stop'. |
| 139 |
| 140 Raises: |
| 141 AuthSystemError if auth_params_file is suddenly no longer valid. |
| 142 """ |
| 143 with self._lock: |
| 144 assert self._auth_params_reader, '"start" was not called' |
| 145 raw_val = self._auth_params_reader.last_value |
| 146 try: |
| 147 val = process_auth_params_json(raw_val) |
| 148 return val.swarming_http_headers |
| 149 except ValueError as e: |
| 150 raise AuthSystemError('Cannot parse bot_auth_params.json: %s' % e) |
| OLD | NEW |