[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 <yiwzhang@google.com>
Commit-Queue: Allen Li <ayatane@chromium.org>
changes/96/5739296/4
Allen Li 9 months ago committed by LUCI CQ
parent dbaad43b23
commit a61e517b45

@ -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)

@ -48,6 +48,7 @@ import clang_format
import gclient_paths import gclient_paths
import gclient_utils import gclient_utils
import gerrit_util import gerrit_util
import git_auth
import git_common import git_common
import git_footers import git_footers
import git_new_branch import git_new_branch
@ -2345,7 +2346,7 @@ class Changelist(object):
return return
if newauth.Enabled(): if newauth.Enabled():
latestVer: int = GitAuthConfigChanger.VERSION latestVer: int = git_auth.GitAuthConfigChanger.VERSION
v: int = 0 v: int = 0
try: try:
v = int( v = int(
@ -3673,16 +3674,16 @@ def ConfigureGitAuth() -> None:
# We want the user's global config. # We want the user's global config.
# We can probably assume the root directory doesn't have any local # We can probably assume the root directory doesn't have any local
# Git configuration. # Git configuration.
c = GitAuthConfigChanger.new_from_env('/') c = git_auth.GitAuthConfigChanger.new_from_env('/')
c.apply_global(os.path.expanduser('~')) c.apply_global(os.path.expanduser('~'))
cwd = os.getcwd() cwd = os.getcwd()
c2 = GitAuthConfigChanger.new_from_env(cwd) c2 = git_auth.GitAuthConfigChanger.new_from_env(cwd)
if c2.mode == c.mode: if c2.mode == c.mode:
logging.debug( logging.debug(
'Local user wants same mode %s as global; clearing local repo auth config', 'Local user wants same mode %s as global; clearing local repo auth config',
c2.mode) c2.mode)
c2.mode = GitAuthMode.NO_AUTH c2.mode = git_auth.GitAuthMode.NO_AUTH
c2.apply(cwd) c2.apply(cwd)
return return
logging.debug('Local user wants mode %s while global user wants mode %s', 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.""" """Configure the current Git repo authentication."""
logging.debug('Configuring current Git repo authentication...') logging.debug('Configuring current Git repo authentication...')
cwd = os.getcwd() cwd = os.getcwd()
c = GitAuthConfigChanger.new_from_env(cwd) c = git_auth.GitAuthConfigChanger.new_from_env(cwd)
c.apply(cwd) c.apply(cwd)
def ClearGitRepoAuth() -> None: def ClearGitRepoAuth() -> None:
"""Clear the current Git repo authentication.""" """Clear the current Git repo authentication."""
logging.debug('Clearing current Git repo authentication...') logging.debug('Clearing current Git repo authentication...')
c = GitAuthConfigChanger.new_from_env(cwd) c = git_auth.GitAuthConfigChanger.new_from_env(cwd)
c.mode = GitAuthMode.NO_AUTH c.mode = git_auth.GitAuthMode.NO_AUTH
c.apply(cwd) 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): class _GitCookiesChecker(object):
"""Provides facilities for validating and suggesting fixes to .gitcookies.""" """Provides facilities for validating and suggesting fixes to .gitcookies."""
def __init__(self): def __init__(self):

Loading…
Cancel
Save