Merge branch 'trunk' into dependabot/go_modules/github.com/cpuguy83/go-md2man/v2-2.0.5

This commit is contained in:
Kynan Ware 2024-09-23 12:27:49 -06:00 committed by GitHub
commit be7631c7c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 237 additions and 29 deletions

2
go.mod
View file

@ -25,7 +25,7 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.1.3
github.com/henvic/httpretty v0.1.4
github.com/in-toto/attestation v1.1.0
github.com/joho/godotenv v1.5.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51

4
go.sum
View file

@ -252,8 +252,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE=
github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE=
github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8=
github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg=
github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=
github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=

View file

@ -52,8 +52,32 @@ func TestTokenFromKeyringForUserErrorsIfUsernameIsBlank(t *testing.T) {
require.ErrorContains(t, err, "username cannot be blank")
}
func TestHasActiveToken(t *testing.T) {
// Given the user has logged in for a host
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "", false)
require.NoError(t, err)
// When we check if that host has an active token
hasActiveToken := authCfg.HasActiveToken("github.com")
// Then there is an active token
require.True(t, hasActiveToken, "expected there to be an active token")
}
func TestHasNoActiveToken(t *testing.T) {
// Given there are no users logged in for a host
authCfg := newTestAuthConfig(t)
// When we check if any host has an active token
hasActiveToken := authCfg.HasActiveToken("github.com")
// Then there is no active token
require.False(t, hasActiveToken, "expected there to be no active token")
}
func TestTokenStoredInConfig(t *testing.T) {
// When the user has logged in insecurely
// Given the user has logged in insecurely
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "", false)
require.NoError(t, err)

View file

@ -217,6 +217,12 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) {
return token, source
}
// HasActiveToken returns true when a token for the hostname is present.
func (c *AuthConfig) HasActiveToken(hostname string) bool {
token, _ := c.ActiveToken(hostname)
return token != ""
}
// HasEnvToken returns true when a token has been specified in an
// environment variable, else returns false.
func (c *AuthConfig) HasEnvToken() bool {

View file

@ -93,6 +93,9 @@ type Migration interface {
// with knowledge on how to access encrypted storage when neccesarry.
// Behavior is scoped to authentication specific tasks.
type AuthConfig interface {
// HasActiveToken returns true when a token for the hostname is present.
HasActiveToken(hostname string) bool
// ActiveToken will retrieve the active auth token for the given hostname, searching environment variables,
// general configuration, and finally encrypted storage.
ActiveToken(hostname string) (token string, source string)

View file

@ -92,7 +92,7 @@ func (p *surveyPrompter) InputHostname() (string, error) {
var result string
err := p.ask(
&survey.Input{
Message: "GHE hostname:",
Message: "Hostname:",
}, &result, survey.WithValidator(func(v interface{}) error {
return ghinstance.HostnameValidator(v.(string))
}))

View file

@ -69,6 +69,15 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
}
if ghinstance.IsTenancy(opts.Hostname) {
c, err := f.Config()
if err != nil {
return err
}
if !c.Authentication().HasActiveToken(opts.Hostname) {
return fmt.Errorf("not authenticated with %s", opts.Hostname)
}
hc, err := f.HttpClient()
if err != nil {
return err
@ -94,6 +103,7 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
},
}
cmdutil.DisableAuthCheck(&trustedRootCmd)
trustedRootCmd.Flags().StringVarP(&opts.TufUrl, "tuf-url", "", "", "URL to the TUF repository mirror")
trustedRootCmd.Flags().StringVarP(&opts.TufRootPath, "tuf-root", "", "", "Path to the TUF root.json file on disk")
trustedRootCmd.MarkFlagsRequiredTogether("tuf-url", "tuf-root")
@ -116,6 +126,12 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
// Disable local caching, so we get up-to-date response from TUF repository
tufOpt.CacheValidity = 0
// Target will be either the default trusted root, or the trust domain-qualified one
ghTR := defaultTR
if opts.TrustDomain != "" {
ghTR = fmt.Sprintf("%s.%s", opts.TrustDomain, defaultTR)
}
if opts.TufUrl != "" && opts.TufRootPath != "" {
tufRoot, err := os.ReadFile(opts.TufRootPath)
if err != nil {
@ -126,7 +142,7 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
tufOpt.RepositoryBaseURL = opts.TufUrl
tufOptions = append(tufOptions, tufConfig{
tufOptions: tufOpt,
targets: []string{defaultTR},
targets: []string{ghTR},
})
} else {
// Get from both Sigstore public good and GitHub private instance
@ -137,14 +153,9 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
tufOpt = verification.GitHubTUFOptions()
tufOpt.CacheValidity = 0
targets := []string{defaultTR}
if opts.TrustDomain != "" {
targets = append(targets, fmt.Sprintf("%s.%s",
opts.TrustDomain, defaultTR))
}
tufOptions = append(tufOptions, tufConfig{
tufOptions: tufOpt,
targets: targets,
targets: []string{ghTR},
})
}

View file

@ -3,6 +3,7 @@ package trustedroot
import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"
@ -10,8 +11,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
)
@ -19,6 +25,9 @@ func TestNewTrustedRootCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
Config: func() (gh.Config, error) {
return &ghmock.ConfigMock{}, nil
},
}
testcases := []struct {
@ -72,6 +81,83 @@ func TestNewTrustedRootCmd(t *testing.T) {
}
}
func TestNewTrustedRootWithTenancy(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
var testReg httpmock.Registry
var metaResp = api.MetaResponse{
Domains: api.Domain{
ArtifactAttestations: api.ArtifactAttestations{
TrustDomain: "foo",
},
},
}
testReg.Register(httpmock.REST(http.MethodGet, "meta"),
httpmock.StatusJSONResponse(200, &metaResp))
httpClientFunc := func() (*http.Client, error) {
reg := &testReg
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
}
cli := "--hostname foo-bar.ghe.com"
t.Run("Host with NO auth configured", func(t *testing.T) {
f := &cmdutil.Factory{
IOStreams: testIO,
Config: func() (gh.Config, error) {
return &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &stubAuthConfig{hasActiveToken: false}
},
}, nil
},
}
cmd := NewTrustedRootCmd(f, func(_ *Options) error {
return nil
})
argv := strings.Split(cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
assert.Error(t, err)
assert.ErrorContains(t, err, "not authenticated")
})
t.Run("Host with auth configured", func(t *testing.T) {
f := &cmdutil.Factory{
IOStreams: testIO,
Config: func() (gh.Config, error) {
return &ghmock.ConfigMock{
AuthenticationFunc: func() gh.AuthConfig {
return &stubAuthConfig{hasActiveToken: true}
},
}, nil
},
HttpClient: httpClientFunc,
}
cmd := NewTrustedRootCmd(f, func(_ *Options) error {
return nil
})
argv := strings.Split(cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
assert.NoError(t, err)
})
}
var newTUFErrClient tufClientInstantiator = func(o *tuf.Options) (*tuf.Client, error) {
return nil, fmt.Errorf("failed to create TUF client")
}
@ -99,3 +185,14 @@ func TestGetTrustedRoot(t *testing.T) {
})
}
type stubAuthConfig struct {
config.AuthConfig
hasActiveToken bool
}
var _ gh.AuthConfig = (*stubAuthConfig)(nil)
func (c *stubAuthConfig) HasActiveToken(host string) bool {
return c.hasActiveToken
}

