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 |