From a61e517b453dc6e1b9e0616cdc3ae2f9102c1f1c Mon Sep 17 00:00:00 2001 From: Allen Li Date: Thu, 25 Jul 2024 22:03:29 +0000 Subject: [PATCH] [git_auth] Move Git auth stuff to new file Needs to be imported by gclient Bug: b/351071334 Change-Id: Ibcb7c5541ffab6f9302efad863d0c59b8041122d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/5739296 Reviewed-by: Yiwei Zhang Commit-Queue: Allen Li --- git_auth.py | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++ git_cl.py | 209 ++-------------------------------------------------- 2 files changed, 217 insertions(+), 201 deletions(-) create mode 100644 git_auth.py diff --git a/git_auth.py b/git_auth.py new file mode 100644 index 000000000..73842984b --- /dev/null +++ b/git_auth.py @@ -0,0 +1,209 @@ +# Copyright (c) 2024 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +"""Defines utilities for setting up Git authentication.""" + +from __future__ import annotations + +import enum +import functools +from typing import Callable +import urllib.parse + +import scm + + +class GitAuthMode(enum.Enum): + """Modes to pass to GitAuthConfigChanger""" + NO_AUTH = 1 + NEW_AUTH = 2 + NEW_AUTH_SSO = 3 + + +class GitAuthConfigChanger(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 = 2 + + def __init__( + self, + *, + mode: GitAuthMode, + remote_url: str, + set_config_func: Callable[..., None] = scm.GIT.SetConfig, + ): + """Create a new GitAuthConfigChanger. + + 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: GitAuthMode = mode + + self._remote_url: str = remote_url + self._set_config_func: Callable[..., str] = set_config_func + + @functools.cached_property + def _shortname(self) -> str: + parts: urllib.parse.SplitResult = urllib.parse.urlsplit( + self._remote_url) + name: str = parts.netloc.split('.')[0] + if name.endswith('-review'): + name = name[:-len('-review')] + return name + + @functools.cached_property + def _base_url(self) -> str: + # Base URL looks like https://chromium.googlesource.com/ + parts: urllib.parse.SplitResult = urllib.parse.urlsplit( + self._remote_url) + return parts._replace(path='/', query='', fragment='').geturl() + + @classmethod + def new_from_env(cls, cwd: str) -> 'GitAuthConfigChanger': + """Create a GitAuthConfigChanger 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`. + """ + cl = Changelist() + # This is determined either from the branch or repo config. + # + # Example: chromium-review.googlesource.com + gerrit_host: str = 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: str = cl.GetRemoteUrl() + + return cls( + mode=cls._infer_mode(cwd, gerrit_host), + remote_url=remote_url, + ) + + @staticmethod + def _infer_mode(cwd: str, gerrit_host: str) -> GitAuthMode: + """Infer default mode to use.""" + if not newauth.Enabled(): + return GitAuthMode.NO_AUTH + email: str = scm.GIT.GetConfig(cwd, 'user.email', default='') + if gerrit_util.ShouldUseSSO(gerrit_host, email): + return GitAuthMode.NEW_AUTH_SSO + return GitAuthMode.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._base_url}.helper' + if self.mode == GitAuthMode.NEW_AUTH: + self._set_config(cwd, cred_key, '', modify_all=True) + self._set_config(cwd, cred_key, 'luci', append=True) + elif self.mode == GitAuthMode.NEW_AUTH_SSO: + self._set_config(cwd, cred_key, None, modify_all=True) + elif self.mode == GitAuthMode.NO_AUTH: + self._set_config(cwd, cred_key, None, modify_all=True) + else: + raise TypeError(f'Invalid mode {self.mode!r}') + + def _apply_sso(self, cwd: str) -> None: + """Apply config changes relating to SSO.""" + sso_key: str = f'url.sso://{self._shortname}/.insteadOf' + if self.mode == GitAuthMode.NEW_AUTH: + self._set_config(cwd, 'protocol.sso.allow', None) + self._set_config(cwd, sso_key, None, modify_all=True) + elif self.mode == GitAuthMode.NEW_AUTH_SSO: + self._set_config(cwd, 'protocol.sso.allow', 'always') + self._set_config(cwd, sso_key, self._base_url, modify_all=True) + elif self.mode == GitAuthMode.NO_AUTH: + self._set_config(cwd, 'protocol.sso.allow', None) + self._set_config(cwd, sso_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.""" + # TODO(ayatane): Clear invalid setting. Remove line after a few weeks + self._set_config(cwd, 'http.gitcookies', None, modify_all=True) + if self.mode == GitAuthMode.NEW_AUTH: + # Override potential global setting + self._set_config(cwd, 'http.cookieFile', '', modify_all=True) + elif self.mode == GitAuthMode.NEW_AUTH_SSO: + # Override potential global setting + self._set_config(cwd, 'http.cookieFile', '', modify_all=True) + elif self.mode == GitAuthMode.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._base_url}.helper' + if self.mode == GitAuthMode.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 == GitAuthMode.NEW_AUTH_SSO: + # Avoid editing the user's config in case they manually + # configured something. + pass + elif self.mode == GitAuthMode.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 _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 == GitAuthMode.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 == GitAuthMode.NEW_AUTH_SSO: + self._set_config(cwd, + 'protocol.sso.allow', + 'always', + scope='global') + self._set_config(cwd, + sso_key, + self._base_url, + scope='global', + modify_all=True) + elif self.mode == GitAuthMode.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) diff --git a/git_cl.py b/git_cl.py index be46068dd..b8458bd10 100755 --- a/git_cl.py +++ b/git_cl.py @@ -48,6 +48,7 @@ import clang_format import gclient_paths import gclient_utils import gerrit_util +import git_auth import git_common import git_footers import git_new_branch @@ -2345,7 +2346,7 @@ class Changelist(object): return if newauth.Enabled(): - latestVer: int = GitAuthConfigChanger.VERSION + latestVer: int = git_auth.GitAuthConfigChanger.VERSION v: int = 0 try: v = int( @@ -3673,16 +3674,16 @@ def ConfigureGitAuth() -> None: # We want the user's global config. # We can probably assume the root directory doesn't have any local # Git configuration. - c = GitAuthConfigChanger.new_from_env('/') + c = git_auth.GitAuthConfigChanger.new_from_env('/') c.apply_global(os.path.expanduser('~')) cwd = os.getcwd() - c2 = GitAuthConfigChanger.new_from_env(cwd) + c2 = git_auth.GitAuthConfigChanger.new_from_env(cwd) if c2.mode == c.mode: logging.debug( 'Local user wants same mode %s as global; clearing local repo auth config', c2.mode) - c2.mode = GitAuthMode.NO_AUTH + c2.mode = git_auth.GitAuthMode.NO_AUTH c2.apply(cwd) return logging.debug('Local user wants mode %s while global user wants mode %s', @@ -3695,212 +3696,18 @@ def ConfigureGitRepoAuth() -> None: """Configure the current Git repo authentication.""" logging.debug('Configuring current Git repo authentication...') cwd = os.getcwd() - c = GitAuthConfigChanger.new_from_env(cwd) + c = git_auth.GitAuthConfigChanger.new_from_env(cwd) c.apply(cwd) def ClearGitRepoAuth() -> None: """Clear the current Git repo authentication.""" logging.debug('Clearing current Git repo authentication...') - c = GitAuthConfigChanger.new_from_env(cwd) - c.mode = GitAuthMode.NO_AUTH + c = git_auth.GitAuthConfigChanger.new_from_env(cwd) + c.mode = git_auth.GitAuthMode.NO_AUTH c.apply(cwd) -class GitAuthMode(enum.Enum): - """Modes to pass to GitAuthConfigChanger""" - NO_AUTH = 1 - NEW_AUTH = 2 - NEW_AUTH_SSO = 3 - - -class GitAuthConfigChanger(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 = 2 - - def __init__( - self, - *, - mode: GitAuthMode, - remote_url: str, - set_config_func: Callable[..., None] = scm.GIT.SetConfig, - ): - """Create a new GitAuthConfigChanger. - - 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: GitAuthMode = mode - - self._remote_url: str = remote_url - self._set_config_func: Callable[..., str] = set_config_func - - @functools.cached_property - def _shortname(self) -> str: - parts: urllib.parse.SplitResult = urllib.parse.urlsplit( - self._remote_url) - name: str = parts.netloc.split('.')[0] - if name.endswith('-review'): - name = name[:-len('-review')] - return name - - @functools.cached_property - def _base_url(self) -> str: - # Base URL looks like https://chromium.googlesource.com/ - parts: urllib.parse.SplitResult = urllib.parse.urlsplit( - self._remote_url) - return parts._replace(path='/', query='', fragment='').geturl() - - @classmethod - def new_from_env(cls, cwd: str) -> 'GitAuthConfigChanger': - """Create a GitAuthConfigChanger 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`. - """ - cl = Changelist() - # This is determined either from the branch or repo config. - # - # Example: chromium-review.googlesource.com - gerrit_host: str = 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: str = cl.GetRemoteUrl() - - return cls( - mode=cls._infer_mode(cwd, gerrit_host), - remote_url=remote_url, - ) - - @staticmethod - def _infer_mode(cwd: str, gerrit_host: str) -> GitAuthMode: - """Infer default mode to use.""" - if not newauth.Enabled(): - return GitAuthMode.NO_AUTH - email: str = scm.GIT.GetConfig(cwd, 'user.email', default='') - if gerrit_util.ShouldUseSSO(gerrit_host, email): - return GitAuthMode.NEW_AUTH_SSO - return GitAuthMode.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._base_url}.helper' - if self.mode == GitAuthMode.NEW_AUTH: - self._set_config(cwd, cred_key, '', modify_all=True) - self._set_config(cwd, cred_key, 'luci', append=True) - elif self.mode == GitAuthMode.NEW_AUTH_SSO: - self._set_config(cwd, cred_key, None, modify_all=True) - elif self.mode == GitAuthMode.NO_AUTH: - self._set_config(cwd, cred_key, None, modify_all=True) - else: - raise TypeError(f'Invalid mode {self.mode!r}') - - def _apply_sso(self, cwd: str) -> None: - """Apply config changes relating to SSO.""" - sso_key: str = f'url.sso://{self._shortname}/.insteadOf' - if self.mode == GitAuthMode.NEW_AUTH: - self._set_config(cwd, 'protocol.sso.allow', None) - self._set_config(cwd, sso_key, None, modify_all=True) - elif self.mode == GitAuthMode.NEW_AUTH_SSO: - self._set_config(cwd, 'protocol.sso.allow', 'always') - self._set_config(cwd, sso_key, self._base_url, modify_all=True) - elif self.mode == GitAuthMode.NO_AUTH: - self._set_config(cwd, 'protocol.sso.allow', None) - self._set_config(cwd, sso_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.""" - # TODO(ayatane): Clear invalid setting. Remove line after a few weeks - self._set_config(cwd, 'http.gitcookies', None, modify_all=True) - if self.mode == GitAuthMode.NEW_AUTH: - # Override potential global setting - self._set_config(cwd, 'http.cookieFile', '', modify_all=True) - elif self.mode == GitAuthMode.NEW_AUTH_SSO: - # Override potential global setting - self._set_config(cwd, 'http.cookieFile', '', modify_all=True) - elif self.mode == GitAuthMode.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._base_url}.helper' - if self.mode == GitAuthMode.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 == GitAuthMode.NEW_AUTH_SSO: - # Avoid editing the user's config in case they manually - # configured something. - pass - elif self.mode == GitAuthMode.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 _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 == GitAuthMode.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 == GitAuthMode.NEW_AUTH_SSO: - self._set_config(cwd, - 'protocol.sso.allow', - 'always', - scope='global') - self._set_config(cwd, - sso_key, - self._base_url, - scope='global', - modify_all=True) - elif self.mode == GitAuthMode.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) - - class _GitCookiesChecker(object): """Provides facilities for validating and suggesting fixes to .gitcookies.""" def __init__(self):