Skip to content

feat: tokens #4380

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

Merged
merged 17 commits into from
Oct 6, 2022
4 changes: 2 additions & 2 deletions cli/cliui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
// Special type formatting.
switch val := v.(type) {
case time.Time:
v = val.Format(time.Stamp)
v = val.Format(time.RFC3339)
case *time.Time:
if val != nil {
v = val.Format(time.Stamp)
v = val.Format(time.RFC3339)
}
case fmt.Stringer:
if val != nil {
Expand Down
22 changes: 11 additions & 11 deletions cli/cliui/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ func Test_DisplayTable(t *testing.T) {
t.Parallel()

expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } Aug 2 15:49:10 <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } Aug 2 15:49:10 <nil>
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
`

// Test with non-pointer values.
Expand All @@ -158,10 +158,10 @@ baz 30 [] baz1 31 <nil> <nil> baz3
t.Parallel()

expected := `
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } Aug 2 15:49:10 <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } Aug 2 15:49:10 <nil>
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } Aug 2 15:49:10 Aug 2 15:49:10
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
`

out, err := cliui.DisplayTable(in, "name", nil)
Expand All @@ -175,9 +175,9 @@ foo 10 [a b c] foo1 11 foo2 12 foo3

expected := `
NAME SUB 1 NAME SUB 3 INNER NAME TIME
foo foo1 foo3 Aug 2 15:49:10
bar bar1 bar3 Aug 2 15:49:10
baz baz1 baz3 Aug 2 15:49:10
foo foo1 foo3 2022-08-02T15:49:10Z
bar bar1 bar3 2022-08-02T15:49:10Z
baz baz1 baz3 2022-08-02T15:49:10Z
`

out, err := cliui.DisplayTable(in, "", []string{"name", "sub_1_name", "sub_3 inner name", "time"})
Expand Down
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func Core() []*cobra.Command {
users(),
versionCmd(),
workspaceAgent(),
tokens(),
}
}

Expand Down
155 changes: 155 additions & 0 deletions cli/tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cli

import (
"fmt"
"strings"
"time"

"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)

func tokens() *cobra.Command {
cmd := &cobra.Command{
Use: "tokens",
Short: "Manage personal access tokens",
Long: "Tokens are used to authenticate automated clients to Coder.",
Aliases: []string{"token"},
Example: formatExamples(
example{
Description: "Create a token for automation",
Command: "coder tokens create",
},
example{
Description: "List your tokens",
Command: "coder tokens ls",
},
example{
Description: "Remove a token by ID",
Command: "coder tokens rm WuoWs4ZsMX",
},
),
}
cmd.AddCommand(
createToken(),
listTokens(),
removeToken(),
)

return cmd
}

func createToken() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a tokens",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}

res, err := client.CreateToken(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("create tokens: %w", err)
}

cmd.Println(cliui.Styles.Wrap.Render(
"Here is your token. 🪄",
))
cmd.Println()
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(res.Key)))
cmd.Println()
cmd.Println(cliui.Styles.Wrap.Render(
fmt.Sprintf("You can use this token by setting the --%s CLI flag, the %s environment variable, or the %q HTTP header.", varToken, envSessionToken, codersdk.SessionTokenKey),
))

return nil
},
}

return cmd
}

type tokenRow struct {
ID string `table:"ID"`
LastUsed time.Time `table:"Last Used"`
ExpiresAt time.Time `table:"Expires At"`
CreatedAt time.Time `table:"Created At"`
}

func listTokens() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List tokens",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}

keys, err := client.GetTokens(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("create tokens: %w", err)
}

if len(keys) == 0 {
cmd.Println(cliui.Styles.Wrap.Render(
"No tokens found.",
))
}

var rows []tokenRow
for _, key := range keys {
rows = append(rows, tokenRow{
ID: key.ID,
LastUsed: key.LastUsed,
ExpiresAt: key.ExpiresAt,
CreatedAt: key.CreatedAt,
})
}

out, err := cliui.DisplayTable(rows, "", nil)
if err != nil {
return err
}

_, err = fmt.Fprintln(cmd.OutOrStdout(), out)
return err
},
}

return cmd
}

func removeToken() *cobra.Command {
cmd := &cobra.Command{
Use: "remove [id]",
Aliases: []string{"rm"},
Short: "Delete a token",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}

err = client.DeleteAPIKey(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return xerrors.Errorf("delete api key: %w", err)
}

cmd.Println(cliui.Styles.Wrap.Render(
"Token has been deleted.",
))

return nil
},
}

return cmd
}
66 changes: 66 additions & 0 deletions cli/tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cli_test

import (
"bytes"
"regexp"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
)

func TestTokens(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)

// helpful empty response
cmd, root := clitest.New(t, "tokens", "ls")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
cmd.SetOut(buf)
err := cmd.Execute()
require.NoError(t, err)
res := buf.String()
require.Contains(t, res, "tokens found")

cmd, root = clitest.New(t, "tokens", "create")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
err = cmd.Execute()
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
// find API key in format "XXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXX"
r := regexp.MustCompile("[a-zA-Z0-9]{10}-[a-zA-Z0-9]{22}")
require.Regexp(t, r, res)
key := r.FindString(res)
id := key[:10]

cmd, root = clitest.New(t, "tokens", "ls")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
err = cmd.Execute()
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
require.Contains(t, res, "ID")
require.Contains(t, res, "EXPIRES AT")
require.Contains(t, res, "CREATED AT")
require.Contains(t, res, "LAST USED")
require.Contains(t, res, id)

cmd, root = clitest.New(t, "tokens", "rm", id)
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
err = cmd.Execute()
require.NoError(t, err)
res = buf.String()
require.NotEmpty(t, res)
require.Contains(t, res, "deleted")
}
Loading