Repository: cli/cli
Pull request #9000 (2024-04-25 13:32:21)

proof of concept for flag-level disable auth check
Relates #8995 

Building upon the existing command-level disable auth check logic, this commit adds flag-level disable auth check logic for any flag set with `-b,--bundle` flag of `gh attestation verify` being the first use case.

Subsequent commit to build out testing is needed as IsAuthCheckEnabled does not have tests.

### Testing

1. **Build changes and setup temporary working directory**
   
   ```shell
   andyfeller@Andys-MBP:cli/cli ‹andyfeller/flag-level-disableauth›$ make
   go build -trimpath -ldflags "-X github.com/cli/cli/v2/internal/build.Date=2024-04-25 -X github.com/cli/cli/v2/internal/build.Version=v2.48.0-9-g2d910406 " -o bin/gh ./cmd/gh
   
   andyfeller@Andys-MBP:cli/cli ‹andyfeller/flag-level-disableauth›$ mkdir tmp
   andyfeller@Andys-MBP:cli/cli ‹andyfeller/flag-level-disableauth›$ cd tmp 
   ```

1. **Download release artifact(s) built with attestation signatures captured**
   
   ```shell
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth›$ ../bin/gh release download v0.0.10 --repo advanced-security/gh-sbom
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ll
   total 152360
   -rw-r--r--  1 andyfeller  staff   5.8M Apr 25 09:41 android-amd64
   -rw-r--r--  1 andyfeller  staff   5.6M Apr 25 09:41 android-arm64
   -rw-r--r--  1 andyfeller  staff   5.6M Apr 25 09:41 darwin-amd64
   -rw-r--r--  1 andyfeller  staff   5.5M Apr 25 09:41 darwin-arm64
   -rw-r--r--  1 andyfeller  staff   5.0M Apr 25 09:41 freebsd-386
   -rw-r--r--  1 andyfeller  staff   5.3M Apr 25 09:41 freebsd-amd64
   -rw-r--r--  1 andyfeller  staff   5.1M Apr 25 09:41 freebsd-arm64
   -rw-r--r--  1 andyfeller  staff   5.1M Apr 25 09:41 linux-386
   -rw-r--r--  1 andyfeller  staff   5.3M Apr 25 09:41 linux-amd64
   -rw-r--r--  1 andyfeller  staff   5.2M Apr 25 09:41 linux-arm
   -rw-r--r--  1 andyfeller  staff   5.1M Apr 25 09:41 linux-arm64
   -rw-r--r--  1 andyfeller  staff   5.3M Apr 25 09:41 windows-386.exe
   -rw-r--r--  1 andyfeller  staff   5.5M Apr 25 09:41 windows-amd64.exe
   -rw-r--r--  1 andyfeller  staff   5.2M Apr 25 09:41 windows-arm64.exe
   ```

1. **Download artifact attestation bundle for artifact and verify**
   
   ```shell
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ../bin/gh attestation download darwin-amd64 --repo advanced-security/gh-sbom
   Wrote attestations to file sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437.jsonl.
   Any previous content has been overwritten
   
   The trusted metadata is now available at sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437.jsonl


   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ../bin/gh attestation verify darwin-amd64 --bundle sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437.jsonl --repo advanced-security/gh-sbom
   Loaded digest sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437 for file://darwin-amd64
   Loaded 2 attestations from sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437.jsonl
   ✓ Verification succeeded!
   
   sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437 was attested by:
   REPO                       PREDICATE_TYPE                  WORKFLOW                                        
   advanced-security/gh-sbom  https://slsa.dev/provenance/v1  .github/workflows/release.yml@refs/tags/v0.0.10
   advanced-security/gh-sbom  https://slsa.dev/provenance/v1  .github/workflows/release.yml@refs/tags/vtesting
   ```

1. **Logout of `github.com`**

   ```shell
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ../bin/gh auth status
   staffship-01.ghe.com
     ✓ Logged in to staffship-01.ghe.com account andyfeller (keyring)
     - Active account: true
     - Git operations protocol: https
     - Token: gho_************************************
     - Token scopes: 'gist', 'read:org', 'repo', 'workflow'
   
   github.ghe.com
     ✓ Logged in to github.ghe.com account andyfeller (keyring)
     - Active account: true
     - Git operations protocol: https
     - Token: gho_************************************
     - Token scopes: 'gist', 'read:org', 'repo', 'workflow'
   
   github.com
     ✓ Logged in to github.com account andyfeller (keyring)
     - Active account: true
     - Git operations protocol: ssh
     - Token: gho_************************************
     - Token scopes: 'admin:public_key', 'gist', 'read:org', 'repo'
   
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ../bin/gh auth logout
   ? What account do you want to log out of? andyfeller (github.com)
   ✓ Logged out of github.com account andyfeller
   
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ../bin/gh auth status
   staffship-01.ghe.com
     ✓ Logged in to staffship-01.ghe.com account andyfeller (keyring)
     - Active account: true
     - Git operations protocol: https
     - Token: gho_************************************
     - Token scopes: 'gist', 'read:org', 'repo', 'workflow'
   
   github.ghe.com
     ✓ Logged in to github.ghe.com account andyfeller (keyring)
     - Active account: true
     - Git operations protocol: https
     - Token: gho_************************************
     - Token scopes: 'gist', 'read:org', 'repo', 'workflow'
   ```