View file

@ -59,6 +59,9 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
Long: heredoc.Docf(`
Authenticate with a GitHub host.
The default hostname is %[1]sgithub.com%[1]s. This can be overridden using the %[1]s--hostname%[1]s
flag.
The default authentication mode is a web-based browser flow. After completion, an
authentication token will be stored securely in the system credential store.
If a credential store is not found or there is an issue using it gh will fallback
@ -222,21 +225,19 @@ func loginRun(opts *LoginOptions) error {
}
func promptForHostname(opts *LoginOptions) (string, error) {
options := []string{"GitHub.com", "GitHub Enterprise Server"}
options := []string{"GitHub.com", "Other"}
hostType, err := opts.Prompter.Select(
"What account do you want to log into?",
"Where do you use GitHub?",
options[0],
options)
if err != nil {
return "", err
}
isEnterprise := hostType == 1
hostname := ghinstance.Default()
if isEnterprise {
hostname, err = opts.Prompter.InputHostname()
isGitHubDotCom := hostType == 0
if isGitHubDotCom {
return ghinstance.Default(), nil
}
return hostname, err
return opts.Prompter.InputHostname()
}

View file

@ -2,6 +2,7 @@ package login
import (
"bytes"
"fmt"
"net/http"
"regexp"
"runtime"
@ -546,7 +547,7 @@ func Test_loginRun_Survey(t *testing.T) {
wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://rebecca.chambers/settings/tokens"),
},
{
name: "choose enterprise",
name: "choose Other",
wantHosts: heredoc.Doc(`
brad.vickers:
users:
@ -563,8 +564,8 @@ func Test_loginRun_Survey(t *testing.T) {
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What account do you want to log into?":
return prompter.IndexFor(opts, "GitHub Enterprise Server")
case "Where do you use GitHub?":
return prompter.IndexFor(opts, "Other")
case "What is your preferred protocol for Git operations on this host?":
return prompter.IndexFor(opts, "HTTPS")
case "How would you like to authenticate GitHub CLI?":
@ -606,7 +607,7 @@ func Test_loginRun_Survey(t *testing.T) {
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What account do you want to log into?":
case "Where do you use GitHub?":
return prompter.IndexFor(opts, "GitHub.com")
case "What is your preferred protocol for Git operations on this host?":
return prompter.IndexFor(opts, "HTTPS")
@ -640,7 +641,7 @@ func Test_loginRun_Survey(t *testing.T) {
prompterStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "What account do you want to log into?":
case "Where do you use GitHub?":
return prompter.IndexFor(opts, "GitHub.com")
case "What is your preferred protocol for Git operations on this host?":
return prompter.IndexFor(opts, "SSH")
@ -803,3 +804,50 @@ func Test_loginRun_Survey(t *testing.T) {
})
}
}
func Test_promptForHostname(t *testing.T) {
tests := []struct {
name string
options []string
selectedIndex int
// This is so we can test that the options in the function don't change
expectedSelection string
inputHostname string
expect string
}{
{
name: "select 'GitHub.com'",
selectedIndex: 0,
expectedSelection: "GitHub.com",
expect: "github.com",
},
{
name: "select 'Other'",
selectedIndex: 1,
expectedSelection: "Other",
inputHostname: "github.enterprise.com",
expect: "github.enterprise.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
promptMock := &prompter.PrompterMock{
SelectFunc: func(_ string, _ string, options []string) (int, error) {
if options[tt.selectedIndex] != tt.expectedSelection {
return 0, fmt.Errorf("expected %s at index %d, but got %s", tt.expectedSelection, tt.selectedIndex, options[tt.selectedIndex])
}
return tt.selectedIndex, nil
},
InputHostnameFunc: func() (string, error) {
return tt.inputHostname, nil
},
}
opts := &LoginOptions{
Prompter: promptMock,
}
hostname, err := promptForHostname(opts)
require.NoError(t, err)
require.Equal(t, tt.expect, hostname)
})
}
}

