Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 7a9addf

Browse files
authored
Add new tokens command for managing API tokens (#170)
1 parent 9ead247 commit 7a9addf

File tree

6 files changed

+246
-8
lines changed

6 files changed

+246
-8
lines changed

ci/integration/statictokens_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package integration
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
"testing"
10+
11+
"cdr.dev/coder-cli/pkg/tcli"
12+
)
13+
14+
func TestStaticAuth(t *testing.T) {
15+
t.Parallel()
16+
t.Skip()
17+
run(t, "static-auth-test", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) {
18+
headlessLogin(ctx, t, c)
19+
20+
c.Run(ctx, "coder tokens ls").Assert(t,
21+
tcli.Success(),
22+
)
23+
24+
var result *tcli.CommandResult
25+
tokenName := randString(5)
26+
c.Run(ctx, "coder tokens create "+tokenName).Assert(t,
27+
tcli.Success(),
28+
tcli.GetResult(&result),
29+
)
30+
31+
// remove loging credentials
32+
c.Run(ctx, "rm -rf ~/.config/coder").Assert(t,
33+
tcli.Success(),
34+
)
35+
36+
// make requests with token environment variable authentication
37+
cmd := exec.CommandContext(ctx, "sh", "-c",
38+
fmt.Sprintf("export CODER_URL=%s && export CODER_TOKEN=$(cat) && coder envs ls", os.Getenv("CODER_URL")),
39+
)
40+
cmd.Stdin = strings.NewReader(string(result.Stdout))
41+
c.RunCmd(cmd).Assert(t,
42+
tcli.Success(),
43+
)
44+
45+
// should error when the environment variabels aren't set
46+
c.Run(ctx, "coder envs ls").Assert(t,
47+
tcli.Error(),
48+
)
49+
})
50+
}

coder-sdk/request.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (c Client) requestBody(ctx context.Context, method, path string, in, out in
5151
// Responses in the 100 are handled by the http lib, in the 200 range, we have a success.
5252
// Consider anything at or above 300 to be an error.
5353
if resp.StatusCode > 299 {
54-
return fmt.Errorf("unexpected status code: %w", bodyError(resp))
54+
return fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, bodyError(resp))
5555
}
5656

5757
// If we expect a payload, process it as json.

coder-sdk/tokens.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package coder
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
)
8+
9+
type APIToken struct {
10+
ID string `json:"id"`
11+
Name string `json:"name"`
12+
Application bool `json:"application"`
13+
UserID string `json:"user_id"`
14+
LastUsed time.Time `json:"last_used"`
15+
}
16+
17+
type CreateAPITokenReq struct {
18+
Name string `json:"name"`
19+
}
20+
21+
type createAPITokenResp struct {
22+
Key string `json:"key"`
23+
}
24+
25+
func (c Client) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) {
26+
var resp createAPITokenResp
27+
err := c.requestBody(ctx, http.MethodPost, "/api/api-keys/"+userID, req, &resp)
28+
if err != nil {
29+
return "", err
30+
}
31+
return resp.Key, nil
32+
}
33+
34+
func (c Client) APITokens(ctx context.Context, userID string) ([]APIToken, error) {
35+
var tokens []APIToken
36+
if err := c.requestBody(ctx, http.MethodGet, "/api/api-keys/"+userID, nil, &tokens); err != nil {
37+
return nil, err
38+
}
39+
return tokens, nil
40+
}
41+
42+
func (c Client) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) {
43+
var token APIToken
44+
if err := c.requestBody(ctx, http.MethodGet, "/api/api-keys/"+userID+"/"+tokenID, nil, &token); err != nil {
45+
return nil, err
46+
}
47+
return &token, nil
48+
}
49+
50+
func (c Client) DeleteAPIToken(ctx context.Context, userID, tokenID string) error {
51+
return c.requestBody(ctx, http.MethodDelete, "/api/api-keys/"+userID+"/"+tokenID, nil, nil)
52+
}
53+
54+
func (c Client) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) {
55+
var resp createAPITokenResp
56+
if err := c.requestBody(ctx, http.MethodPost, "/api/api-keys/"+userID+"/"+tokenID+"/regen", nil, &resp); err != nil {
57+
return "", err
58+
}
59+
return resp.Key, nil
60+
}

internal/cmd/auth.go

+19-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/http"
77
"net/url"
8+
"os"
89

910
"golang.org/x/xerrors"
1011

@@ -19,15 +20,26 @@ var errNeedLogin = clog.Fatal(
1920
clog.Hintf(`did you run "coder login [https://coder.domain.com]"?`),
2021
)
2122

