123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- # 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
- import logging
- import os
- from typing import TYPE_CHECKING, Callable
- 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 = 2
- 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:
- 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, 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,
- )
- @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
- 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._base_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}')
- 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 == ConfigMode.NEW_AUTH:
- self._set_config(cwd, 'protocol.sso.allow', None)
- self._set_config(cwd, sso_key, None, 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._base_url, 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)
- 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 == 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._base_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}')
- 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._base_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 Configure(cwd: str, cl: git_cl.Changelist) -> None:
- """Configure Git authentication.
- This may modify the global Git config and the local repo config as
- needed.
- """
- logging.debug('Configuring Git authentication...')
- logging.debug('Configuring global Git authentication...')
- # We want the user's global config.
- # We can probably assume the root directory doesn't have any local
- # Git configuration.
- c = ConfigChanger.new_from_env('/', cl)
- c.apply_global(os.path.expanduser('~'))
- c2 = ConfigChanger.new_from_env(cwd, cl)
- if c2.mode == c.mode:
- logging.debug(
- 'Local user wants same mode %s as global;'
- ' clearing local repo auth config', c2.mode)
- c2.mode = ConfigMode.NO_AUTH
- c2.apply(cwd)
- return
- logging.debug('Local user wants mode %s while global user wants mode %s',
- c2.mode, c.mode)
- logging.debug('Configuring current Git repo authentication...')
- c2.apply(cwd)
- def ConfigureRepo(cwd: str, cl: git_cl.Changelist) -> None:
- """Configure the current Git repo authentication."""
- logging.debug('Configuring current Git repo authentication...')
- c = ConfigChanger.new_from_env(cwd, cl)
- c.apply(cwd)
- def ClearRepoConfig(cwd: str, cl: git_cl.Changelist) -> None:
- """Clear the current Git repo authentication."""
- logging.debug('Clearing current Git repo authentication...')
- c = ConfigChanger.new_from_env(cwd, cl)
- c.mode = ConfigMode.NO_AUTH
- c.apply(cwd)
|