Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitcadf135

Browse files
authored
feat: add scoped token support to CLI (#19985)
<!--If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.-->Add support for scoped API tokens in CLIThis PR adds CLI support for creating and viewing API tokens with scopes and allow lists. It includes:- New `--scope` and `--allow` flags for the `tokens create` command- A new `tokens view` command to display detailed information about a token- Updated table columns in `tokens list` to show scopes and allow list entries- Updated help text and examplesThese changes enable users to create tokens with limited permissions through the CLI, similar to the existing functionality in the web UI.
1 parented3d6fa commitcadf135

12 files changed

+330
-18
lines changed

‎cli/allowlistflag.go‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cli
2+
3+
import (
4+
"encoding/csv"
5+
"strings"
6+
7+
"github.com/spf13/pflag"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/v2/codersdk"
11+
)
12+
13+
var (
14+
_ pflag.SliceValue=&AllowListFlag{}
15+
_ pflag.Value=&AllowListFlag{}
16+
)
17+
18+
// AllowListFlag implements pflag.SliceValue for codersdk.APIAllowListTarget entries.
19+
typeAllowListFlag []codersdk.APIAllowListTarget
20+
21+
funcAllowListFlagOf(al*[]codersdk.APIAllowListTarget)*AllowListFlag {
22+
return (*AllowListFlag)(al)
23+
}
24+
25+
func (aAllowListFlag)String()string {
26+
returnstrings.Join(a.GetSlice(),",")
27+
}
28+
29+
func (aAllowListFlag)Value() []codersdk.APIAllowListTarget {
30+
return []codersdk.APIAllowListTarget(a)
31+
}
32+
33+
func (AllowListFlag)Type()string {return"allow-list" }
34+
35+
func (a*AllowListFlag)Set(setstring)error {
36+
values,err:=csv.NewReader(strings.NewReader(set)).Read()
37+
iferr!=nil {
38+
returnxerrors.Errorf("parse allow list entries as csv: %w",err)
39+
}
40+
for_,v:=rangevalues {
41+
iferr:=a.Append(v);err!=nil {
42+
returnerr
43+
}
44+
}
45+
returnnil
46+
}
47+
48+
func (a*AllowListFlag)Append(valuestring)error {
49+
value=strings.TrimSpace(value)
50+
ifvalue=="" {
51+
returnxerrors.New("allow list entry cannot be empty")
52+
}
53+
vartarget codersdk.APIAllowListTarget
54+
iferr:=target.UnmarshalText([]byte(value));err!=nil {
55+
returnerr
56+
}
57+
58+
*a=append(*a,target)
59+
returnnil
60+
}
61+
62+
func (a*AllowListFlag)Replace(items []string)error {
63+
*a= []codersdk.APIAllowListTarget{}
64+
for_,item:=rangeitems {
65+
iferr:=a.Append(item);err!=nil {
66+
returnerr
67+
}
68+
}
69+
returnnil
70+
}
71+
72+
func (a*AllowListFlag)GetSlice() []string {
73+
out:=make([]string,len(*a))
74+
fori,entry:=range*a {
75+
out[i]=entry.String()
76+
}
77+
returnout
78+
}

‎cli/testdata/coder_tokens_--help.golden‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ USAGE:
1616

1717
$ coder tokens ls
1818

19+
- Create a scoped token:
20+
21+
$ coder tokens create --scope workspace:read --allow workspace:<uuid>
22+
1923
- Remove a token by ID:
2024

2125
$ coder tokens rm WuoWs4ZsMX
@@ -24,6 +28,7 @@ SUBCOMMANDS:
2428
create Create a token
2529
list List tokens
2630
remove Delete a token
31+
view Display detailed information about a token
2732

2833
———
2934
Run `coder --help` for a list of global options.

‎cli/testdata/coder_tokens_create_--help.golden‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ USAGE:
66
Create a token
77

88
OPTIONS:
9+
--allow allow-list
10+
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).
11+
912
--lifetime string, $CODER_TOKEN_LIFETIME
1013
Specify a duration for the lifetime of the token.
1114

1215
-n, --name string, $CODER_TOKEN_NAME
1316
Specify a human-readable name.
1417

