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

Lines changed: 50 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 60 additions & 0 deletions
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

Lines changed: 19 additions & 7 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 115 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy