Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profile/token): Allow user to specify how long token must be valid. #1340

Merged
merged 3 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 81 additions & 35 deletions pkg/commands/profile/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -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",
},
},
},
Expand All @@ -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{
Expand Down Expand Up @@ -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{
Expand All @@ -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)
Expand Down
14 changes: 11 additions & 3 deletions pkg/commands/profile/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions pkg/errors/remediation_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <NAME> update' to refresh the token.",
"Run 'fastly --profile <NAME> sso' to refresh the token.",
}, " ")
Loading