Skip to content

Commit a32169c

Browse files
authored
feat: add flag to see all tokens if owner (#6227)
* added query for tokens by user id * updated query args * adding owner col * fix request params * update-golden-files * added owners col to ls table output * added ttoken translations * prettier * format table according to arg * using slice.Contains * refactored token state * cleanup
1 parent 7a52a9c commit a32169c

File tree

22 files changed

+347
-213
lines changed

22 files changed

+347
-213
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"stretchr",
114114
"STTY",
115115
"stuntest",
116+
"tanstack",
116117
"tailbroker",
117118
"tailcfg",
118119
"tailexchange",

cli/testdata/coder_tokens_list_--help.golden

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ Aliases:
77
list, ls
88

99
Flags:
10+
-a, --all Specifies whether all users' tokens will be listed or not (must have
11+
Owner role to see all tokens).
1012
-c, --column strings Columns to display in table output. Available columns: id, last used,
11-
expires at, created at (default [id,last used,expires at,created at])
13+
expires at, created at, owner (default [id,last used,expires
14+
at,created at])
1215
-h, --help help for list
1316
-o, --output string Output format. Available formats: table, json (default "table")
1417

cli/tokens.go

+67-8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package cli
22

33
import (
44
"fmt"
5+
"os"
56
"strings"
67
"time"
78

9+
"github.com/google/uuid"
810
"github.com/spf13/cobra"
11+
"golang.org/x/exp/slices"
912
"golang.org/x/xerrors"
1013

1114
"github.com/coder/coder/cli/cliflag"
@@ -83,12 +86,47 @@ func createToken() *cobra.Command {
8386
return cmd
8487
}
8588

89+
// tokenListRow is the type provided to the OutputFormatter.
90+
type tokenListRow struct {
91+
// For JSON format:
92+
codersdk.APIKey `table:"-"`
93+
94+
// For table format:
95+
ID string `json:"-" table:"id,default_sort"`
96+
LastUsed time.Time `json:"-" table:"last used"`
97+
ExpiresAt time.Time `json:"-" table:"expires at"`
98+
CreatedAt time.Time `json:"-" table:"created at"`
99+
Owner string `json:"-" table:"owner"`
100+
}
101+
102+
func tokenListRowFromToken(token codersdk.APIKey, usersByID map[uuid.UUID]codersdk.User) tokenListRow {
103+
user := usersByID[token.UserID]
104+
105+
return tokenListRow{
106+
APIKey: token,
107+
ID: token.ID,
108+
LastUsed: token.LastUsed,
109+
ExpiresAt: token.ExpiresAt,
110+
CreatedAt: token.CreatedAt,
111+
Owner: user.Username,
112+
}
113+
}
114+
86115
func listTokens() *cobra.Command {
87-
formatter := cliui.NewOutputFormatter(
88-
cliui.TableFormat([]codersdk.APIKey{}, nil),
89-
cliui.JSONFormat(),
90-
)
116+
// we only display the 'owner' column if the --all argument is passed in
117+
defaultCols := []string{"id", "last used", "expires at", "created at"}
118+
if slices.Contains(os.Args, "-a") || slices.Contains(os.Args, "--all") {
119+
defaultCols = append(defaultCols, "owner")
120+
}
91121

122+
var (
123+
all bool
124+
displayTokens []tokenListRow
125+
formatter = cliui.NewOutputFormatter(
126+
cliui.TableFormat([]tokenListRow{}, defaultCols),
127+
cliui.JSONFormat(),
128+
)
129+
)
92130
cmd := &cobra.Command{
93131
Use: "list",
94132
Aliases: []string{"ls"},
@@ -99,18 +137,36 @@ func listTokens() *cobra.Command {
99137
return xerrors.Errorf("create codersdk client: %w", err)
100138
}
101139

102-
keys, err := client.Tokens(cmd.Context(), codersdk.Me)
140+
tokens, err := client.Tokens(cmd.Context(), codersdk.Me, codersdk.TokensFilter{
141+
IncludeAll: all,
142+
})
103143
if err != nil {
104-
return xerrors.Errorf("create tokens: %w", err)
144+
return xerrors.Errorf("list tokens: %w", err)
105145
}
106146

107-
if len(keys) == 0 {
147+
if len(tokens) == 0 {
108148
cmd.Println(cliui.Styles.Wrap.Render(
109149
"No tokens found.",
110150
))
111151
}
112152

113-
out, err := formatter.Format(cmd.Context(), keys)
153+
userRes, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
154+
if err != nil {
155+
return err
156+
}
157+
158+
usersByID := map[uuid.UUID]codersdk.User{}
159+
for _, user := range userRes.Users {
160+
usersByID[user.ID] = user
161+
}
162+
163+
displayTokens = make([]tokenListRow, len(tokens))
164+
165+
for i, token := range tokens {
166+
displayTokens[i] = tokenListRowFromToken(token, usersByID)
167+
}
168+
169+
out, err := formatter.Format(cmd.Context(), displayTokens)
114170
if err != nil {
115171
return err
116172
}
@@ -120,6 +176,9 @@ func listTokens() *cobra.Command {
120176
},
121177
}
122178

179+
cmd.Flags().BoolVarP(&all, "all", "a", false,
180+
"Specifies whether all users' tokens will be listed or not (must have Owner role to see all tokens).")
181+
123182
formatter.AttachFlags(cmd)
124183
return cmd
125184
}

coderd/apikey.go

+29-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"net"
1010
"net/http"
11+
"strconv"
1112
"time"
1213

1314
"github.com/go-chi/chi/v5"
@@ -175,15 +176,35 @@ func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
175176
// @Success 200 {array} codersdk.APIKey
176177
// @Router /users/{user}/keys/tokens [get]
177178
func (api *API) tokens(rw http.ResponseWriter, r *http.Request) {
178-
ctx := r.Context()
179+
var (
180+
ctx = r.Context()
181+
user = httpmw.UserParam(r)
182+
keys []database.APIKey
183+
err error
184+
queryStr = r.URL.Query().Get("include_all")
185+
includeAll, _ = strconv.ParseBool(queryStr)
186+
)
179187

180-
keys, err := api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeToken)
181-
if err != nil {
182-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
183-
Message: "Internal error fetching API keys.",
184-
Detail: err.Error(),
185-
})
186-
return
188+
if includeAll {
189+
// get tokens for all users
190+
keys, err = api.Database.GetAPIKeysByLoginType(ctx, database.LoginTypeToken)
191+
if err != nil {
192+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
193+
Message: "Internal error fetching API keys.",
194+
Detail: err.Error(),
195+
})
196+
return
197+
}
198+
} else {
199+
// get user's tokens only
200+
keys, err = api.Database.GetAPIKeysByUserID(ctx, database.GetAPIKeysByUserIDParams{LoginType: database.LoginTypeToken, UserID: user.ID})
201+
if err != nil {
202+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
203+
Message: "Internal error fetching API keys.",
204+
Detail: err.Error(),
205+
})
206+
return
207+
}
187208
}
188209

