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

Add new tokens command for managing API tokens #170

Merged
merged 8 commits into from
Nov 3, 2020
50 changes: 50 additions & 0 deletions ci/integration/statictokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package integration

import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"

"cdr.dev/coder-cli/pkg/tcli"
)

func TestStaticAuth(t *testing.T) {
t.Parallel()
t.Skip()
run(t, "static-auth-test", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) {
headlessLogin(ctx, t, c)

c.Run(ctx, "coder tokens ls").Assert(t,
tcli.Success(),
)

var result *tcli.CommandResult
tokenName := randString(5)
c.Run(ctx, "coder tokens create "+tokenName).Assert(t,
tcli.Success(),
tcli.GetResult(&result),
)

// remove loging credentials
c.Run(ctx, "rm -rf ~/.config/coder").Assert(t,
tcli.Success(),
)

// make requests with token environment variable authentication
cmd := exec.CommandContext(ctx, "sh", "-c",
fmt.Sprintf("export CODER_URL=%s && export CODER_TOKEN=$(cat) && coder envs ls", os.Getenv("CODER_URL")),
)
cmd.Stdin = strings.NewReader(string(result.Stdout))
c.RunCmd(cmd).Assert(t,
tcli.Success(),
)

// should error when the environment variabels aren't set
c.Run(ctx, "coder envs ls").Assert(t,
tcli.Error(),
)
})
}
2 changes: 1 addition & 1 deletion coder-sdk/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c Client) requestBody(ctx context.Context, method, path string, in, out in
// Responses in the 100 are handled by the http lib, in the 200 range, we have a success.
// Consider anything at or above 300 to be an error.
if resp.StatusCode > 299 {
return fmt.Errorf("unexpected status code: %w", bodyError(resp))
return fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, bodyError(resp))
}

// If we expect a payload, process it as json.
Expand Down
60 changes: 60 additions & 0 deletions coder-sdk/tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package coder

import (
"context"
"net/http"
"time"
)

type APIToken struct {
ID string `json:"id"`
Name string `json:"name"`
Application bool `json:"application"`
UserID string `json:"user_id"`
LastUsed time.Time `json:"last_used"`
}

type CreateAPITokenReq struct {
Name string `json:"name"`
}

type createAPITokenResp struct {
Key string `json:"key"`
}

func (c Client) CreateAPIToken(ctx context.Context, userID string, req CreateAPITokenReq) (token string, _ error) {
var resp createAPITokenResp
err := c.requestBody(ctx, http.MethodPost, "/api/api-keys/"+userID, req, &resp)
if err != nil {
return "", err
}
return resp.Key, nil
}

func (c Client) APITokens(ctx context.Context, userID string) ([]APIToken, error) {
var tokens []APIToken
if err := c.requestBody(ctx, http.MethodGet, "/api/api-keys/"+userID, nil, &tokens); err != nil {
return nil, err
}
return tokens, nil
}

func (c Client) APITokenByID(ctx context.Context, userID, tokenID string) (*APIToken, error) {
var token APIToken
if err := c.requestBody(ctx, http.MethodGet, "/api/api-keys/"+userID+"/"+tokenID, nil, &token); err != nil {
return nil, err
}
return &token, nil
}

func (c Client) DeleteAPIToken(ctx context.Context, userID, tokenID string) error {
return c.requestBody(ctx, http.MethodDelete, "/api/api-keys/"+userID+"/"+tokenID, nil, nil)
}

func (c Client) RegenerateAPIToken(ctx context.Context, userID, tokenID string) (token string, _ error) {
var resp createAPITokenResp
if err := c.requestBody(ctx, http.MethodPost, "/api/api-keys/"+userID+"/"+tokenID+"/regen", nil, &resp); err != nil {
return "", err
}
return resp.Key, nil
}
26 changes: 19 additions & 7 deletions internal/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"os"

"golang.org/x/xerrors"

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

const tokenEnv = "CODER_TOKEN"
const urlEnv = "CODER_URL"

func newClient(ctx context.Context) (*coder.Client, error) {
sessionToken, err := config.Session.Read()
if err != nil {
return nil, errNeedLogin
}
var (
err error
sessionToken = os.Getenv(tokenEnv)
rawURL = os.Getenv(urlEnv)
)

rawURL, err := config.URL.Read()
if err != nil {
return nil, errNeedLogin
if sessionToken == "" || rawURL == "" {
sessionToken, err = config.Session.Read()
if err != nil {
return nil, errNeedLogin
}

rawURL, err = config.URL.Read()
if err != nil {
return nil, errNeedLogin
}
}

u, err := url.Parse(rawURL)
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func Make() *cobra.Command {
envsCmd(),
syncCmd(),
urlCmd(),
tokensCmd(),
resourceCmd(),
completionCmd(),
genDocsCmd(app),
Expand Down
115 changes: 115 additions & 0 deletions internal/cmd/tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package cmd

import (
"fmt"

"cdr.dev/coder-cli/coder-sdk"
"cdr.dev/coder-cli/pkg/tablewriter"
"github.com/spf13/cobra"
)

func tokensCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tokens",
Short: "manage Coder API tokens for the active user",
Hidden: true,
Long: "Create and manage API Tokens for authenticating the CLI.\n" +
"Statically authenticate using the token value with the " + "`" + "CODER_TOKEN" + "`" + " and " + "`" + "CODER_URL" + "`" + " environment variables.",
}
cmd.AddCommand(
lsTokensCmd(),
createTokensCmd(),
rmTokenCmd(),
regenTokenCmd(),
)
return cmd
}

func lsTokensCmd() *cobra.Command {
return &cobra.Command{
Use: "ls",
Short: "show the user's active API tokens",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient(ctx)
if err != nil {
return err
}

tokens, err := client.APITokens(ctx, coder.Me)
if err != nil {
return err
}

err = tablewriter.WriteTable(len(tokens), func(i int) interface{} { return tokens[i] })
if err != nil {
return err
}

return nil
},
}
}

func createTokensCmd() *cobra.Command {
return &cobra.Command{
Use: "create [token_name]",
Short: "create generates a new API token and prints it to stdout",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient(ctx)
if err != nil {
return err
}
token, err := client.CreateAPIToken(ctx, coder.Me, coder.CreateAPITokenReq{
Name: args[0],
})
if err != nil {
return err
}
fmt.Println(token)
return nil
},
}
}

func rmTokenCmd() *cobra.Command {
return &cobra.Command{
Use: "rm [token_id]",
Short: "remove an API token by its unique ID",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient(ctx)
if err != nil {
return err
}
if err = client.DeleteAPIToken(ctx, coder.Me, args[0]); err != nil {
return err
}
return nil
},
}
}

func regenTokenCmd() *cobra.Command {
return &cobra.Command{
Use: "regen [token_id]",
Short: "regenerate an API token by its unique ID and print the new token to stdout",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient(ctx)
if err != nil {
return err
}
token, err := client.RegenerateAPIToken(ctx, coder.Me, args[0])
if err != nil {
return nil
}
fmt.Println(token)
return nil
},
}
}