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

Add support to generate API token with tanzu api-token create #820

Merged
merged 2 commits into from
Oct 8, 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
1 change: 1 addition & 0 deletions docs/cli/commands/tanzu.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The Tanzu CLI

### SEE ALSO

* [tanzu api-token](tanzu_api-token.md) - Manage API Tokens for Tanzu Platform Self-managed
* [tanzu completion](tanzu_completion.md) - Output shell completion code
* [tanzu config](tanzu_config.md) - Configuration for the CLI
* [tanzu context](tanzu_context.md) - Configure and manage contexts for the Tanzu CLI
Expand Down
15 changes: 15 additions & 0 deletions docs/cli/commands/tanzu_api-token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## tanzu api-token

Manage API Tokens for Tanzu Platform Self-managed

### Options

```
-h, --help help for api-token
```

### SEE ALSO

* [tanzu](tanzu.md) - The Tanzu CLI
* [tanzu api-token create](tanzu_api-token_create.md) - Create a new API Token for Tanzu Platform Self-managed

29 changes: 29 additions & 0 deletions docs/cli/commands/tanzu_api-token_create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## tanzu api-token create

Create a new API Token for Tanzu Platform Self-managed

```
tanzu api-token create [flags]
```

### Examples

```

# Create an API Token for the Tanzu Platform Self-managed
tanzu api-token create

# Note: The retrieved token can be used as the value of TANZU_API_TOKEN
# environment variable when running 'tanzu login' for non-interactive workflow.
```

### Options

```
-h, --help help for create
```

### SEE ALSO

* [tanzu api-token](tanzu_api-token.md) - Manage API Tokens for Tanzu Platform Self-managed

22 changes: 20 additions & 2 deletions docs/quickstart/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,18 +232,27 @@ Notes:

##### API Token

To authenticate with the Tanzu Platform Endpoint for non-interactive login, you must first generate an API token.

###### Generating an API Token