189210
keys, err = AuthorizeFilter(api.HTTPAuth, r, rbac.ActionRead, keys)

coderd/apikey_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ func TestTokenCRUD(t *testing.T) {
2424
defer cancel()
2525
client := coderdtest.New(t, nil)
2626
_ = coderdtest.CreateFirstUser(t, client)
27-
keys, err := client.Tokens(ctx, codersdk.Me)
27+
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
2828
require.NoError(t, err)
2929
require.Empty(t, keys)
3030

3131
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
3232
require.NoError(t, err)
3333
require.Greater(t, len(res.Key), 2)
3434

35-
keys, err = client.Tokens(ctx, codersdk.Me)
35+
keys, err = client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
3636
require.NoError(t, err)
3737
require.EqualValues(t, len(keys), 1)
3838
require.Contains(t, res.Key, keys[0].ID)
@@ -45,7 +45,7 @@ func TestTokenCRUD(t *testing.T) {
4545

4646
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
4747
require.NoError(t, err)
48-
keys, err = client.Tokens(ctx, codersdk.Me)
48+
keys, err = client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
4949
require.NoError(t, err)
5050
require.Empty(t, keys)
5151
}
@@ -64,7 +64,7 @@ func TestTokenScoped(t *testing.T) {
6464
require.NoError(t, err)
6565
require.Greater(t, len(res.Key), 2)
6666

67-
keys, err := client.Tokens(ctx, codersdk.Me)
67+
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
6868
require.NoError(t, err)
6969
require.EqualValues(t, len(keys), 1)
7070
require.Contains(t, res.Key, keys[0].ID)
@@ -83,7 +83,7 @@ func TestTokenDuration(t *testing.T) {
8383
Lifetime: time.Hour * 24 * 7,
8484
})
8585
require.NoError(t, err)
86-
keys, err := client.Tokens(ctx, codersdk.Me)
86+
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
8787
require.NoError(t, err)
8888
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*6*24))
8989
require.Less(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*8*24))