18+
--scope string-array
19+
Repeatable scope to attach to the token (e.g. workspace:read).
20+
1521
-u, --user string, $CODER_TOKEN_USER
1622
Specify the user to create the token for (Only works if logged in user
1723
is admin).

‎cli/testdata/coder_tokens_list_--help.golden‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ OPTIONS:
1212
Specifies whether all users' tokens will be listed or not (must have
1313
Owner role to see all tokens).
1414

15-
-c, --column [id|name|last used|expires at|created at|owner] (default: id,name,last used,expires at,created at)
15+
-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)
1616
Columns to display in table output.
1717

1818
-o, --output table|json (default: table)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder tokens view [flags] <name|id>
5+
6+
Display detailed information about a token
7+
8+
OPTIONS:
9+
-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)
10+
Columns to display in table output.
11+
12+
-o, --output table|json (default: table)
13+
Output format.
14+
15+
———
16+
Run `coder --help` for a list of global options.

‎cli/tokens.go‎

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"fmt"
55
"os"
66
"slices"
7+
"sort"
78
"strings"
89
"time"
910

1011
"golang.org/x/xerrors"
1112

1213
"github.com/coder/coder/v2/cli/cliui"
14+
"github.com/coder/coder/v2/coderd/util/slice"
1315
"github.com/coder/coder/v2/codersdk"
1416
"github.com/coder/serpent"
1517
)
@@ -27,6 +29,10 @@ func (r *RootCmd) tokens() *serpent.Command {
2729
Description:"List your tokens",
2830
Command:"coder tokens ls",
2931
},
32+
Example{
33+
Description:"Create a scoped token",
34+
Command:"coder tokens create --scope workspace:read --allow workspace:<uuid>",
35+
},
3036
Example{
3137
Description:"Remove a token by ID",
3238
Command:"coder tokens rm WuoWs4ZsMX",
@@ -39,6 +45,7 @@ func (r *RootCmd) tokens() *serpent.Command {
3945
Children: []*serpent.Command{
4046
r.createToken(),
4147
r.listTokens(),
48+
r.viewToken(),
4249
r.removeToken(),
4350
},
4451
}
@@ -50,6 +57,8 @@ func (r *RootCmd) createToken() *serpent.Command {
5057
tokenLifetimestring
5158
namestring
5259
userstring
60+
scopes []string
61+
allowList []codersdk.APIAllowListTarget
5362
)
5463
cmd:=&serpent.Command{
5564
Use:"create",
@@ -88,10 +97,18 @@ func (r *RootCmd) createToken() *serpent.Command {
8897
}
8998
}
9099

91-
res,err:=client.CreateToken(inv.Context(),userID, codersdk.CreateTokenRequest{
100+
req:= codersdk.CreateTokenRequest{
92101
Lifetime:parsedLifetime,
93102
TokenName:name,
94-
})
103+
}
104+
iflen(req.Scopes)==0 {
105+
req.Scopes= slice.StringEnums[codersdk.APIKeyScope](scopes)
106+
}
107+
iflen(allowList)>0 {
108+
req.AllowList=append([]codersdk.APIAllowListTarget(nil),allowList...)
109+
}
110+
111+
res,err:=client.CreateToken(inv.Context(),userID,req)
95112
iferr!=nil {
96113
returnxerrors.Errorf("create tokens: %w",err)
97114
}
@@ -123,6 +140,16 @@ func (r *RootCmd) createToken() *serpent.Command {
123140
Description:"Specify the user to create the token for (Only works if logged in user is admin).",
124141
Value:serpent.StringOf(&user),
125142
},
143+
{
144+
Flag:"scope",
145+
Description:"Repeatable scope to attach to the token (e.g. workspace:read).",
146+
Value:serpent.StringArrayOf(&scopes),
147+
},
148+
{
149+
Flag:"allow",
150+
Description:"Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).",
151+
Value:AllowListFlagOf(&allowList),
152+
},
126153
}
127154

128155
returncmd
@@ -136,27 +163,56 @@ type tokenListRow struct {
136163
// For table format:
137164
IDstring`json:"-" table:"id,default_sort"`
138165
TokenNamestring`json:"token_name" table:"name"`
166+
Scopesstring`json:"-" table:"scopes"`
167+
Allowstring`json:"-" table:"allow list"`
139168
LastUsed time.Time`json:"-" table:"last used"`
140169
ExpiresAt time.Time`json:"-" table:"expires at"`
141170
CreatedAt time.Time`json:"-" table:"created at"`
142171
Ownerstring`json:"-" table:"owner"`
143172
}
144173

