From 19c3e9068d4552dcd9279438601611545e31d010 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 25 Mar 2024 17:43:55 -0700 Subject: [PATCH] Allow more profile commands to be run while unauthenticated Some of the profile/profiles commands required the CLI to be authenticated unnecessarily. This fixes that and makes authentication optional for `profile set` when validating the org/project_id. --- .changelog/47.txt | 3 ++ .../commands/profile/profiles/activate.go | 1 + internal/commands/profile/profiles/create.go | 1 + internal/commands/profile/profiles/delete.go | 1 + internal/commands/profile/profiles/list.go | 1 + internal/commands/profile/profiles/rename.go | 3 +- internal/commands/profile/set.go | 25 ++++++++-- internal/commands/profile/set_test.go | 48 +++++++++++++------ internal/pkg/auth/auth.go | 18 +++++++ 9 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 .changelog/47.txt diff --git a/.changelog/47.txt b/.changelog/47.txt new file mode 100644 index 00000000..4eaf0432 --- /dev/null +++ b/.changelog/47.txt @@ -0,0 +1,3 @@ +```release-note:bug +profile: Some profile and profiles commands required authentication unnecessarily. +``` diff --git a/internal/commands/profile/profiles/activate.go b/internal/commands/profile/profiles/activate.go index bc22f8b4..0f49dfac 100644 --- a/internal/commands/profile/profiles/activate.go +++ b/internal/commands/profile/profiles/activate.go @@ -37,6 +37,7 @@ func NewCmdActivate(ctx *cmd.Context) *cmd.Command { }, }, }, + NoAuthRequired: true, RunF: func(c *cmd.Command, args []string) error { opts.Name = args[0] l, err := profile.NewLoader() diff --git a/internal/commands/profile/profiles/create.go b/internal/commands/profile/profiles/create.go index 75dca9f0..b70ce6ec 100644 --- a/internal/commands/profile/profiles/create.go +++ b/internal/commands/profile/profiles/create.go @@ -48,6 +48,7 @@ func NewCmdCreate(ctx *cmd.Context) *cmd.Command { }, }, }, + NoAuthRequired: true, RunF: func(c *cmd.Command, args []string) error { opts.Name = args[0] l, err := profile.NewLoader() diff --git a/internal/commands/profile/profiles/delete.go b/internal/commands/profile/profiles/delete.go index 4fa4e6d6..0fcce502 100644 --- a/internal/commands/profile/profiles/delete.go +++ b/internal/commands/profile/profiles/delete.go @@ -41,6 +41,7 @@ func NewCmdDelete(ctx *cmd.Context) *cmd.Command { `), }, }, + NoAuthRequired: true, Args: cmd.PositionalArguments{ Autocomplete: predictProfiles(true, false), Args: []cmd.PositionalArgument{ diff --git a/internal/commands/profile/profiles/list.go b/internal/commands/profile/profiles/list.go index caf0cde3..f809d978 100644 --- a/internal/commands/profile/profiles/list.go +++ b/internal/commands/profile/profiles/list.go @@ -29,6 +29,7 @@ func NewCmdList(ctx *cmd.Context) *cmd.Command { Command: "$ hcp profile profiles list", }, }, + NoAuthRequired: true, RunF: func(c *cmd.Command, args []string) error { l, err := profile.NewLoader() if err != nil { diff --git a/internal/commands/profile/profiles/rename.go b/internal/commands/profile/profiles/rename.go index aa5651de..2e324e9e 100644 --- a/internal/commands/profile/profiles/rename.go +++ b/internal/commands/profile/profiles/rename.go @@ -25,7 +25,7 @@ func NewCmdRename(ctx *cmd.Context) *cmd.Command { Examples: []cmd.Example{ { Preamble: heredoc.New(ctx.IO).Must(` - To rename profile {{ template "mdCodeOrBold" "my-profile" }} to + To rename profile {{ template "mdCodeOrBold" "my-profile" }} to {{ template "mdCodeOrBold" "new-profile" }}, run: `), Command: "$ hcp profile profiles rename my-profile --new-name=new-profile", @@ -51,6 +51,7 @@ func NewCmdRename(ctx *cmd.Context) *cmd.Command { }, }, }, + NoAuthRequired: true, RunF: func(c *cmd.Command, args []string) error { opts.ExistingName = args[0] l, err := profile.NewLoader() diff --git a/internal/commands/profile/set.go b/internal/commands/profile/set.go index b20f3c36..d3c69bc9 100644 --- a/internal/commands/profile/set.go +++ b/internal/commands/profile/set.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/organization_service" "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/project_service" + "github.com/hashicorp/hcp/internal/pkg/auth" "github.com/hashicorp/hcp/internal/pkg/cmd" "github.com/hashicorp/hcp/internal/pkg/heredoc" "github.com/hashicorp/hcp/internal/pkg/iostreams" @@ -24,6 +25,7 @@ func NewCmdSet(ctx *cmd.Context) *cmd.Command { Profile: ctx.Profile, ProjectService: project_service.New(ctx.HCP, nil), OrganizationService: organization_service.New(ctx.HCP, nil), + isAuthed: auth.IsAuthenticated, } cmd := &cmd.Command{ @@ -69,6 +71,7 @@ func NewCmdSet(ctx *cmd.Context) *cmd.Command { AdditionalDocs: []cmd.DocSection{ availablePropertiesDoc(ctx.IO), }, + NoAuthRequired: true, RunF: func(c *cmd.Command, args []string) error { opts.Property = args[0] opts.Value = args[1] @@ -79,11 +82,15 @@ func NewCmdSet(ctx *cmd.Context) *cmd.Command { return cmd } +type isAuthedFn func() (bool, error) + type SetOpts struct { Ctx context.Context IO iostreams.IOStreams Profile *profile.Profile + isAuthed isAuthedFn + // Resource Manager client ProjectService project_service.ClientService OrganizationService organization_service.ClientService @@ -168,7 +175,13 @@ func setRun(opts *SetOpts) error { } func (o *SetOpts) validateProject() (bool, error) { - if o.Property != "project_id" || !o.IO.CanPrompt() { + // If the CLI is not authenticated, skip validation. + isAuthed, err := o.isAuthed() + if err != nil { + return false, err + } + + if o.Property != "project_id" || !o.IO.CanPrompt() || !isAuthed { return true, nil } @@ -178,7 +191,7 @@ func (o *SetOpts) validateProject() (bool, error) { ID: o.Value, Context: o.Ctx, } - _, err := o.ProjectService.ProjectServiceGet(params, nil) + _, err = o.ProjectService.ProjectServiceGet(params, nil) if err != nil { var listErr *project_service.ProjectServiceGetDefault if errors.As(err, &listErr) { @@ -211,7 +224,13 @@ func (o *SetOpts) validateProject() (bool, error) { } func (o *SetOpts) validateOrg() (bool, error) { - if o.Property != "organization_id" || !o.IO.CanPrompt() { + // If the CLI is not authenticated, skip validation. + isAuthed, err := o.isAuthed() + if err != nil { + return false, err + } + + if o.Property != "organization_id" || !o.IO.CanPrompt() || !isAuthed { return true, nil } diff --git a/internal/commands/profile/set_test.go b/internal/commands/profile/set_test.go index 62734494..7cd6f5dc 100644 --- a/internal/commands/profile/set_test.go +++ b/internal/commands/profile/set_test.go @@ -85,6 +85,7 @@ func TestSet(t *testing.T) { ProjectService: nil, Property: c.Property, Value: c.Value, + isAuthed: func() (bool, error) { return true, nil }, } err := setRun(o) @@ -113,7 +114,7 @@ func TestSet_Project(t *testing.T) { Property: "project_id", } - setup := func(quiet, tty bool, projectID string) { + setup := func(quiet, tty, authed bool, projectID string) { o.Value = projectID io.SetQuiet(quiet) io.InputTTY = tty @@ -121,6 +122,7 @@ func TestSet_Project(t *testing.T) { io.Input.Reset() io.Error.Reset() io.Output.Reset() + o.isAuthed = func() (bool, error) { return authed, nil } } checkProject := func(expected string) { @@ -129,9 +131,9 @@ func TestSet_Project(t *testing.T) { r.Equal(expected, loadedProfile.ProjectID) } - // Run with quiet off, TTY's, and return that the user has access to the project + // Run with quiet off, TTY's, authenticated, and return that the user has access to the project { - setup(false, true, "123") + setup(false, true, true, "123") psvc.EXPECT().ProjectServiceGet(mock.MatchedBy(func(getReq *project_service.ProjectServiceGetParams) bool { return getReq != nil && getReq.ID == o.Value }), mock.Anything).Once().Return(nil, nil) @@ -141,7 +143,7 @@ func TestSet_Project(t *testing.T) { // Call again but return permission denied and accept the prompt { - setup(false, true, "accept") + setup(false, true, true, "accept") psvc.EXPECT().ProjectServiceGet(mock.MatchedBy(func(getReq *project_service.ProjectServiceGetParams) bool { return getReq != nil && getReq.ID == o.Value }), mock.Anything).Once().Return(nil, project_service.NewProjectServiceGetDefault(http.StatusForbidden)) @@ -158,7 +160,7 @@ func TestSet_Project(t *testing.T) { // Call again but return permission denied and do not accept the prompt { - setup(false, true, "no-accept") + setup(false, true, true, "no-accept") psvc.EXPECT().ProjectServiceGet(mock.MatchedBy(func(getReq *project_service.ProjectServiceGetParams) bool { return getReq != nil && getReq.ID == o.Value }), mock.Anything).Once().Return(nil, project_service.NewProjectServiceGetDefault(http.StatusForbidden)) @@ -175,7 +177,7 @@ func TestSet_Project(t *testing.T) { // Run again but with quiet { - setup(true, true, "789") + setup(true, true, true, "789") r.NoError(setRun(o)) r.NotContains(io.Error.String(), "Are you sure you wish to set the") checkProject("789") @@ -183,11 +185,19 @@ func TestSet_Project(t *testing.T) { // Run again but with no quiet but no tty { - setup(false, false, "012") + setup(false, false, true, "012") r.NoError(setRun(o)) r.NotContains(io.Error.String(), "Are you sure you wish to set the") checkProject("012") } + + // Run again but unauthenticated + { + setup(true, true, false, "789") + r.NoError(setRun(o)) + r.NotContains(io.Error.String(), "Are you sure you wish to set the") + checkProject("789") + } } func TestSet_Organization(t *testing.T) { @@ -205,7 +215,7 @@ func TestSet_Organization(t *testing.T) { Property: "organization_id", } - setup := func(quiet, tty bool, orgID string) { + setup := func(quiet, tty, authed bool, orgID string) { o.Value = orgID io.SetQuiet(quiet) io.InputTTY = tty @@ -213,6 +223,7 @@ func TestSet_Organization(t *testing.T) { io.Input.Reset() io.Error.Reset() io.Output.Reset() + o.isAuthed = func() (bool, error) { return authed, nil } } orgResp := func(ids ...string) *organization_service.OrganizationServiceListOK { @@ -236,9 +247,9 @@ func TestSet_Organization(t *testing.T) { r.Equal(expected, loadedProfile.OrganizationID) } - // Run with quiet off, TTY's, and return the user is a member of the org + // Run with quiet off, TTY's, authenticated, and return the user is a member of the org { - setup(false, true, "member") + setup(false, true, true, "member") osvc.EXPECT().OrganizationServiceList(mock.Anything, mock.Anything).Once().Return(orgResp("1", "2", "member"), nil) r.NoError(setRun(o)) checkOrg("member") @@ -246,7 +257,7 @@ func TestSet_Organization(t *testing.T) { // Not be a member, expect prompting and respond no { - setup(false, true, "not-a-member") + setup(false, true, true, "not-a-member") osvc.EXPECT().OrganizationServiceList(mock.Anything, mock.Anything).Once().Return(orgResp("1", "2", "123"), nil) // Answer yes to prompt @@ -261,7 +272,7 @@ func TestSet_Organization(t *testing.T) { // Not be a member, expect prompting and respond yes { - setup(false, true, "not-a-member") + setup(false, true, true, "not-a-member") osvc.EXPECT().OrganizationServiceList(mock.Anything, mock.Anything).Once().Return(orgResp("1", "2", "123"), nil) // Answer yes to prompt @@ -276,21 +287,28 @@ func TestSet_Organization(t *testing.T) { // Do not be a member; but quiet { - setup(true, true, "not-a-member-quiet") + setup(true, true, true, "not-a-member-quiet") r.NoError(setRun(o)) checkOrg("not-a-member-quiet") } // Do not be a member; but no tty { - setup(false, false, "not-a-member-no-tty") + setup(false, false, true, "not-a-member-no-tty") + r.NoError(setRun(o)) + checkOrg("not-a-member-no-tty") + } + + // Do not be a member; but unauthenticated + { + setup(false, false, false, "not-a-member-no-tty") r.NoError(setRun(o)) checkOrg("not-a-member-no-tty") } // Return error { - setup(false, true, "error") + setup(false, true, true, "error") osvc.EXPECT().OrganizationServiceList(mock.Anything, mock.Anything).Once(). Return(nil, organization_service.NewOrganizationServiceListDefault(http.StatusInternalServerError)) diff --git a/internal/pkg/auth/auth.go b/internal/pkg/auth/auth.go index 2af86e2d..68130563 100644 --- a/internal/pkg/auth/auth.go +++ b/internal/pkg/auth/auth.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "time" hcpconf "github.com/hashicorp/hcp-sdk-go/config" "github.com/mitchellh/go-homedir" @@ -65,3 +66,20 @@ func GetHCPCredFilePath(credFileDir string) (string, error) { credFilePath := filepath.Join(dir, CredFileName) return credFilePath, nil } + +// IsAuthenticated returns if there is a valid token available. +func IsAuthenticated() (bool, error) { + // Create the HCP Config + hcpCfg, err := GetHCPConfig(hcpconf.WithoutBrowserLogin()) + if err != nil { + return false, fmt.Errorf("failed to instantiate HCP config to check authentication status: %w", err) + } + + if tkn, err := hcpCfg.Token(); err != nil { + return false, nil + } else if !tkn.Expiry.After(time.Now()) { + return false, nil + } + + return true, nil +}