feat: add ability to name tokens (#6365)

* add tokens switch

* reorged TokensPage

* using Trans component for description

* using Trans component on DeleteDialog

* add owner col

* simplify hook return

* lint

* type for response

* added flag for name

* fixed auth

* lint, prettier, tests

* added unique index for login type token

* remove tokens by name

* better check for unique constraint

* docs

* test: Fix dbfake to insert token name

* fix doc tests

* Update cli/tokens.go

Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>

* Update coderd/database/migrations/000102_add_apikey_name.down.sql

Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>

* add more specificity to IsUniqueViolation check

* fix tests

* Fix AutorizeAllEndpoints

* rename migration

---------

Co-authored-by: Steven Masley <stevenmasley@coder.com>
Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
Kira Pilot
2023-03-02 09:39:38 -08:00
committed by GitHub
co-authored by Steven Masley Steven Masley
parent e3a4861e93
commit 71d1e63af0
37 changed files with 447 additions and 63 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ Get Started:
$ coder tokens rm WuoWs4ZsMX
Commands:
create Create a tokens
create Create a token
list List tokens
remove Delete a token
+2 -1
View File
@@ -1,4 +1,4 @@
Create a tokens
Create a token
Usage:
coder tokens create [flags]
@@ -7,6 +7,7 @@ Flags:
-h, --help help for create
--lifetime duration Specify a duration for the lifetime of the token.
Consumes $CODER_TOKEN_LIFETIME (default 720h0m0s)
-n, --name string Specify a human-readable name.
Global Flags:
--global-config coder Path to the global coder config directory.
+3 -3
View File
@@ -9,9 +9,9 @@ Aliases:
Flags:
-a, --all Specifies whether all users' tokens will be listed or not (must have
Owner role to see all tokens).
-c, --column strings Columns to display in table output. Available columns: id, last used,
expires at, created at, owner (default [id,last used,expires
at,created at])
-c, --column strings Columns to display in table output. Available columns: id, name, last
used, expires at, created at, owner (default [id,name,last
used,expires at,created at])
-h, --help help for list
-o, --output string Output format. Available formats: table, json (default "table")
+1 -1
View File
@@ -1,7 +1,7 @@
Delete a token
Usage:
coder tokens remove [id] [flags]
coder tokens remove [name] [flags]
Aliases:
remove, rm
+18 -6
View File
@@ -49,10 +49,13 @@ func tokens() *cobra.Command {
}
func createToken() *cobra.Command {
var tokenLifetime time.Duration
var (
tokenLifetime time.Duration
name string
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a tokens",
Short: "Create a token",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
@@ -60,7 +63,8 @@ func createToken() *cobra.Command {
}
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{
Lifetime: tokenLifetime,
Lifetime: tokenLifetime,
TokenName: name,
})
if err != nil {
return xerrors.Errorf("create tokens: %w", err)
@@ -81,6 +85,7 @@ func createToken() *cobra.Command {
}
cliflag.DurationVarP(cmd.Flags(), &tokenLifetime, "lifetime", "", "CODER_TOKEN_LIFETIME", 30*24*time.Hour, "Specify a duration for the lifetime of the token.")
cmd.Flags().StringVarP(&name, "name", "n", "", "Specify a human-readable name.")
return cmd
}
@@ -92,6 +97,7 @@ type tokenListRow struct {
// For table format:
ID string `json:"-" table:"id,default_sort"`
TokenName string `json:"token_name" table:"name"`
LastUsed time.Time `json:"-" table:"last used"`
ExpiresAt time.Time `json:"-" table:"expires at"`
CreatedAt time.Time `json:"-" table:"created at"`
@@ -102,6 +108,7 @@ func tokenListRowFromToken(token codersdk.APIKeyWithOwner) tokenListRow {
return tokenListRow{
APIKey: token.APIKey,
ID: token.ID,
TokenName: token.TokenName,
LastUsed: token.LastUsed,
ExpiresAt: token.ExpiresAt,
CreatedAt: token.CreatedAt,
@@ -111,7 +118,7 @@ func tokenListRowFromToken(token codersdk.APIKeyWithOwner) tokenListRow {
func listTokens() *cobra.Command {
// we only display the 'owner' column if the --all argument is passed in
defaultCols := []string{"id", "last used", "expires at", "created at"}
defaultCols := []string{"id", "name", "last used", "expires at", "created at"}
if slices.Contains(os.Args, "-a") || slices.Contains(os.Args, "--all") {
defaultCols = append(defaultCols, "owner")
}
@@ -172,7 +179,7 @@ func listTokens() *cobra.Command {
func removeToken() *cobra.Command {
cmd := &cobra.Command{
Use: "remove [id]",
Use: "remove [name]",
Aliases: []string{"rm"},
Short: "Delete a token",
Args: cobra.ExactArgs(1),
@@ -182,7 +189,12 @@ func removeToken() *cobra.Command {
return xerrors.Errorf("create codersdk client: %w", err)
}
err = client.DeleteAPIKey(cmd.Context(), codersdk.Me, args[0])
token, err := client.APIKeyByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return xerrors.Errorf("fetch api key by name %s: %w", args[0], err)
}
err = client.DeleteAPIKey(cmd.Context(), codersdk.Me, token.ID)
if err != nil {
return xerrors.Errorf("delete api key: %w", err)
}
+2 -2
View File
@@ -33,7 +33,7 @@ func TestTokens(t *testing.T) {
res := buf.String()
require.Contains(t, res, "tokens found")
cmd, root = clitest.New(t, "tokens", "create")
cmd, root = clitest.New(t, "tokens", "create", "--name", "token-one")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
@@ -73,7 +73,7 @@ func TestTokens(t *testing.T) {
require.Len(t, tokens, 1)
require.Equal(t, id, tokens[0].ID)
cmd, root = clitest.New(t, "tokens", "rm", id)
cmd, root = clitest.New(t, "tokens", "rm", "token-one")
clitest.SetupConfig(t, client, root)
buf = new(bytes.Buffer)
cmd.SetOut(buf)
+51 -2
View File
@@ -3337,6 +3337,48 @@ const docTemplate = `{
}
}
},
"/users/{user}/keys/tokens/{keyname}": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get API key by token name",
"operationId": "get-api-key-by-token-name",
"parameters": [
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "string",
"description": "Key Name",
"name": "keyname",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.APIKey"
}
}
}
}
},
"/users/{user}/keys/{keyid}": {
"get": {
"security": [
@@ -3350,8 +3392,8 @@ const docTemplate = `{
"tags": [
"Users"
],
"summary": "Get API key",
"operationId": "get-api-key",
"summary": "Get API key by ID",
"operationId": "get-api-key-by-id",
"parameters": [
{
"type": "string",
@@ -5342,6 +5384,7 @@ const docTemplate = `{
"lifetime_seconds",
"login_type",
"scope",
"token_name",
"updated_at",
"user_id"
],
@@ -5388,6 +5431,9 @@ const docTemplate = `{
}
]
},
"token_name": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
@@ -6003,6 +6049,9 @@ const docTemplate = `{
"$ref": "#/definitions/codersdk.APIKeyScope"
}
]
},
"token_name": {
"type": "string"
}
}
},
+47 -2
View File
@@ -2931,6 +2931,44 @@
}
}
},
"/users/{user}/keys/tokens/{keyname}": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Get API key by token name",
"operationId": "get-api-key-by-token-name",
"parameters": [
{
"type": "string",
"description": "User ID, name, or me",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "string",
"description": "Key Name",
"name": "keyname",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.APIKey"
}
}
}
}
},
"/users/{user}/keys/{keyid}": {
"get": {
"security": [
@@ -2940,8 +2978,8 @@
],
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Get API key",
"operationId": "get-api-key",
"summary": "Get API key by ID",
"operationId": "get-api-key-by-id",
"parameters": [
{
"type": "string",
@@ -4735,6 +4773,7 @@
"lifetime_seconds",
"login_type",
"scope",
"token_name",
"updated_at",
"user_id"
],
@@ -4773,6 +4812,9 @@
}
]
},
"token_name": {
"type": "string"
},
"updated_at": {
"type": "string",
"format": "date-time"
@@ -5326,6 +5368,9 @@
"$ref": "#/definitions/codersdk.APIKeyScope"
}
]
},
"token_name": {
"type": "string"
}
}
},
+63 -3
View File
@@ -13,6 +13,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/tabbed/pqtype"
"golang.org/x/xerrors"
@@ -62,6 +63,12 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
lifeTime = createToken.Lifetime
}
tokenName := namesgenerator.GetRandomName(1)
if len(createToken.TokenName) != 0 {
tokenName = createToken.TokenName
}
err := api.validateAPIKeyLifetime(lifeTime)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -77,8 +84,19 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
ExpiresAt: database.Now().Add(lifeTime),
Scope: scope,
LifetimeSeconds: int64(lifeTime.Seconds()),
TokenName: tokenName,
})
if err != nil {
if database.IsUniqueViolation(err, database.UniqueIndexApiKeyName) {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("A token with name %q already exists.", tokenName),
Validations: []codersdk.ValidationError{{
Field: "name",
Detail: "This value is already in use and should be unique.",
}},
})
return
}
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to create API key.",
Detail: err.Error(),
@@ -133,8 +151,8 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusCreated, codersdk.GenerateAPIKeyResponse{Key: cookie.Value})
}
// @Summary Get API key
// @ID get-api-key
// @Summary Get API key by ID
// @ID get-api-key-by-id
// @Security CoderSessionToken
// @Produce json
// @Tags Users
@@ -142,7 +160,7 @@ func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
// @Param keyid path string true "Key ID" format(uuid)
// @Success 200 {object} codersdk.APIKey
// @Router /users/{user}/keys/{keyid} [get]
func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
func (api *API) apiKeyByID(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
keyID := chi.URLParam(r, "keyid")
@@ -167,6 +185,46 @@ func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key))
}
// @Summary Get API key by token name
// @ID get-api-key-by-token-name
// @Security CoderSessionToken
// @Produce json
// @Tags Users
// @Param user path string true "User ID, name, or me"
// @Param keyname path string true "Key Name" format(string)
// @Success 200 {object} codersdk.APIKey
// @Router /users/{user}/keys/tokens/{keyname} [get]
func (api *API) apiKeyByName(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
user = httpmw.UserParam(r)
tokenName = chi.URLParam(r, "keyname")
)
token, err := api.Database.GetAPIKeyByName(ctx, database.GetAPIKeyByNameParams{
TokenName: tokenName,
UserID: user.ID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching API key.",
Detail: err.Error(),
})
return
}
if !api.Authorize(r, rbac.ActionRead, token) {
httpapi.ResourceNotFound(rw)
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(token))
}
// @Summary Get user tokens
// @ID get-user-tokens
// @Security CoderSessionToken
@@ -305,6 +363,7 @@ type createAPIKeyParams struct {
ExpiresAt time.Time
LifetimeSeconds int64
Scope database.APIKeyScope
TokenName string
}
func (api *API) validateAPIKeyLifetime(lifetime time.Duration) error {
@@ -374,6 +433,7 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
HashedSecret: hashed[:],
LoginType: params.LoginType,
Scope: scope,
TokenName: params.TokenName,
})
if err != nil {
return nil, nil, xerrors.Errorf("insert API key: %w", err)
+4 -1
View File
@@ -559,9 +559,12 @@ func New(options *Options) *API {
r.Route("/tokens", func(r chi.Router) {
r.Post("/", api.postToken)
r.Get("/", api.tokens)
r.Route("/{keyname}", func(r chi.Router) {
r.Get("/", api.apiKeyByName)
})
})
r.Route("/{keyid}", func(r chi.Router) {
r.Get("/", api.apiKey)
r.Get("/", api.apiKeyByID)
r.Delete("/", api.deleteAPIKey)
})
})
+8 -2
View File
@@ -95,6 +95,10 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
AssertObject: rbac.ResourceAPIKey,
AssertAction: rbac.ActionRead,
},
"GET:/api/v2/users/{user}/keys/tokens/{keyname}": {
AssertObject: rbac.ResourceAPIKey,
AssertAction: rbac.ActionRead,
},
"GET:/api/v2/workspacebuilds/{workspacebuild}": {
AssertAction: rbac.ActionRead,
AssertObject: workspaceRBACObj,
@@ -342,8 +346,9 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
t.Fail()
}
_, err := client.CreateToken(ctx, admin.UserID.String(), codersdk.CreateTokenRequest{
Lifetime: time.Hour,
Scope: codersdk.APIKeyScopeAll,
Lifetime: time.Hour,
Scope: codersdk.APIKeyScopeAll,
TokenName: namesgenerator.GetRandomName(1),
})
require.NoError(t, err, "create token")
@@ -419,6 +424,7 @@ func NewAuthTester(ctx context.Context, t *testing.T, client *codersdk.Client, a
"{templatename}": template.Name,
"{workspace_and_agent}": workspace.Name + "." + workspace.LatestBuild.Resources[0].Agents[0].Name,
"{keyid}": apiKey.ID,
"{keyname}": apiKey.TokenName,
// Only checking template scoped params here
"parameters/{scope}/{id}": fmt.Sprintf("parameters/%s/%s",
string(templateParam.Scope), templateParam.ScopeID.String()),
+4
View File
@@ -36,6 +36,10 @@ func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
}
func (q *querier) GetAPIKeyByName(ctx context.Context, arg database.GetAPIKeyByNameParams) (database.APIKey, error) {
return fetch(q.log, q.auth, q.db.GetAPIKeyByName)(ctx, arg)
}
func (q *querier) GetAPIKeysByLoginType(ctx context.Context, loginType database.LoginType) ([]database.APIKey, error) {
return fetchWithPostFilter(q.auth, q.db.GetAPIKeysByLoginType)(ctx, loginType)
}
+10
View File
@@ -23,6 +23,16 @@ func (s *MethodTestSuite) TestAPIKey() {
key, _ := dbgen.APIKey(s.T(), db, database.APIKey{})
check.Args(key.ID).Asserts(key, rbac.ActionRead).Returns(key)
}))
s.Run("GetAPIKeyByName", s.Subtest(func(db database.Store, check *expects) {
key, _ := dbgen.APIKey(s.T(), db, database.APIKey{
TokenName: "marge-cat",
LoginType: database.LoginTypeToken,
})
check.Args(database.GetAPIKeyByNameParams{
TokenName: key.TokenName,
UserID: key.UserID,
}).Asserts(key, rbac.ActionRead).Returns(key)
}))
s.Run("GetAPIKeysByLoginType", s.Subtest(func(db database.Store, check *expects) {
a, _ := dbgen.APIKey(s.T(), db, database.APIKey{LoginType: database.LoginTypePassword})
b, _ := dbgen.APIKey(s.T(), db, database.APIKey{LoginType: database.LoginTypePassword})
+16
View File
@@ -464,6 +464,21 @@ func (q *fakeQuerier) GetAPIKeyByID(_ context.Context, id string) (database.APIK
return database.APIKey{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetAPIKeyByName(_ context.Context, params database.GetAPIKeyByNameParams) (database.APIKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
if params.TokenName == "" {
return database.APIKey{}, sql.ErrNoRows
}
for _, apiKey := range q.apiKeys {
if params.UserID == apiKey.UserID && params.TokenName == apiKey.TokenName {
return apiKey, nil
}
}
return database.APIKey{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetAPIKeysLastUsedAfter(_ context.Context, after time.Time) ([]database.APIKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -2515,6 +2530,7 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
LastUsed: arg.LastUsed,
LoginType: arg.LoginType,
Scope: arg.Scope,
TokenName: arg.TokenName,
}
q.apiKeys = append(q.apiKeys, key)
return key, nil
+1
View File
@@ -90,6 +90,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey) (key database
UpdatedAt: takeFirst(seed.UpdatedAt, database.Now()),
LoginType: takeFirst(seed.LoginType, database.LoginTypePassword),
Scope: takeFirst(seed.Scope, database.APIKeyScopeAll),
TokenName: takeFirst(seed.TokenName),
})
require.NoError(t, err, "insert api key")
return key, fmt.Sprintf("%s-%s", key.ID, secret)
+4 -1
View File
@@ -134,7 +134,8 @@ CREATE TABLE api_keys (
login_type login_type NOT NULL,
lifetime_seconds bigint DEFAULT 86400 NOT NULL,
ip_address inet DEFAULT '0.0.0.0'::inet NOT NULL,
scope api_key_scope DEFAULT 'all'::api_key_scope NOT NULL
scope api_key_scope DEFAULT 'all'::api_key_scope NOT NULL,
token_name text DEFAULT ''::text NOT NULL
);
COMMENT ON COLUMN api_keys.hashed_secret IS 'hashed_secret contains a SHA256 hash of the key secret. This is considered a secret and MUST NOT be returned from the API as it is used for API key encryption in app proxying code.';
@@ -748,6 +749,8 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
CREATE INDEX idx_api_keys_user ON api_keys USING btree (user_id);
CREATE INDEX idx_audit_log_organization_id ON audit_logs USING btree (organization_id);
@@ -0,0 +1,8 @@
BEGIN;
DROP INDEX idx_api_key_name;
ALTER TABLE ONLY api_keys
DROP COLUMN IF EXISTS token_name;
COMMIT;
@@ -0,0 +1,17 @@
BEGIN;
ALTER TABLE ONLY api_keys
ADD COLUMN IF NOT EXISTS token_name text NOT NULL DEFAULT '';
UPDATE
api_keys
SET
token_name = gen_random_uuid ()::text
WHERE
login_type = 'token';
CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name)
WHERE
(login_type = 'token');
COMMIT;
+1
View File
@@ -1216,6 +1216,7 @@ type APIKey struct {
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
Scope APIKeyScope `db:"scope" json:"scope"`
TokenName string `db:"token_name" json:"token_name"`
}
type AuditLog struct {
+2
View File
@@ -30,6 +30,8 @@ type sqlcQuerier interface {
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
// there is no unique constraint on empty token names
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error)
GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error)
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
+53 -6
View File
@@ -43,7 +43,7 @@ func (q *sqlQuerier) DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
SELECT
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name
FROM
api_keys
WHERE
@@ -67,12 +67,52 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
&i.LifetimeSeconds,
&i.IPAddress,
&i.Scope,
&i.TokenName,
)
return i, err
}
const getAPIKeyByName = `-- name: GetAPIKeyByName :one
SELECT
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name
FROM
api_keys
WHERE
user_id = $1 AND
token_name = $2 AND
token_name != ''
LIMIT
1
`
type GetAPIKeyByNameParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
TokenName string `db:"token_name" json:"token_name"`
}
// there is no unique constraint on empty token names
func (q *sqlQuerier) GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error) {
row := q.db.QueryRowContext(ctx, getAPIKeyByName, arg.UserID, arg.TokenName)
var i APIKey
err := row.Scan(
&i.ID,
&i.HashedSecret,
&i.UserID,
&i.LastUsed,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.LoginType,
&i.LifetimeSeconds,
&i.IPAddress,
&i.Scope,
&i.TokenName,
)
return i, err
}
const getAPIKeysByLoginType = `-- name: GetAPIKeysByLoginType :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE login_type = $1
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name FROM api_keys WHERE login_type = $1
`
func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error) {
@@ -96,6 +136,7 @@ func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginT
&i.LifetimeSeconds,
&i.IPAddress,
&i.Scope,
&i.TokenName,
); err != nil {
return nil, err
}
@@ -111,7 +152,7 @@ func (q *sqlQuerier) GetAPIKeysByLoginType(ctx context.Context, loginType LoginT
}
const getAPIKeysByUserID = `-- name: GetAPIKeysByUserID :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE login_type = $1 AND user_id = $2
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name FROM api_keys WHERE login_type = $1 AND user_id = $2
`
type GetAPIKeysByUserIDParams struct {
@@ -140,6 +181,7 @@ func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUse
&i.LifetimeSeconds,
&i.IPAddress,
&i.Scope,
&i.TokenName,
); err != nil {
return nil, err
}
@@ -155,7 +197,7 @@ func (q *sqlQuerier) GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUse
}
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope FROM api_keys WHERE last_used > $1
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name FROM api_keys WHERE last_used > $1
`
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
@@ -179,6 +221,7 @@ func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.
&i.LifetimeSeconds,
&i.IPAddress,
&i.Scope,
&i.TokenName,
); err != nil {
return nil, err
}
@@ -206,7 +249,8 @@ INSERT INTO
created_at,
updated_at,
login_type,
scope
scope,
token_name
)
VALUES
($1,
@@ -215,7 +259,7 @@ VALUES
WHEN 0 THEN 86400
ELSE $2::bigint
END
, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope
, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, lifetime_seconds, ip_address, scope, token_name
`
type InsertAPIKeyParams struct {
@@ -230,6 +274,7 @@ type InsertAPIKeyParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
LoginType LoginType `db:"login_type" json:"login_type"`
Scope APIKeyScope `db:"scope" json:"scope"`
TokenName string `db:"token_name" json:"token_name"`
}
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
@@ -245,6 +290,7 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
arg.UpdatedAt,
arg.LoginType,
arg.Scope,
arg.TokenName,
)
var i APIKey
err := row.Scan(
@@ -259,6 +305,7 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
&i.LifetimeSeconds,
&i.IPAddress,
&i.Scope,
&i.TokenName,
)
return i, err
}
+16 -2
View File
@@ -8,6 +8,19 @@ WHERE
LIMIT
1;
-- name: GetAPIKeyByName :one
SELECT
*
FROM
api_keys
WHERE
user_id = @user_id AND
token_name = @token_name AND
-- there is no unique constraint on empty token names
token_name != ''
LIMIT
1;
-- name: GetAPIKeysLastUsedAfter :many
SELECT * FROM api_keys WHERE last_used > $1;
@@ -30,7 +43,8 @@ INSERT INTO
created_at,
updated_at,
login_type,
scope
scope,
token_name
)
VALUES
(@id,
@@ -39,7 +53,7 @@ VALUES
WHEN 0 THEN 86400
ELSE @lifetime_seconds::bigint
END
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @scope) RETURNING *;
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @scope, @token_name) RETURNING *;
-- name: UpdateAPIKeyByID :exec
UPDATE
+1
View File
@@ -23,6 +23,7 @@ const (
UniqueWorkspaceBuildsJobIDKey UniqueConstraint = "workspace_builds_job_id_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_key UNIQUE (job_id);
UniqueWorkspaceBuildsWorkspaceIDBuildNumberKey UniqueConstraint = "workspace_builds_workspace_id_build_number_key" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_build_number_key UNIQUE (workspace_id, build_number);
UniqueWorkspaceResourceMetadataName UniqueConstraint = "workspace_resource_metadata_name" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_name UNIQUE (workspace_resource_id, key);
UniqueIndexApiKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name));
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
+1
View File
@@ -1176,5 +1176,6 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
LoginType: codersdk.LoginType(k.LoginType),
Scope: codersdk.APIKeyScope(k.Scope),
LifetimeSeconds: k.LifetimeSeconds,
TokenName: k.TokenName,
}
}
+7 -7
View File
@@ -266,7 +266,7 @@ func TestPostLogin(t *testing.T) {
defer cancel()
split := strings.Split(client.SessionToken(), "-")
key, err := client.APIKey(ctx, admin.UserID.String(), split[0])
key, err := client.APIKeyByID(ctx, admin.UserID.String(), split[0])
require.NoError(t, err, "fetch login key")
require.Equal(t, int64(86400), key.LifetimeSeconds, "default should be 86400")
@@ -274,7 +274,7 @@ func TestPostLogin(t *testing.T) {
token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
require.NoError(t, err, "make new token api key")
split = strings.Split(token.Key, "-")
apiKey, err := client.APIKey(ctx, admin.UserID.String(), split[0])
apiKey, err := client.APIKeyByID(ctx, admin.UserID.String(), split[0])
require.NoError(t, err, "fetch api key")
require.True(t, apiKey.ExpiresAt.After(time.Now().Add(time.Hour*24*29)), "default tokens lasts more than 29 days")
@@ -356,7 +356,7 @@ func TestPostLogout(t *testing.T) {
defer cancel()
keyID := strings.Split(client.SessionToken(), "-")[0]
apiKey, err := client.APIKey(ctx, admin.UserID.String(), keyID)
apiKey, err := client.APIKeyByID(ctx, admin.UserID.String(), keyID)
require.NoError(t, err)
require.Equal(t, keyID, apiKey.ID, "API key should exist in the database")
@@ -385,7 +385,7 @@ func TestPostLogout(t *testing.T) {
}
require.True(t, found, "auth cookie should be returned")
_, err = client.APIKey(ctx, admin.UserID.String(), keyID)
_, err = client.APIKeyByID(ctx, admin.UserID.String(), keyID)
sdkErr := &codersdk.Error{}
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode(), "Expecting 401")
@@ -723,7 +723,7 @@ func TestUpdateUserPassword(t *testing.T) {
// Trying to get an API key should fail since our client's token
// has been deleted.
_, err = client.APIKey(ctx, user.UserID.String(), apikey1.Key)
_, err = client.APIKeyByID(ctx, user.UserID.String(), apikey1.Key)
require.Error(t, err)
cerr := coderdtest.SDKError(t, err)
require.Equal(t, http.StatusUnauthorized, cerr.StatusCode())
@@ -738,12 +738,12 @@ func TestUpdateUserPassword(t *testing.T) {
// Trying to get an API key should fail since all keys are deleted
// on password change.
_, err = client.APIKey(ctx, user.UserID.String(), apikey1.Key)
_, err = client.APIKeyByID(ctx, user.UserID.String(), apikey1.Key)
require.Error(t, err)
cerr = coderdtest.SDKError(t, err)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
_, err = client.APIKey(ctx, user.UserID.String(), apikey2.Key)
_, err = client.APIKeyByID(ctx, user.UserID.String(), apikey2.Key)
require.Error(t, err)
cerr = coderdtest.SDKError(t, err)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
+2 -2
View File
@@ -438,7 +438,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
// Get the current user and API key.
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
currentAPIKey, err := client.APIKey(ctx, firstUser.UserID.String(), strings.Split(client.SessionToken(), "-")[0])
currentAPIKey, err := client.APIKeyByID(ctx, firstUser.UserID.String(), strings.Split(client.SessionToken(), "-")[0])
require.NoError(t, err)
// Try to load the application without authentication.
@@ -500,7 +500,7 @@ func TestWorkspaceApplicationAuth(t *testing.T) {
apiKey := cookies[0].Value
// Fetch the API key.
apiKeyInfo, err := client.APIKey(ctx, firstUser.UserID.String(), strings.Split(apiKey, "-")[0])
apiKeyInfo, err := client.APIKeyByID(ctx, firstUser.UserID.String(), strings.Split(apiKey, "-")[0])
require.NoError(t, err)
require.Equal(t, user.ID, apiKeyInfo.UserID)
require.Equal(t, codersdk.LoginTypePassword, apiKeyInfo.LoginType)
+20 -4
View File
@@ -20,6 +20,7 @@ type APIKey struct {
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" validate:"required" enums:"all,application_connect"`
TokenName string `json:"token_name" validate:"required"`
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
}
@@ -44,8 +45,9 @@ const (
)
type CreateTokenRequest struct {
Lifetime time.Duration `json:"lifetime"`
Scope APIKeyScope `json:"scope" enums:"all,application_connect"`
Lifetime time.Duration `json:"lifetime"`
Scope APIKeyScope `json:"scope" enums:"all,application_connect"`
TokenName string `json:"token_name"`
}
// GenerateAPIKeyResponse contains an API key for a user.
@@ -119,8 +121,8 @@ func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter)
return apiKey, json.NewDecoder(res.Body).Decode(&apiKey)
}
// APIKey returns the api key by id.
func (c *Client) APIKey(ctx context.Context, userID string, id string) (*APIKey, error) {
// APIKeyByID returns the api key by id.
func (c *Client) APIKeyByID(ctx context.Context, userID string, id string) (*APIKey, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
if err != nil {
return nil, err
@@ -133,6 +135,20 @@ func (c *Client) APIKey(ctx context.Context, userID string, id string) (*APIKey,
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}
// APIKeyByName returns the api key by name.
func (c *Client) APIKeyByName(ctx context.Context, userID string, name string) (*APIKey, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens/%s", userID, name), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode > http.StatusCreated {
return nil, ReadBodyAsError(res)
}
apiKey := &APIKey{}
return apiKey, json.NewDecoder(res.Body).Decode(apiKey)
}
// DeleteAPIKey deletes API key by id.
func (c *Client) DeleteAPIKey(ctx context.Context, userID string, id string) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s/keys/%s", userID, id), nil)
+1 -1
View File
@@ -11,7 +11,7 @@ We track the following resources:
| <b>Resource<b> | |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| APIKey<br><i>write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>expires_at</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>false</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>false</td></tr></tbody></table> |
| APIKey<br><i>write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>expires_at</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>false</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>false</td></tr></tbody></table> |
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr></tbody></table> |
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
+9 -5
View File
@@ -368,6 +368,7 @@
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
@@ -384,6 +385,7 @@
| `lifetime_seconds` | integer | true | | |
| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | |
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | true | | |
| `token_name` | string | true | | |
| `updated_at` | string | true | | |
| `user_id` | string | true | | |
@@ -1141,16 +1143,18 @@ CreateParameterRequest is a structure used to create a new parameter value for a
```json
{
"lifetime": 0,
"scope": "all"
"scope": "all",
"token_name": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ---------- | -------------------------------------------- | -------- | ------------ | ----------- |
| `lifetime` | integer | false | | |
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
| Name | Type | Required | Restrictions | Description |
| ------------ | -------------------------------------------- | -------- | ------------ | ----------- |
| `lifetime` | integer | false | | |
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
| `token_name` | string | false | | |
#### Enumerated Values
+53 -2
View File
@@ -577,6 +577,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
@@ -603,6 +604,7 @@ Status Code **200**
| `» lifetime_seconds` | integer | true | | |
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | true | | |
| `» scope` | [codersdk.APIKeyScope](schemas.md#codersdkapikeyscope) | true | | |
| `» token_name` | string | true | | |
| `» updated_at` | string(date-time) | true | | |
| `» user_id` | string(uuid) | true | | |
@@ -638,7 +640,8 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens \
```json
{
"lifetime": 0,
"scope": "all"
"scope": "all",
"token_name": "string"
}
```
@@ -667,7 +670,54 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get API key
## Get API key by token name
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/{keyname} \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /users/{user}/keys/tokens/{keyname}`
### Parameters
| Name | In | Type | Required | Description |
| --------- | ---- | -------------- | -------- | -------------------- |
| `user` | path | string | true | User ID, name, or me |
| `keyname` | path | string(string) | true | Key Name |
### Example responses
> 200 Response
```json
{
"created_at": "2019-08-24T14:15:22Z",
"expires_at": "2019-08-24T14:15:22Z",
"id": "string",
"last_used": "2019-08-24T14:15:22Z",
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.APIKey](schemas.md#codersdkapikey) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get API key by ID
### Code samples
@@ -700,6 +750,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/{keyid} \
"lifetime_seconds": 0,
"login_type": "password",
"scope": "all",
"token_name": "string",
"updated_at": "2019-08-24T14:15:22Z",
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
}
+5 -5
View File
@@ -28,8 +28,8 @@ coder tokens [flags]
## Subcommands
| Name | Purpose |
| -------------------------------------------- | --------------- |
| [<code>create</code>](./coder_tokens_create) | Create a tokens |
| [<code>list</code>](./coder_tokens_list) | List tokens |
| [<code>remove</code>](./coder_tokens_remove) | Delete a token |
| Name | Purpose |
| -------------------------------------------- | -------------- |
| [<code>create</code>](./coder_tokens_create) | Create a token |
| [<code>list</code>](./coder_tokens_list) | List tokens |
| [<code>remove</code>](./coder_tokens_remove) | Delete a token |
+8 -1
View File
@@ -2,7 +2,7 @@
# coder tokens create
Create a tokens
Create a token
## Usage
@@ -20,3 +20,10 @@ Specify a duration for the lifetime of the token.
| --- | --- |
| Consumes | <code>$CODER_TOKEN_LIFETIME</code> |
| Default | <code>720h0m0s</code> |
### --name, -n
Specify a human-readable name.
<br/>
| | |
| --- | --- |
+2 -2
View File
@@ -22,11 +22,11 @@ Specifies whether all users' tokens will be listed or not (must have Owner role
### --column, -c
Columns to display in table output. Available columns: id, last used, expires at, created at, owner
Columns to display in table output. Available columns: id, name, last used, expires at, created at, owner
<br/>
| | |
| --- | --- |
| Default | <code>[id,last used,expires at,created at]</code> |
| Default | <code>[id,name,last used,expires at,created at]</code> |
### --output, -o
+1 -1
View File
@@ -7,5 +7,5 @@ Delete a token
## Usage
```console
coder tokens remove [id] [flags]
coder tokens remove [name] [flags]
```
+1
View File
@@ -146,6 +146,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"lifetime_seconds": ActionIgnore,
"ip_address": ActionIgnore,
"scope": ActionIgnore,
"token_name": ActionIgnore,
},
// TODO: track an ID here when the below ticket is completed:
// https://github.com/coder/coder/pull/6012
+2
View File
@@ -10,6 +10,7 @@ export interface APIKey {
readonly updated_at: string
readonly login_type: LoginType
readonly scope: APIKeyScope
readonly token_name: string
readonly lifetime_seconds: number
}
@@ -224,6 +225,7 @@ export interface CreateTokenRequest {
// This is likely an enum in an external package ("time.Duration")
readonly lifetime: number
readonly scope: APIKeyScope
readonly token_name: string
}
// From codersdk/users.go
+2
View File
@@ -41,6 +41,7 @@ export const MockTokens: TypesGen.APIKey[] = [
login_type: "token",
scope: "all",
lifetime_seconds: 2592000,
token_name: "token-one",
},
{
id: "tBoVE3dqLl",
@@ -52,6 +53,7 @@ export const MockTokens: TypesGen.APIKey[] = [
login_type: "token",
scope: "all",
lifetime_seconds: 2592000,
token_name: "token-two",
},
]