Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2ce8e3739 | |||
| 1eb446d220 |
+5
@@ -16,6 +16,10 @@ USAGE:
|
||||
|
||||
$ coder tokens ls
|
||||
|
||||
- Create a scoped token:
|
||||
|
||||
$ coder tokens create --scope workspace:read --allow workspace:<uuid>
|
||||
|
||||
- Remove a token by ID:
|
||||
|
||||
$ coder tokens rm WuoWs4ZsMX
|
||||
@@ -24,6 +28,7 @@ SUBCOMMANDS:
|
||||
create Create a token
|
||||
list List tokens
|
||||
remove Delete a token
|
||||
view Display detailed information about a token
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
|
||||
@@ -6,12 +6,18 @@ USAGE:
|
||||
Create a token
|
||||
|
||||
OPTIONS:
|
||||
--allow allowList
|
||||
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).
|
||||
|
||||
--lifetime string, $CODER_TOKEN_LIFETIME
|
||||
Specify a duration for the lifetime of the token.
|
||||
|
||||
-n, --name string, $CODER_TOKEN_NAME
|
||||
Specify a human-readable name.
|
||||
|
||||
--scope scope
|
||||
Repeatable scope to attach to the token (e.g. workspace:read).
|
||||
|
||||
-u, --user string, $CODER_TOKEN_USER
|
||||
Specify the user to create the token for (Only works if logged in user
|
||||
is admin).
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ OPTIONS:
|
||||
Specifies whether all users' tokens will be listed or not (must have
|
||||
Owner role to see all tokens).
|
||||
|
||||
-c, --column [id|name|last used|expires at|created at|owner] (default: id,name,last used,expires at,created at)
|
||||
-c, --column [id|name|scopes|allow list|last used|expires at|created at|owner] (default: id,name,scopes,allow list,last used,expires at,created at)
|
||||
Columns to display in table output.
|
||||
|
||||
-o, --output table|json (default: table)
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder tokens view [flags] <name|id>
|
||||
|
||||
Display detailed information about a token
|
||||
|
||||
OPTIONS:
|
||||
-c, --column [id|name|scopes|allow list|last used|expires at|created at|owner] (default: id,name,scopes,allow list,last used,expires at,created at,owner)
|
||||
Columns to display in table output.
|
||||
|
||||
-o, --output table|json (default: table)
|
||||
Output format.
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
+105
-5
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +28,10 @@ func (r *RootCmd) tokens() *serpent.Command {
|
||||
Description: "List your tokens",
|
||||
Command: "coder tokens ls",
|
||||
},
|
||||
Example{
|
||||
Description: "Create a scoped token",
|
||||
Command: "coder tokens create --scope workspace:read --allow workspace:<uuid>",
|
||||
},
|
||||
Example{
|
||||
Description: "Remove a token by ID",
|
||||
Command: "coder tokens rm WuoWs4ZsMX",
|
||||
@@ -39,6 +44,7 @@ func (r *RootCmd) tokens() *serpent.Command {
|
||||
Children: []*serpent.Command{
|
||||
r.createToken(),
|
||||
r.listTokens(),
|
||||
r.viewToken(),
|
||||
r.removeToken(),
|
||||
},
|
||||
}
|
||||
@@ -50,6 +56,8 @@ func (r *RootCmd) createToken() *serpent.Command {
|
||||
tokenLifetime string
|
||||
name string
|
||||
user string
|
||||
scopes []codersdk.APIKeyScope
|
||||
allowList []codersdk.APIAllowListTarget
|
||||
)
|
||||
cmd := &serpent.Command{
|
||||
Use: "create",
|
||||
@@ -88,10 +96,18 @@ func (r *RootCmd) createToken() *serpent.Command {
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.CreateToken(inv.Context(), userID, codersdk.CreateTokenRequest{
|
||||
req := codersdk.CreateTokenRequest{
|
||||
Lifetime: parsedLifetime,
|
||||
TokenName: name,
|
||||
})
|
||||
}
|
||||
if len(scopes) > 0 {
|
||||
req.Scopes = append([]codersdk.APIKeyScope(nil), scopes...)
|
||||
}
|
||||
if len(allowList) > 0 {
|
||||
req.AllowList = append([]codersdk.APIAllowListTarget(nil), allowList...)
|
||||
}
|
||||
|
||||
res, err := client.CreateToken(inv.Context(), userID, req)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create tokens: %w", err)
|
||||
}
|
||||
@@ -123,6 +139,16 @@ func (r *RootCmd) createToken() *serpent.Command {
|
||||
Description: "Specify the user to create the token for (Only works if logged in user is admin).",
|
||||
Value: serpent.StringOf(&user),
|
||||
},
|
||||
{
|
||||
Flag: "scope",
|
||||
Description: "Repeatable scope to attach to the token (e.g. workspace:read).",
|
||||
Value: newScopeFlag(&scopes),
|
||||
},
|
||||
{
|
||||
Flag: "allow",
|
||||
Description: "Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).",
|
||||
Value: newAllowListFlag(&allowList),
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
@@ -136,6 +162,8 @@ type tokenListRow struct {
|
||||
// For table format:
|
||||
ID string `json:"-" table:"id,default_sort"`
|
||||
TokenName string `json:"token_name" table:"name"`
|
||||
Scopes string `json:"-" table:"scopes"`
|
||||
Allow string `json:"-" table:"allow list"`
|
||||
LastUsed time.Time `json:"-" table:"last used"`
|
||||
ExpiresAt time.Time `json:"-" table:"expires at"`
|
||||
CreatedAt time.Time `json:"-" table:"created at"`
|
||||
@@ -143,20 +171,50 @@ type tokenListRow struct {
|
||||
}
|
||||
|
||||
func tokenListRowFromToken(token codersdk.APIKeyWithOwner) tokenListRow {
|
||||
return tokenListRowFromKey(token.APIKey, token.Username)
|
||||
}
|
||||
|
||||
func tokenListRowFromKey(token codersdk.APIKey, owner string) tokenListRow {
|
||||
return tokenListRow{
|
||||
APIKey: token.APIKey,
|
||||
APIKey: token,
|
||||
ID: token.ID,
|
||||
TokenName: token.TokenName,
|
||||
Scopes: joinScopes(token.Scopes),
|
||||
Allow: joinAllowList(token.AllowList),
|
||||
LastUsed: token.LastUsed,
|
||||
ExpiresAt: token.ExpiresAt,
|
||||
CreatedAt: token.CreatedAt,
|
||||
Owner: token.Username,
|
||||
Owner: owner,
|
||||
}
|
||||
}
|
||||
|
||||
func joinScopes(scopes []codersdk.APIKeyScope) string {
|
||||
if len(scopes) == 0 {
|
||||
return ""
|
||||
}
|
||||
vals := make([]string, len(scopes))
|
||||
for i, scope := range scopes {
|
||||
vals[i] = string(scope)
|
||||
}
|
||||
sort.Strings(vals)
|
||||
return strings.Join(vals, ", ")
|
||||
}
|
||||
|
||||
func joinAllowList(entries []codersdk.APIAllowListTarget) string {
|
||||
if len(entries) == 0 {
|
||||
return ""
|
||||
}
|
||||
vals := make([]string, len(entries))
|
||||
for i, entry := range entries {
|
||||
vals[i] = entry.String()
|
||||
}
|
||||
sort.Strings(vals)
|
||||
return strings.Join(vals, ", ")
|
||||
}
|
||||
|
||||
func (r *RootCmd) listTokens() *serpent.Command {
|
||||
// we only display the 'owner' column if the --all argument is passed in
|
||||
defaultCols := []string{"id", "name", "last used", "expires at", "created at"}
|
||||
defaultCols := []string{"id", "name", "scopes", "allow list", "last used", "expires at", "created at"}
|
||||
if slices.Contains(os.Args, "-a") || slices.Contains(os.Args, "--all") {
|
||||
defaultCols = append(defaultCols, "owner")
|
||||
}
|
||||
@@ -226,6 +284,48 @@ func (r *RootCmd) listTokens() *serpent.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) viewToken() *serpent.Command {
|
||||
formatter := cliui.NewOutputFormatter(
|
||||
cliui.TableFormat([]tokenListRow{}, []string{"id", "name", "scopes", "allow list", "last used", "expires at", "created at", "owner"}),
|
||||
cliui.JSONFormat(),
|
||||
)
|
||||
|
||||
cmd := &serpent.Command{
|
||||
Use: "view <name|id>",
|
||||
Short: "Display detailed information about a token",
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireNArgs(1),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
client, err := r.InitClient(inv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenName := inv.Args[0]
|
||||
token, err := client.APIKeyByName(inv.Context(), codersdk.Me, tokenName)
|
||||
if err != nil {
|
||||
maybeID := strings.Split(tokenName, "-")[0]
|
||||
token, err = client.APIKeyByID(inv.Context(), codersdk.Me, maybeID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch api key by name or id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
row := tokenListRowFromKey(*token, "")
|
||||
out, err := formatter.Format(inv.Context(), []tokenListRow{row})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(inv.Stdout, out)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
formatter.AttachOptions(&cmd.Options)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) removeToken() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "remove <name|id|token>",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// allowListFlag implements pflag.SliceValue for codersdk.APIAllowListTarget entries.
|
||||
type allowListFlag struct {
|
||||
targets *[]codersdk.APIAllowListTarget
|
||||
}
|
||||
|
||||
func newAllowListFlag(dst *[]codersdk.APIAllowListTarget) *allowListFlag {
|
||||
return &allowListFlag{targets: dst}
|
||||
}
|
||||
|
||||
func (a *allowListFlag) ensureSlice() error {
|
||||
if a.targets == nil {
|
||||
return xerrors.New("allow list destination is nil")
|
||||
}
|
||||
if *a.targets == nil {
|
||||
*a.targets = make([]codersdk.APIAllowListTarget, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *allowListFlag) String() string {
|
||||
if a.targets == nil || len(*a.targets) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, len(*a.targets))
|
||||
for i, t := range *a.targets {
|
||||
parts[i] = t.String()
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (a *allowListFlag) Set(raw string) error {
|
||||
if err := a.ensureSlice(); err != nil {
|
||||
return err
|
||||
}
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return xerrors.New("allow list entry cannot be empty")
|
||||
}
|
||||
var target codersdk.APIAllowListTarget
|
||||
if err := target.UnmarshalText([]byte(raw)); err != nil {
|
||||
return err
|
||||
}
|
||||
*a.targets = append(*a.targets, target)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*allowListFlag) Type() string { return "allowList" }
|
||||
|
||||
func (a *allowListFlag) Append(value string) error {
|
||||
return a.Set(value)
|
||||
}
|
||||
|
||||
func (a *allowListFlag) Replace(items []string) error {
|
||||
if err := a.ensureSlice(); err != nil {
|
||||
return err
|
||||
}
|
||||
(*a.targets) = (*a.targets)[:0]
|
||||
for _, item := range items {
|
||||
if err := a.Set(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *allowListFlag) GetSlice() []string {
|
||||
if a.targets == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(*a.targets))
|
||||
for i, t := range *a.targets {
|
||||
out[i] = t.String()
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// scopeFlag stores repeatable --scope values as typed APIKeyScope.
|
||||
type scopeFlag struct {
|
||||
scopes *[]codersdk.APIKeyScope
|
||||
}
|
||||
|
||||
func newScopeFlag(dst *[]codersdk.APIKeyScope) *scopeFlag {
|
||||
return &scopeFlag{scopes: dst}
|
||||
}
|
||||
|
||||
func (s *scopeFlag) ensureSlice() error {
|
||||
if s.scopes == nil {
|
||||
return xerrors.New("scope destination is nil")
|
||||
}
|
||||
if *s.scopes == nil {
|
||||
*s.scopes = make([]codersdk.APIKeyScope, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scopeFlag) String() string {
|
||||
if s.scopes == nil || len(*s.scopes) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, len(*s.scopes))
|
||||
for i, scope := range *s.scopes {
|
||||
parts[i] = string(scope)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func (s *scopeFlag) Set(raw string) error {
|
||||
if err := s.ensureSlice(); err != nil {
|
||||
return err
|
||||
}
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return xerrors.New("scope cannot be empty")
|
||||
}
|
||||
*s.scopes = append(*s.scopes, codersdk.APIKeyScope(raw))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*scopeFlag) Type() string { return "scope" }
|
||||
|
||||
func (s *scopeFlag) Append(value string) error {
|
||||
return s.Set(value)
|
||||
}
|
||||
|
||||
func (s *scopeFlag) Replace(items []string) error {
|
||||
if err := s.ensureSlice(); err != nil {
|
||||
return err
|
||||
}
|
||||
(*s.scopes) = (*s.scopes)[:0]
|
||||
for _, item := range items {
|
||||
if err := s.Set(item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scopeFlag) GetSlice() []string {
|
||||
if s.scopes == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, len(*s.scopes))
|
||||
for i, scope := range *s.scopes {
|
||||
out[i] = string(scope)
|
||||
}
|
||||
return out
|
||||
}
|
||||
+56
-3
@@ -4,10 +4,13 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -46,6 +49,18 @@ func TestTokens(t *testing.T) {
|
||||
require.NotEmpty(t, res)
|
||||
id := res[:10]
|
||||
|
||||
allowWorkspaceID := uuid.New()
|
||||
allowSpec := fmt.Sprintf("workspace:%s", allowWorkspaceID.String())
|
||||
inv, root = clitest.New(t, "tokens", "create", "--name", "scoped-token", "--scope", string(codersdk.APIKeyScopeWorkspaceRead), "--allow", allowSpec)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
buf = new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
res = buf.String()
|
||||
require.NotEmpty(t, res)
|
||||
scopedTokenID := res[:10]
|
||||
|
||||
// Test creating a token for second user from first user's (admin) session
|
||||
inv, root = clitest.New(t, "tokens", "create", "--name", "token-two", "--user", secondUser.ID.String())
|
||||
clitest.SetupConfig(t, client, root)
|
||||
@@ -67,7 +82,7 @@ func TestTokens(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
res = buf.String()
|
||||
require.NotEmpty(t, res)
|
||||
// Result should only contain the token created for the admin user
|
||||
// Result should only contain the tokens created for the admin user
|
||||
require.Contains(t, res, "ID")
|
||||
require.Contains(t, res, "EXPIRES AT")
|
||||
require.Contains(t, res, "CREATED AT")
|
||||
@@ -76,6 +91,16 @@ func TestTokens(t *testing.T) {
|
||||
// Result should not contain the token created for the second user
|
||||
require.NotContains(t, res, secondTokenID)
|
||||
|
||||
inv, root = clitest.New(t, "tokens", "view", "scoped-token")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
buf = new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
res = buf.String()
|
||||
require.Contains(t, res, string(codersdk.APIKeyScopeWorkspaceRead))
|
||||
require.Contains(t, res, allowSpec)
|
||||
|
||||
// Test listing tokens from the second user's session
|
||||
inv, root = clitest.New(t, "tokens", "ls")
|
||||
clitest.SetupConfig(t, secondUserClient, root)
|
||||
@@ -101,6 +126,14 @@ func TestTokens(t *testing.T) {
|
||||
// User (non-admin) should not be able to create a token for another user
|
||||
require.Error(t, err)
|
||||
|
||||
inv, root = clitest.New(t, "tokens", "create", "--name", "invalid-allow", "--allow", "badvalue")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
buf = new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid allow_list entry")
|
||||
|
||||
inv, root = clitest.New(t, "tokens", "ls", "--output=json")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
buf = new(bytes.Buffer)
|
||||
@@ -110,8 +143,17 @@ func TestTokens(t *testing.T) {
|
||||
|
||||
var tokens []codersdk.APIKey
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &tokens))
|
||||
require.Len(t, tokens, 1)
|
||||
require.Equal(t, id, tokens[0].ID)
|
||||
require.Len(t, tokens, 2)
|
||||
tokenByName := make(map[string]codersdk.APIKey, len(tokens))
|
||||
for _, tk := range tokens {
|
||||
tokenByName[tk.TokenName] = tk
|
||||
}
|
||||
require.Contains(t, tokenByName, "token-one")
|
||||
require.Contains(t, tokenByName, "scoped-token")
|
||||
scopedToken := tokenByName["scoped-token"]
|
||||
require.Contains(t, scopedToken.Scopes, codersdk.APIKeyScopeWorkspaceRead)
|
||||
require.Len(t, scopedToken.AllowList, 1)
|
||||
require.Equal(t, allowSpec, scopedToken.AllowList[0].String())
|
||||
|
||||
// Delete by name
|
||||
inv, root = clitest.New(t, "tokens", "rm", "token-one")
|
||||
@@ -135,6 +177,17 @@ func TestTokens(t *testing.T) {
|
||||
require.NotEmpty(t, res)
|
||||
require.Contains(t, res, "deleted")
|
||||
|
||||
// Delete scoped token by ID
|
||||
inv, root = clitest.New(t, "tokens", "rm", scopedTokenID)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
buf = new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
res = buf.String()
|
||||
require.NotEmpty(t, res)
|
||||
require.Contains(t, res, "deleted")
|
||||
|
||||
// Create third token
|
||||
inv, root = clitest.New(t, "tokens", "create", "--name", "token-three")
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
Generated
+6
@@ -11878,6 +11878,12 @@ const docTemplate = `{
|
||||
"user_id"
|
||||
],
|
||||
"properties": {
|
||||
"allow_list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.APIAllowListTarget"
|
||||
}
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
||||
Generated
+6
@@ -10578,6 +10578,12 @@
|
||||
"user_id"
|
||||
],
|
||||
"properties": {
|
||||
"allow_list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.APIAllowListTarget"
|
||||
}
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
|
||||
@@ -102,6 +102,10 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
|
||||
}
|
||||
}
|
||||
|
||||
if len(params.AllowList) == 0 {
|
||||
panic(fmt.Sprintf("developer error: API key %s has empty allow list", keyID))
|
||||
}
|
||||
|
||||
token := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||
|
||||
return database.InsertAPIKeyParams{
|
||||
|
||||
@@ -51,6 +51,8 @@ func TestTokenCRUD(t *testing.T) {
|
||||
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*6))
|
||||
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*24*8))
|
||||
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
|
||||
require.Len(t, keys[0].AllowList, 1)
|
||||
require.Equal(t, "*:*", keys[0].AllowList[0].String())
|
||||
|
||||
// no update
|
||||
|
||||
@@ -86,6 +88,8 @@ func TestTokenScoped(t *testing.T) {
|
||||
require.EqualValues(t, len(keys), 1)
|
||||
require.Contains(t, res.Key, keys[0].ID)
|
||||
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
|
||||
require.Len(t, keys[0].AllowList, 1)
|
||||
require.Equal(t, "*:*", keys[0].AllowList[0].String())
|
||||
}
|
||||
|
||||
// Ensure backward-compat: when a token is created using the legacy singular
|
||||
@@ -132,6 +136,8 @@ func TestTokenLegacySingularScopeCompat(t *testing.T) {
|
||||
require.Len(t, keys, 1)
|
||||
require.Equal(t, tc.scope, keys[0].Scope)
|
||||
require.ElementsMatch(t, keys[0].Scopes, tc.scopes)
|
||||
require.Len(t, keys[0].AllowList, 1)
|
||||
require.Equal(t, "*:*", keys[0].AllowList[0].String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ func ListLazy[F any, T any](convert func(F) T) func(list []F) []T {
|
||||
}
|
||||
}
|
||||
|
||||
func APIAllowListTarget(entry rbac.AllowListElement) codersdk.APIAllowListTarget {
|
||||
return codersdk.APIAllowListTarget{
|
||||
Type: codersdk.RBACResource(entry.Type),
|
||||
ID: entry.ID,
|
||||
}
|
||||
}
|
||||
|
||||
type ExternalAuthMeta struct {
|
||||
Authenticated bool
|
||||
ValidateError string
|
||||
|
||||
@@ -1596,6 +1596,11 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
||||
scopes = append(scopes, codersdk.APIKeyScope(s))
|
||||
}
|
||||
|
||||
allowList := db2sdk.List(k.AllowList, db2sdk.APIAllowListTarget)
|
||||
if len(allowList) == 0 {
|
||||
panic(fmt.Sprintf("developer error: API key %s has empty allow list", k.ID))
|
||||
}
|
||||
|
||||
return codersdk.APIKey{
|
||||
ID: k.ID,
|
||||
UserID: k.UserID,
|
||||
@@ -1608,5 +1613,6 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
||||
Scopes: scopes,
|
||||
LifetimeSeconds: k.LifetimeSeconds,
|
||||
TokenName: k.TokenName,
|
||||
AllowList: allowList,
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -12,17 +12,18 @@ import (
|
||||
|
||||
// APIKey: do not ever return the HashedSecret
|
||||
type APIKey struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
||||
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
|
||||
Scope APIKeyScope `json:"scope" enums:"all,application_connect"` // Deprecated: use Scopes instead.
|
||||
Scopes []APIKeyScope `json:"scopes"`
|
||||
TokenName string `json:"token_name" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
||||
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
|
||||
Scope APIKeyScope `json:"scope" enums:"all,application_connect"` // Deprecated: use Scopes instead.
|
||||
Scopes []APIKeyScope `json:"scopes"`
|
||||
TokenName string `json:"token_name" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
AllowList []APIAllowListTarget `json:"allow_list"`
|
||||
}
|
||||
|
||||
// LoginType is the type of login used to create the API key.
|
||||
|
||||
@@ -1793,6 +1793,11 @@
|
||||
"description": "Delete a token",
|
||||
"path": "reference/cli/tokens_remove.md"
|
||||
},
|
||||
{
|
||||
"title": "tokens view",
|
||||
"description": "Display detailed information about a token",
|
||||
"path": "reference/cli/tokens_view.md"
|
||||
},
|
||||
{
|
||||
"title": "unfavorite",
|
||||
"description": "Remove a workspace from your favorites",
|
||||
|
||||
Generated
+20
-13
@@ -742,6 +742,12 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"allow_list": [
|
||||
{
|
||||
"id": "string",
|
||||
"type": "*"
|
||||
}
|
||||
],
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"expires_at": "2019-08-24T14:15:22Z",
|
||||
"id": "string",
|
||||
@@ -760,19 +766,20 @@
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|-------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `created_at` | string | true | | |
|
||||
| `expires_at` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_used` | string | true | | |
|
||||
| `lifetime_seconds` | integer | true | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | |
|
||||
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `scopes` | array of [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
|
||||
| `token_name` | string | true | | |
|
||||
| `updated_at` | string | true | | |
|
||||
| `user_id` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|---------------------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `allow_list` | array of [codersdk.APIAllowListTarget](#codersdkapiallowlisttarget) | false | | |
|
||||
| `created_at` | string | true | | |
|
||||
| `expires_at` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_used` | string | true | | |
|
||||
| `lifetime_seconds` | integer | true | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | |
|
||||
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `scopes` | array of [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
|
||||
| `token_name` | string | true | | |
|
||||
| `updated_at` | string | true | | |
|
||||
| `user_id` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
|
||||
Generated
+85
-22
@@ -757,6 +757,12 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
|
||||
```json
|
||||
[
|
||||
{
|
||||
"allow_list": [
|
||||
{
|
||||
"id": "string",
|
||||
"type": "*"
|
||||
}
|
||||
],
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"expires_at": "2019-08-24T14:15:22Z",
|
||||
"id": "string",
|
||||
@@ -784,31 +790,76 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------------|--------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» created_at` | string(date-time) | true | | |
|
||||
| `» expires_at` | string(date-time) | true | | |
|
||||
| `» id` | string | true | | |
|
||||
| `» last_used` | string(date-time) | true | | |
|
||||
| `» lifetime_seconds` | integer | true | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | true | | |
|
||||
| `» scope` | [codersdk.APIKeyScope](schemas.md#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `» scopes` | array | false | | |
|
||||
| `» token_name` | string | true | | |
|
||||
| `» updated_at` | string(date-time) | true | | |
|
||||
| `» user_id` | string(uuid) | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------------|----------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» allow_list` | array | false | | |
|
||||
| `»» id` | string | false | | |
|
||||
| `»» type` | [codersdk.RBACResource](schemas.md#codersdkrbacresource) | false | | |
|
||||
| `» created_at` | string(date-time) | true | | |
|
||||
| `» expires_at` | string(date-time) | true | | |
|
||||
| `» id` | string | true | | |
|
||||
| `» last_used` | string(date-time) | true | | |
|
||||
| `» lifetime_seconds` | integer | true | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | true | | |
|
||||
| `» scope` | [codersdk.APIKeyScope](schemas.md#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `» scopes` | array | false | | |
|
||||
| `» token_name` | string | true | | |
|
||||
| `» updated_at` | string(date-time) | true | | |
|
||||
| `» user_id` | string(uuid) | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value |
|
||||
|--------------|-----------------------|
|
||||
| `login_type` | `password` |
|
||||
| `login_type` | `github` |
|
||||
| `login_type` | `oidc` |
|
||||
| `login_type` | `token` |
|
||||
| `scope` | `all` |
|
||||
| `scope` | `application_connect` |
|
||||
| Property | Value |
|
||||
|--------------|------------------------------------|
|
||||
| `type` | `*` |
|
||||
| `type` | `aibridge_interception` |
|
||||
| `type` | `api_key` |
|
||||
| `type` | `assign_org_role` |
|
||||
| `type` | `assign_role` |
|
||||
| `type` | `audit_log` |
|
||||
| `type` | `connection_log` |
|
||||
| `type` | `crypto_key` |
|
||||
| `type` | `debug_info` |
|
||||
| `type` | `deployment_config` |
|
||||
| `type` | `deployment_stats` |
|
||||
| `type` | `file` |
|
||||
| `type` | `group` |
|
||||
| `type` | `group_member` |
|
||||
| `type` | `idpsync_settings` |
|
||||
| `type` | `inbox_notification` |
|
||||
| `type` | `license` |
|
||||
| `type` | `notification_message` |
|
||||
| `type` | `notification_preference` |
|
||||
| `type` | `notification_template` |
|
||||
| `type` | `oauth2_app` |
|
||||
| `type` | `oauth2_app_code_token` |
|
||||
| `type` | `oauth2_app_secret` |
|
||||
| `type` | `organization` |
|
||||
| `type` | `organization_member` |
|
||||
| `type` | `prebuilt_workspace` |
|
||||
| `type` | `provisioner_daemon` |
|
||||
| `type` | `provisioner_jobs` |
|
||||
| `type` | `replicas` |
|
||||
| `type` | `system` |
|
||||
| `type` | `tailnet_coordinator` |
|
||||
| `type` | `task` |
|
||||
| `type` | `template` |
|
||||
| `type` | `usage_event` |
|
||||
| `type` | `user` |
|
||||
| `type` | `user_secret` |
|
||||
| `type` | `webpush_subscription` |
|
||||
| `type` | `workspace` |
|
||||
| `type` | `workspace_agent_devcontainers` |
|
||||
| `type` | `workspace_agent_resource_monitor` |
|
||||
| `type` | `workspace_dormant` |
|
||||
| `type` | `workspace_proxy` |
|
||||
| `login_type` | `password` |
|
||||
| `login_type` | `github` |
|
||||
| `login_type` | `oidc` |
|
||||
| `login_type` | `token` |
|
||||
| `scope` | `all` |
|
||||
| `scope` | `application_connect` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -896,6 +947,12 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/{keyname} \
|
||||
|
||||
```json
|
||||
{
|
||||
"allow_list": [
|
||||
{
|
||||
"id": "string",
|
||||
"type": "*"
|
||||
}
|
||||
],
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"expires_at": "2019-08-24T14:15:22Z",
|
||||
"id": "string",
|
||||
@@ -946,6 +1003,12 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/{keyid} \
|
||||
|
||||
```json
|
||||
{
|
||||
"allow_list": [
|
||||
{
|
||||
"id": "string",
|
||||
"type": "*"
|
||||
}
|
||||
],
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"expires_at": "2019-08-24T14:15:22Z",
|
||||
"id": "string",
|
||||
|
||||
Generated
+10
-5
@@ -25,6 +25,10 @@ Tokens are used to authenticate automated clients to Coder.
|
||||
|
||||
$ coder tokens ls
|
||||
|
||||
- Create a scoped token:
|
||||
|
||||
$ coder tokens create --scope workspace:read --allow workspace:<uuid>
|
||||
|
||||
- Remove a token by ID:
|
||||
|
||||
$ coder tokens rm WuoWs4ZsMX
|
||||
@@ -32,8 +36,9 @@ Tokens are used to authenticate automated clients to Coder.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Name | Purpose |
|
||||
|-------------------------------------------|----------------|
|
||||
| [<code>create</code>](./tokens_create.md) | Create a token |
|
||||
| [<code>list</code>](./tokens_list.md) | List tokens |
|
||||
| [<code>remove</code>](./tokens_remove.md) | Delete a token |
|
||||
| Name | Purpose |
|
||||
|-------------------------------------------|--------------------------------------------|
|
||||
| [<code>create</code>](./tokens_create.md) | Create a token |
|
||||
| [<code>list</code>](./tokens_list.md) | List tokens |
|
||||
| [<code>view</code>](./tokens_view.md) | Display detailed information about a token |
|
||||
| [<code>remove</code>](./tokens_remove.md) | Delete a token |
|
||||
|
||||
Generated
+16
@@ -37,3 +37,19 @@ Specify a human-readable name.
|
||||
| Environment | <code>$CODER_TOKEN_USER</code> |
|
||||
|
||||
Specify the user to create the token for (Only works if logged in user is admin).
|
||||
|
||||
### --scope
|
||||
|
||||
| | |
|
||||
|------|--------------------|
|
||||
| Type | <code>scope</code> |
|
||||
|
||||
Repeatable scope to attach to the token (e.g. workspace:read).
|
||||
|
||||
### --allow
|
||||
|
||||
| | |
|
||||
|------|------------------------|
|
||||
| Type | <code>allowList</code> |
|
||||
|
||||
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).
|
||||
|
||||
Generated
+4
-4
@@ -25,10 +25,10 @@ Specifies whether all users' tokens will be listed or not (must have Owner role
|
||||
|
||||
### -c, --column
|
||||
|
||||
| | |
|
||||
|---------|-------------------------------------------------------------------|
|
||||
| Type | <code>[id\|name\|last used\|expires at\|created at\|owner]</code> |
|
||||
| Default | <code>id,name,last used,expires at,created at</code> |
|
||||
| | |
|
||||
|---------|---------------------------------------------------------------------------------------|
|
||||
| Type | <code>[id\|name\|scopes\|allow list\|last used\|expires at\|created at\|owner]</code> |
|
||||
| Default | <code>id,name,scopes,allow list,last used,expires at,created at</code> |
|
||||
|
||||
Columns to display in table output.
|
||||
|
||||
|
||||
Generated
+30
@@ -0,0 +1,30 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
# tokens view
|
||||
|
||||
Display detailed information about a token
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder tokens view [flags] <name|id>
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### -c, --column
|
||||
|
||||
| | |
|
||||
|---------|---------------------------------------------------------------------------------------|
|
||||
| Type | <code>[id\|name\|scopes\|allow list\|last used\|expires at\|created at\|owner]</code> |
|
||||
| Default | <code>id,name,scopes,allow list,last used,expires at,created at,owner</code> |
|
||||
|
||||
Columns to display in table output.
|
||||
|
||||
### -o, --output
|
||||
|
||||
| | |
|
||||
|---------|--------------------------|
|
||||
| Type | <code>table\|json</code> |
|
||||
| Default | <code>table</code> |
|
||||
|
||||
Output format.
|
||||
Generated
+1
@@ -141,6 +141,7 @@ export interface APIKey {
|
||||
readonly scopes: readonly APIKeyScope[];
|
||||
readonly token_name: string;
|
||||
readonly lifetime_seconds: number;
|
||||
readonly allow_list: readonly APIAllowListTarget[];
|
||||
}
|
||||
|
||||
// From codersdk/apikey.go
|
||||
|
||||
@@ -85,6 +85,7 @@ export const MockToken: TypesGen.APIKeyWithOwner = {
|
||||
login_type: "token",
|
||||
scope: "all",
|
||||
scopes: ["coder:all"],
|
||||
allow_list: [{ type: "*", id: "*" }],
|
||||
lifetime_seconds: 2592000,
|
||||
token_name: "token-one",
|
||||
username: "admin",
|
||||
@@ -102,6 +103,7 @@ export const MockTokens: TypesGen.APIKeyWithOwner[] = [
|
||||
login_type: "token",
|
||||
scope: "all",
|
||||
scopes: ["coder:all"],
|
||||
allow_list: [{ type: "*", id: "*" }],
|
||||
lifetime_seconds: 2592000,
|
||||
token_name: "token-two",
|
||||
username: "admin",
|
||||
|
||||
Reference in New Issue
Block a user