1. **Verify that `gh attestation verify -b` does not require auth with flag**
   
   ```shell
   andyfeller@Andys-MBP:cli/tmp ‹andyfeller/flag-level-disableauth*›$ ../bin/gh attestation verify darwin-amd64 --bundle sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437.jsonl --repo advanced-security/gh-sbom
   Loaded digest sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437 for file://darwin-amd64
   Loaded 2 attestations from sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437.jsonl
   ✓ Verification succeeded!
   
   sha256:ab1bae8f19e92e1dce879373194627c1b322575618d13dd635572777d6df2437 was attested by:
   REPO                       PREDICATE_TYPE                  WORKFLOW                                        
   advanced-security/gh-sbom  https://slsa.dev/provenance/v1  .github/workflows/release.yml@refs/tags/v0.0.10
   advanced-security/gh-sbom  https://slsa.dev/provenance/v1  .github/workflows/release.yml@refs/tags/vtesting
   ```

<!--
  Thank you for contributing to GitHub CLI!
  To reference an open issue, please write this in your description: `Fixes #NUMBER`
-->



Patch:
diff --git a/go.mod b/go.mod
index a59521bce..8dd2ba31d 100644
--- a/go.mod
+++ b/go.mod
@@ -47,7 +47,7 @@ require (
 	golang.org/x/sync v0.6.0
 	golang.org/x/term v0.19.0
 	golang.org/x/text v0.14.0
-	google.golang.org/grpc v1.62.2
+	google.golang.org/grpc v1.62.1
 	google.golang.org/protobuf v1.33.0
 	gopkg.in/h2non/gock.v1 v1.1.2
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index a91ef32f5..6f6ff091d 100644
--- a/go.sum
+++ b/go.sum
@@ -545,8 +545,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7 h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20240311173647-c811ad7063a7/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
-google.golang.org/grpc v1.62.2 h1:iEIj1U5qjyBjzkM5nk3Fq+S1IbjbXSyqeULZ1Nfo4AA=
-google.golang.org/grpc v1.62.2/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
 google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/cmdutil/auth_check.go b/pkg/cmdutil/auth_check.go
index 56ebb0c4d..fb269704f 100644
--- a/pkg/cmdutil/auth_check.go
+++ b/pkg/cmdutil/auth_check.go
@@ -1,16 +1,29 @@
 package cmdutil
 
 import (
+	"reflect"
+
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 )
 
+const skipAuthCheckAnnotation = "skipAuthCheck"
+
 func DisableAuthCheck(cmd *cobra.Command) {
 	if cmd.Annotations == nil {
 		cmd.Annotations = map[string]string{}
 	}
 
-	cmd.Annotations["skipAuthCheck"] = "true"
+	cmd.Annotations[skipAuthCheckAnnotation] = "true"
+}
+
+func DisableAuthCheckFlag(flag *pflag.Flag) {
+	if flag.Annotations == nil {
+		flag.Annotations = map[string][]string{}
+	}
+
+	flag.Annotations[skipAuthCheckAnnotation] = []string{"true"}
 }
 
 func CheckAuth(cfg config.Config) bool {
@@ -32,7 +45,19 @@ func IsAuthCheckEnabled(cmd *cobra.Command) bool {
 	}
 
 	for c := cmd; c.Parent() != nil; c = c.Parent() {
-		if c.Annotations != nil && c.Annotations["skipAuthCheck"] == "true" {
+		// Check whether any command marked as DisableAuthCheck is set
+		if c.Annotations != nil && c.Annotations[skipAuthCheckAnnotation] == "true" {
+			return false
+		}
+
+		// Check whether any flag marked as DisableAuthCheckFlag is set
+		var skipAuthCheck bool
+		c.Flags().Visit(func(f *pflag.Flag) {
+			if f.Annotations != nil && reflect.DeepEqual(f.Annotations[skipAuthCheckAnnotation], []string{"true"}) {
+				skipAuthCheck = true
+			}
+		})
+		if skipAuthCheck {
 			return false
 		}
 	}


Test patch:
diff --git a/pkg/cmd/attestation/attestation.go b/pkg/cmd/attestation/attestation.go
index 75de9eca7..9b3a0cd2c 100644
--- a/pkg/cmd/attestation/attestation.go
+++ b/pkg/cmd/attestation/attestation.go
@@ -15,6 +15,7 @@ func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command {
 		Use:     "attestation [subcommand]",
 		Short:   "Work with artifact attestations",
 		Aliases: []string{"at"},
+		Hidden:  true,
 		Long:    "Download and verify artifact attestations.",
 	}
 
diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go
index a323898ee..12d199e69 100644
--- a/pkg/cmd/attestation/verify/verify.go
+++ b/pkg/cmd/attestation/verify/verify.go
@@ -127,6 +127,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
 
 	// general flags
 	verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
+	cmdutil.DisableAuthCheckFlag(verifyCmd.Flags().Lookup("bundle"))
 	cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
 	verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by")
 	verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go
index 203f203b3..ef778f454 100644
--- a/pkg/cmd/attestation/verify/verify_test.go
+++ b/pkg/cmd/attestation/verify/verify_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/attestation/test"
 	"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
 	"github.com/cli/cli/v2/pkg/cmdutil"
+	"github.com/spf13/cobra"
 
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -237,6 +238,36 @@ func TestNewVerifyCmd(t *testing.T) {
 	}
 }
 
+func TestVerifyCmdAuthChecks(t *testing.T) {
+	f := &cmdutil.Factory{}
+
+	t.Run("by default auth check is required", func(t *testing.T) {
+		cmd := NewVerifyCmd(f, func(o *Options) error {
+			return nil
+		})
+
+		// IsAuthCheckEnabled assumes commands under test are subcommands
+		parent := &cobra.Command{Use: "root"}
+		parent.AddCommand(cmd)
+
+		require.NoError(t, cmd.ParseFlags([]string{}))
+		require.True(t, cmdutil.IsAuthCheckEnabled(cmd), "expected auth check to be required")
+	})
+
+	t.Run("when --bundle flag is provided, auth check is not required", func(t *testing.T) {
+		cmd := NewVerifyCmd(f, func(o *Options) error {
+			return nil
+		})
+
+		// IsAuthCheckEnabled assumes commands under test are subcommands
+		parent := &cobra.Command{Use: "root"}
+		parent.AddCommand(cmd)
+
+		require.NoError(t, cmd.ParseFlags([]string{"--bundle", "not-important"}))
+		require.False(t, cmdutil.IsAuthCheckEnabled(cmd), "expected auth check not to be required due to --bundle flag")
+	})
+}
+
 func TestJSONOutput(t *testing.T) {
 	testIO, _, out, _ := iostreams.Test()
 	opts := Options{
diff --git a/pkg/cmdutil/auth_check_test.go b/pkg/cmdutil/auth_check_test.go
index 25cbae567..02d1fd775 100644
--- a/pkg/cmdutil/auth_check_test.go
+++ b/pkg/cmdutil/auth_check_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/cli/cli/v2/internal/config"
+	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/require"
 )
 
@@ -53,3 +54,66 @@ func Test_CheckAuth(t *testing.T) {
 		})
 	}
 }