- For public endpoints, generate the API token via the [CSP UI](https://console.tanzu.broadcom.com).
- For Self-Managed use cases that rely on UAA, create the API token using the `tanzu api-token create` command.

###### Logging in with an API Token

Example command to log in using an API token:

```console
TANZU_API_TOKEN=<APIToken> tanzu login
TANZU_API_TOKEN=<APIToken> tanzu login [--endpoint <tanzu-platform-endpoint>]
```

Users can persist the environment variable in the CLI configuration file, which will be used for each CLI command
invocation:

```console
tanzu config set env.TANZU_API_TOKEN <api_token>
tanzu login
tanzu login [--endpoint <tanzu-platform-endpoint>]
```

### Creating and connecting to a new context
Expand Down Expand Up @@ -298,6 +307,15 @@ Notes:

##### API Token

To authenticate with the Tanzu Platform Endpoint for non-interactive login, you must first generate an API token.

###### Generating an API Token

- For public endpoints, generate the API token via the [CSP UI](https://console.tanzu.broadcom.com).
- For Self-Managed use cases that rely on UAA, create the API token using the `tanzu api-token create` command.

###### Logging in with an API Token

Example command for creating a tanzu context using an API token:

```console
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/gnostic v0.6.9
github.com/google/go-cmp v0.5.9
github.com/google/go-containerregistry v0.15.2
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.0
Expand Down Expand Up @@ -139,7 +140,6 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/certificate-transparency-go v1.1.6 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-github/v50 v50.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
Expand Down Expand Up @@ -196,6 +196,7 @@ require (
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,7 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand Down
8 changes: 4 additions & 4 deletions pkg/auth/common/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import (
"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
"github.com/vmware-tanzu/tanzu-plugin-runtime/log"

timeutils "github.com/vmware-tanzu/tanzu-cli/pkg/utils/time"
)

const (
extraIDToken = "id_token"
)

var currentTime = time.Now

const (
APITokenType = "api-token"
IDTokenType = "id-token"
Expand Down Expand Up @@ -99,7 +99,7 @@ func GetToken(g *types.GlobalServerAuth, tokenGetter func(refreshOrAPIToken, acc
g.RefreshToken = token.RefreshToken
g.AccessToken = token.AccessToken
g.IDToken = token.IDToken
expiration := currentTime().Local().Add(time.Duration(token.ExpiresIn) * time.Second)
expiration := timeutils.Now().Local().Add(time.Duration(token.ExpiresIn) * time.Second)
anujc25 marked this conversation as resolved.
Show resolved Hide resolved
g.Expiration = expiration
g.Permissions = claims.Permissions

Expand Down Expand Up @@ -173,7 +173,7 @@ func ParseToken(tkn *oauth2.Token, idpType config.IdpType) (*Claims, error) {
func IsExpired(tokenExpiry time.Time) bool {
// refresh at half token life
two := 2
now := currentTime().Unix()
now := timeutils.Now().Unix()
halfDur := -time.Duration((tokenExpiry.Unix()-now)/int64(two)) * time.Second
return tokenExpiry.Add(halfDur).Unix() < now
}
7 changes: 4 additions & 3 deletions pkg/auth/common/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"

timeutils "github.com/vmware-tanzu/tanzu-cli/pkg/utils/time"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
configtypes "github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
)
Expand Down Expand Up @@ -190,7 +191,7 @@ func TestGetToken_Valid_NotExpired(t *testing.T) {
func TestGetToken_Expired(t *testing.T) {
var theOneNow = time.Now()
// override currentTime to always returns same value
currentTime = func() time.Time {
timeutils.Now = func() time.Time {
return theOneNow
}

Expand All @@ -200,7 +201,7 @@ func TestGetToken_Expired(t *testing.T) {
`{"sub":"1234567890","username":"joe","context_name":"1516239022"}`,
)

expireTime := currentTime().Add(-time.Minute * 30)
expireTime := timeutils.Now().Add(-time.Minute * 30)

serverAuth := configtypes.GlobalServerAuth{
Issuer: "https://oidc.example.com",
Expand All @@ -213,7 +214,7 @@ func TestGetToken_Expired(t *testing.T) {
}

newRefreshToken := "LetMeInAgain"
newExpiryTime := currentTime().Local().Add(time.Minute * 30)
newExpiryTime := timeutils.Now().Local().Add(time.Minute * 30)
newExpiry := int64(30 * 60)

tokenGetter := createMockTokenGetter(newRefreshToken, newExpiry)
Expand Down
2 changes: 1 addition & 1 deletion pkg/auth/uaa/tanzu.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func GetAlternateClientID() string {
return clientID
}

func TanzuLogin(issuerURL string, opts ...common.LoginOption) (*common.Token, error) {
var TanzuLogin = func(issuerURL string, opts ...common.LoginOption) (*common.Token, error) {
issuerEndpoints := getIssuerEndpoints(issuerURL)

h := common.NewTanzuLoginHandler(issuerURL, issuerEndpoints.AuthURL, issuerEndpoints.TokenURL, tanzuCLIClientID, tanzuCLIClientSecret, defaultListenAddress, defaultCallbackPath, config.UAAIdpType, nil, nil, term.IsTerminal)
Expand Down
104 changes: 104 additions & 0 deletions pkg/command/apitoken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2024 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package command

import (
"fmt"
"net/url"

"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/vmware-tanzu/tanzu-plugin-runtime/config"
"github.com/vmware-tanzu/tanzu-plugin-runtime/config/types"
"github.com/vmware-tanzu/tanzu-plugin-runtime/plugin"

commonauth "github.com/vmware-tanzu/tanzu-cli/pkg/auth/common"
"github.com/vmware-tanzu/tanzu-cli/pkg/auth/uaa"
"github.com/vmware-tanzu/tanzu-cli/pkg/cli"
"github.com/vmware-tanzu/tanzu-cli/pkg/constants"
)

func newAPITokenCmd() *cobra.Command {
apiTokenCmd := &cobra.Command{
Use: "api-token",
Short: "Manage API Tokens for Tanzu Platform Self-managed",
Aliases: []string{"apitoken"},
Annotations: map[string]string{
"group": string(plugin.SystemCmdGroup),
},
}

apiTokenCmd.SetUsageFunc(cli.SubCmdUsageFunc)
apiTokenCmd.AddCommand(
newAPITokenCreateCmd(),
)

return apiTokenCmd
}

func newAPITokenCreateCmd() *cobra.Command {
createCmd := &cobra.Command{
Use: "create",
Short: "Create a new API Token for Tanzu Platform Self-managed",
Aliases: []string{},
Example: `
# Create an API Token for the Tanzu Platform Self-managed
tanzu api-token create

# Note: The retrieved token can be used as the value of TANZU_API_TOKEN
# environment variable when running 'tanzu login' for non-interactive workflow.`,
RunE: createAPIToken,
ValidArgsFunction: noMoreCompletions,
}

return createCmd
}

