From d63429610940c5aeca4625e013b983e1db707f41 Mon Sep 17 00:00:00 2001 From: Bjorn Neergaard Date: Thu, 25 May 2023 19:05:25 -0600 Subject: [PATCH 1/2] utils/github/api: discover credentials stored by the GitHub CLI --- Library/Homebrew/utils/github/api.rb | 69 ++++++++++++++++++---------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/Library/Homebrew/utils/github/api.rb b/Library/Homebrew/utils/github/api.rb index d5e9bdb4dd..7d03b0e403 100644 --- a/Library/Homebrew/utils/github/api.rb +++ b/Library/Homebrew/utils/github/api.rb @@ -10,9 +10,9 @@ module GitHub def self.pat_blurb(scopes = ALL_SCOPES) <<~EOS Create a GitHub personal access token: - #{Formatter.url( - "https://github.com/settings/tokens/new?scopes=#{scopes.join(",")}&description=Homebrew", - )} + #{Formatter.url( + "https://github.com/settings/tokens/new?scopes=#{scopes.join(",")}&description=Homebrew", + )} #{Utils::Shell.set_variable_in_profile("HOMEBREW_GITHUB_API_TOKEN", "your_token_here")} EOS end @@ -62,30 +62,36 @@ module GitHub # Error when authentication fails. class AuthenticationFailedError < Error - def initialize(github_message) + def initialize(credentials_type, github_message) @github_message = github_message message = +"GitHub API Error: #{github_message}\n" - message << if Homebrew::EnvConfig.github_api_token + message << case credentials_type + when :github_cli_token <<~EOS - HOMEBREW_GITHUB_API_TOKEN may be invalid or expired; check: - #{Formatter.url("https://github.com/settings/tokens")} + Your GitHub CLI login session may be invalid. + Refresh it with: + gh auth login --hostname github.com EOS - else + when :keychain_username_password <<~EOS The GitHub credentials in the macOS keychain may be invalid. Clear them with: printf "protocol=https\\nhost=github.com\\n" | git credential-osxkeychain erase - #{GitHub.pat_blurb} + EOS + when :env_token + <<~EOS + HOMEBREW_GITHUB_API_TOKEN may be invalid or expired; check: + #{Formatter.url("https://github.com/settings/tokens")} EOS end super message.freeze end end - # Error when the user has no GitHub API credentials set at all (macOS keychain or envvar). + # Error when the user has no GitHub API credentials set at all (macOS keychain, GitHub CLI or envvar). class MissingAuthenticationError < Error def initialize - message = +"No GitHub credentials found in macOS Keychain or environment.\n" + message = +"No GitHub credentials found in macOS Keychain, GitHub CLI or the environment.\n" message << GitHub.pat_blurb super message end @@ -112,6 +118,19 @@ module GitHub JSON::ParserError, ].freeze + # Gets the token from the GitHub CLI for github.com. + sig { returns(T.nilable(String)) } + def self.github_cli_token + env = { "PATH" => PATH.new(Formula["gh"].opt_bin, ENV.fetch("PATH")) } + gh_out, _, result = system_command "gh", + args: ["auth", "token", "--hostname", "github.com"], + env: env, + print_stderr: false + return unless result.success? + + gh_out.chomp + end + # Gets the password field from `git-credential-osxkeychain` for github.com, # but only if that password looks like a GitHub Personal Access Token. sig { returns(T.nilable(String)) } @@ -137,21 +156,31 @@ module GitHub nil end + # odeprecated: Not really deprecated; change the order to prefer `github_cli_token` over + # `keychain_username_password` during the next major/minor release. def self.credentials - @credentials ||= Homebrew::EnvConfig.github_api_token || keychain_username_password + @credentials ||= Homebrew::EnvConfig.github_api_token || keychain_username_password || github_cli_token end sig { returns(Symbol) } def self.credentials_type - if Homebrew::EnvConfig.github_api_token + if Homebrew::EnvConfig.github_api_token.present? :env_token - elsif keychain_username_password + elsif keychain_username_password.present? :keychain_username_password + elsif github_cli_token.present? + :github_cli_token else :none end end + CREDENTIAL_NAMES = { + env_token: "HOMEBREW_GITHUB_API_TOKEN", + github_cli_token: "GitHub CLI login", + keychain_username_password: "macOS Keychain GitHub", + }.freeze + # Given an API response from GitHub, warn the user if their credentials # have insufficient permissions. def self.credentials_error_message(response_headers, needed_scopes) @@ -165,13 +194,7 @@ module GitHub needed_scopes = needed_scopes.to_a.join(", ").presence || "none" credentials_scopes = "none" if credentials_scopes.blank? - what = case credentials_type - when :keychain_username_password - "macOS keychain GitHub" - when :env_token - "HOMEBREW_GITHUB_API_TOKEN" - end - + what = CREDENTIAL_NAMES.fetch(credentials_type) @credentials_error_message ||= onoe <<~EOS Your #{what} credentials do not have sufficient scope! Scopes required: #{needed_scopes} @@ -296,14 +319,14 @@ module GitHub case http_code when "401" - raise AuthenticationFailedError, message + raise AuthenticationFailedError.new(credentials_type, message) when "403" if meta.fetch("x-ratelimit-remaining", 1).to_i <= 0 reset = meta.fetch("x-ratelimit-reset").to_i raise RateLimitExceededError.new(reset, message) end - raise AuthenticationFailedError, message + raise AuthenticationFailedError.new(credentials_type, message) when "404" raise MissingAuthenticationError if credentials_type == :none && scopes.present? From fc63aca46a60252f6f82ba4373024ae8eaf9e9c6 Mon Sep 17 00:00:00 2001 From: Bjorn Neergaard Date: Thu, 25 May 2023 20:33:49 -0600 Subject: [PATCH 2/2] utils/github/api: refactor keychain code to use system_command --- Library/Homebrew/utils/github/api.rb | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Library/Homebrew/utils/github/api.rb b/Library/Homebrew/utils/github/api.rb index 7d03b0e403..4375e6882b 100644 --- a/Library/Homebrew/utils/github/api.rb +++ b/Library/Homebrew/utils/github/api.rb @@ -135,11 +135,14 @@ module GitHub # but only if that password looks like a GitHub Personal Access Token. sig { returns(T.nilable(String)) } def self.keychain_username_password - github_credentials = Utils.popen_write("git", "credential-osxkeychain", "get") do |pipe| - pipe.write "protocol=https\nhost=github.com\n" - end - github_username = github_credentials[/username=(.+)/, 1] - github_password = github_credentials[/password=(.+)/, 1] + git_credential_out, _, result = system_command "git", + args: ["credential-osxkeychain", "get"], + input: ["protocol=https\n", "host=github.com\n"], + print_stderr: false + return unless result.success? + + github_username = git_credential_out[/username=(.+)/, 1] + github_password = git_credential_out[/password=(.+)/, 1] return unless github_username # Don't use passwords from the keychain unless they look like @@ -148,12 +151,6 @@ module GitHub return unless GITHUB_PERSONAL_ACCESS_TOKEN_REGEX.match?(github_password) github_password - rescue Errno::EPIPE - # The above invocation via `Utils.popen` can fail, causing the pipe to be - # prematurely closed (before we can write to it) and thus resulting in a - # broken pipe error. The root cause is usually a missing or malfunctioning - # `git-credential-osxkeychain` helper. - nil end # odeprecated: Not really deprecated; change the order to prefer `github_cli_token` over