Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(79)

Side by Side Diff: rietveld.py

Issue 183793010: Added OAuth2 authentication to apply_issue (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Added another option Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # coding: utf-8 1 # coding: utf-8
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 """Defines class Rietveld to easily access a rietveld instance. 5 """Defines class Rietveld to easily access a rietveld instance.
6 6
7 Security implications: 7 Security implications:
8 8
9 The following hypothesis are made: 9 The following hypothesis are made:
10 - Rietveld enforces: 10 - Rietveld enforces:
11 - Nobody else than issue owner can upload a patch set 11 - Nobody else than issue owner can upload a patch set
12 - Verifies the issue owner credentials when creating new issues 12 - Verifies the issue owner credentials when creating new issues
13 - A issue owner can't change once the issue is created 13 - A issue owner can't change once the issue is created
14 - A patch set cannot be modified 14 - A patch set cannot be modified
15 """ 15 """
16 16
17 import copy 17 import copy
18 import json 18 import json
19 import logging 19 import logging
20 import re 20 import re
21 import ssl 21 import ssl
22 import time 22 import time
23 import urllib
23 import urllib2 24 import urllib2
24 25
26 import patch
27
25 from third_party import upload 28 from third_party import upload
26 import patch 29 from third_party.oauth2client.client import SignedJwtAssertionCredentials
30 from third_party import httplib2
27 31
28 # Hack out upload logging.info() 32 # Hack out upload logging.info()
29 upload.logging = logging.getLogger('upload') 33 upload.logging = logging.getLogger('upload')
30 # Mac pylint choke on this line. 34 # Mac pylint choke on this line.
31 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103 35 upload.logging.setLevel(logging.WARNING) # pylint: disable=E1103
32 36
33 37
34 class Rietveld(object): 38 class Rietveld(object):
35 """Accesses rietveld.""" 39 """Accesses rietveld."""
36 def __init__(self, url, email, password, extra_headers=None): 40 def __init__(self, url, email, password, extra_headers=None):
37 self.url = url.rstrip('/') 41 self.url = url.rstrip('/')
38 # TODO(maruel): It's not awesome but maybe necessary to retrieve the value. 42 # TODO(maruel): It's not awesome but maybe necessary to retrieve the value.
39 # It happens when the presubmit check is ran out of process, the cookie 43 # It happens when the presubmit check is ran out of process, the cookie
40 # needed to be recreated from the credentials. Instead, it should pass the 44 # needed to be recreated from the credentials. Instead, it should pass the
41 # email and the cookie. 45 # email and the cookie.
42 self.email = email
43 self.password = password
44 if email and password: 46 if email and password:
45 get_creds = lambda: (email, password) 47 get_creds = lambda: (email, password)
46 self.rpc_server = upload.HttpRpcServer( 48 self.rpc_server = upload.HttpRpcServer(
47 self.url, 49 self.url,
48 get_creds, 50 get_creds,
49 extra_headers=extra_headers or {}) 51 extra_headers=extra_headers or {})
50 else: 52 else:
51 if email == '': 53 if email == '':
52 # If email is given as an empty string, then assume we want to make 54 # If email is given as an empty string, then assume we want to make
53 # requests that do not need authentication. Bypass authentication by 55 # requests that do not need authentication. Bypass authentication by
(...skipping 377 matching lines...) Expand 10 before | Expand all | Expand 10 after
431 raise 433 raise
432 # If reaching this line, loop again. Uses a small backoff. 434 # If reaching this line, loop again. Uses a small backoff.
433 time.sleep(1+maxtries*2) 435 time.sleep(1+maxtries*2)
434 finally: 436 finally:
435 upload.ErrorExit = old_error_exit 437 upload.ErrorExit = old_error_exit
436 438
437 # DEPRECATED. 439 # DEPRECATED.
438 Send = get 440 Send = get
439 441
440 442
443 class OAuthRpcServer():
M-A Ruel 2014/03/13 01:31:27 (object)
pgervais 2014/03/13 22:38:38 Good catch, thanks.
444 def __init__(self,
445 host,
446 client_id,
447 client_private_key,
448 private_key_password='notasecret',
449 user_agent=None,
450 timeout=None,
451 extra_headers=None):
452 """Wrapper around httplib2.Http() that handles authentication.
453
454 client_id: client id for service account
455 client_private_key: encrypted private key, as a string
456 private_key_password: password used to decrypt the private key
457 """
458
459 # Enforce https
460 host_parts = host.split('://')
M-A Ruel 2014/03/13 01:31:27 that's unorthodox.
Vadim Sh. 2014/03/13 18:28:42 Yeah.. Use urlparse module.
pgervais 2014/03/13 22:38:38 That's better indeed. But calling split() does exa
461 if host_parts[0] == 'https': # fine
462 self.host = host
463
464 elif len(host_parts) == 1: # no protocol specified
465 self.host = 'https://' + host
466
467 elif len(host_parts) == 2 and host_parts[0] == 'http':
468 upload.logging.warning('Changing protocol to https')
469 self.host = 'https://' + host_parts[1]
470
471 else:
472 msg = 'Invalid url provided: %s' % host
473 upload.logging.error(msg)
474 raise ValueError(msg)
475
476 if self.host.endswith('/'):
Vadim Sh. 2014/03/13 18:28:42 self.host.rstrip('/')
pgervais 2014/03/13 22:38:38 Done.
477 self.host = self.host[:-1]
478
479 self.extra_headers = extra_headers or {}
480 creds = SignedJwtAssertionCredentials(
481 client_id,
482 client_private_key,
483 'https://www.googleapis.com/auth/userinfo.email',
484 private_key_password,
485 user_agent=user_agent)
486
487 http = httplib2.Http(timeout=timeout)
488 self._http = creds.authorize(http)
Vadim Sh. 2014/03/13 18:28:42 It makes the network call here, right? How apply_i
pgervais 2014/03/13 22:38:38 This only decorates the http object, no network ac
489
M-A Ruel 2014/03/13 01:31:27 remove one line
pgervais 2014/03/13 22:38:38 Done.
490
491 def Send(self,
492 request_path,
493 payload=None,
494 content_type="application/octet-stream",
M-A Ruel 2014/03/13 01:31:27 use single quote throughout.
pgervais 2014/03/13 22:38:38 I don't know why, but I find it horribly difficult
495 timeout=None,
496 extra_headers=None,
497 **kwargs):
498 """Send a POST or GET request to the server.
499
500 Args:
501 request_path: path on the server to hit. This is concatenated with the
M-A Ruel 2014/03/13 01:31:27 Align +2
pgervais 2014/03/13 22:38:38 Done.
502 value of 'host' provided to the constructor.
503 payload: request is a POST if not None, GET otherwise
504 timeout: in seconds
505 extra_headers: (dict)
506 """
507 # This method signature should match upload.py:AbstractRpcServer.Send()
508 method = 'GET'
509
510 if extra_headers:
511 headers = self.extra_headers.copy()
512 headers.update(headers)
M-A Ruel 2014/03/13 01:31:27 ugh?
pgervais 2014/03/13 22:38:38 It took me some time to recall what I meant here :
513 else:
514 headers = self.extra_headers
M-A Ruel 2014/03/13 01:31:27 I'd do the copy unconditionally for simplicity; it
pgervais 2014/03/13 22:38:38 Done.
515
516 if payload is not None:
517 method = 'POST'
518 headers['Content-Type'] = content_type
519 raise NotImplementedError('POST requests are not yet supported.')
520
M-A Ruel 2014/03/13 01:31:27 never 2 empty lines inside a function
pgervais 2014/03/13 22:38:38 Done.
521
522 prev_timeout = self._http.timeout
523 try:
524 if timeout:
525 self._http.timeout = timeout
M-A Ruel 2014/03/13 01:31:27 That's a tad surprising.
526 # TODO(pgervais) implement some kind of retry mechanism (see upload.py).
527 url = self.host + request_path
528 print('Trying URL: ' + url)
M-A Ruel 2014/03/13 01:31:27 Remove.
529 args = dict(kwargs)
M-A Ruel 2014/03/13 01:31:27 It's a dict already.
pgervais 2014/03/13 22:38:38 I copy-pasted part of this code from upload.py, an
530 if args:
531 url += "?" + urllib.urlencode(args)
532
533 ret = self._http.request(url,
534 method=method,
535 body=payload,
536 headers=headers)
537 return ret[1]
538
539 finally:
540 self._http.timeout = prev_timeout
M-A Ruel 2014/03/13 01:31:27 Not a fan.
pgervais 2014/03/13 22:38:38 I have no strong opinion on that matter. I'm fine
541
542
543 class OAuthRietveld(Rietveld):
M-A Ruel 2014/03/13 01:31:27 But it's really JwtOAuth2Rietveld. Vadim can confi
Vadim Sh. 2014/03/13 18:28:42 I don't have preferred semantic, but I agree that
pgervais 2014/03/13 22:38:38 Done.
544 """Access to Rietveld using OAuth authentication.
545
546 This class is supposed to be used only by bots, since this kind of
547 access is restricted to service accounts.
548 """
549 # The parent__init__ is not called on purpose.
550 def __init__(self, # pylint: disable=W0231
Vadim Sh. 2014/03/13 18:28:42 Put pylint disable after ':'.
pgervais 2014/03/13 22:38:38 Done.
551 url,
552 client_id,
553 client_private_key_file,
554 private_key_password='notasecret',
555 extra_headers=None):
556 self.url = url.rstrip('/')
557 with open(client_private_key_file, 'rb') as f:
558 client_private_key = f.read()
559 self.rpc_server = OAuthRpcServer(url,
560 client_id,
561 client_private_key,
562 private_key_password=private_key_password,
563 extra_headers=extra_headers or {})
564 self._xsrf_token = None
565 self._xsrf_token_time = None
566
567
441 class CachingRietveld(Rietveld): 568 class CachingRietveld(Rietveld):
442 """Caches the common queries. 569 """Caches the common queries.
443 570
444 Not to be used in long-standing processes, like the commit queue. 571 Not to be used in long-standing processes, like the commit queue.
445 """ 572 """
446 def __init__(self, *args, **kwargs): 573 def __init__(self, *args, **kwargs):
447 super(CachingRietveld, self).__init__(*args, **kwargs) 574 super(CachingRietveld, self).__init__(*args, **kwargs)
448 self._cache = {} 575 self._cache = {}
449 576
450 def _lookup(self, function_name, args, update): 577 def _lookup(self, function_name, args, update):
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after
559 def trigger_try_jobs( # pylint:disable=R0201 686 def trigger_try_jobs( # pylint:disable=R0201
560 self, issue, patchset, reason, clobber, revision, builders_and_tests, 687 self, issue, patchset, reason, clobber, revision, builders_and_tests,
561 master=None): 688 master=None):
562 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % 689 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' %
563 (builders_and_tests, issue)) 690 (builders_and_tests, issue))
564 691
565 def trigger_distributed_try_jobs( # pylint:disable=R0201 692 def trigger_distributed_try_jobs( # pylint:disable=R0201
566 self, issue, patchset, reason, clobber, revision, masters): 693 self, issue, patchset, reason, clobber, revision, masters):
567 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' % 694 logging.info('ReadOnlyRietveld: triggering try jobs %r for issue %d' %
568 (masters, issue)) 695 (masters, issue))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698