23+
const tokenEnv = "CODER_TOKEN"
24+
const urlEnv = "CODER_URL"
25+
2226
func newClient(ctx context.Context) (*coder.Client, error) {
23-
sessionToken, err := config.Session.Read()
24-
if err != nil {
25-
return nil, errNeedLogin
26-
}
27+
var (
28+
err error
29+
sessionToken = os.Getenv(tokenEnv)
30+
rawURL = os.Getenv(urlEnv)
31+
)
2732

28-
rawURL, err := config.URL.Read()
29-
if err != nil {
30-
return nil, errNeedLogin
33+
if sessionToken == "" || rawURL == "" {
34+
sessionToken, err = config.Session.Read()
35+
if err != nil {
36+
return nil, errNeedLogin
37+
}
38+
39+
rawURL, err = config.URL.Read()
40+
if err != nil {
41+
return nil, errNeedLogin
42+
}
3143
}
3244

3345
u, err := url.Parse(rawURL)

internal/cmd/cmd.go

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func Make() *cobra.Command {
3030
envsCmd(),
3131
syncCmd(),
3232
urlCmd(),
33+
tokensCmd(),
3334
resourceCmd(),
3435
completionCmd(),
3536
genDocsCmd(app),

internal/cmd/tokens.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"cdr.dev/coder-cli/coder-sdk"
7+
"cdr.dev/coder-cli/pkg/tablewriter"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func tokensCmd() *cobra.Command {
12+
cmd := &cobra.Command{
13+
Use: "tokens",
14+
Short: "manage Coder API tokens for the active user",
15+
Hidden: true,
16+
Long: "Create and manage API Tokens for authenticating the CLI.\n" +
17+
"Statically authenticate using the token value with the " + "`" + "CODER_TOKEN" + "`" + " and " + "`" + "CODER_URL" + "`" + " environment variables.",
18+
}
19+
cmd.AddCommand(
20+
lsTokensCmd(),
21+
createTokensCmd(),
22+
rmTokenCmd(),
23+
regenTokenCmd(),
24+
)
25+
return cmd
26+
}
27+
28+
func lsTokensCmd() *cobra.Command {
29+
return &cobra.Command{
30+
Use: "ls",
31+
Short: "show the user's active API tokens",
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
ctx := cmd.Context()
34+
client, err := newClient(ctx)
35+
if err != nil {
36+
return err
37+
}
38+
39+
tokens, err := client.APITokens(ctx, coder.Me)
40+
if err != nil {
41+
return err
42+
}
43+
44+
err = tablewriter.WriteTable(len(tokens), func(i int) interface{} { return tokens[i] })
45+
if err != nil {
46+
return err
47+
}
48+
49+
return nil
50+
},
51+
}
52+
}
53+
54+
func createTokensCmd() *cobra.Command {
55+
return &cobra.Command{
56+
Use: "create [token_name]",
57+
Short: "create generates a new API token and prints it to stdout",
58+
Args: cobra.ExactArgs(1),
59+
RunE: func(cmd *cobra.Command, args []string) error {
60+
ctx := cmd.Context()
61+
client, err := newClient(ctx)
62+
if err != nil {
63+
return err
64+
}
65+
token, err := client.CreateAPIToken(ctx, coder.Me, coder.CreateAPITokenReq{
66+
Name: args[0],
67+
})
68+
if err != nil {
69+
return err
70+
}
71+
fmt.Println(token)
72+
return nil
73+
},
74+
}
75+
}
76+
77+
func rmTokenCmd() *cobra.Command {
78+
return &cobra.Command{
79+
Use: "rm [token_id]",
80+
Short: "remove an API token by its unique ID",
81+
Args: cobra.ExactArgs(1),
82+
RunE: func(cmd *cobra.Command, args []string) error {
83+
ctx := cmd.Context()
84+
client, err := newClient(ctx)
85+
if err != nil {
86+
return err
87+
}
88+
if err = client.DeleteAPIToken(ctx, coder.Me, args[0]); err != nil {
89+
return err
90+
}
91+
return nil
92+
},
93+
}
94+
}
95+
96+
func regenTokenCmd() *cobra.Command {
97+
return &cobra.Command{
98+
Use: "regen [token_id]",
99+
Short: "regenerate an API token by its unique ID and print the new token to stdout",
100+
Args: cobra.ExactArgs(1),
101+
RunE: func(cmd *cobra.Command, args []string) error {
102+
ctx := cmd.Context()
103+
client, err := newClient(ctx)
104+
if err != nil {
105+
return err
106+
}
107+
token, err := client.RegenerateAPIToken(ctx, coder.Me, args[0])
108+
if err != nil {
109+
return nil
110+
}
111+
fmt.Println(token)
112+
return nil
113+
},
114+
}
115+
}

0 commit comments

Comments
 (0)