From 990c006f2815b1f4be47e9a91ce408ad7e6cee72 Mon Sep 17 00:00:00 2001 From: Zach <3724288+zedkipp@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:40:32 -0600 Subject: [PATCH] feat(coderd/database): add value_key_id column to user_secrets for encryption (#23997) Add a nullable `value_key_id` column to the `user_secrets` table with a foreign key to `dbcrypt_keys`. This is the column dbcrypt uses to track which encryption key encrypted a given secret's value. This is required for encryption of user secret values. The column was missing from the original migration (000357). --- coderd/database/dump.sql | 6 +++++- coderd/database/foreign_key_constraint.go | 1 + .../000460_user_secrets_value_key_id.down.sql | 3 +++ .../000460_user_secrets_value_key_id.up.sql | 5 +++++ coderd/database/models.go | 19 ++++++++++--------- coderd/database/queries.sql.go | 15 ++++++++++----- 6 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 coderd/database/migrations/000460_user_secrets_value_key_id.down.sql create mode 100644 coderd/database/migrations/000460_user_secrets_value_key_id.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 68a17e99a5..d37e98a646 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2808,7 +2808,8 @@ CREATE TABLE user_secrets ( env_name text DEFAULT ''::text NOT NULL, file_path text DEFAULT ''::text NOT NULL, created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + value_key_id text ); CREATE TABLE user_status_changes ( @@ -4306,6 +4307,9 @@ ALTER TABLE ONLY user_links ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY user_secrets + ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); + ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index c326777574..ff4c021d77 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -101,6 +101,7 @@ const ( ForeignKeyUserLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "user_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest); ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000460_user_secrets_value_key_id.down.sql b/coderd/database/migrations/000460_user_secrets_value_key_id.down.sql new file mode 100644 index 0000000000..e0e9c9f65f --- /dev/null +++ b/coderd/database/migrations/000460_user_secrets_value_key_id.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE user_secrets + DROP CONSTRAINT user_secrets_value_key_id_fkey, + DROP COLUMN value_key_id; diff --git a/coderd/database/migrations/000460_user_secrets_value_key_id.up.sql b/coderd/database/migrations/000460_user_secrets_value_key_id.up.sql new file mode 100644 index 0000000000..9e4d9efdb0 --- /dev/null +++ b/coderd/database/migrations/000460_user_secrets_value_key_id.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE user_secrets + ADD COLUMN value_key_id TEXT; + +ALTER TABLE ONLY user_secrets + ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest); diff --git a/coderd/database/models.go b/coderd/database/models.go index f34c5b6b71..913ff51bf4 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -5264,15 +5264,16 @@ type UserLink struct { } type UserSecret struct { - ID uuid.UUID `db:"id" json:"id"` - UserID uuid.UUID `db:"user_id" json:"user_id"` - Name string `db:"name" json:"name"` - Description string `db:"description" json:"description"` - Value string `db:"value" json:"value"` - EnvName string `db:"env_name" json:"env_name"` - FilePath string `db:"file_path" json:"file_path"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID uuid.UUID `db:"id" json:"id"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + Name string `db:"name" json:"name"` + Description string `db:"description" json:"description"` + Value string `db:"value" json:"value"` + EnvName string `db:"env_name" json:"env_name"` + FilePath string `db:"file_path" json:"file_path"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ValueKeyID sql.NullString `db:"value_key_id" json:"value_key_id"` } // Tracks the history of user status changes diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index faef8406f3..4d28a5c15d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -22477,7 +22477,7 @@ INSERT INTO user_secrets ( file_path ) VALUES ( $1, $2, $3, $4, $5, $6, $7 -) RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at +) RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id ` type CreateUserSecretParams struct { @@ -22511,6 +22511,7 @@ func (q *sqlQuerier) CreateUserSecret(ctx context.Context, arg CreateUserSecretP &i.FilePath, &i.CreatedAt, &i.UpdatedAt, + &i.ValueKeyID, ) return i, err } @@ -22526,7 +22527,7 @@ func (q *sqlQuerier) DeleteUserSecret(ctx context.Context, id uuid.UUID) error { } const getUserSecret = `-- name: GetUserSecret :one -SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at FROM user_secrets +SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id FROM user_secrets WHERE id = $1 ` @@ -22543,12 +22544,13 @@ func (q *sqlQuerier) GetUserSecret(ctx context.Context, id uuid.UUID) (UserSecre &i.FilePath, &i.CreatedAt, &i.UpdatedAt, + &i.ValueKeyID, ) return i, err } const getUserSecretByUserIDAndName = `-- name: GetUserSecretByUserIDAndName :one -SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at FROM user_secrets +SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id FROM user_secrets WHERE user_id = $1 AND name = $2 ` @@ -22570,12 +22572,13 @@ func (q *sqlQuerier) GetUserSecretByUserIDAndName(ctx context.Context, arg GetUs &i.FilePath, &i.CreatedAt, &i.UpdatedAt, + &i.ValueKeyID, ) return i, err } const listUserSecrets = `-- name: ListUserSecrets :many -SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at FROM user_secrets +SELECT id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id FROM user_secrets WHERE user_id = $1 ORDER BY name ASC ` @@ -22599,6 +22602,7 @@ func (q *sqlQuerier) ListUserSecrets(ctx context.Context, userID uuid.UUID) ([]U &i.FilePath, &i.CreatedAt, &i.UpdatedAt, + &i.ValueKeyID, ); err != nil { return nil, err } @@ -22622,7 +22626,7 @@ SET file_path = $5, updated_at = CURRENT_TIMESTAMP WHERE id = $1 -RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at +RETURNING id, user_id, name, description, value, env_name, file_path, created_at, updated_at, value_key_id ` type UpdateUserSecretParams struct { @@ -22652,6 +22656,7 @@ func (q *sqlQuerier) UpdateUserSecret(ctx context.Context, arg UpdateUserSecretP &i.FilePath, &i.CreatedAt, &i.UpdatedAt, + &i.ValueKeyID, ) return i, err }