diff --git a/git_auth.py b/git_auth.py index 5f28810883..ccf49da5f1 100644 --- a/git_auth.py +++ b/git_auth.py @@ -11,261 +11,13 @@ import contextlib import functools import logging import os -from typing import TYPE_CHECKING, Callable, NamedTuple, TextIO +from typing import Callable, NamedTuple, TextIO import urllib.parse import gerrit_util import newauth import scm -if TYPE_CHECKING: - # Causes import cycle if imported normally - import git_cl - - -class ConfigMode(enum.Enum): - """Modes to pass to ConfigChanger""" - NO_AUTH = 1 - NEW_AUTH = 2 - NEW_AUTH_SSO = 3 - - -class ConfigChanger(object): - """Changes Git auth config as needed for Gerrit.""" - - # Can be used to determine whether this version of the config has - # been applied to a Git repo. - # - # Increment this when making changes to the config, so that reliant - # code can determine whether the config needs to be re-applied. - VERSION: int = 6 - - def __init__( - self, - *, - mode: ConfigMode, - remote_url: str, - set_config_func: Callable[..., None] = scm.GIT.SetConfig, - ): - """Create a new ConfigChanger. - - Args: - mode: How to configure auth - remote_url: Git repository's remote URL, e.g., - https://chromium.googlesource.com/chromium/tools/depot_tools.git - set_config_func: Function used to set configuration. Used - for testing. - """ - self.mode: ConfigMode = mode - - self._remote_url: str = remote_url - self._set_config_func: Callable[..., None] = set_config_func - - @functools.cached_property - def _shortname(self) -> str: - # Example: chromium - parts: urllib.parse.SplitResult = urllib.parse.urlsplit( - self._remote_url) - return _url_shortname(parts) - - @functools.cached_property - def _host_url(self) -> str: - # Example: https://chromium.googlesource.com - # Example: https://chromium-review.googlesource.com - parts: urllib.parse.SplitResult = urllib.parse.urlsplit( - self._remote_url) - return _url_host_url(parts) - - @functools.cached_property - def _root_url(self) -> str: - # Example: https://chromium.googlesource.com/ - # Example: https://chromium-review.googlesource.com/ - parts: urllib.parse.SplitResult = urllib.parse.urlsplit( - self._remote_url) - return _url_root_url(parts) - - @classmethod - def new_from_env(cls, cwd: str, cl: git_cl.Changelist) -> ConfigChanger: - """Create a ConfigChanger by inferring from env. - - The Gerrit host is inferred from the current repo/branch. - The user, which is used to determine the mode, is inferred using - git-config(1) in the given `cwd`. - """ - # This is determined either from the branch or repo config. - # - # Example: chromium-review.googlesource.com - gerrit_host = cl.GetGerritHost() - # This depends on what the user set for their remote. - # There are a couple potential variations for the same host+repo. - # - # Example: - # https://chromium.googlesource.com/chromium/tools/depot_tools.git - remote_url = cl.GetRemoteUrl() - - if gerrit_host is None or remote_url is None: - raise Exception( - 'Error Git auth settings inferring from environment:' - f' {gerrit_host=} {remote_url=}') - assert gerrit_host is not None - assert remote_url is not None - - return cls( - mode=cls._infer_mode(cwd, gerrit_host), - remote_url=remote_url, - ) - - @classmethod - def new_for_remote(cls, cwd: str, remote_url: str) -> ConfigChanger: - """Create a ConfigChanger for the given Gerrit host. - - The user, which is used to determine the mode, is inferred using - git-config(1) in the given `cwd`. - """ - c = cls( - mode=ConfigMode.NEW_AUTH, - remote_url=remote_url, - ) - assert c._shortname, "Short name is empty" - c.mode = cls._infer_mode(cwd, c._shortname + '-review.googlesource.com') - return c - - @staticmethod - def _infer_mode(cwd: str, gerrit_host: str) -> ConfigMode: - """Infer default mode to use.""" - if not newauth.Enabled(): - return ConfigMode.NO_AUTH - email: str = scm.GIT.GetConfig(cwd, 'user.email') or '' - if gerrit_util.ShouldUseSSO(gerrit_host, email): - return ConfigMode.NEW_AUTH_SSO - if not gerrit_util.GitCredsAuthenticator.gerrit_account_exists( - gerrit_host): - return ConfigMode.NO_AUTH - return ConfigMode.NEW_AUTH - - def apply(self, cwd: str) -> None: - """Apply config changes to the Git repo directory.""" - self._apply_cred_helper(cwd) - self._apply_sso(cwd) - self._apply_gitcookies(cwd) - - def apply_global(self, cwd: str) -> None: - """Apply config changes to the global (user) Git config. - - This will make the instance's mode (e.g., SSO or not) the global - default for the Gerrit host, if not overridden by a specific Git repo. - """ - self._apply_global_cred_helper(cwd) - self._apply_global_sso(cwd) - - def _apply_cred_helper(self, cwd: str) -> None: - """Apply config changes relating to credential helper.""" - cred_key: str = f'credential.{self._host_url}.helper' - if self.mode == ConfigMode.NEW_AUTH: - self._set_config(cwd, cred_key, '', modify_all=True) - self._set_config(cwd, cred_key, 'luci', append=True) - elif self.mode == ConfigMode.NEW_AUTH_SSO: - self._set_config(cwd, cred_key, None, modify_all=True) - elif self.mode == ConfigMode.NO_AUTH: - self._set_config(cwd, cred_key, None, modify_all=True) - else: - raise TypeError(f'Invalid mode {self.mode!r}') - - # Cleanup old from version 4 - old_key: str = f'credential.{self._root_url}.helper' - self._set_config(cwd, old_key, None, modify_all=True) - - def _apply_sso(self, cwd: str) -> None: - """Apply config changes relating to SSO.""" - sso_key: str = f'url.sso://{self._shortname}/.insteadOf' - http_key: str = f'url.{self._remote_url}.insteadOf' - if self.mode == ConfigMode.NEW_AUTH: - self._set_config(cwd, 'protocol.sso.allow', None) - self._set_config(cwd, sso_key, None, modify_all=True) - # Shadow a potential global SSO rewrite rule. - self._set_config(cwd, http_key, self._remote_url, modify_all=True) - elif self.mode == ConfigMode.NEW_AUTH_SSO: - self._set_config(cwd, 'protocol.sso.allow', 'always') - self._set_config(cwd, sso_key, self._root_url, modify_all=True) - self._set_config(cwd, http_key, None, modify_all=True) - elif self.mode == ConfigMode.NO_AUTH: - self._set_config(cwd, 'protocol.sso.allow', None) - self._set_config(cwd, sso_key, None, modify_all=True) - self._set_config(cwd, http_key, None, modify_all=True) - else: - raise TypeError(f'Invalid mode {self.mode!r}') - - def _apply_gitcookies(self, cwd: str) -> None: - """Apply config changes relating to gitcookies.""" - if self.mode == ConfigMode.NEW_AUTH: - # Override potential global setting - self._set_config(cwd, 'http.cookieFile', '', modify_all=True) - elif self.mode == ConfigMode.NEW_AUTH_SSO: - # Override potential global setting - self._set_config(cwd, 'http.cookieFile', '', modify_all=True) - elif self.mode == ConfigMode.NO_AUTH: - self._set_config(cwd, 'http.cookieFile', None, modify_all=True) - else: - raise TypeError(f'Invalid mode {self.mode!r}') - - def _apply_global_cred_helper(self, cwd: str) -> None: - """Apply config changes relating to credential helper.""" - cred_key: str = f'credential.{self._host_url}.helper' - if self.mode == ConfigMode.NEW_AUTH: - self._set_config(cwd, cred_key, '', scope='global', modify_all=True) - self._set_config(cwd, cred_key, 'luci', scope='global', append=True) - elif self.mode == ConfigMode.NEW_AUTH_SSO: - # Avoid editing the user's config in case they manually - # configured something. - pass - elif self.mode == ConfigMode.NO_AUTH: - # Avoid editing the user's config in case they manually - # configured something. - pass - else: - raise TypeError(f'Invalid mode {self.mode!r}') - - # Cleanup old from version 4 - old_key: str = f'credential.{self._root_url}.helper' - self._set_config(cwd, old_key, None, modify_all=True, scope='global') - - def _apply_global_sso(self, cwd: str) -> None: - """Apply config changes relating to SSO.""" - sso_key: str = f'url.sso://{self._shortname}/.insteadOf' - if self.mode == ConfigMode.NEW_AUTH: - # Do not unset protocol.sso.allow because it may be used by - # other hosts. - self._set_config(cwd, - sso_key, - None, - scope='global', - modify_all=True) - elif self.mode == ConfigMode.NEW_AUTH_SSO: - self._set_config(cwd, - 'protocol.sso.allow', - 'always', - scope='global') - self._set_config(cwd, - sso_key, - self._root_url, - scope='global', - modify_all=True) - elif self.mode == ConfigMode.NO_AUTH: - # Avoid editing the user's config in case they manually - # configured something. - pass - else: - raise TypeError(f'Invalid mode {self.mode!r}') - - def _set_config(self, *args, **kwargs) -> None: - self._set_config_func(*args, **kwargs) - - -def ClearRepoConfig(cwd: str, cl: git_cl.Changelist) -> None: - """Clear the current Git repo authentication.""" - # TODO(ayatane): Disable prior to removal - return - class _ConfigError(Exception): """Subclass for errors raised by ConfigWizard. diff --git a/tests/git_auth_test.py b/tests/git_auth_test.py index 7e08b2f8e7..31f7525565 100755 --- a/tests/git_auth_test.py +++ b/tests/git_auth_test.py @@ -24,183 +24,6 @@ import scm import scm_mock -class TestConfigChanger(unittest.TestCase): - - maxDiff = None - - def setUp(self): - self._global_state_view: Iterable[tuple[str, - list[str]]] = scm_mock.GIT(self) - - @property - def global_state(self): - return dict(self._global_state_view) - - def test_apply_new_auth(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': { - 'credential.https://chromium.googlesource.com.helper': - ['', 'luci'], - 'http.cookiefile': [''], - 'url.https://chromium.googlesource.com/chromium/tools/depot_tools.git.insteadof': - [ - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git' - ], - }, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_new_auth_sso(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH_SSO, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': { - 'protocol.sso.allow': ['always'], - 'url.sso://chromium/.insteadof': - ['https://chromium.googlesource.com/'], - 'http.cookiefile': [''], - }, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_no_auth(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NO_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': {}, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_chain_sso_new(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH_SSO, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': { - 'credential.https://chromium.googlesource.com.helper': - ['', 'luci'], - 'http.cookiefile': [''], - 'url.https://chromium.googlesource.com/chromium/tools/depot_tools.git.insteadof': - [ - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git' - ], - }, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_chain_new_sso(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH_SSO, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': { - 'protocol.sso.allow': ['always'], - 'url.sso://chromium/.insteadof': - ['https://chromium.googlesource.com/'], - 'http.cookiefile': [''], - }, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_chain_new_no(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NO_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': {}, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_chain_sso_no(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH_SSO, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NO_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply('/some/fake/dir') - want = { - '/some/fake/dir': {}, - } - self.assertEqual(scm.GIT._dump_config_state(), want) - - def test_apply_global_new_auth(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply_global('/some/fake/dir') - want = { - 'credential.https://chromium.googlesource.com.helper': ['', 'luci'], - } - self.assertEqual(self.global_state, want) - - def test_apply_global_new_auth_sso(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH_SSO, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply_global('/some/fake/dir') - want = { - 'protocol.sso.allow': ['always'], - 'url.sso://chromium/.insteadof': - ['https://chromium.googlesource.com/'], - } - self.assertEqual(self.global_state, want) - - def test_apply_global_chain_sso_new(self): - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH_SSO, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply_global('/some/fake/dir') - git_auth.ConfigChanger( - mode=git_auth.ConfigMode.NEW_AUTH, - remote_url= - 'https://chromium.googlesource.com/chromium/tools/depot_tools.git', - ).apply_global('/some/fake/dir') - want = { - 'protocol.sso.allow': ['always'], - 'credential.https://chromium.googlesource.com.helper': ['', 'luci'], - } - self.assertEqual(self.global_state, want) - - class TestParseCookiefile(unittest.TestCase): def test_ignore_comments(self):