coderd/coderdtest/authorize.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,9 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
347347
})
348348
require.NoError(t, err, "create token")
349349

350-
apiKeys, err := client.Tokens(ctx, admin.UserID.String())
350+
apiKeys, err := client.Tokens(ctx, admin.UserID.String(), codersdk.TokensFilter{
351+
IncludeAll: true,
352+
})
351353
require.NoError(t, err, "get tokens")
352354
apiKey := apiKeys[0]
353355

coderd/database/dbauthz/querier.go

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ func (q *querier) GetAPIKeysByLoginType(ctx context.Context, loginType database.
4040
return fetchWithPostFilter(q.auth, q.db.GetAPIKeysByLoginType)(ctx, loginType)
4141
}
4242

43+
func (q *querier) GetAPIKeysByUserID(ctx context.Context, params database.GetAPIKeysByUserIDParams) ([]database.APIKey, error) {
44+
return fetchWithPostFilter(q.auth, q.db.GetAPIKeysByUserID)(ctx, database.GetAPIKeysByUserIDParams{LoginType: params.LoginType, UserID: params.UserID})
45+
}
46+
4347
func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]database.APIKey, error) {
4448
return fetchWithPostFilter(q.auth, q.db.GetAPIKeysLastUsedAfter)(ctx, lastUsed)
4549
}

coderd/database/dbauthz/querier_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ func (s *MethodTestSuite) TestAPIKey() {
3131
Asserts(a, rbac.ActionRead, b, rbac.ActionRead).
3232
Returns(slice.New(a, b))
3333
}))
34+
s.Run("GetAPIKeysByUserID", s.Subtest(func(db database.Store, check *expects) {
35+
idAB := uuid.New()
36+
idC := uuid.New()
37+
38+
keyA, _ := dbgen.APIKey(s.T(), db, database.APIKey{UserID: idAB, LoginType: database.LoginTypeToken})
39+
keyB, _ := dbgen.APIKey(s.T(), db, database.APIKey{UserID: idAB, LoginType: database.LoginTypeToken})
40+
_, _ = dbgen.APIKey(s.T(), db, database.APIKey{UserID: idC, LoginType: database.LoginTypeToken})
41+
42+
check.Args(database.GetAPIKeysByUserIDParams{LoginType: database.LoginTypeToken, UserID: idAB}).
43+
Asserts(keyA, rbac.ActionRead, keyB, rbac.ActionRead).
44+
Returns(slice.New(keyA, keyB))
45+
}))
3446
s.Run("GetAPIKeysLastUsedAfter", s.Subtest(func(db database.Store, check *expects) {
3547
a, _ := dbgen.APIKey(s.T(), db, database.APIKey{LastUsed: time.Now().Add(time.Hour)})
3648
b, _ := dbgen.APIKey(s.T(), db, database.APIKey{LastUsed: time.Now().Add(time.Hour)})

coderd/database/dbfake/databasefake.go

+13
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,19 @@ func (q *fakeQuerier) GetAPIKeysByLoginType(_ context.Context, t database.LoginT
484484
return apiKeys, nil
485485
}
486486

487+
func (q *fakeQuerier) GetAPIKeysByUserID(_ context.Context, params database.GetAPIKeysByUserIDParams) ([]database.APIKey, error) {
488+
q.mutex.RLock()
489+
defer q.mutex.RUnlock()
490+
491+
apiKeys := make([]database.APIKey, 0)
492+
for _, key := range q.apiKeys {
493+
if key.UserID == params.UserID && key.LoginType == params.LoginType {
494+
apiKeys = append(apiKeys, key)
495+
}
496+
}
497+
return apiKeys, nil
498+
}
499+
487500
func (q *fakeQuerier) DeleteAPIKeyByID(_ context.Context, id string) error {
488501
q.mutex.Lock()
489502
defer q.mutex.Unlock()

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/apikeys.sql

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ SELECT * FROM api_keys WHERE last_used > $1;
1414
-- name: GetAPIKeysByLoginType :many
1515
SELECT * FROM api_keys WHERE login_type = $1;
1616

17+
-- name: GetAPIKeysByUserID :many
18+
SELECT * FROM api_keys WHERE login_type = $1 AND user_id = $2;
19+
1720
-- name: InsertAPIKey :one
1821
INSERT INTO
1922
api_keys (

0 commit comments

Comments
 (0)