git_auth.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # Copyright (c) 2024 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Defines utilities for setting up Git authentication."""
  5. from __future__ import annotations
  6. import enum
  7. import functools
  8. from typing import Callable
  9. import urllib.parse
  10. import scm
  11. class ConfigMode(enum.Enum):
  12. """Modes to pass to ConfigChanger"""
  13. NO_AUTH = 1
  14. NEW_AUTH = 2
  15. NEW_AUTH_SSO = 3
  16. class ConfigChanger(object):
  17. """Changes Git auth config as needed for Gerrit."""
  18. # Can be used to determine whether this version of the config has
  19. # been applied to a Git repo.
  20. #
  21. # Increment this when making changes to the config, so that reliant
  22. # code can determine whether the config needs to be re-applied.
  23. VERSION: int = 2
  24. def __init__(
  25. self,
  26. *,
  27. mode: ConfigMode,
  28. remote_url: str,
  29. set_config_func: Callable[..., None] = scm.GIT.SetConfig,
  30. ):
  31. """Create a new ConfigChanger.
  32. Args:
  33. mode: How to configure auth
  34. remote_url: Git repository's remote URL, e.g.,
  35. https://chromium.googlesource.com/chromium/tools/depot_tools.git
  36. set_config_func: Function used to set configuration. Used
  37. for testing.
  38. """
  39. self.mode: ConfigMode = mode
  40. self._remote_url: str = remote_url
  41. self._set_config_func: Callable[..., str] = set_config_func
  42. @functools.cached_property
  43. def _shortname(self) -> str:
  44. parts: urllib.parse.SplitResult = urllib.parse.urlsplit(
  45. self._remote_url)
  46. name: str = parts.netloc.split('.')[0]
  47. if name.endswith('-review'):
  48. name = name[:-len('-review')]
  49. return name
  50. @functools.cached_property
  51. def _base_url(self) -> str:
  52. # Base URL looks like https://chromium.googlesource.com/
  53. parts: urllib.parse.SplitResult = urllib.parse.urlsplit(
  54. self._remote_url)
  55. return parts._replace(path='/', query='', fragment='').geturl()
  56. @classmethod
  57. def new_from_env(cls, cwd: str) -> 'ConfigChanger':
  58. """Create a ConfigChanger by inferring from env.
  59. The Gerrit host is inferred from the current repo/branch.
  60. The user, which is used to determine the mode, is inferred using
  61. git-config(1) in the given `cwd`.
  62. """
  63. cl = Changelist()
  64. # This is determined either from the branch or repo config.
  65. #
  66. # Example: chromium-review.googlesource.com
  67. gerrit_host: str = cl.GetGerritHost()
  68. # This depends on what the user set for their remote.
  69. # There are a couple potential variations for the same host+repo.
  70. #
  71. # Example:
  72. # https://chromium.googlesource.com/chromium/tools/depot_tools.git
  73. remote_url: str = cl.GetRemoteUrl()
  74. return cls(
  75. mode=cls._infer_mode(cwd, gerrit_host),
  76. remote_url=remote_url,
  77. )
  78. @staticmethod
  79. def _infer_mode(cwd: str, gerrit_host: str) -> ConfigMode:
  80. """Infer default mode to use."""
  81. if not newauth.Enabled():
  82. return ConfigMode.NO_AUTH
  83. email: str = scm.GIT.GetConfig(cwd, 'user.email', default='')
  84. if gerrit_util.ShouldUseSSO(gerrit_host, email):
  85. return ConfigMode.NEW_AUTH_SSO
  86. return ConfigMode.NEW_AUTH
  87. def apply(self, cwd: str) -> None:
  88. """Apply config changes to the Git repo directory."""
  89. self._apply_cred_helper(cwd)
  90. self._apply_sso(cwd)
  91. self._apply_gitcookies(cwd)
  92. def apply_global(self, cwd: str) -> None:
  93. """Apply config changes to the global (user) Git config.
  94. This will make the instance's mode (e.g., SSO or not) the global
  95. default for the Gerrit host, if not overridden by a specific Git repo.
  96. """
  97. self._apply_global_cred_helper(cwd)
  98. self._apply_global_sso(cwd)
  99. def _apply_cred_helper(self, cwd: str) -> None:
  100. """Apply config changes relating to credential helper."""
  101. cred_key: str = f'credential.{self._base_url}.helper'
  102. if self.mode == ConfigMode.NEW_AUTH:
  103. self._set_config(cwd, cred_key, '', modify_all=True)
  104. self._set_config(cwd, cred_key, 'luci', append=True)
  105. elif self.mode == ConfigMode.NEW_AUTH_SSO:
  106. self._set_config(cwd, cred_key, None, modify_all=True)
  107. elif self.mode == ConfigMode.NO_AUTH:
  108. self._set_config(cwd, cred_key, None, modify_all=True)
  109. else:
  110. raise TypeError(f'Invalid mode {self.mode!r}')
  111. def _apply_sso(self, cwd: str) -> None:
  112. """Apply config changes relating to SSO."""
  113. sso_key: str = f'url.sso://{self._shortname}/.insteadOf'
  114. if self.mode == ConfigMode.NEW_AUTH:
  115. self._set_config(cwd, 'protocol.sso.allow', None)
  116. self._set_config(cwd, sso_key, None, modify_all=True)
  117. elif self.mode == ConfigMode.NEW_AUTH_SSO:
  118. self._set_config(cwd, 'protocol.sso.allow', 'always')
  119. self._set_config(cwd, sso_key, self._base_url, modify_all=True)
  120. elif self.mode == ConfigMode.NO_AUTH:
  121. self._set_config(cwd, 'protocol.sso.allow', None)
  122. self._set_config(cwd, sso_key, None, modify_all=True)
  123. else:
  124. raise TypeError(f'Invalid mode {self.mode!r}')
  125. def _apply_gitcookies(self, cwd: str) -> None:
  126. """Apply config changes relating to gitcookies."""
  127. # TODO(ayatane): Clear invalid setting. Remove line after a few weeks
  128. self._set_config(cwd, 'http.gitcookies', None, modify_all=True)
  129. if self.mode == ConfigMode.NEW_AUTH:
  130. # Override potential global setting
  131. self._set_config(cwd, 'http.cookieFile', '', modify_all=True)
  132. elif self.mode == ConfigMode.NEW_AUTH_SSO:
  133. # Override potential global setting
  134. self._set_config(cwd, 'http.cookieFile', '', modify_all=True)
  135. elif self.mode == ConfigMode.NO_AUTH:
  136. self._set_config(cwd, 'http.cookieFile', None, modify_all=True)
  137. else:
  138. raise TypeError(f'Invalid mode {self.mode!r}')
  139. def _apply_global_cred_helper(self, cwd: str) -> None:
  140. """Apply config changes relating to credential helper."""
  141. cred_key: str = f'credential.{self._base_url}.helper'
  142. if self.mode == ConfigMode.NEW_AUTH:
  143. self._set_config(cwd, cred_key, '', scope='global', modify_all=True)
  144. self._set_config(cwd, cred_key, 'luci', scope='global', append=True)
  145. elif self.mode == ConfigMode.NEW_AUTH_SSO:
  146. # Avoid editing the user's config in case they manually
  147. # configured something.
  148. pass
  149. elif self.mode == ConfigMode.NO_AUTH:
  150. # Avoid editing the user's config in case they manually
  151. # configured something.
  152. pass
  153. else:
  154. raise TypeError(f'Invalid mode {self.mode!r}')
  155. def _apply_global_sso(self, cwd: str) -> None:
  156. """Apply config changes relating to SSO."""
  157. sso_key: str = f'url.sso://{self._shortname}/.insteadOf'
  158. if self.mode == ConfigMode.NEW_AUTH:
  159. # Do not unset protocol.sso.allow because it may be used by
  160. # other hosts.
  161. self._set_config(cwd,
  162. sso_key,
  163. None,
  164. scope='global',
  165. modify_all=True)
  166. elif self.mode == ConfigMode.NEW_AUTH_SSO:
  167. self._set_config(cwd,
  168. 'protocol.sso.allow',
  169. 'always',
  170. scope='global')
  171. self._set_config(cwd,
  172. sso_key,
  173. self._base_url,
  174. scope='global',
  175. modify_all=True)
  176. elif self.mode == ConfigMode.NO_AUTH:
  177. # Avoid editing the user's config in case they manually
  178. # configured something.
  179. pass
  180. else:
  181. raise TypeError(f'Invalid mode {self.mode!r}')
  182. def _set_config(self, *args, **kwargs) -> None:
  183. self._set_config_func(*args, **kwargs)