func createAPIToken(cmd *cobra.Command, _ []string) (err error) {
c, err := config.GetActiveContext(types.ContextTypeTanzu)
if err != nil {
return errors.New("no active context found for Tanzu Platform. Please login to Tanzu Platform first to generate an API token")
}
if c == nil || c.GlobalOpts == nil || c.GlobalOpts.Auth.Issuer == "" {
return errors.New("invalid active context found for Tanzu Platform. Please login to Tanzu Platform first to generate an API token")
}
// Make sure it is of type tanzu with tanzuIdpType as `uaa` else return error
if idpType, exist := c.AdditionalMetadata[config.TanzuIdpTypeKey]; !exist || idpType != string(config.UAAIdpType) {
return errors.New("command no supported. Please refer to documentation on how to generate an API token for a public Tanzu Platform endpoint via https://console.tanzu.broadcom.com")
}

var token *commonauth.Token
// If user chooses to use a specific local listener port, use it
// Also specify the client ID to use for token generation
loginOptions := []commonauth.LoginOption{
commonauth.WithListenerPortFromEnv(constants.TanzuCLIOAuthLocalListenerPort),
commonauth.WithClientID(uaa.GetAlternateClientID()),
}

token, err = uaa.TanzuLogin(c.GlobalOpts.Auth.Issuer, loginOptions...)
if err != nil {
return errors.Wrap(err, "unable to login")
}

// Get tanzu platform endpoint as best effort from the existing context
tpEndpoint := "<tanzu-platform-endpoint>"
if hubEndpoint, exist := c.AdditionalMetadata[config.TanzuHubEndpointKey]; exist && hubEndpoint != nil {
u, err := url.Parse(hubEndpoint.(string))
if err == nil {
tpEndpoint = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
}

cyanBold := color.New(color.FgCyan).Add(color.Bold)
bold := color.New(color.Bold)

fmt.Fprint(cmd.OutOrStdout(), bold.Sprint("==\n\n"))
fmt.Fprintf(cmd.OutOrStdout(), "%s Your generated API token is: %s\n\n", bold.Sprint("API Token Generation Successful!"), cyanBold.Sprint(token.RefreshToken))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have to workshop the err/output messages in a followup. Some thoughts

  • Is it better to output the token in stdout, and show the rest in stderr?
  • do we want to advocate TANZU_API_TOKEN=%s tanzu login, or mention the config file way to persist the env var?

The messages in these function

  • may confuse users who are interacting with the public endpoint, who ends up creating a valid tanzu endpoint and still get a surprise when this fails for him
  • the instructions for regenerate and re-login might be confusing to the user of the current CLI instance (who has obviously logged in)

Copy link
Contributor Author

@anujc25 anujc25 Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to output the token in stdout, and show the rest in stderr?

I would prefer using stdout for both the token and the rest of the output. Since this is an output message intended for the user, it's more conventional to keep all output in stdout.

fmt.Fprintf(cmd.OutOrStdout(), "For Tanzu CLI use in non-interactive settings, set the environment variable %s before authenticating with the command %s\n\n", cyanBold.Sprintf("TANZU_API_TOKEN=%s", token.RefreshToken), cyanBold.Sprintf("tanzu login --endpoint %s", tpEndpoint))
fmt.Fprint(cmd.OutOrStdout(), "Please copy and save your token securely. Note that you will need to regenerate a new token before expiration time and login again to continue using the CLI.\n")

return nil
}
Loading
Loading