mirror of
https://github.com/coder/coder.git
synced 2026-06-06 14:38:23 +00:00
508114d484
Add dbcrypt support for user secret values. When database encryption is enabled, secret values are transparently encrypted on write and decrypted on read through the existing dbcrypt store wrapper. - Wrap `CreateUserSecret`, `GetUserSecretByUserIDAndName`, `ListUserSecretsWithValues`, and `UpdateUserSecretByUserIDAndName` in enterprise/dbcrypt/dbcrypt.go. - Add rotate and decrypt support for user secrets in enterprise/dbcrypt/cliutil.go (`server dbcrypt rotate` and `server dbcrypt decrypt`). - Add internal tests covering encrypt-on-create, decrypt-on-read, re-encrypt-on-update, and plaintext passthrough when no cipher is configured.
1485 lines
50 KiB
Go
1485 lines
50 KiB
Go
package dbcrypt
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/lib/pq"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbmock"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
)
|
|
|
|
func TestUserLinks(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("InsertUserLink", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.UserLink(t, crypt, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
require.Equal(t, "access", link.OAuthAccessToken)
|
|
require.Equal(t, "refresh", link.OAuthRefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthAccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthRefreshTokenKeyID.String)
|
|
|
|
rawLink, err := db.GetUserLinkByLinkedID(ctx, link.LinkedID)
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("UpdateUserLink", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.UserLink(t, crypt, database.UserLink{
|
|
UserID: user.ID,
|
|
})
|
|
|
|
expectedClaims := database.UserLinkClaims{
|
|
IDTokenClaims: map[string]interface{}{
|
|
"sub": "123",
|
|
"groups": []interface{}{
|
|
"foo", "bar",
|
|
},
|
|
},
|
|
UserInfoClaims: map[string]interface{}{
|
|
"number": float64(2),
|
|
"struct": map[string]interface{}{
|
|
"number": float64(2),
|
|
},
|
|
},
|
|
MergedClaims: map[string]interface{}{
|
|
"sub": "123",
|
|
"groups": []interface{}{
|
|
"foo", "bar",
|
|
},
|
|
"number": float64(2),
|
|
"struct": map[string]interface{}{
|
|
"number": float64(2),
|
|
},
|
|
},
|
|
}
|
|
|
|
updated, err := crypt.UpdateUserLink(ctx, database.UpdateUserLinkParams{
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
UserID: link.UserID,
|
|
LoginType: link.LoginType,
|
|
Claims: expectedClaims,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", updated.OAuthAccessToken)
|
|
require.Equal(t, "refresh", updated.OAuthRefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthAccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthRefreshTokenKeyID.String)
|
|
|
|
rawLink, err := db.GetUserLinkByLinkedID(ctx, link.LinkedID)
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "refresh")
|
|
require.EqualValues(t, expectedClaims, rawLink.Claims)
|
|
})
|
|
|
|
t.Run("UpdateExternalAuthLinkRefreshToken", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{
|
|
UserID: user.ID,
|
|
})
|
|
|
|
err := crypt.UpdateExternalAuthLinkRefreshToken(ctx, database.UpdateExternalAuthLinkRefreshTokenParams{
|
|
OAuthRefreshToken: "",
|
|
OAuthRefreshTokenKeyID: link.OAuthRefreshTokenKeyID.String,
|
|
OldOauthRefreshToken: link.OAuthRefreshToken,
|
|
UpdatedAt: dbtime.Now(),
|
|
ProviderID: link.ProviderID,
|
|
UserID: link.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
rawLink, err := db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
|
ProviderID: link.ProviderID,
|
|
UserID: link.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "")
|
|
})
|
|
|
|
t.Run("GetUserLinkByLinkedID", func(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.UserLink(t, crypt, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
|
|
link, err := crypt.GetUserLinkByLinkedID(ctx, link.LinkedID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", link.OAuthAccessToken)
|
|
require.Equal(t, "refresh", link.OAuthRefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthAccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthRefreshTokenKeyID.String)
|
|
|
|
rawLink, err := db.GetUserLinkByLinkedID(ctx, link.LinkedID)
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("DecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
link := dbgen.UserLink(t, db, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: fakeBase64RandomData(t, 32),
|
|
OAuthRefreshToken: fakeBase64RandomData(t, 32),
|
|
OAuthAccessTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
OAuthRefreshTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
|
|
_, err := crypt.GetUserLinkByLinkedID(ctx, link.LinkedID)
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
})
|
|
})
|
|
|
|
t.Run("GetUserLinksByUserID", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.UserLink(t, crypt, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
links, err := crypt.GetUserLinksByUserID(ctx, link.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, links, 1)
|
|
require.Equal(t, "access", links[0].OAuthAccessToken)
|
|
require.Equal(t, "refresh", links[0].OAuthRefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), links[0].OAuthAccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), links[0].OAuthRefreshTokenKeyID.String)
|
|
|
|
rawLinks, err := db.GetUserLinksByUserID(ctx, link.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, rawLinks, 1)
|
|
requireEncryptedEquals(t, ciphers[0], rawLinks[0].OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], rawLinks[0].OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("Empty", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, crypt, _ := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
links, err := crypt.GetUserLinksByUserID(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, links)
|
|
})
|
|
|
|
t.Run("DecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
_ = dbgen.UserLink(t, db, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: fakeBase64RandomData(t, 32),
|
|
OAuthRefreshToken: fakeBase64RandomData(t, 32),
|
|
OAuthAccessTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
OAuthRefreshTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
_, err := crypt.GetUserLinksByUserID(ctx, user.ID)
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
})
|
|
})
|
|
|
|
t.Run("GetUserLinkByUserIDLoginType", func(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.UserLink(t, crypt, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
|
|
link, err := crypt.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
|
|
UserID: link.UserID,
|
|
LoginType: link.LoginType,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", link.OAuthAccessToken)
|
|
require.Equal(t, "refresh", link.OAuthRefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthAccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), link.OAuthRefreshTokenKeyID.String)
|
|
|
|
rawLink, err := db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
|
|
UserID: link.UserID,
|
|
LoginType: link.LoginType,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("DecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
link := dbgen.UserLink(t, db, database.UserLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: fakeBase64RandomData(t, 32),
|
|
OAuthRefreshToken: fakeBase64RandomData(t, 32),
|
|
OAuthAccessTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
OAuthRefreshTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
|
|
_, err := crypt.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{
|
|
UserID: link.UserID,
|
|
LoginType: link.LoginType,
|
|
})
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestExternalAuthLinks(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("InsertExternalAuthLink", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
require.Equal(t, "access", link.OAuthAccessToken)
|
|
require.Equal(t, "refresh", link.OAuthRefreshToken)
|
|
|
|
link, err := db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
|
ProviderID: link.ProviderID,
|
|
UserID: link.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], link.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], link.OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("UpdateExternalAuthLink", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{})
|
|
updated, err := crypt.UpdateExternalAuthLink(ctx, database.UpdateExternalAuthLinkParams{
|
|
ProviderID: link.ProviderID,
|
|
UserID: link.UserID,
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "access", updated.OAuthAccessToken)
|
|
require.Equal(t, "refresh", updated.OAuthRefreshToken)
|
|
|
|
link, err = db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
|
ProviderID: link.ProviderID,
|
|
UserID: link.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], link.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], link.OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("GetExternalAuthLink", func(t *testing.T) {
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
link, err := db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
|
UserID: link.UserID,
|
|
ProviderID: link.ProviderID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], link.OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], link.OAuthRefreshToken, "refresh")
|
|
})
|
|
t.Run("DecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
link := dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{
|
|
OAuthAccessToken: fakeBase64RandomData(t, 32),
|
|
OAuthRefreshToken: fakeBase64RandomData(t, 32),
|
|
OAuthAccessTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
OAuthRefreshTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
|
|
_, err := crypt.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
|
|
UserID: link.UserID,
|
|
ProviderID: link.ProviderID,
|
|
})
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
})
|
|
})
|
|
|
|
t.Run("GetExternalAuthLinksByUserID", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: "access",
|
|
OAuthRefreshToken: "refresh",
|
|
})
|
|
links, err := crypt.GetExternalAuthLinksByUserID(ctx, link.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, links, 1)
|
|
require.Equal(t, "access", links[0].OAuthAccessToken)
|
|
require.Equal(t, "refresh", links[0].OAuthRefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), links[0].OAuthAccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), links[0].OAuthRefreshTokenKeyID.String)
|
|
|
|
rawLinks, err := db.GetExternalAuthLinksByUserID(ctx, link.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, rawLinks, 1)
|
|
requireEncryptedEquals(t, ciphers[0], rawLinks[0].OAuthAccessToken, "access")
|
|
requireEncryptedEquals(t, ciphers[0], rawLinks[0].OAuthRefreshToken, "refresh")
|
|
})
|
|
|
|
t.Run("DecryptErr", func(t *testing.T) {
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
link := dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{
|
|
UserID: user.ID,
|
|
OAuthAccessToken: fakeBase64RandomData(t, 32),
|
|
OAuthRefreshToken: fakeBase64RandomData(t, 32),
|
|
OAuthAccessTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
OAuthRefreshTokenKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
_, err := crypt.GetExternalAuthLinksByUserID(ctx, link.UserID)
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestCryptoKeys(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
t.Run("InsertCryptoKey", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, crypt, ciphers := setup(t)
|
|
key := dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Secret: sql.NullString{String: "test", Valid: true},
|
|
})
|
|
require.Equal(t, "test", key.Secret.String)
|
|
|
|
key, err := db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
|
Feature: key.Feature,
|
|
Sequence: key.Sequence,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
|
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
|
|
})
|
|
|
|
t.Run("GetCryptoKeys", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Secret: sql.NullString{String: "test", Valid: true},
|
|
})
|
|
keys, err := crypt.GetCryptoKeys(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
require.Equal(t, "test", keys[0].Secret.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), keys[0].SecretKeyID.String)
|
|
|
|
keys, err = db.GetCryptoKeys(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
requireEncryptedEquals(t, ciphers[0], keys[0].Secret.String, "test")
|
|
require.Equal(t, ciphers[0].HexDigest(), keys[0].SecretKeyID.String)
|
|
})
|
|
|
|
t.Run("GetLatestCryptoKeyByFeature", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Secret: sql.NullString{String: "test", Valid: true},
|
|
})
|
|
key, err := crypt.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "test", key.Secret.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
|
|
|
key, err = db.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
|
|
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
|
})
|
|
|
|
t.Run("GetCryptoKeyByFeatureAndSequence", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
key := dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Secret: sql.NullString{String: "test", Valid: true},
|
|
})
|
|
key, err := crypt.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
|
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
|
Sequence: key.Sequence,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "test", key.Secret.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
|
|
|
key, err = db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
|
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
|
Sequence: key.Sequence,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
|
|
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
|
})
|
|
|
|
t.Run("UpdateCryptoKeyDeletesAt", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, crypt, ciphers := setup(t)
|
|
key := dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Secret: sql.NullString{String: "test", Valid: true},
|
|
})
|
|
key, err := crypt.UpdateCryptoKeyDeletesAt(ctx, database.UpdateCryptoKeyDeletesAtParams{
|
|
Feature: key.Feature,
|
|
Sequence: key.Sequence,
|
|
DeletesAt: sql.NullTime{
|
|
Time: time.Now().Add(time.Hour),
|
|
Valid: true,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "test", key.Secret.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
|
|
})
|
|
|
|
t.Run("GetCryptoKeysByFeature", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
expected := dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Sequence: 2,
|
|
Feature: database.CryptoKeyFeatureTailnetResume,
|
|
Secret: sql.NullString{String: "test", Valid: true},
|
|
})
|
|
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
|
|
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
|
Sequence: 43,
|
|
})
|
|
keys, err := crypt.GetCryptoKeysByFeature(ctx, database.CryptoKeyFeatureTailnetResume)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
require.Equal(t, "test", keys[0].Secret.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), keys[0].SecretKeyID.String)
|
|
require.Equal(t, expected.Sequence, keys[0].Sequence)
|
|
require.Equal(t, expected.Feature, keys[0].Feature)
|
|
|
|
keys, err = db.GetCryptoKeysByFeature(ctx, database.CryptoKeyFeatureTailnetResume)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
requireEncryptedEquals(t, ciphers[0], keys[0].Secret.String, "test")
|
|
require.Equal(t, ciphers[0].HexDigest(), keys[0].SecretKeyID.String)
|
|
require.Equal(t, expected.Sequence, keys[0].Sequence)
|
|
require.Equal(t, expected.Feature, keys[0].Feature)
|
|
})
|
|
|
|
t.Run("DecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
key := dbgen.CryptoKey(t, db, database.CryptoKey{
|
|
Secret: sql.NullString{
|
|
String: fakeBase64RandomData(t, 32),
|
|
Valid: true,
|
|
},
|
|
SecretKeyID: sql.NullString{
|
|
String: ciphers[0].HexDigest(),
|
|
Valid: true,
|
|
},
|
|
})
|
|
_, err := crypt.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
|
|
Feature: key.Feature,
|
|
Sequence: key.Sequence,
|
|
})
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
})
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given: a cipher is loaded
|
|
cipher := initCipher(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
rawDB, _ := dbtestutil.NewDB(t)
|
|
|
|
// Before: no keys should be present
|
|
keys, err := rawDB.GetDBCryptKeys(ctx)
|
|
require.NoError(t, err, "no error should be returned")
|
|
require.Empty(t, keys, "no keys should be present")
|
|
|
|
// When: we init the crypt db
|
|
_, err = New(ctx, rawDB, cipher)
|
|
require.NoError(t, err)
|
|
|
|
// Then: a new key is inserted
|
|
keys, err = rawDB.GetDBCryptKeys(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1, "one key should be present")
|
|
require.Equal(t, cipher.HexDigest(), keys[0].ActiveKeyDigest.String, "key digest mismatch")
|
|
require.Empty(t, keys[0].RevokedKeyDigest.String, "key should not be revoked")
|
|
requireEncryptedEquals(t, cipher, keys[0].Test, "coder")
|
|
})
|
|
|
|
t.Run("MissingKey", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: there exist two valid encryption keys
|
|
cipher1 := initCipher(t)
|
|
cipher2 := initCipher(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
rawDB, _ := dbtestutil.NewDB(t)
|
|
|
|
// Given: key 1 is already present in the database
|
|
err := rawDB.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
|
|
Number: 1,
|
|
ActiveKeyDigest: cipher1.HexDigest(),
|
|
Test: fakeBase64RandomData(t, 32),
|
|
})
|
|
require.NoError(t, err, "no error should be returned")
|
|
keys, err := rawDB.GetDBCryptKeys(ctx)
|
|
require.NoError(t, err, "no error should be returned")
|
|
require.Len(t, keys, 1, "one key should be present")
|
|
|
|
// When: we init the crypt db with no keys
|
|
_, err = New(ctx, rawDB)
|
|
// Then: we error because we don't know how to decrypt the existing key
|
|
require.Error(t, err, "expected an error")
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
|
|
// When: we init the crypt db with key 2
|
|
_, err = New(ctx, rawDB, cipher2)
|
|
|
|
// Then: we error because the key is not revoked and we don't know how to decrypt it
|
|
require.Error(t, err, "expected an error")
|
|
require.ErrorAs(t, err, &derr, "expected a decrypt error")
|
|
|
|
// When: the existing key is marked as having been revoked
|
|
err = rawDB.RevokeDBCryptKey(ctx, cipher1.HexDigest())
|
|
require.NoError(t, err, "no error should be returned")
|
|
|
|
// And: we init the crypt db with key 2
|
|
_, err = New(ctx, rawDB, cipher2)
|
|
|
|
// Then: we succeed
|
|
require.NoError(t, err)
|
|
|
|
// And: key 2 should now be the active key
|
|
keys, err = rawDB.GetDBCryptKeys(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 2, "two keys should be present")
|
|
require.EqualValues(t, keys[0].Number, 1, "key number mismatch")
|
|
require.Empty(t, keys[0].ActiveKeyDigest.String, "key should not be active")
|
|
require.Equal(t, cipher1.HexDigest(), keys[0].RevokedKeyDigest.String, "key should be revoked")
|
|
|
|
require.EqualValues(t, keys[1].Number, 2, "key number mismatch")
|
|
require.Equal(t, cipher2.HexDigest(), keys[1].ActiveKeyDigest.String, "key digest mismatch")
|
|
require.Empty(t, keys[1].RevokedKeyDigest.String, "key should not be revoked")
|
|
requireEncryptedEquals(t, cipher2, keys[1].Test, "coder")
|
|
})
|
|
|
|
t.Run("NoKeys", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given: no cipher is loaded
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
rawDB, _ := dbtestutil.NewDB(t)
|
|
|
|
keys, err := rawDB.GetDBCryptKeys(ctx)
|
|
require.NoError(t, err, "no error should be returned")
|
|
require.Empty(t, keys, "no keys should be present")
|
|
|
|
// When: we init the crypt db with no ciphers
|
|
_, err = New(ctx, rawDB)
|
|
|
|
// Then: it should succeed.
|
|
require.NoError(t, err, "dbcrypt.New should work with no keys against an unencrypted database")
|
|
|
|
// Assert invariant: no keys are inserted
|
|
keys, err = rawDB.GetDBCryptKeys(ctx)
|
|
require.NoError(t, err, "no error should be returned")
|
|
require.Empty(t, keys, "no keys should be present")
|
|
|
|
// Insert a key
|
|
require.NoError(t, rawDB.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
|
|
Number: 1,
|
|
ActiveKeyDigest: "whatever",
|
|
Test: fakeBase64RandomData(t, 32),
|
|
}))
|
|
|
|
// This should fail as we do not know how to decrypt the key:
|
|
_, err = New(ctx, rawDB)
|
|
require.Error(t, err)
|
|
// Until we revoke the key:
|
|
require.NoError(t, rawDB.RevokeDBCryptKey(ctx, "whatever"))
|
|
_, err = New(ctx, rawDB)
|
|
require.NoError(t, err, "the above should still hold if the key is revoked")
|
|
})
|
|
|
|
t.Run("PrimaryRevoked", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given: a cipher is loaded
|
|
cipher := initCipher(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
rawDB, _ := dbtestutil.NewDB(t)
|
|
|
|
// And: the cipher is revoked before we init the crypt db
|
|
err := rawDB.InsertDBCryptKey(ctx, database.InsertDBCryptKeyParams{
|
|
Number: 1,
|
|
ActiveKeyDigest: cipher.HexDigest(),
|
|
Test: fakeBase64RandomData(t, 32),
|
|
})
|
|
require.NoError(t, err, "no error should be returned")
|
|
err = rawDB.RevokeDBCryptKey(ctx, cipher.HexDigest())
|
|
require.NoError(t, err, "no error should be returned")
|
|
|
|
// Then: when we init the crypt db, we error because the key is revoked
|
|
_, err = New(ctx, rawDB, cipher)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "has been revoked")
|
|
})
|
|
|
|
t.Run("Retry", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Given: a cipher is loaded
|
|
cipher := initCipher(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
testVal, err := cipher.Encrypt([]byte("coder"))
|
|
key := database.DBCryptKey{
|
|
Number: 1,
|
|
ActiveKeyDigest: sql.NullString{String: cipher.HexDigest(), Valid: true},
|
|
Test: b64encode(testVal),
|
|
}
|
|
require.NoError(t, err)
|
|
t.Cleanup(cancel)
|
|
|
|
// And: a database that returns an error once when we try to serialize a key
|
|
ctrl := gomock.NewController(t)
|
|
mockDB := dbmock.NewMockStore(ctrl)
|
|
|
|
gomock.InOrder(
|
|
// First try: we get a serialization error.
|
|
expectInTx(mockDB),
|
|
mockDB.EXPECT().GetDBCryptKeys(gomock.Any()).Times(1).Return([]database.DBCryptKey{}, nil),
|
|
mockDB.EXPECT().InsertDBCryptKey(gomock.Any(), gomock.Any()).Times(1).Return(&pq.Error{Code: "40001"}),
|
|
// Second try: we get the key we wanted to insert initially.
|
|
expectInTx(mockDB),
|
|
mockDB.EXPECT().GetDBCryptKeys(gomock.Any()).Times(1).Return([]database.DBCryptKey{key}, nil),
|
|
)
|
|
|
|
_, err = New(ctx, mockDB, cipher)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestEncryptDecryptField(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, cryptDB, ciphers := setup(t)
|
|
field := "coder"
|
|
digest := sql.NullString{}
|
|
require.NoError(t, cryptDB.encryptField(&field, &digest))
|
|
require.Equal(t, ciphers[0].HexDigest(), digest.String)
|
|
requireEncryptedEquals(t, ciphers[0], field, "coder")
|
|
require.NoError(t, cryptDB.decryptField(&field, digest))
|
|
require.Equal(t, "coder", field)
|
|
})
|
|
|
|
t.Run("NoKeys", func(t *testing.T) {
|
|
t.Parallel()
|
|
// With no keys, encryption and decryption are both no-ops.
|
|
_, cryptDB := setupNoCiphers(t)
|
|
field := "coder"
|
|
digest := sql.NullString{}
|
|
require.NoError(t, cryptDB.encryptField(&field, &digest))
|
|
require.Empty(t, digest.String)
|
|
require.Equal(t, "coder", field)
|
|
require.NoError(t, cryptDB.decryptField(&field, digest))
|
|
require.Equal(t, "coder", field)
|
|
})
|
|
|
|
t.Run("MissingKey", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, cryptDB, ciphers := setup(t)
|
|
field := "coder"
|
|
digest := sql.NullString{}
|
|
err := cryptDB.encryptField(&field, &digest)
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], field, "coder")
|
|
require.Equal(t, ciphers[0].HexDigest(), digest.String)
|
|
require.True(t, digest.Valid)
|
|
|
|
digest = sql.NullString{String: "missing", Valid: true}
|
|
var derr *DecryptFailedError
|
|
err = cryptDB.decryptField(&field, digest)
|
|
require.Error(t, err)
|
|
require.ErrorAs(t, err, &derr)
|
|
})
|
|
|
|
t.Run("CantEncryptOrDecryptNil", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, cryptDB, _ := setup(t)
|
|
require.ErrorContains(t, cryptDB.encryptField(nil, nil), "developer error")
|
|
require.ErrorContains(t, cryptDB.decryptField(nil, sql.NullString{}), "developer error")
|
|
})
|
|
|
|
t.Run("EncryptEmptyString", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, cryptDB, ciphers := setup(t)
|
|
field := ""
|
|
digest := sql.NullString{}
|
|
require.NoError(t, cryptDB.encryptField(&field, &digest))
|
|
requireEncryptedEquals(t, ciphers[0], field, "")
|
|
require.Equal(t, ciphers[0].HexDigest(), digest.String)
|
|
require.NoError(t, cryptDB.decryptField(&field, digest))
|
|
require.Empty(t, field)
|
|
})
|
|
|
|
t.Run("DecryptEmptyString", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, cryptDB, ciphers := setup(t)
|
|
field := ""
|
|
digest := sql.NullString{String: ciphers[0].HexDigest(), Valid: true}
|
|
err := cryptDB.decryptField(&field, digest)
|
|
// Currently this has to fail because the ciphertext must at least
|
|
// have a nonce. This may need to be changed depending on future
|
|
// ciphers.
|
|
require.ErrorContains(t, err, "ciphertext too short")
|
|
})
|
|
|
|
t.Run("InvalidBase64", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, cryptDB, ciphers := setup(t)
|
|
field := "not valid base64"
|
|
digest := sql.NullString{String: ciphers[0].HexDigest(), Valid: true}
|
|
err := cryptDB.decryptField(&field, digest)
|
|
require.ErrorContains(t, err, "illegal base64 data")
|
|
})
|
|
}
|
|
|
|
func expectInTx(mdb *dbmock.MockStore) *gomock.Call {
|
|
return mdb.EXPECT().InTx(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
|
|
func(f func(store database.Store) error, _ *database.TxOptions) error {
|
|
return f(mdb)
|
|
},
|
|
)
|
|
}
|
|
|
|
func requireEncryptedEquals(t *testing.T, c Cipher, value, expected string) {
|
|
t.Helper()
|
|
data, err := base64.StdEncoding.DecodeString(value)
|
|
require.NoError(t, err, "invalid base64")
|
|
got, err := c.Decrypt(data)
|
|
require.NoError(t, err, "failed to decrypt data")
|
|
require.Equal(t, expected, string(got), "decrypted data does not match")
|
|
}
|
|
|
|
func initCipher(t *testing.T) *aes256 {
|
|
t.Helper()
|
|
key := make([]byte, 32) // AES-256 key size is 32 bytes
|
|
_, err := io.ReadFull(rand.Reader, key)
|
|
require.NoError(t, err)
|
|
c, err := cipherAES256(key)
|
|
require.NoError(t, err)
|
|
return c
|
|
}
|
|
|
|
func setup(t *testing.T) (db database.Store, cryptDB *dbCrypt, cs []Cipher) {
|
|
t.Helper()
|
|
rawDB, _ := dbtestutil.NewDB(t)
|
|
cs = append(cs, initCipher(t))
|
|
cdb, err := New(context.Background(), rawDB, cs...)
|
|
require.NoError(t, err)
|
|
cryptDB, ok := cdb.(*dbCrypt)
|
|
require.True(t, ok)
|
|
return rawDB, cryptDB, cs
|
|
}
|
|
|
|
func setupNoCiphers(t *testing.T) (db database.Store, cryptodb *dbCrypt) {
|
|
t.Helper()
|
|
rawDB, _ := dbtestutil.NewDB(t)
|
|
cdb, err := New(context.Background(), rawDB)
|
|
require.NoError(t, err)
|
|
cryptDB, ok := cdb.(*dbCrypt)
|
|
require.True(t, ok)
|
|
return rawDB, cryptDB
|
|
}
|
|
|
|
func fakeBase64RandomData(t *testing.T, n int) string {
|
|
t.Helper()
|
|
b := make([]byte, n)
|
|
_, err := io.ReadFull(rand.Reader, b)
|
|
require.NoError(t, err)
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// requireMCPServerConfigDecrypted verifies all encrypted fields on an
|
|
// MCPServerConfig match the expected plaintext values and carry the
|
|
// correct key-ID.
|
|
func requireMCPServerConfigDecrypted(
|
|
t *testing.T,
|
|
cfg database.MCPServerConfig,
|
|
ciphers []Cipher,
|
|
wantSecret, wantAPIKey, wantHeaders string,
|
|
) {
|
|
t.Helper()
|
|
require.Equal(t, wantSecret, cfg.OAuth2ClientSecret)
|
|
require.Equal(t, wantAPIKey, cfg.APIKeyValue)
|
|
require.Equal(t, wantHeaders, cfg.CustomHeaders)
|
|
require.Equal(t, ciphers[0].HexDigest(), cfg.OAuth2ClientSecretKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), cfg.APIKeyValueKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), cfg.CustomHeadersKeyID.String)
|
|
}
|
|
|
|
// requireMCPServerConfigRawEncrypted reads the config from the raw
|
|
// (unwrapped) store and asserts every secret field is encrypted.
|
|
func requireMCPServerConfigRawEncrypted(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
rawDB database.Store,
|
|
cfgID uuid.UUID,
|
|
ciphers []Cipher,
|
|
wantSecret, wantAPIKey, wantHeaders string,
|
|
) {
|
|
t.Helper()
|
|
raw, err := rawDB.GetMCPServerConfigByID(ctx, cfgID)
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], raw.OAuth2ClientSecret, wantSecret)
|
|
requireEncryptedEquals(t, ciphers[0], raw.APIKeyValue, wantAPIKey)
|
|
requireEncryptedEquals(t, ciphers[0], raw.CustomHeaders, wantHeaders)
|
|
}
|
|
|
|
func TestMCPServerConfigs(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
const (
|
|
//nolint:gosec // test credentials
|
|
oauthSecret = "my-oauth-secret"
|
|
apiKeyValue = "my-api-key"
|
|
customHeaders = `{"X-Custom":"header-value"}`
|
|
)
|
|
// insertConfig is a small helper that creates a user and an MCP
|
|
// server config through the encrypted store, returning both.
|
|
insertConfig := func(t *testing.T, crypt *dbCrypt, ciphers []Cipher) database.MCPServerConfig {
|
|
t.Helper()
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
cfg, err := crypt.InsertMCPServerConfig(ctx, database.InsertMCPServerConfigParams{
|
|
DisplayName: "Test MCP Server",
|
|
Slug: "test-mcp-" + uuid.New().String()[:8],
|
|
Description: "test description",
|
|
Url: "https://mcp.example.com",
|
|
Transport: "streamable_http",
|
|
AuthType: "oauth2",
|
|
OAuth2ClientID: "client-id",
|
|
OAuth2ClientSecret: oauthSecret,
|
|
APIKeyValue: apiKeyValue,
|
|
CustomHeaders: customHeaders,
|
|
ToolAllowList: []string{},
|
|
ToolDenyList: []string{},
|
|
Availability: "force_on",
|
|
Enabled: true,
|
|
CreatedBy: user.ID,
|
|
UpdatedBy: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireMCPServerConfigDecrypted(t, cfg, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
return cfg
|
|
}
|
|
|
|
t.Run("InsertMCPServerConfig", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("GetMCPServerConfigByID", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
got, err := crypt.GetMCPServerConfigByID(ctx, cfg.ID)
|
|
require.NoError(t, err)
|
|
requireMCPServerConfigDecrypted(t, got, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("GetMCPServerConfigBySlug", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
got, err := crypt.GetMCPServerConfigBySlug(ctx, cfg.Slug)
|
|
require.NoError(t, err)
|
|
requireMCPServerConfigDecrypted(t, got, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("GetMCPServerConfigs", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
cfgs, err := crypt.GetMCPServerConfigs(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cfgs, 1)
|
|
requireMCPServerConfigDecrypted(t, cfgs[0], ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("GetMCPServerConfigsByIDs", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
cfgs, err := crypt.GetMCPServerConfigsByIDs(ctx, []uuid.UUID{cfg.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, cfgs, 1)
|
|
requireMCPServerConfigDecrypted(t, cfgs[0], ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("GetEnabledMCPServerConfigs", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
cfgs, err := crypt.GetEnabledMCPServerConfigs(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cfgs, 1)
|
|
requireMCPServerConfigDecrypted(t, cfgs[0], ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("GetForcedMCPServerConfigs", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
cfgs, err := crypt.GetForcedMCPServerConfigs(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, cfgs, 1)
|
|
requireMCPServerConfigDecrypted(t, cfgs[0], ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, oauthSecret, apiKeyValue, customHeaders)
|
|
})
|
|
|
|
t.Run("UpdateMCPServerConfig", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg := insertConfig(t, crypt, ciphers)
|
|
|
|
const (
|
|
//nolint:gosec // test credential
|
|
newSecret = "updated-oauth-secret"
|
|
newAPIKey = "updated-api-key"
|
|
newHeaders = `{"X-New":"new-value"}`
|
|
)
|
|
updated, err := crypt.UpdateMCPServerConfig(ctx, database.UpdateMCPServerConfigParams{
|
|
ID: cfg.ID,
|
|
DisplayName: cfg.DisplayName,
|
|
Slug: cfg.Slug,
|
|
Description: cfg.Description,
|
|
Url: cfg.Url,
|
|
Transport: cfg.Transport,
|
|
AuthType: cfg.AuthType,
|
|
OAuth2ClientID: cfg.OAuth2ClientID,
|
|
OAuth2ClientSecret: newSecret,
|
|
APIKeyValue: newAPIKey,
|
|
CustomHeaders: newHeaders,
|
|
ToolAllowList: cfg.ToolAllowList,
|
|
ToolDenyList: cfg.ToolDenyList,
|
|
Availability: cfg.Availability,
|
|
Enabled: cfg.Enabled,
|
|
UpdatedBy: cfg.CreatedBy.UUID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireMCPServerConfigDecrypted(t, updated, ciphers, newSecret, newAPIKey, newHeaders)
|
|
requireMCPServerConfigRawEncrypted(ctx, t, db, cfg.ID, ciphers, newSecret, newAPIKey, newHeaders)
|
|
})
|
|
}
|
|
|
|
func TestMCPServerUserTokens(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
const (
|
|
accessToken = "access-token-value"
|
|
refreshToken = "refresh-token-value"
|
|
)
|
|
|
|
// insertConfigAndToken creates a user, an MCP server config, and a
|
|
// user token through the encrypted store.
|
|
insertConfigAndToken := func(
|
|
t *testing.T,
|
|
crypt *dbCrypt,
|
|
ciphers []Cipher,
|
|
) (database.MCPServerConfig, database.MCPServerUserToken) {
|
|
t.Helper()
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
cfg, err := crypt.InsertMCPServerConfig(ctx, database.InsertMCPServerConfigParams{
|
|
DisplayName: "Token Test MCP",
|
|
Slug: "tok-mcp-" + uuid.New().String()[:8],
|
|
Url: "https://mcp.example.com",
|
|
Transport: "streamable_http",
|
|
AuthType: "oauth2",
|
|
ToolAllowList: []string{},
|
|
ToolDenyList: []string{},
|
|
Availability: "default_off",
|
|
Enabled: true,
|
|
CreatedBy: user.ID,
|
|
UpdatedBy: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tok, err := crypt.UpsertMCPServerUserToken(ctx, database.UpsertMCPServerUserTokenParams{
|
|
MCPServerConfigID: cfg.ID,
|
|
UserID: user.ID,
|
|
AccessToken: accessToken,
|
|
RefreshToken: refreshToken,
|
|
TokenType: "Bearer",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, accessToken, tok.AccessToken)
|
|
require.Equal(t, refreshToken, tok.RefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), tok.AccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), tok.RefreshTokenKeyID.String)
|
|
return cfg, tok
|
|
}
|
|
|
|
t.Run("UpsertMCPServerUserToken", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg, tok := insertConfigAndToken(t, crypt, ciphers)
|
|
|
|
// Verify the raw DB values are encrypted.
|
|
rawTok, err := db.GetMCPServerUserToken(ctx, database.GetMCPServerUserTokenParams{
|
|
MCPServerConfigID: cfg.ID,
|
|
UserID: tok.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawTok.AccessToken, accessToken)
|
|
requireEncryptedEquals(t, ciphers[0], rawTok.RefreshToken, refreshToken)
|
|
})
|
|
|
|
t.Run("GetMCPServerUserToken", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg, tok := insertConfigAndToken(t, crypt, ciphers)
|
|
|
|
got, err := crypt.GetMCPServerUserToken(ctx, database.GetMCPServerUserTokenParams{
|
|
MCPServerConfigID: cfg.ID,
|
|
UserID: tok.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, accessToken, got.AccessToken)
|
|
require.Equal(t, refreshToken, got.RefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), got.AccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), got.RefreshTokenKeyID.String)
|
|
|
|
// Raw values must be encrypted.
|
|
rawTok, err := db.GetMCPServerUserToken(ctx, database.GetMCPServerUserTokenParams{
|
|
MCPServerConfigID: cfg.ID,
|
|
UserID: tok.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawTok.AccessToken, accessToken)
|
|
requireEncryptedEquals(t, ciphers[0], rawTok.RefreshToken, refreshToken)
|
|
})
|
|
|
|
t.Run("GetMCPServerUserTokensByUserID", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
cfg, tok := insertConfigAndToken(t, crypt, ciphers)
|
|
|
|
toks, err := crypt.GetMCPServerUserTokensByUserID(ctx, tok.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, toks, 1)
|
|
require.Equal(t, accessToken, toks[0].AccessToken)
|
|
require.Equal(t, refreshToken, toks[0].RefreshToken)
|
|
require.Equal(t, ciphers[0].HexDigest(), toks[0].AccessTokenKeyID.String)
|
|
require.Equal(t, ciphers[0].HexDigest(), toks[0].RefreshTokenKeyID.String)
|
|
|
|
// Raw values must be encrypted.
|
|
rawTok, err := db.GetMCPServerUserToken(ctx, database.GetMCPServerUserTokenParams{
|
|
MCPServerConfigID: cfg.ID,
|
|
UserID: tok.UserID,
|
|
})
|
|
require.NoError(t, err)
|
|
requireEncryptedEquals(t, ciphers[0], rawTok.AccessToken, accessToken)
|
|
requireEncryptedEquals(t, ciphers[0], rawTok.RefreshToken, refreshToken)
|
|
})
|
|
}
|
|
|
|
func TestUserChatProviderKeys(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
const (
|
|
//nolint:gosec // test credentials
|
|
initialAPIKey = "sk-initial-api-key-value"
|
|
//nolint:gosec // test credentials
|
|
updatedAPIKey = "sk-updated-api-key-value"
|
|
)
|
|
|
|
insertProviderAndKey := func(
|
|
t *testing.T,
|
|
crypt *dbCrypt,
|
|
ciphers []Cipher,
|
|
) (database.ChatProvider, database.UserChatProviderKey) {
|
|
t.Helper()
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
provider, err := crypt.InsertChatProvider(ctx, database.InsertChatProviderParams{
|
|
Provider: "openai",
|
|
DisplayName: "OpenAI",
|
|
APIKey: "",
|
|
Enabled: true,
|
|
AllowUserApiKey: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
key, err := crypt.UpsertUserChatProviderKey(ctx, database.UpsertUserChatProviderKeyParams{
|
|
UserID: user.ID,
|
|
ChatProviderID: provider.ID,
|
|
APIKey: initialAPIKey,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialAPIKey, key.APIKey)
|
|
require.Equal(t, ciphers[0].HexDigest(), key.ApiKeyKeyID.String)
|
|
return provider, key
|
|
}
|
|
|
|
getUserChatProviderKey := func(t *testing.T, store interface {
|
|
GetUserChatProviderKeys(context.Context, uuid.UUID) ([]database.UserChatProviderKey, error)
|
|
}, userID uuid.UUID, providerID uuid.UUID,
|
|
) database.UserChatProviderKey {
|
|
t.Helper()
|
|
keys, err := store.GetUserChatProviderKeys(ctx, userID)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
require.Equal(t, providerID, keys[0].ChatProviderID)
|
|
return keys[0]
|
|
}
|
|
|
|
t.Run("UpsertUserChatProviderKeyCreatesValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
provider, key := insertProviderAndKey(t, crypt, ciphers)
|
|
|
|
got := getUserChatProviderKey(t, crypt, key.UserID, provider.ID)
|
|
require.Equal(t, key.ID, got.ID)
|
|
require.Equal(t, initialAPIKey, got.APIKey)
|
|
require.Equal(t, ciphers[0].HexDigest(), got.ApiKeyKeyID.String)
|
|
|
|
rawKey := getUserChatProviderKey(t, db, key.UserID, provider.ID)
|
|
require.NotEqual(t, initialAPIKey, rawKey.APIKey)
|
|
requireEncryptedEquals(t, ciphers[0], rawKey.APIKey, initialAPIKey)
|
|
})
|
|
|
|
t.Run("GetUserChatProviderKeys", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, crypt, ciphers := setup(t)
|
|
_, key := insertProviderAndKey(t, crypt, ciphers)
|
|
|
|
keys, err := crypt.GetUserChatProviderKeys(ctx, key.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
require.Equal(t, key.ID, keys[0].ID)
|
|
require.Equal(t, initialAPIKey, keys[0].APIKey)
|
|
require.Equal(t, ciphers[0].HexDigest(), keys[0].ApiKeyKeyID.String)
|
|
})
|
|
|
|
t.Run("UpsertUserChatProviderKeyUpdatesValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
provider, key := insertProviderAndKey(t, crypt, ciphers)
|
|
|
|
updated, err := crypt.UpsertUserChatProviderKey(ctx, database.UpsertUserChatProviderKeyParams{
|
|
UserID: key.UserID,
|
|
ChatProviderID: provider.ID,
|
|
APIKey: updatedAPIKey,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, key.ID, updated.ID)
|
|
require.Equal(t, key.CreatedAt, updated.CreatedAt)
|
|
require.False(t, updated.UpdatedAt.Before(key.UpdatedAt))
|
|
require.Equal(t, updatedAPIKey, updated.APIKey)
|
|
require.Equal(t, ciphers[0].HexDigest(), updated.ApiKeyKeyID.String)
|
|
|
|
got := getUserChatProviderKey(t, crypt, key.UserID, provider.ID)
|
|
require.Equal(t, updatedAPIKey, got.APIKey)
|
|
require.Equal(t, ciphers[0].HexDigest(), got.ApiKeyKeyID.String)
|
|
|
|
keys, err := crypt.GetUserChatProviderKeys(ctx, key.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, keys, 1)
|
|
require.Equal(t, updatedAPIKey, keys[0].APIKey)
|
|
|
|
rawKey := getUserChatProviderKey(t, db, key.UserID, provider.ID)
|
|
require.NotEqual(t, updatedAPIKey, rawKey.APIKey)
|
|
requireEncryptedEquals(t, ciphers[0], rawKey.APIKey, updatedAPIKey)
|
|
})
|
|
}
|
|
|
|
func TestUserSecrets(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
|
|
const (
|
|
//nolint:gosec // test credentials
|
|
initialValue = "super-secret-value-initial"
|
|
//nolint:gosec // test credentials
|
|
updatedValue = "super-secret-value-updated"
|
|
)
|
|
|
|
insertUserSecret := func(
|
|
t *testing.T,
|
|
crypt *dbCrypt,
|
|
ciphers []Cipher,
|
|
) database.UserSecret {
|
|
t.Helper()
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
secret, err := crypt.CreateUserSecret(ctx, database.CreateUserSecretParams{
|
|
ID: uuid.New(),
|
|
UserID: user.ID,
|
|
Name: "test-secret-" + uuid.NewString()[:8],
|
|
Value: initialValue,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialValue, secret.Value)
|
|
if len(ciphers) > 0 {
|
|
require.Equal(t, ciphers[0].HexDigest(), secret.ValueKeyID.String)
|
|
}
|
|
return secret
|
|
}
|
|
|
|
t.Run("CreateUserSecretEncryptsValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
secret := insertUserSecret(t, crypt, ciphers)
|
|
|
|
// Reading through crypt should return plaintext.
|
|
got, err := crypt.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialValue, got.Value)
|
|
|
|
// Reading through raw DB should return encrypted value.
|
|
raw, err := db.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, initialValue, raw.Value)
|
|
requireEncryptedEquals(t, ciphers[0], raw.Value, initialValue)
|
|
})
|
|
|
|
t.Run("ListUserSecretsWithValuesDecrypts", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, crypt, ciphers := setup(t)
|
|
secret := insertUserSecret(t, crypt, ciphers)
|
|
|
|
secrets, err := crypt.ListUserSecretsWithValues(ctx, secret.UserID)
|
|
require.NoError(t, err)
|
|
require.Len(t, secrets, 1)
|
|
require.Equal(t, initialValue, secrets[0].Value)
|
|
})
|
|
|
|
t.Run("UpdateUserSecretReEncryptsValue", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
secret := insertUserSecret(t, crypt, ciphers)
|
|
|
|
updated, err := crypt.UpdateUserSecretByUserIDAndName(ctx, database.UpdateUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
UpdateValue: true,
|
|
Value: updatedValue,
|
|
ValueKeyID: sql.NullString{},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, updatedValue, updated.Value)
|
|
require.Equal(t, ciphers[0].HexDigest(), updated.ValueKeyID.String)
|
|
|
|
// Raw DB should have new encrypted value.
|
|
raw, err := db.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, updatedValue, raw.Value)
|
|
requireEncryptedEquals(t, ciphers[0], raw.Value, updatedValue)
|
|
})
|
|
|
|
t.Run("NoCipherStoresPlaintext", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt := setupNoCiphers(t)
|
|
user := dbgen.User(t, crypt, database.User{})
|
|
|
|
secret, err := crypt.CreateUserSecret(ctx, database.CreateUserSecretParams{
|
|
ID: uuid.New(),
|
|
UserID: user.ID,
|
|
Name: "plaintext-secret",
|
|
Value: initialValue,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialValue, secret.Value)
|
|
require.False(t, secret.ValueKeyID.Valid)
|
|
|
|
// Raw DB should also have plaintext.
|
|
raw, err := db.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: user.ID,
|
|
Name: "plaintext-secret",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, initialValue, raw.Value)
|
|
require.False(t, raw.ValueKeyID.Valid)
|
|
})
|
|
|
|
t.Run("UpdateMetadataOnlySkipsEncryption", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
secret := insertUserSecret(t, crypt, ciphers)
|
|
|
|
// Read the raw encrypted value from the database.
|
|
rawBefore, err := db.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Perform a metadata-only update (no value change).
|
|
updated, err := crypt.UpdateUserSecretByUserIDAndName(ctx, database.UpdateUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
UpdateValue: false,
|
|
Value: "",
|
|
ValueKeyID: sql.NullString{},
|
|
UpdateDescription: true,
|
|
Description: "updated description",
|
|
UpdateEnvName: false,
|
|
EnvName: "",
|
|
UpdateFilePath: false,
|
|
FilePath: "",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "updated description", updated.Description)
|
|
require.Equal(t, initialValue, updated.Value)
|
|
|
|
// Read the raw encrypted value again.
|
|
rawAfter, err := db.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: secret.UserID,
|
|
Name: secret.Name,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, rawBefore.Value, rawAfter.Value)
|
|
require.Equal(t, rawBefore.ValueKeyID, rawAfter.ValueKeyID)
|
|
})
|
|
|
|
t.Run("GetUserSecretDecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
dbgen.UserSecret(t, db, database.UserSecret{
|
|
UserID: user.ID,
|
|
Name: "corrupt-secret",
|
|
Value: fakeBase64RandomData(t, 32),
|
|
ValueKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
|
|
_, err := crypt.GetUserSecretByUserIDAndName(ctx, database.GetUserSecretByUserIDAndNameParams{
|
|
UserID: user.ID,
|
|
Name: "corrupt-secret",
|
|
})
|
|
require.Error(t, err)
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr)
|
|
})
|
|
|
|
t.Run("ListUserSecretsWithValuesDecryptErr", func(t *testing.T) {
|
|
t.Parallel()
|
|
db, crypt, ciphers := setup(t)
|
|
user := dbgen.User(t, db, database.User{})
|
|
dbgen.UserSecret(t, db, database.UserSecret{
|
|
UserID: user.ID,
|
|
Name: "corrupt-list-secret",
|
|
Value: fakeBase64RandomData(t, 32),
|
|
ValueKeyID: sql.NullString{String: ciphers[0].HexDigest(), Valid: true},
|
|
})
|
|
|
|
_, err := crypt.ListUserSecretsWithValues(ctx, user.ID)
|
|
require.Error(t, err)
|
|
var derr *DecryptFailedError
|
|
require.ErrorAs(t, err, &derr)
|
|
})
|
|
}
|