summaryrefslogtreecommitdiffstats
path: root/project.py
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2016-08-15 21:23:44 -0400
committerMike Frysinger <vapier@google.com>2016-08-16 13:02:52 -0400
commit40252c20f75188147558c0135bf71de907e01442 (patch)
treed20ffbc8160430782680e10ce121b678cdc8e940 /project.py
parent69297c1b771bbbd05b63e965a524de6860d15d8c (diff)
downloadgit-repo-40252c20f75188147558c0135bf71de907e01442.tar.gz
RepoHook: allow users to approve hooks via manifests
The constant prompting when registered hooks change can be tedious and has a large multiplication factor when the project is large (e.g. the AOSP). It gets worse as people want to write more checks, hooks, docs, and tests (or fix bugs), but every CL that goes in will trigger a new prompt to approve. Let's tweak our trust model when it comes to hooks. Since people start off by calling `repo init` with a URL to a manifest, and that manifest defines all the hooks, anchor trust in that. This requires that we get the manifest over a trusted link (e.g. https or ssh) so that it can't be MITM-ed. If the user chooses to use an untrusted link (e.g. git or http), then we'll fallback to the existing hash based approval. Bug: Issue 226 Change-Id: I77be9e4397383f264fcdaefb582e345ea4069a13
Diffstat (limited to 'project.py')
-rw-r--r--project.py103
1 files changed, 81 insertions, 22 deletions
diff --git a/project.py b/project.py
index 918ee09c..7f9a8957 100644
--- a/project.py
+++ b/project.py
@@ -40,7 +40,13 @@ from trace import IsTrace, Trace
40from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M 40from git_refs import GitRefs, HEAD, R_HEADS, R_TAGS, R_PUB, R_M
41 41
42from pyversion import is_python3 42from pyversion import is_python3
43if not is_python3(): 43if is_python3():
44 import urllib.parse
45else:
46 import imp
47 import urlparse
48 urllib = imp.new_module('urllib')
49 urllib.parse = urlparse
44 # pylint:disable=W0622 50 # pylint:disable=W0622
45 input = raw_input 51 input = raw_input
46 # pylint:enable=W0622 52 # pylint:enable=W0622
@@ -343,6 +349,7 @@ class RepoHook(object):
343 hook_type, 349 hook_type,
344 hooks_project, 350 hooks_project,
345 topdir, 351 topdir,
352 manifest_url,
346 abort_if_user_denies=False): 353 abort_if_user_denies=False):
347 """RepoHook constructor. 354 """RepoHook constructor.
348 355
@@ -356,11 +363,13 @@ class RepoHook(object):
356 topdir: Repo's top directory (the one containing the .repo directory). 363 topdir: Repo's top directory (the one containing the .repo directory).
357 Scripts will run with CWD as this directory. If you have a manifest, 364 Scripts will run with CWD as this directory. If you have a manifest,
358 this is manifest.topdir 365 this is manifest.topdir
366 manifest_url: The URL to the manifest git repo.
359 abort_if_user_denies: If True, we'll throw a HookError() if the user 367 abort_if_user_denies: If True, we'll throw a HookError() if the user
360 doesn't allow us to run the hook. 368 doesn't allow us to run the hook.
361 """ 369 """
362 self._hook_type = hook_type 370 self._hook_type = hook_type
363 self._hooks_project = hooks_project 371 self._hooks_project = hooks_project
372 self._manifest_url = manifest_url
364 self._topdir = topdir 373 self._topdir = topdir
365 self._abort_if_user_denies = abort_if_user_denies 374 self._abort_if_user_denies = abort_if_user_denies
366 375
@@ -409,9 +418,9 @@ class RepoHook(object):
409 def _CheckForHookApproval(self): 418 def _CheckForHookApproval(self):
410 """Check to see whether this hook has been approved. 419 """Check to see whether this hook has been approved.
411 420
412 We'll look at the hash of all of the hooks. If this matches the hash that 421 We'll accept approval of manifest URLs if they're using secure transports.
413 the user last approved, we're done. If it doesn't, we'll ask the user 422 This way the user can say they trust the manifest hoster. For insecure
414 about approval. 423 hosts, we fall back to checking the hash of the hooks repo.
415 424
416 Note that we ask permission for each individual hook even though we use 425 Note that we ask permission for each individual hook even though we use
417 the hash of all hooks when detecting changes. We'd like the user to be 426 the hash of all hooks when detecting changes. We'd like the user to be
@@ -425,44 +434,58 @@ class RepoHook(object):
425 HookError: Raised if the user doesn't approve and abort_if_user_denies 434 HookError: Raised if the user doesn't approve and abort_if_user_denies
426 was passed to the consturctor. 435 was passed to the consturctor.
427 """ 436 """
428 hooks_config = self._hooks_project.config 437 if self._ManifestUrlHasSecureScheme():
429 git_approval_key = 'repo.hooks.%s.approvedhash' % self._hook_type 438 return self._CheckForHookApprovalManifest()
439 else:
440 return self._CheckForHookApprovalHash()
441
442 def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
443 changed_prompt):
444 """Check for approval for a particular attribute and hook.
445
446 Args:
447 subkey: The git config key under [repo.hooks.<hook_type>] to store the
448 last approved string.
449 new_val: The new value to compare against the last approved one.
450 main_prompt: Message to display to the user to ask for approval.
451 changed_prompt: Message explaining why we're re-asking for approval.
452
453 Returns:
454 True if this hook is approved to run; False otherwise.
430 455
431 # Get the last hash that the user approved for this hook; may be None. 456 Raises:
432 old_hash = hooks_config.GetString(git_approval_key) 457 HookError: Raised if the user doesn't approve and abort_if_user_denies
458 was passed to the consturctor.
459 """
460 hooks_config = self._hooks_project.config
461 git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
433 462
434 # Get the current hash so we can tell if scripts changed since approval. 463 # Get the last value that the user approved for this hook; may be None.
435 new_hash = self._GetHash() 464 old_val = hooks_config.GetString(git_approval_key)
436 465
437 if old_hash is not None: 466 if old_val is not None:
438 # User previously approved hook and asked not to be prompted again. 467 # User previously approved hook and asked not to be prompted again.
439 if new_hash == old_hash: 468 if new_val == old_val:
440 # Approval matched. We're done. 469 # Approval matched. We're done.
441 return True 470 return True
442 else: 471 else:
443 # Give the user a reason why we're prompting, since they last told 472 # Give the user a reason why we're prompting, since they last told
444 # us to "never ask again". 473 # us to "never ask again".
445 prompt = 'WARNING: Scripts have changed since %s was allowed.\n\n' % ( 474 prompt = 'WARNING: %s\n\n' % (changed_prompt,)
446 self._hook_type)
447 else: 475 else:
448 prompt = '' 476 prompt = ''
449 477
450 # Prompt the user if we're not on a tty; on a tty we'll assume "no". 478 # Prompt the user if we're not on a tty; on a tty we'll assume "no".
451 if sys.stdout.isatty(): 479 if sys.stdout.isatty():
452 prompt += ('Repo %s run the script:\n' 480 prompt += main_prompt + ' (yes/always/NO)? '
453 ' %s\n'
454 '\n'
455 'Do you want to allow this script to run '
456 '(yes/yes-never-ask-again/NO)? ') % (self._GetMustVerb(),
457 self._script_fullpath)
458 response = input(prompt).lower() 481 response = input(prompt).lower()
459 print() 482 print()
460 483
461 # User is doing a one-time approval. 484 # User is doing a one-time approval.
462 if response in ('y', 'yes'): 485 if response in ('y', 'yes'):
463 return True 486 return True
464 elif response == 'yes-never-ask-again': 487 elif response == 'always':
465 hooks_config.SetString(git_approval_key, new_hash) 488 hooks_config.SetString(git_approval_key, new_val)
466 return True 489 return True
467 490
468 # For anything else, we'll assume no approval. 491 # For anything else, we'll assume no approval.
@@ -472,6 +495,42 @@ class RepoHook(object):
472 495
473 return False 496 return False
474 497
498 def _ManifestUrlHasSecureScheme(self):
499 """Check if the URI for the manifest is a secure transport."""
500 secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
501 parse_results = urllib.parse.urlparse(self._manifest_url)
502 return parse_results.scheme in secure_schemes
503
504 def _CheckForHookApprovalManifest(self):
505 """Check whether the user has approved this manifest host.
506
507 Returns:
508 True if this hook is approved to run; False otherwise.
509 """
510 return self._CheckForHookApprovalHelper(
511 'approvedmanifest',
512 self._manifest_url,
513 'Run hook scripts from %s' % (self._manifest_url,),
514 'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
515
516 def _CheckForHookApprovalHash(self):
517 """Check whether the user has approved the hooks repo.
518
519 Returns:
520 True if this hook is approved to run; False otherwise.
521 """
522 prompt = ('Repo %s run the script:\n'
523 ' %s\n'
524 '\n'
525 'Do you want to allow this script to run '
526 '(yes/yes-never-ask-again/NO)? ') % (self._GetMustVerb(),
527 self._script_fullpath)
528 return self._CheckForHookApprovalHelper(
529 'approvedhash',
530 self._GetHash(),
531 prompt,
532 'Scripts have changed since %s was allowed.' % (self._hook_type,))
533
475 def _ExecuteHook(self, **kwargs): 534 def _ExecuteHook(self, **kwargs):
476 """Actually execute the given hook. 535 """Actually execute the given hook.
477 536