Merge pull request #7597 from n1lesh/n1lesh-remove-scope

Add remove/reset to auth refresh
This commit is contained in:
William Martin 2023-06-26 22:54:51 +02:00 committed by GitHub
commit c4e648a8ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 179 additions and 13 deletions

View file

@ -24,9 +24,11 @@ type RefreshOptions struct {
MainExecutable string
Hostname string
Scopes []string
AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
Hostname string
Scopes []string
RemoveScopes []string
ResetScopes bool
AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
Interactive bool
InsecureStorage bool
@ -62,9 +64,12 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
your gh credentials to have. If no scopes are provided, the command
maintains previously added scopes.
The command can only add additional scopes, but not remove previously
added ones. To reset scopes to the default minimum set of scopes, you
will need to create new credentials using the auth login command.
The --remove-scope flag accepts a comma separated list of scopes you
want to remove from your gh credentials. Scope removal is idempotent.
The minimum set of scopes ("repo", "read:org" and "gist") cannot be removed.
The --reset-scopes flag resets the scopes for your gh credentials to
the default set of scopes for your auth flow.
`),
Example: heredoc.Doc(`
$ gh auth refresh --scopes write:org,read:public_key
@ -72,6 +77,12 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
$ gh auth refresh
# => open a browser to ensure your authentication credentials have the correct minimum scopes
$ gh auth refresh --remove-scope delete_repo
# => open a browser to idempotently remove the delete_repo scope
$ gh auth refresh --reset-scopes
# => open a browser to re-authenticate with the default minimum scopes
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.Interactive = opts.IO.CanPrompt()
@ -90,6 +101,8 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication")
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have")
cmd.Flags().StringSliceVarP(&opts.RemoveScopes, "remove-scope", "r", nil, "The authentication scope to remove from gh")
cmd.Flags().BoolVarP(&opts.ResetScopes, "reset-scopes", "R", false, "Reset authentication scopes to the default minimum set of scopes")
// secure storage became the default on 2023/4/04; this flag is left as a no-op for backwards compatibility
var secureStorage bool
cmd.Flags().BoolVar(&secureStorage, "secure-storage", false, "Save authentication credentials in secure credential store")
@ -144,12 +157,14 @@ func refreshRun(opts *RefreshOptions) error {
}
var additionalScopes []string
if oldToken, _ := authCfg.Token(hostname); oldToken != "" {
if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
for _, s := range strings.Split(oldScopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
additionalScopes = append(additionalScopes, s)
if !opts.ResetScopes {
if oldToken, _ := authCfg.Token(hostname); oldToken != "" {
if oldScopes, err := shared.GetScopes(opts.HttpClient, hostname, oldToken); err == nil {
for _, s := range strings.Split(oldScopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
additionalScopes = append(additionalScopes, s)
}
}
}
}
@ -168,7 +183,24 @@ func refreshRun(opts *RefreshOptions) error {
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
}
if err := opts.AuthFlow(authCfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...), opts.Interactive, !opts.InsecureStorage); err != nil {
additionalScopes = append(opts.Scopes, additionalScopes...)
if len(opts.RemoveScopes) > 0 {
var scopes []string
for _, scope := range additionalScopes {
var match bool
for _, rmScope := range opts.RemoveScopes {
if rmScope == scope {
match = true
break
}
}
if !match {
scopes = append(scopes, scope)
}
}
additionalScopes = scopes
}
if err := opts.AuthFlow(authCfg, opts.IO, hostname, additionalScopes, opts.Interactive, !opts.InsecureStorage); err != nil {
return err
}

View file

@ -99,6 +99,46 @@ func Test_NewCmdRefresh(t *testing.T) {
InsecureStorage: true,
},
},
{
name: "reset scopes",
tty: true,
cli: "--reset-scopes",
wants: RefreshOptions{
ResetScopes: true,
},
},
{
name: "reset scopes shorthand",
tty: true,
cli: "-R",
wants: RefreshOptions{
ResetScopes: true,
},
},
{
name: "remove scope",
tty: true,
cli: "--remove-scope read:public_key",
wants: RefreshOptions{
RemoveScopes: []string{"read:public_key"},
},
},
{
name: "remove multiple scopes",
tty: true,
cli: "--remove-scope workflow,read:public_key",
wants: RefreshOptions{
RemoveScopes: []string{"workflow", "read:public_key"},
},
},
{
name: "remove scope shorthand",
tty: true,
cli: "-r read:public_key",
wants: RefreshOptions{
RemoveScopes: []string{"read:public_key"},
},
},
}
for _, tt := range tests {
@ -280,6 +320,100 @@ func Test_refreshRun(t *testing.T) {
scopes: nil,
},
},
{
name: "reset scopes",
cfgHosts: []string{
"github.com",
},
oldScopes: "delete_repo, codespace",
opts: &RefreshOptions{
Hostname: "github.com",
ResetScopes: true,
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: nil,
secureStorage: true,
},
},
{
name: "reset scopes and add some scopes",
cfgHosts: []string{
"github.com",
},
oldScopes: "repo:invite, delete_repo, codespace",
opts: &RefreshOptions{
Scopes: []string{"public_key:read", "workflow"},
ResetScopes: true,
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{"public_key:read", "workflow"},
secureStorage: true,
},
},
{
name: "remove scopes",
cfgHosts: []string{
"github.com",
},
oldScopes: "delete_repo, codespace, repo:invite, public_key:read",
opts: &RefreshOptions{
Hostname: "github.com",
RemoveScopes: []string{"delete_repo", "repo:invite"},
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{"codespace", "public_key:read"},
secureStorage: true,
},
},
{
name: "remove scope but no old scope",
cfgHosts: []string{
"github.com",
},
opts: &RefreshOptions{
Hostname: "github.com",
RemoveScopes: []string{"delete_repo"},
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: nil,
secureStorage: true,
},
},
{
name: "remove and add scopes at the same time",
cfgHosts: []string{
"github.com",
},
oldScopes: "repo:invite, delete_repo, codespace",
opts: &RefreshOptions{
Scopes: []string{"repo:invite", "public_key:read", "workflow"},
RemoveScopes: []string{"codespace", "repo:invite", "workflow"},
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{"public_key:read", "delete_repo"},
secureStorage: true,
},
},
{
name: "remove scopes that don't exist",
cfgHosts: []string{
"github.com",
},
oldScopes: "repo:invite, delete_repo, codespace",
opts: &RefreshOptions{
RemoveScopes: []string{"codespace", "repo:invite", "public_key:read"},
},
wantAuthArgs: authArgs{
hostname: "github.com",
scopes: []string{"delete_repo"},
secureStorage: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {