Compare commits

...

2 Commits

Author SHA1 Message Date
Thomas Kosiewski d2ce8e3739 feat: add scope and allowlist support to tokens CLI
This commit adds comprehensive support for token scoping and 
allow-listing in the CLI token management commands:

- Add --scope flag to create scoped tokens with specific 
  permissions
- Add --allow flag to create tokens restricted to specific 
  resources
- Display scopes and allow-list in token list/view commands
- Add tokens view subcommand for detailed token inspection
- Update help text and documentation with scoping examples
- Add comprehensive test coverage for new functionality
2025-10-22 11:54:41 +01:00
Thomas Kosiewski 1eb446d220 feat: add allow_list field to API key responses for resource scoping
Add allow_list field to API key data structures and ensure proper
JSON serialization across backend and frontend. Initialize with 
default wildcard entry (*:*) for backward compatibility with
existing API keys that don't have explicit resource restrictions.

Fixes #19854
2025-10-22 11:53:50 +01:00
24 changed files with 575 additions and 64 deletions
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>",
+85
View File
@@ -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
}
+81
View File
@@ -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
View File
@@ -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)
+6
View File
@@ -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"
+6
View File
@@ -10578,6 +10578,12 @@
"user_id"
],
"properties": {
"allow_list": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.APIAllowListTarget"
}
},
"created_at": {
"type": "string",
"format": "date-time"
+4
View File
@@ -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{
+6
View File
@@ -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())
})
}
}
+7
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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.
+5
View File
@@ -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",
+20 -13
View File
@@ -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
+85 -22
View File
@@ -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",
+10 -5
View File
@@ -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 |
+16
View File
@@ -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-...).
+4 -4
View File
@@ -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.
+30
View File
@@ -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.
+1
View File
@@ -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
+2
View File
@@ -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",