mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +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
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)
|
||||
|
||||
Reference in New Issue
Block a user