View file

@ -36,7 +36,7 @@ func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobr
Long: heredoc.Docf(`
This command configures %[1]sgit%[1]s to use GitHub CLI as a credential helper.
For more information on git credential helpers please reference:
https://git-scm.com/docs/gitcredentials.
<https://git-scm.com/docs/gitcredentials>.
By default, GitHub CLI will be set as the credential helper for all authenticated hosts.
If there is no authenticated hosts the command fails with an error.

View file

@ -280,9 +280,13 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error {
}
if asset == nil {
cs := m.io.ColorScheme()
errorMessageInRed := fmt.Sprintf(cs.Red("%[1]s unsupported for %[2]s."), repo.RepoName(), platform)
issueCreateCommand := generateMissingBinaryIssueCreateCommand(repo.RepoOwner(), repo.RepoName(), platform)
return fmt.Errorf(
"%[1]s unsupported for %[2]s. Open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`",
repo.RepoName(), platform, repo.RepoOwner())
"%[1]s\n\nTo request support for %[2]s, open an issue on the extension's repo by running the following command:\n\n `%[3]s`",
errorMessageInRed, platform, issueCreateCommand)
}
name := repo.RepoName()
@ -334,6 +338,15 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error {
return nil
}
func generateMissingBinaryIssueCreateCommand(repoOwner string, repoName string, currentPlatform string) string {
issueBody := generateMissingBinaryIssueBody(currentPlatform)
return fmt.Sprintf("gh issue create -R %[1]s/%[2]s --title \"Add support for the %[3]s architecture\" --body \"%[4]s\"", repoOwner, repoName, currentPlatform, issueBody)
}
func generateMissingBinaryIssueBody(currentPlatform string) string {
return fmt.Sprintf("This extension does not support the %[1]s architecture. I tried to install it on a %[1]s machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see <https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions>.", currentPlatform)
}
func writeManifest(dir, name string, data []byte) (writeErr error) {
path := filepath.Join(dir, name)
var f *os.File

View file

@ -906,7 +906,7 @@ func TestManager_Install_binary_unsupported(t *testing.T) {
m := newTestManager(tempDir, &client, nil, ios)
err := m.Install(repo, "")
assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`")
assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64.\n\nTo request support for windows-amd64, open an issue on the extension's repo by running the following command:\n\n\t`gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see <https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions>.\"`")
assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())

View file

@ -93,7 +93,12 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
To create a remote repository from an existing local repository, specify the source directory with %[1]s--source%[1]s.
By default, the remote repository name will be the name of the source directory.
Pass %[1]s--push%[1]s to push any local commits to the new repository.
For language or platform .gitignore templates to use with %[1]s--gitignore%[1]s, <https://github.com/github/gitignore>.
For license keywords to use with %[1]s--license%[1]s, <https://choosealicense.com/>.
`, "`"),
Example: heredoc.Doc(`
# create a repository interactively