mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
> Mux is working on behalf of Mike. ## Summary Add a per-user synthetic API key for chatd AI Gateway attribution. Chatd resolves the key from the chat owner, extends it before expiry, and discards the generated bearer token so the key is never a usable credential. There is no mapping table. The key is resolved from `api_keys` by a deterministic token name (`chatd_<owner_id>_session_token`), mirroring the provisionerd session token model, with three deltas that chatd needs: - **Login type guard**: token names are unvalidated user input, so a user can create a bearer token with the colliding name. The lookup excludes `login_type = 'token'` rows, so chatd never picks up (or extends) a real user token. Synthetic keys are minted with the owner's login type, which is never `token`. - **In-place expiry extension instead of delete-and-reinsert**: chat generations have no stop boundary, and an in-flight generation may have already delegated the current key ID to aibridged. Extending `expires_at` keeps the key ID stable forever. - **Advisory-lock mint**: the unique index on token names is partial (`WHERE login_type = 'token'`), so nothing DB-enforces uniqueness for synthetic keys. A per-user advisory lock serializes concurrent mints. Keys carry a minimal scope (`api_key:read`) as defense in depth; the delegated gateway path never evaluates scopes and the secret is discarded at mint. Migration 000544 removes the foreign keys from the legacy message and queue `api_key_id` columns while chatd continues stamping them for rolling compatibility. Stale IDs are tolerated because routing uses `chats.owner_id`. Individual key deletion, delete-all, and password reset remove the key without changing chat history or queue versions, and the next lookup remints it. Suspension does not delete the key; delegated gateway authorization rejects inactive users at request time. This is the first PR in a three-PR rollout and must be fully deployed before #27171. Refs https://linear.app/codercom/issue/CODAGT-561/maintain-synthetic-api-key-per-user-per-chat
156 lines
3.6 KiB
SQL
156 lines
3.6 KiB
SQL
-- name: GetAPIKeyByID :one
|
|
SELECT
|
|
*
|
|
FROM
|
|
api_keys
|
|
WHERE
|
|
id = $1
|
|
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: GetChatGatewayAPIKey :one
|
|
SELECT
|
|
*
|
|
FROM
|
|
api_keys
|
|
WHERE
|
|
user_id = @user_id AND
|
|
token_name = @token_name AND
|
|
-- Token names are unvalidated user input, so a user could create a token
|
|
-- with the chat gateway name. Excluding login_type 'token' ensures chatd
|
|
-- never picks up (and extends) a real bearer token. Synthetic gateway
|
|
-- keys are minted with the owner's login type, which is never 'token'.
|
|
login_type != 'token'
|
|
ORDER BY
|
|
created_at ASC, id ASC
|
|
LIMIT
|
|
1;
|
|
|
|
-- name: GetAPIKeysLastUsedAfter :many
|
|
SELECT * FROM api_keys WHERE last_used > $1;
|
|
|
|
-- name: GetAPIKeysByLoginType :many
|
|
SELECT * FROM api_keys WHERE login_type = $1
|
|
AND (@include_expired::bool OR expires_at > now());
|
|
|
|
-- name: GetAPIKeysByUserID :many
|
|
SELECT * FROM api_keys WHERE login_type = $1 AND user_id = $2
|
|
AND (@include_expired::bool OR expires_at > now());
|
|
|
|
-- name: InsertAPIKey :one
|
|
INSERT INTO
|
|
api_keys (
|
|
id,
|
|
lifetime_seconds,
|
|
hashed_secret,
|
|
ip_address,
|
|
user_id,
|
|
last_used,
|
|
expires_at,
|
|
created_at,
|
|
updated_at,
|
|
login_type,
|
|
scopes,
|
|
allow_list,
|
|
token_name
|
|
)
|
|
VALUES
|
|
(@id,
|
|
-- If the lifetime is set to 0, default to 24hrs
|
|
CASE @lifetime_seconds::bigint
|
|
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, @scopes, @allow_list, @token_name) RETURNING *;
|
|
|
|
-- name: UpdateAPIKeyByID :exec
|
|
UPDATE
|
|
api_keys
|
|
SET
|
|
last_used = $2,
|
|
expires_at = $3,
|
|
ip_address = $4
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: DeleteAPIKeyByID :exec
|
|
DELETE FROM
|
|
api_keys
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: DeleteApplicationConnectAPIKeysByUserID :exec
|
|
DELETE FROM
|
|
api_keys
|
|
WHERE
|
|
user_id = $1 AND
|
|
'coder:application_connect'::api_key_scope = ANY(scopes);
|
|
|
|
-- name: DeleteAPIKeysByUserID :exec
|
|
DELETE FROM
|
|
api_keys
|
|
WHERE
|
|
user_id = $1;
|
|
|
|
-- name: DeleteExpiredAPIKeys :execrows
|
|
WITH expired_keys AS (
|
|
SELECT id
|
|
FROM api_keys
|
|
-- expired keys only
|
|
WHERE expires_at < @before::timestamptz
|
|
LIMIT @limit_count
|
|
)
|
|
DELETE FROM
|
|
api_keys
|
|
USING
|
|
expired_keys
|
|
WHERE
|
|
api_keys.id = expired_keys.id;
|
|
|
|
-- name: ExpirePrebuildsAPIKeys :exec
|
|
-- Firstly, collect api_keys owned by the prebuilds user that correlate
|
|
-- to workspaces no longer owned by the prebuilds user.
|
|
WITH unexpired_prebuilds_workspace_session_tokens AS (
|
|
SELECT id, SUBSTRING(token_name FROM 38 FOR 36)::uuid AS workspace_id
|
|
FROM api_keys
|
|
WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
|
|
AND expires_at > @now::timestamptz
|
|
AND token_name SIMILAR TO 'c42fdf75-3097-471c-8c33-fb52454d81c0_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}_session_token'
|
|
),
|
|
stale_prebuilds_workspace_session_tokens AS (
|
|
SELECT upwst.id
|
|
FROM unexpired_prebuilds_workspace_session_tokens upwst
|
|
LEFT JOIN workspaces w
|
|
ON w.id = upwst.workspace_id
|
|
WHERE w.owner_id <> 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
|
|
),
|
|
-- Next, collect api_keys that belong to the prebuilds user but have no token name.
|
|
-- These were most likely created via 'coder login' as the prebuilds user.
|
|
unnamed_prebuilds_api_keys AS (
|
|
SELECT id
|
|
FROM api_keys
|
|
WHERE user_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid
|
|
AND token_name = ''
|
|
AND expires_at > @now::timestamptz
|
|
)
|
|
UPDATE api_keys
|
|
SET expires_at = @now::timestamptz
|
|
WHERE id IN (
|
|
SELECT id FROM stale_prebuilds_workspace_session_tokens
|
|
UNION
|
|
SELECT id FROM unnamed_prebuilds_api_keys
|
|
);
|