+
+func Test_IsAuthCheckEnabled(t *testing.T) {
+	tests := []struct {
+		name               string
+		init               func() (*cobra.Command, error)
+		isAuthCheckEnabled bool
+	}{
+		{
+			name: "no annotations",
+			init: func() (*cobra.Command, error) {
+				cmd := &cobra.Command{}
+				cmd.Flags().Bool("flag", false, "")
+				return cmd, nil
+			},
+			isAuthCheckEnabled: true,
+		},
+		{
+			name: "command-level disable",
+			init: func() (*cobra.Command, error) {
+				cmd := &cobra.Command{}
+				DisableAuthCheck(cmd)
+				return cmd, nil
+			},
+			isAuthCheckEnabled: false,
+		},
+		{
+			name: "command with flag-level disable, flag not set",
+			init: func() (*cobra.Command, error) {
+				cmd := &cobra.Command{}
+				cmd.Flags().Bool("flag", false, "")
+				DisableAuthCheckFlag(cmd.Flag("flag"))
+				return cmd, nil
+			},
+			isAuthCheckEnabled: true,
+		},
+		{
+			name: "command with flag-level disable, flag set",
+			init: func() (*cobra.Command, error) {
+				cmd := &cobra.Command{}
+				cmd.Flags().Bool("flag", false, "")
+				if err := cmd.Flags().Set("flag", "true"); err != nil {
+					return nil, err
+				}
+
+				DisableAuthCheckFlag(cmd.Flag("flag"))
+				return cmd, nil
+			},
+			isAuthCheckEnabled: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			cmd, err := tt.init()
+			require.NoError(t, err)
+
+			// IsAuthCheckEnabled assumes commands under test are subcommands
+			parent := &cobra.Command{Use: "root"}
+			parent.AddCommand(cmd)
+			require.Equal(t, tt.isAuthCheckEnabled, IsAuthCheckEnabled(cmd))
+		})
+	}
+}