diff --git a/pkg/commands/profile/profile_test.go b/pkg/commands/profile/profile_test.go index 0ab737ad6..c998d3f0d 100644 --- a/pkg/commands/profile/profile_test.go +++ b/pkg/commands/profile/profile_test.go @@ -404,7 +404,7 @@ func TestProfileToken(t *testing.T) { scenarios := []testutil.CLIScenario{ { - Name: "validate the active profile token is displayed by default", + Name: "validate the active profile non-OIDC token is displayed by default", Env: &testutil.EnvConfig{ Opts: &testutil.EnvOpts{ Copy: []testutil.FileIO{ @@ -421,25 +421,21 @@ func TestProfileToken(t *testing.T) { ConfigFile: &config.File{ Profiles: config.Profiles{ "foo": &config.Profile{ - Default: true, - Email: "foo@example.com", - Token: "123", - RefreshTokenCreated: now.Unix(), - RefreshTokenTTL: 600, + Default: true, + Email: "foo@example.com", + Token: "123", }, "bar": &config.Profile{ - Default: false, - Email: "bar@example.com", - Token: "456", - RefreshTokenCreated: now.Unix(), - RefreshTokenTTL: 600, + Default: false, + Email: "bar@example.com", + Token: "456", }, }, }, WantOutput: "123", }, { - Name: "validate token is displayed for the specified profile", + Name: "validate non-OIDC token is displayed for the specified profile", Args: "bar", // we choose a non-default profile Env: &testutil.EnvConfig{ Opts: &testutil.EnvOpts{ @@ -457,25 +453,21 @@ func TestProfileToken(t *testing.T) { ConfigFile: &config.File{ Profiles: config.Profiles{ "foo": &config.Profile{ - Default: true, - Email: "foo@example.com", - Token: "123", - RefreshTokenCreated: now.Unix(), - RefreshTokenTTL: 600, + Default: true, + Email: "foo@example.com", + Token: "123", }, "bar": &config.Profile{ - Default: false, - Email: "bar@example.com", - Token: "456", - RefreshTokenCreated: now.Unix(), - RefreshTokenTTL: 600, + Default: false, + Email: "bar@example.com", + Token: "456", }, }, }, WantOutput: "456", }, { - Name: "validate token is displayed for the specified profile using global --profile", + Name: "validate non-OIDC token is displayed for the specified profile using global --profile", Args: "--profile bar", // we choose a non-default profile Env: &testutil.EnvConfig{ Opts: &testutil.EnvOpts{ @@ -493,18 +485,14 @@ func TestProfileToken(t *testing.T) { ConfigFile: &config.File{ Profiles: config.Profiles{ "foo": &config.Profile{ - Default: true, - Email: "foo@example.com", - Token: "123", - RefreshTokenCreated: now.Unix(), - RefreshTokenTTL: 600, + Default: true, + Email: "foo@example.com", + Token: "123", }, "bar": &config.Profile{ - Default: false, - Email: "bar@example.com", - Token: "456", - RefreshTokenCreated: now.Unix(), - RefreshTokenTTL: 600, + Default: false, + Email: "bar@example.com", + Token: "456", }, }, }, @@ -529,7 +517,7 @@ func TestProfileToken(t *testing.T) { WantError: "profile 'unknown' does not exist", }, { - Name: "validate that an expired token generates an error", + Name: "validate that an expired OIDC token generates an error", Env: &testutil.EnvConfig{ Opts: &testutil.EnvOpts{ Copy: []testutil.FileIO{ @@ -557,7 +545,7 @@ func TestProfileToken(t *testing.T) { WantError: fmt.Sprintf("the token in profile 'foo' expired at '%s'", now.Add(time.Duration(-600)*time.Second).UTC().Format(fsttime.Format)), }, { - Name: "validate that a soon-to-expire token generates an error", + Name: "validate that a soon-to-expire OIDC token generates an error", Env: &testutil.EnvConfig{ Opts: &testutil.EnvOpts{ Copy: []testutil.FileIO{ @@ -584,6 +572,64 @@ func TestProfileToken(t *testing.T) { }, WantError: fmt.Sprintf("the token in profile 'foo' will expire at '%s'", now.Add(time.Duration(30)*time.Second).UTC().Format(fsttime.Format)), }, + { + Name: "validate that a soon-to-expire OIDC token with a non-default TTL does not generate an error", + Args: "--ttl 30s", + Env: &testutil.EnvConfig{ + Opts: &testutil.EnvOpts{ + Copy: []testutil.FileIO{ + { + Src: filepath.Join("testdata", "config.toml"), + Dst: "config.toml", + }, + }, + }, + EditScenario: func(scenario *testutil.CLIScenario, rootdir string) { + scenario.ConfigPath = filepath.Join(rootdir, "config.toml") + }, + }, + ConfigFile: &config.File{ + Profiles: config.Profiles{ + "foo": &config.Profile{ + Default: true, + Email: "foo@example.com", + Token: "123", + RefreshTokenCreated: now.Unix(), + RefreshTokenTTL: 60, + }, + }, + }, + WantOutput: "123", + }, + { + Name: "validate that an OIDC token with a long non-default TTL generates an error", + Args: "--ttl 1800s", + Env: &testutil.EnvConfig{ + Opts: &testutil.EnvOpts{ + Copy: []testutil.FileIO{ + { + Src: filepath.Join("testdata", "config.toml"), + Dst: "config.toml", + }, + }, + }, + EditScenario: func(scenario *testutil.CLIScenario, rootdir string) { + scenario.ConfigPath = filepath.Join(rootdir, "config.toml") + }, + }, + ConfigFile: &config.File{ + Profiles: config.Profiles{ + "foo": &config.Profile{ + Default: true, + Email: "foo@example.com", + Token: "123", + RefreshTokenCreated: now.Unix(), + RefreshTokenTTL: 1200, + }, + }, + }, + WantError: fmt.Sprintf("the token in profile 'foo' will expire at '%s'", now.Add(time.Duration(1200)*time.Second).UTC().Format(fsttime.Format)), + }, } testutil.RunCLIScenarios(t, []string{root.CommandName, "token"}, scenarios) diff --git a/pkg/commands/profile/token.go b/pkg/commands/profile/token.go index c2567bad2..1cb56b97e 100644 --- a/pkg/commands/profile/token.go +++ b/pkg/commands/profile/token.go @@ -17,7 +17,8 @@ import ( // TokenCommand represents a Kingpin command. type TokenCommand struct { argparser.Base - profile string + profile string + tokenTTL time.Duration } // NewTokenCommand returns a new command registered in the parent. @@ -26,6 +27,7 @@ func NewTokenCommand(parent argparser.Registerer, g *global.Data) *TokenCommand c.Globals = g c.CmdClause = parent.Command("token", "Print API token (defaults to the 'active' profile)") c.CmdClause.Arg("profile", "Print API token for the named profile").Short('p').StringVar(&c.profile) + c.CmdClause.Flag("ttl", "Amount of time for which the token must be valid (in seconds 's', minutes 'm', or hours 'h')").Default(defaultTokenTTL.String()).DurationVar(&c.tokenTTL) return &c } @@ -47,7 +49,7 @@ func (c *TokenCommand) Exec(_ io.Reader, out io.Writer) (err error) { if name != "" { if p := profile.Get(name, c.Globals.Config.Profiles); p != nil { - if err = checkTokenValidity(name, p, defaultTokenTTL); err != nil { + if err = checkTokenValidity(name, p, c.tokenTTL); err != nil { return err } text.Output(out, p.Token) @@ -62,7 +64,7 @@ func (c *TokenCommand) Exec(_ io.Reader, out io.Writer) (err error) { // If no 'profile' arg or global --profile, then we'll use 'active' profile. if name, p := profile.Default(c.Globals.Config.Profiles); p != nil { - if err = checkTokenValidity(name, p, defaultTokenTTL); err != nil { + if err = checkTokenValidity(name, p, c.tokenTTL); err != nil { return err } text.Output(out, p.Token) @@ -75,6 +77,12 @@ func (c *TokenCommand) Exec(_ io.Reader, out io.Writer) (err error) { } func checkTokenValidity(profileName string, p *config.Profile, ttl time.Duration) (err error) { + // if the token in the profile was not obtained via OIDC, + // there is no expiration information available + if p.RefreshTokenCreated == 0 { + return nil + } + var msg string expiry := time.Unix(p.RefreshTokenCreated, 0).Add(time.Duration(p.RefreshTokenTTL) * time.Second) diff --git a/pkg/errors/remediation_error.go b/pkg/errors/remediation_error.go index 00e7637ce..7e191ee29 100644 --- a/pkg/errors/remediation_error.go +++ b/pkg/errors/remediation_error.go @@ -166,7 +166,7 @@ var InvalidStaticConfigRemediation = strings.Join([]string{ "https://github.com/fastly/cli/issues/new?labels=bug&template=bug_report.md", }, " ") -// TokenExpirationRemediation indicates that a stored token has expired. +// TokenExpirationRemediation indicates that a stored OIDC token has expired. var TokenExpirationRemediation = strings.Join([]string{ - "Run 'fastly --profile update' to refresh the token.", + "Run 'fastly --profile sso' to refresh the token.", }, " ")