@@ -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 tokenLifetime string
5158 name string
5259 user string
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+ if len (req .Scopes ) == 0 {
105+ req .Scopes = slice.StringEnums [codersdk.APIKeyScope ](scopes )
106+ }
107+ if len (allowList ) > 0 {
108+ req .AllowList = append ([]codersdk.APIAllowListTarget (nil ), allowList ... )
109+ }
110+
111+ res , err := client .CreateToken (inv .Context (), userID , req )
95112 if err != nil {
96113 return xerrors .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 return cmd
@@ -136,27 +163,59 @@ type tokenListRow struct {
136163 // For table format:
137164 ID string `json:"-" table:"id,default_sort"`
138165 TokenName string `json:"token_name" table:"name"`
166+ Scopes string `json:"-" table:"scopes"`
167+ Allow string `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 Owner string `json:"-" table:"owner"`
143172}
144173
145174func tokenListRowFromToken (token codersdk.APIKeyWithOwner ) tokenListRow {
175+ return tokenListRowFromKey (token .APIKey , token .Username )
176+ }
177+
178+ func tokenListRowFromKey (token codersdk.APIKey , owner string ) tokenListRow {
146179 return tokenListRow {
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+ func joinScopes (scopes []codersdk.APIKeyScope ) string {
193+ if len (scopes ) == 0 {
194+ return ""
195+ }
196+ vals := make ([]string , len (scopes ))
197+ for i , scope := range scopes {
198+ vals [i ] = string (scope )
199+ }
200+ sort .Strings (vals )
201+ return strings .Join (vals , ", " )
202+ }
203+
204+ func joinAllowList (entries []codersdk.APIAllowListTarget ) string {
205+ if len (entries ) == 0 {
206+ return ""
207+ }
208+ vals := make ([]string , len (entries ))
209+ for i , entry := range entries {
210+ vals [i ] = entry .String ()
211+ }
212+ sort .Strings (vals )
213+ return strings .Join (vals , ", " )
214+ }
215+
157216func (r * RootCmd ) listTokens () * serpent.Command {
158217 // we only display the 'owner' column if the --all argument is passed in
159- defaultCols := []string {"id" , "name" , "last used" , "expires at" , "created at" }
218+ defaultCols := []string {"id" , "name" , "scopes" , "allow list" , " last used" , "expires at" , "created at" }
160219 if slices .Contains (os .Args , "-a" ) || slices .Contains (os .Args , "--all" ) {
161220 defaultCols = append (defaultCols , "owner" )
162221 }
@@ -226,6 +285,48 @@ func (r *RootCmd) listTokens() *serpent.Command {
226285 return cmd
227286}
228287
288+ func (r * RootCmd ) viewToken () * serpent.Command {
289+ formatter := cliui .NewOutputFormatter (
290+ cliui .TableFormat ([]tokenListRow {}, []string {"id" , "name" , "scopes" , "allow list" , "last used" , "expires at" , "created at" , "owner" }),
291+ cliui .JSONFormat (),
292+ )
293+
294+ cmd := & serpent.Command {
295+ Use : "view <name|id>" ,
296+ Short : "Display detailed information about a token" ,
297+ Middleware : serpent .Chain (
298+ serpent .RequireNArgs (1 ),
299+ ),
300+ Handler : func (inv * serpent.Invocation ) error {
301+ client , err := r .InitClient (inv )
302+ if err != nil {
303+ return err
304+ }
305+
306+ tokenName := inv .Args [0 ]
307+ token , err := client .APIKeyByName (inv .Context (), codersdk .Me , tokenName )
308+ if err != nil {
309+ maybeID := strings .Split (tokenName , "-" )[0 ]
310+ token , err = client .APIKeyByID (inv .Context (), codersdk .Me , maybeID )
311+ if err != nil {
312+ return xerrors .Errorf ("fetch api key by name or id: %w" , err )
313+ }
314+ }
315+
316+ row := tokenListRowFromKey (* token , "" )
317+ out , err := formatter .Format (inv .Context (), []tokenListRow {row })
318+ if err != nil {
319+ return err
320+ }
321+ _ , err = fmt .Fprintln (inv .Stdout , out )
322+ return err
323+ },
324+ }
325+
326+ formatter .AttachOptions (& cmd .Options )
327+ return cmd
328+ }
329+
229330func (r * RootCmd ) removeToken () * serpent.Command {
230331 cmd := & serpent.Command {
231332 Use : "remove <name|id|token>" ,
0 commit comments