| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The LUCI Authors. All rights reserved. | 2 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
| 5 | 5 |
| 6 import base64 | 6 import base64 |
| 7 import BaseHTTPServer | 7 import BaseHTTPServer |
| 8 import collections | 8 import collections |
| 9 import json | 9 import json |
| 10 import logging | 10 import logging |
| 11 import os | 11 import os |
| 12 import re | 12 import re |
| 13 import SocketServer | 13 import SocketServer |
| 14 import sys | 14 import sys |
| 15 import threading | 15 import threading |
| 16 import time | 16 import time |
| 17 | 17 |
| 18 | 18 |
| 19 # OAuth access token with its expiration time. | 19 # OAuth access token with its expiration time. |
| 20 AccessToken = collections.namedtuple('AccessToken', [ | 20 AccessToken = collections.namedtuple('AccessToken', [ |
| 21 'access_token', # urlsafe str with the token | 21 'access_token', # urlsafe str with the token |
| 22 'expiry', # expiration time as unix timestamp in seconds | 22 'expiry', # expiration time as unix timestamp in seconds |
| 23 ]) | 23 ]) |
| 24 | 24 |
| 25 | 25 |
| 26 class TokenError(Exception): | 26 class TokenError(Exception): |
| 27 """Raised by TokenProvider if the token can't be created. | 27 """Raised by TokenProvider if the token can't be created (fatal error). |
| 28 | 28 |
| 29 See TokenProvider docs for more info. | 29 See TokenProvider docs for more info. |
| 30 """ | 30 """ |
| 31 | 31 |
| 32 def __init__(self, code, msg, fatal=False): | 32 def __init__(self, code, msg): |
| 33 super(TokenError, self).__init__(msg) | 33 super(TokenError, self).__init__(msg) |
| 34 self.code = code | 34 self.code = code |
| 35 self.fatal = fatal | |
| 36 | 35 |
| 37 | 36 |
| 38 class RPCError(Exception): | 37 class RPCError(Exception): |
| 39 """Raised by LocalAuthServer RPC handlers to reply with HTTP error status.""" | 38 """Raised by LocalAuthServer RPC handlers to reply with HTTP error status.""" |
| 40 | 39 |
| 41 def __init__(self, code, msg): | 40 def __init__(self, code, msg): |
| 42 super(RPCError, self).__init__(msg) | 41 super(RPCError, self).__init__(msg) |
| 43 self.code = code | 42 self.code = code |
| 44 | 43 |
| 45 | 44 |
| 46 class TokenProvider(object): | 45 class TokenProvider(object): |
| 47 """Interface for an object that can create OAuth tokens on demand. | 46 """Interface for an object that can create OAuth tokens on demand. |
| 48 | 47 |
| 49 Defined as a concrete class only for documentation purposes. | 48 Defined as a concrete class only for documentation purposes. |
| 50 """ | 49 """ |
| 51 | 50 |
| 52 def generate_token(self, account_id, scopes): | 51 def generate_token(self, account_id, scopes): |
| 53 """Generates a new access token with given scopes. | 52 """Generates a new access token with given scopes. |
| 54 | 53 |
| 55 Will be called from multiple threads (possibly concurrently) whenever | 54 Will be called from multiple threads (possibly concurrently) whenever |
| 56 LocalAuthServer needs to refresh a token with particular scopes. | 55 LocalAuthServer needs to refresh a token with particular scopes. |
| 57 | 56 |
| 58 Can rise RPCError exceptions. They will be immediately converted to | 57 Can rise RPCError exceptions. They will be immediately converted to |
| 59 corresponding RPC error replies (e.g. HTTP 500). This is appropriate for | 58 corresponding RPC error replies (e.g. HTTP 500). This is appropriate for |
| 60 low-level or transient errors. | 59 low-level or transient errors. |
| 61 | 60 |
| 62 Can also raise TokenError. It will be converted to GetOAuthToken reply with | 61 Can also raise TokenError. It will be converted to GetOAuthToken reply with |
| 63 non-zero error_code. It will also optionally be cached, so that the provider | 62 non-zero error_code. It will also be cached, so that the provider would |
| 64 would never be called again for the same set of scopes. This is appropriate | 63 never be called again for the same set of scopes. This is appropriate for |
| 65 for high-level or fatal errors. | 64 high-level fatal errors. |
| 66 | 65 |
| 67 Returns AccessToken on success. | 66 Returns AccessToken on success. |
| 68 """ | 67 """ |
| 69 raise NotImplementedError() | 68 raise NotImplementedError() |
| 70 | 69 |
| 71 | 70 |
| 72 class LocalAuthServer(object): | 71 class LocalAuthServer(object): |
| 73 """LocalAuthServer handles /rpc/LuciLocalAuthService.* requests. | 72 """LocalAuthServer handles /rpc/LuciLocalAuthService.* requests. |
| 74 | 73 |
| 75 It exposes an HTTP JSON RPC API that is used by task processes to grab an | 74 It exposes an HTTP JSON RPC API that is used by task processes to grab an |
| (...skipping 168 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 244 # Do the refresh outside of the RPC server lock to unblock other clients | 243 # Do the refresh outside of the RPC server lock to unblock other clients |
| 245 # that are hitting the cache. The token provider should implement its own | 244 # that are hitting the cache. The token provider should implement its own |
| 246 # synchronization. | 245 # synchronization. |
| 247 if need_refresh: | 246 if need_refresh: |
| 248 try: | 247 try: |
| 249 tok_or_err = token_provider.generate_token(account_id, scopes) | 248 tok_or_err = token_provider.generate_token(account_id, scopes) |
| 250 assert isinstance(tok_or_err, AccessToken), tok_or_err | 249 assert isinstance(tok_or_err, AccessToken), tok_or_err |
| 251 except TokenError as exc: | 250 except TokenError as exc: |
| 252 tok_or_err = exc | 251 tok_or_err = exc |
| 253 # Cache the token or fatal errors (to avoid useless retry later). | 252 # Cache the token or fatal errors (to avoid useless retry later). |
| 254 if isinstance(tok_or_err, AccessToken) or tok_or_err.fatal: | 253 with self._lock: |
| 255 with self._lock: | 254 if not self._server: |
| 256 if not self._server: | 255 raise RPCError(503, 'Stopped already.') |
| 257 raise RPCError(503, 'Stopped already.') | 256 self._cache[cache_key] = tok_or_err |
| 258 self._cache[cache_key] = tok_or_err | |
| 259 | 257 |
| 260 # Done. | 258 # Done. |
| 261 if isinstance(tok_or_err, AccessToken): | 259 if isinstance(tok_or_err, AccessToken): |
| 262 return { | 260 return { |
| 263 'access_token': tok_or_err.access_token, | 261 'access_token': tok_or_err.access_token, |
| 264 'expiry': int(tok_or_err.expiry), | 262 'expiry': int(tok_or_err.expiry), |
| 265 } | 263 } |
| 266 if isinstance(tok_or_err, TokenError): | 264 if isinstance(tok_or_err, TokenError): |
| 267 return { | 265 return { |
| 268 'error_code': tok_or_err.code, | 266 'error_code': tok_or_err.code, |
| (...skipping 153 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 422 while True: | 420 while True: |
| 423 time.sleep(1) | 421 time.sleep(1) |
| 424 except KeyboardInterrupt: | 422 except KeyboardInterrupt: |
| 425 pass | 423 pass |
| 426 finally: | 424 finally: |
| 427 server.stop() | 425 server.stop() |
| 428 | 426 |
| 429 | 427 |
| 430 if __name__ == '__main__': | 428 if __name__ == '__main__': |
| 431 testing_main() | 429 testing_main() |
| OLD | NEW |