mirror of https://github.com/coder/coder.git
feat: tokens (#4380)
This commit is contained in:
parent
fe7c9f8ec1
commit
f5df54831a
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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"})
|
||||
|
|
|
@ -93,6 +93,7 @@ func Core() []*cobra.Command {
|
|||
users(),
|
||||
versionCmd(),
|
||||
workspaceAgent(),
|
||||
tokens(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/tabbed/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
)
|
||||
|
||||
// Creates a new token API key that effectively doesn't expire.
|
||||
func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := httpmw.UserParam(r)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
// tokens last 100 years
|
||||
lifeTime := time.Hour * 876000
|
||||
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
|
||||
UserID: user.ID,
|
||||
LoginType: database.LoginTypeToken,
|
||||
ExpiresAt: database.Now().Add(lifeTime),
|
||||
LifetimeSeconds: int64(lifeTime.Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to create API key.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value})
|
||||
}
|
||||
|
||||
func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
keyID := chi.URLParam(r, "keyid")
|
||||
key, err := api.Database.GetAPIKeyByID(ctx, keyID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching API key.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key))
|
||||
}
|
||||
|
||||
func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeToken)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, []codersdk.APIKey{})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching API keys.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var apiKeys []codersdk.APIKey
|
||||
for _, key := range keys {
|
||||
apiKeys = append(apiKeys, convertAPIKey(key))
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, apiKeys)
|
||||
}
|
||||
|
||||
func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionDelete, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
keyID := chi.URLParam(r, "keyid")
|
||||
err := api.Database.DeleteAPIKeyByID(ctx, keyID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error deleting API key.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Generates a new ID and secret for an API key.
|
||||
func generateAPIKeyIDSecret() (id string, secret string, err error) {
|
||||
// Length of an API Key ID.
|
||||
id, err = cryptorand.String(10)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Length of an API Key secret.
|
||||
secret, err = cryptorand.String(22)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return id, secret, nil
|
||||
}
|
||||
|
||||
type createAPIKeyParams struct {
|
||||
UserID uuid.UUID
|
||||
RemoteAddr string
|
||||
LoginType database.LoginType
|
||||
|
||||
// Optional.
|
||||
ExpiresAt time.Time
|
||||
LifetimeSeconds int64
|
||||
Scope database.APIKeyScope
|
||||
}
|
||||
|
||||
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) {
|
||||
keyID, keySecret, err := generateAPIKeyIDSecret()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("generate API key: %w", err)
|
||||
}
|
||||
hashed := sha256.Sum256([]byte(keySecret))
|
||||
|
||||
// Default expires at to now+lifetime, or just 24hrs if not set
|
||||
if params.ExpiresAt.IsZero() {
|
||||
if params.LifetimeSeconds != 0 {
|
||||
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
|
||||
} else {
|
||||
params.ExpiresAt = database.Now().Add(24 * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(params.RemoteAddr)
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
ip = net.IPv4(0, 0, 0, 0)
|
||||
}
|
||||
bitlen := len(ip) * 8
|
||||
|
||||
scope := database.APIKeyScopeAll
|
||||
if params.Scope != "" {
|
||||
scope = params.Scope
|
||||
}
|
||||
|
||||
key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{
|
||||
ID: keyID,
|
||||
UserID: params.UserID,
|
||||
LifetimeSeconds: params.LifetimeSeconds,
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(bitlen, bitlen),
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
// Make sure in UTC time for common time zone
|
||||
ExpiresAt: params.ExpiresAt.UTC(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
HashedSecret: hashed[:],
|
||||
LoginType: params.LoginType,
|
||||
Scope: scope,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert API key: %w", err)
|
||||
}
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)},
|
||||
})
|
||||
|
||||
// This format is consumed by the APIKey middleware.
|
||||
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||
return &http.Cookie{
|
||||
Name: codersdk.SessionTokenKey,
|
||||
Value: sessionToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: api.SecureAuthCookie,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
keys, err := client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, keys)
|
||||
|
||||
res, err := client.CreateToken(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, len(res.Key), 2)
|
||||
|
||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, len(keys), 1)
|
||||
require.Contains(t, res.Key, keys[0].ID)
|
||||
// expires_at must be greater than 50 years
|
||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
|
||||
|
||||
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
|
||||
require.NoError(t, err)
|
||||
keys, err = client.GetTokens(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, keys)
|
||||
}
|
|
@ -399,8 +399,14 @@ func New(options *Options) *API {
|
|||
r.Get("/roles", api.userRoles)
|
||||
|
||||
r.Route("/keys", func(r chi.Router) {
|
||||
r.Post("/", api.postAPIKey)
|
||||
r.Get("/{keyid}", api.apiKey)
|
||||
r.Route("/tokens", func(r chi.Router) {
|
||||
r.Post("/", api.postToken)
|
||||
r.Get("/", api.tokens)
|
||||
})
|
||||
r.Route("/{keyid}", func(r chi.Router) {
|
||||
r.Get("/", api.apiKey)
|
||||
r.Delete("/", api.deleteAPIKey)
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/organizations", func(r chi.Router) {
|
||||
|
|
|
@ -279,6 +279,19 @@ func (q *fakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time
|
|||
return apiKeys, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetAPIKeysByLoginType(_ context.Context, t database.LoginType) ([]database.APIKey, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
apiKeys := make([]database.APIKey, 0)
|
||||
for _, key := range q.apiKeys {
|
||||
if key.LoginType == t {
|
||||
apiKeys = append(apiKeys, key)
|
||||
}
|
||||
}
|
||||
return apiKeys, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
|
|
@ -33,7 +33,8 @@ CREATE TYPE log_source AS ENUM (
|
|||
CREATE TYPE login_type AS ENUM (
|
||||
'password',
|
||||
'github',
|
||||
'oidc'
|
||||
'oidc',
|
||||
'token'
|
||||
);
|
||||
|
||||
CREATE TYPE parameter_destination_scheme AS ENUM (
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- You cannot safely remove values from enums https://www.postgresql.org/docs/current/datatype-enum.html
|
||||
-- You cannot create a new type and do a rename because objects depend on this type now.
|
|
@ -0,0 +1 @@
|
|||
ALTER TYPE login_type ADD VALUE IF NOT EXISTS 'token';
|
|
@ -120,6 +120,7 @@ const (
|
|||
LoginTypePassword LoginType = "password"
|
||||
LoginTypeGithub LoginType = "github"
|
||||
LoginTypeOIDC LoginType = "oidc"
|
||||
LoginTypeToken LoginType = "token"
|
||||
)
|
||||
|
||||
func (e *LoginType) Scan(src interface{}) error {
|
||||
|
|
|
@ -25,6 +25,7 @@ type querier interface {
|
|||
DeleteOldAgentStats(ctx context.Context) error
|
||||
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error)
|
||||
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
|
||||
GetActiveUserCount(ctx context.Context) (int64, error)
|
||||
GetAuditLogCount(ctx context.Context, arg GetAuditLogCountParams) (int64, error)
|
||||
|
|
|
@ -175,6 +175,45 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE login_type = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAPIKeysByLoginType, loginType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []APIKey
|
||||
for rows.Next() {
|
||||
var i APIKey
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.HashedSecret,
|
||||
&i.UserID,
|
||||
&i.LastUsed,
|
||||
&i.ExpiresAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LoginType,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
&i.Scope,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE last_used > $1
|
||||
`
|
||||
|
|
|
@ -11,6 +11,9 @@ LIMIT
|
|||
-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT * FROM api_keys WHERE last_used > $1;
|
||||
|
||||
-- name: GetAPIKeysByLoginType :many
|
||||
SELECT * FROM api_keys WHERE login_type = $1;
|
||||
|
||||
-- name: InsertAPIKey :one
|
||||
INSERT INTO
|
||||
api_keys (
|
||||
|
|
161
coderd/users.go
161
coderd/users.go
|
@ -3,21 +3,17 @@ package coderd
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/tabbed/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
@ -31,7 +27,6 @@ import (
|
|||
"github.com/coder/coder/coderd/userpassword"
|
||||
"github.com/coder/coder/coderd/util/slice"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/coder/coder/examples"
|
||||
)
|
||||
|
||||
|
@ -943,69 +938,6 @@ func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
// Creates a new session key, used for logging in via the CLI.
|
||||
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := httpmw.UserParam(r)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
lifeTime := time.Hour * 24 * 7
|
||||
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
|
||||
UserID: user.ID,
|
||||
LoginType: database.LoginTypePassword,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
// All api generated keys will last 1 week. Browser login tokens have
|
||||
// a shorter life.
|
||||
ExpiresAt: database.Now().Add(lifeTime),
|
||||
LifetimeSeconds: int64(lifeTime.Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to create API key.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// We intentionally do not set the cookie on the response here.
|
||||
// Setting the cookie will couple the browser sesion to the API
|
||||
// key we return here, meaning logging out of the website would
|
||||
// invalid your CLI key.
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value})
|
||||
}
|
||||
|
||||
func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceAPIKey.WithOwner(user.ID.String())) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
keyID := chi.URLParam(r, "keyid")
|
||||
key, err := api.Database.GetAPIKeyByID(ctx, keyID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching API key.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key))
|
||||
}
|
||||
|
||||
// Clear the user's session cookie.
|
||||
func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
@ -1078,99 +1010,6 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
// Generates a new ID and secret for an API key.
|
||||
func generateAPIKeyIDSecret() (id string, secret string, err error) {
|
||||
// Length of an API Key ID.
|
||||
id, err = cryptorand.String(10)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Length of an API Key secret.
|
||||
secret, err = cryptorand.String(22)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return id, secret, nil
|
||||
}
|
||||
|
||||
type createAPIKeyParams struct {
|
||||
UserID uuid.UUID
|
||||
RemoteAddr string
|
||||
LoginType database.LoginType
|
||||
|
||||
// Optional.
|
||||
ExpiresAt time.Time
|
||||
LifetimeSeconds int64
|
||||
Scope database.APIKeyScope
|
||||
}
|
||||
|
||||
func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*http.Cookie, error) {
|
||||
keyID, keySecret, err := generateAPIKeyIDSecret()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("generate API key: %w", err)
|
||||
}
|
||||
hashed := sha256.Sum256([]byte(keySecret))
|
||||
|
||||
// Default expires at to now+lifetime, or just 24hrs if not set
|
||||
if params.ExpiresAt.IsZero() {
|
||||
if params.LifetimeSeconds != 0 {
|
||||
params.ExpiresAt = database.Now().Add(time.Duration(params.LifetimeSeconds) * time.Second)
|
||||
} else {
|
||||
params.ExpiresAt = database.Now().Add(24 * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(params.RemoteAddr)
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
ip = net.IPv4(0, 0, 0, 0)
|
||||
}
|
||||
bitlen := len(ip) * 8
|
||||
|
||||
scope := database.APIKeyScopeAll
|
||||
if params.Scope != "" {
|
||||
scope = params.Scope
|
||||
}
|
||||
|
||||
key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{
|
||||
ID: keyID,
|
||||
UserID: params.UserID,
|
||||
LifetimeSeconds: params.LifetimeSeconds,
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.CIDRMask(bitlen, bitlen),
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
// Make sure in UTC time for common time zone
|
||||
ExpiresAt: params.ExpiresAt.UTC(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
HashedSecret: hashed[:],
|
||||
LoginType: params.LoginType,
|
||||
Scope: scope,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert API key: %w", err)
|
||||
}
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)},
|
||||
})
|
||||
|
||||
// This format is consumed by the APIKey middleware.
|
||||
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||
return &http.Cookie{
|
||||
Name: codersdk.SessionTokenKey,
|
||||
Value: sessionToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: api.SecureAuthCookie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
codersdk.CreateUserRequest
|
||||
LoginType database.LoginType
|
||||
|
|
|
@ -285,16 +285,15 @@ func TestPostLogin(t *testing.T) {
|
|||
require.NoError(t, err, "fetch login key")
|
||||
require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400")
|
||||
|
||||
// Generated tokens have a longer life
|
||||
token, err := client.CreateAPIKey(ctx, admin.UserID.String())
|
||||
require.NoError(t, err, "make new api key")
|
||||
// tokens have a longer life
|
||||
token, err := client.CreateToken(ctx, codersdk.Me)
|
||||
require.NoError(t, err, "make new token api key")
|
||||
split = strings.Split(token.Key, "-")
|
||||
apiKey, err := client.GetAPIKey(ctx, admin.UserID.String(), split[0])
|
||||
require.NoError(t, err, "fetch api key")
|
||||
|
||||
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*6)), "api key lasts more than 6 days")
|
||||
require.True(t, apiKey.ExpiresAt.After(key.ExpiresAt.Add(time.Hour)), "api key should be longer expires")
|
||||
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "api key should have longer lifetime")
|
||||
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*438300)), "tokens lasts more than 50 years")
|
||||
require.Greater(t, apiKey.LifetimeSeconds, key.LifetimeSeconds, "token should have longer lifetime")
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1195,36 +1194,18 @@ func TestGetUsers(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPostAPIKey(t *testing.T) {
|
||||
func TestPostTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("InvalidUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
client.SessionToken = ""
|
||||
_, err := client.CreateAPIKey(ctx, codersdk.Me)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
apiKey, err := client.CreateAPIKey(ctx, codersdk.Me)
|
||||
require.NotNil(t, apiKey)
|
||||
require.GreaterOrEqual(t, len(apiKey.Key), 2)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
apiKey, err := client.CreateToken(ctx, codersdk.Me)
|
||||
require.NotNil(t, apiKey)
|
||||
require.GreaterOrEqual(t, len(apiKey.Key), 2)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWorkspacesByUser(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package codersdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type APIKey struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
// NOTE: do not ever return the HashedSecret
|
||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
LoginType LoginType `json:"login_type" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
}
|
||||
|
||||
type LoginType string
|
||||
|
||||
const (
|
||||
LoginTypePassword LoginType = "password"
|
||||
LoginTypeGithub LoginType = "github"
|
||||
LoginTypeOIDC LoginType = "oidc"
|
||||
LoginTypeToken LoginType = "token"
|
||||
)
|
||||
|
||||
// CreateToken generates an API key that doesn't expire.
|
||||
func (c *Client) CreateToken(ctx context.Context, userID string) (*GenerateAPIKeyResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &GenerateAPIKeyResponse{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
}
|
||||
|
||||
// GetTokens list machine API keys.
|
||||
func (c *Client) GetTokens(ctx context.Context, userID string) ([]APIKey, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusOK {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
var apiKey = []APIKey{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
|
||||
}
|
||||
|
||||
// GetAPIKey returns the api key by id.
|
||||
func (c *Client) GetAPIKey(ctx context.Context, userID string, id string) (*APIKey, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &APIKey{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
}
|
||||
|
||||
// DeleteAPIKey deletes API key by id.
|
||||
func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) error {
|
||||
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusNoContent {
|
||||
return readBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -22,14 +22,6 @@ const (
|
|||
UserStatusSuspended UserStatus = "suspended"
|
||||
)
|
||||
|
||||
type LoginType string
|
||||
|
||||
const (
|
||||
LoginTypePassword LoginType = "password"
|
||||
LoginTypeGithub LoginType = "github"
|
||||
LoginTypeOIDC LoginType = "oidc"
|
||||
)
|
||||
|
||||
type UsersRequest struct {
|
||||
Search string `json:"search,omitempty" typescript:"-"`
|
||||
// Filter users by status.
|
||||
|
@ -55,18 +47,6 @@ type User struct {
|
|||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
// NOTE: do not ever return the HashedSecret
|
||||
UserID uuid.UUID `json:"user_id" validate:"required"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required"`
|
||||
LoginType LoginType `json:"login_type" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
}
|
||||
|
||||
type CreateFirstUserRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
|
@ -287,33 +267,6 @@ func (c *Client) GetUserRoles(ctx context.Context, user string) (UserRoles, erro
|
|||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates an API key for the user ID provided.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, user string) (*GenerateAPIKeyResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", user), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &GenerateAPIKeyResponse{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
}
|
||||
|
||||
func (c *Client) GetAPIKey(ctx context.Context, user string, id string) (*APIKey, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", user, id), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode > http.StatusCreated {
|
||||
return nil, readBodyAsError(res)
|
||||
}
|
||||
apiKey := &APIKey{}
|
||||
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
|
||||
}
|
||||
|
||||
// LoginWithPassword creates a session token authenticating with an email and password.
|
||||
// Call `SetSessionToken()` to apply the newly acquired token to the client.
|
||||
func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by 'make coder/scripts/apitypings/main.go'. DO NOT EDIT.
|
||||
|
||||
// From codersdk/users.go
|
||||
// From codersdk/apikey.go
|
||||
export interface APIKey {
|
||||
readonly id: string
|
||||
readonly user_id: string
|
||||
|
@ -701,8 +701,8 @@ export type LogLevel = "debug" | "error" | "info" | "trace" | "warn"
|
|||
// From codersdk/provisionerdaemons.go
|
||||
export type LogSource = "provisioner" | "provisioner_daemon"
|
||||
|
||||
// From codersdk/users.go
|
||||
export type LoginType = "github" | "oidc" | "password"
|
||||
// From codersdk/apikey.go
|
||||
export type LoginType = "github" | "oidc" | "password" | "token"
|
||||
|
||||
// From codersdk/parameters.go
|
||||
export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable"
|
||||
|
|
Loading…
Reference in New Issue