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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user