From d3155e1cab6dc17169ffb68f1aa3900ab900a911 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 26 May 2026 21:08:07 +0100 Subject: [PATCH] test(enterprise/cli): add test to prove fix for #25699 (#25701) Adds an end-to-end enterprise CLI test to ensure legacy AI provider keys seeded at server startup are encrypted at rest when DBCrypt external token encryption is enabled, preventing regressions related to #25699. > Partially implemented by Coder Agents, and massaged afterwards by me. --- enterprise/cli/server_dbcrypt_test.go | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/enterprise/cli/server_dbcrypt_test.go b/enterprise/cli/server_dbcrypt_test.go index 3893cfb6d3..6ce112f425 100644 --- a/enterprise/cli/server_dbcrypt_test.go +++ b/enterprise/cli/server_dbcrypt_test.go @@ -12,9 +12,11 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/xerrors" + "github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/enterprise/cli" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/testutil" @@ -354,6 +356,91 @@ func requireEncryptedWithCipher(ctx context.Context, t *testing.T, db database.S require.Equal(t, c.HexDigest(), userAIProviderKeys[0].ApiKeyKeyID.String) } +// TestServerAIProviderKeysEncryptedWithDBCrypt starts a real enterprise server +// with external token encryption and AI provider config, then verifies that +// seeded AI provider keys are encrypted at rest. +func TestServerAIProviderKeysEncryptedWithDBCrypt(t *testing.T) { + t.Parallel() + + // Given: a 32-byte encryption key, base64-encoded. + rawKey := testutil.MustRandString(t, 32) + b64Key := base64.StdEncoding.EncodeToString([]byte(rawKey)) + + ciphers, err := dbcrypt.NewCiphers([]byte(rawKey)) + require.NoError(t, err) + expectedDigest := ciphers[0].HexDigest() + + dbURL, err := dbtestutil.Open(t) + require.NoError(t, err) + + const testAPIKey = "sk-test-key-that-must-be-encrypted-at-rest" + + // Given: enterprise server with encryption and a legacy AI provider. + var root cli.RootCmd + cmd, err := root.Command(root.EnterpriseSubcommands()) + require.NoError(t, err) + + inv, cfg := clitest.NewWithCommand(t, cmd, + "server", + "--postgres-url="+dbURL, + "--http-address", ":0", + "--access-url", "http://example.com", + "--external-token-encryption-keys", b64Key, + "--aibridge-enabled", + "--aibridge-openai-key", testAPIKey, + ) + + // When: the server starts up and seeds ai providers from env + ctx := testutil.Context(t, testutil.WaitLong) + clitest.Start(t, inv.WithContext(ctx)) + _ = waitAccessURL(t, cfg) + + // Open a RAW database connection to inspect the actual stored values. + sqlDB, err := sql.Open("postgres", dbURL) + require.NoError(t, err) + t.Cleanup(func() { _ = sqlDB.Close() }) + rawDB := database.New(sqlDB) + + // Then: we expect a single provider to be seeded in the db. + providers, err := rawDB.GetAIProviders(ctx, database.GetAIProvidersParams{ + IncludeDeleted: true, + IncludeDisabled: true, + }) + require.NoError(t, err) + require.Len(t, providers, 1, "expected exactly one provider") + provider := providers[0] + require.Equal(t, "openai", provider.Name, "unexpected provider name") + + // Then: provider must exist. + require.NotEmpty(t, provider.ID, + "seeded AI provider 'openai' should exist in database") + + keys, err := rawDB.GetAIProviderKeysByProviderID(ctx, provider.ID) + require.NoError(t, err) + require.Len(t, keys, 1, "should have exactly one provider key") + + rawKeyRow := keys[0] + + // Then: key_id must be populated + require.True(t, rawKeyRow.ApiKeyKeyID.Valid, + "api_key_key_id must be set when dbcrypt is active; NULL means the key was written without encryption (the bug from PR #25699)") + require.Equal(t, expectedDigest, rawKeyRow.ApiKeyKeyID.String, + "api_key_key_id should match the active cipher's hex digest") + + // Then: the stored value must NOT be plaintext. + require.NotEqual(t, testAPIKey, rawKeyRow.APIKey, + "raw stored api_key must not be plaintext when encryption is active") + + // Then: the stored value decrypts to the original key. + ciphertext, err := base64.StdEncoding.DecodeString(rawKeyRow.APIKey) + require.NoError(t, err, "encrypted api_key should be valid base64") + + plaintext, err := ciphers[0].Decrypt(ciphertext) + require.NoError(t, err, "should be able to decrypt the stored key with the configured cipher") + require.Equal(t, testAPIKey, string(plaintext), + "decrypted value should match original API key") +} + // nullCipher is a dbcrypt.Cipher that does not encrypt or decrypt. // used for testing type nullCipher struct{}