mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
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:
co-authored by
Steven Masley
Steven Masley
parent
e3a4861e93
commit
71d1e63af0
+1
-1
@@ -22,7 +22,7 @@ Get Started:
|
||||
[;m$ coder tokens rm WuoWs4ZsMX[0m
|
||||
|
||||
Commands:
|
||||
create Create a tokens
|
||||
create Create a token
|
||||
list List tokens
|
||||
remove Delete a token
|
||||
|
||||
|
||||
+2
-1
@@ -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
@@ -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
@@ -1,7 +1,7 @@
|
||||
Delete a token
|
||||
|
||||
Usage:
|
||||
coder tokens remove [id] [flags]
|
||||
coder tokens remove [name] [flags]
|
||||
|
||||
Aliases:
|
||||
remove, rm
|
||||
|
||||
+18
-6
@@ -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
@@ -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)
|
||||
|
||||
Generated
+51
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+47
-2
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+4
-1
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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/>
|
||||
| | |
|
||||
| --- | --- |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@ Delete a token
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder tokens remove [id] [flags]
|
||||
coder tokens remove [name] [flags]
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user