145174
functokenListRowFromToken(token codersdk.APIKeyWithOwner)tokenListRow {
175+
returntokenListRowFromKey(token.APIKey,token.Username)
176+
}
177+
178+
functokenListRowFromKey(token codersdk.APIKey,ownerstring)tokenListRow {
146179
returntokenListRow{
147-
APIKey:token.APIKey,
180+
APIKey:token,
148181
ID:token.ID,
149182
TokenName:token.TokenName,
183+
Scopes:joinScopes(token.Scopes),
184+
Allow:joinAllowList(token.AllowList),
150185
LastUsed:token.LastUsed,
151186
ExpiresAt:token.ExpiresAt,
152187
CreatedAt:token.CreatedAt,
153-
Owner:token.Username,
188+
Owner:owner,
154189
}
155190
}
156191

192+
funcjoinScopes(scopes []codersdk.APIKeyScope)string {
193+
iflen(scopes)==0 {
194+
return""
195+
}
196+
vals:=slice.ToStrings(scopes)
197+
sort.Strings(vals)
198+
returnstrings.Join(vals,", ")
199+
}
200+
201+
funcjoinAllowList(entries []codersdk.APIAllowListTarget)string {
202+
iflen(entries)==0 {
203+
return""
204+
}
205+
vals:=make([]string,len(entries))
206+
fori,entry:=rangeentries {
207+
vals[i]=entry.String()
208+
}
209+
sort.Strings(vals)
210+
returnstrings.Join(vals,", ")
211+
}
212+
157213
func (r*RootCmd)listTokens()*serpent.Command {
158214
// we only display the 'owner' column if the --all argument is passed in
159-
defaultCols:= []string{"id","name","last used","expires at","created at"}
215+
defaultCols:= []string{"id","name","scopes","allow list","last used","expires at","created at"}
160216
ifslices.Contains(os.Args,"-a")||slices.Contains(os.Args,"--all") {
161217
defaultCols=append(defaultCols,"owner")
162218
}
@@ -226,6 +282,48 @@ func (r *RootCmd) listTokens() *serpent.Command {
226282
returncmd
227283
}
228284

285+
func (r*RootCmd)viewToken()*serpent.Command {
286+
formatter:=cliui.NewOutputFormatter(
287+
cliui.TableFormat([]tokenListRow{}, []string{"id","name","scopes","allow list","last used","expires at","created at","owner"}),
288+
cliui.JSONFormat(),
289+
)
290+
291+
cmd:=&serpent.Command{
292+
Use:"view <name|id>",
293+
Short:"Display detailed information about a token",
294+
Middleware:serpent.Chain(
295+
serpent.RequireNArgs(1),
296+
),
297+
Handler:func(inv*serpent.Invocation)error {
298+
client,err:=r.InitClient(inv)
299+
iferr!=nil {
300+
returnerr
301+
}
302+
303+
tokenName:=inv.Args[0]
304+
token,err:=client.APIKeyByName(inv.Context(),codersdk.Me,tokenName)
305+
iferr!=nil {
306+
maybeID:=strings.Split(tokenName,"-")[0]
307+
token,err=client.APIKeyByID(inv.Context(),codersdk.Me,maybeID)
308+
iferr!=nil {
309+
returnxerrors.Errorf("fetch api key by name or id: %w",err)
310+
}
311+
}
312+
313+
row:=tokenListRowFromKey(*token,"")
314+
out,err:=formatter.Format(inv.Context(), []tokenListRow{row})
315+
iferr!=nil {
316+
returnerr
317+
}
318+
_,err=fmt.Fprintln(inv.Stdout,out)
319+
returnerr
320+
},
321+
}
322+
323+
formatter.AttachOptions(&cmd.Options)
324+
returncmd
325+
}
326+
229327
func (r*RootCmd)removeToken()*serpent.Command {
230328
cmd:=&serpent.Command{
231329
Use:"remove <name|id|token>",

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp