Skip to content

Commit 73b1e58

Browse files
committed
fix(credentials): keep an encryption failure on a verified database connection a server error
1 parent ff2bf14 commit 73b1e58

3 files changed

Lines changed: 60 additions & 5 deletions

File tree

‎apps/sim/lib/credentials/database-connections/server.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,24 @@ function secretSchemaFor(spec: DatabaseConnectionSpec) {
150150
return z.object({ type: z.literal(spec.secretType), connection: spec.schema }).strict()
151151
}
152152

153+
/**
154+
* A submitted connection that could not be parsed or did not answer `SELECT 1`:
155+
* the user's input is at fault, unlike a failure to encrypt a verified one.
156+
*/
157+
export class DatabaseConnectionVerificationError extends Error {
158+
constructor(message: string) {
159+
super(message)
160+
this.name = 'DatabaseConnectionVerificationError'
161+
}
162+
}
163+
153164
/**
154165
* Parses a submitted connection, proves it with `SELECT 1` through the block's
155166
* own guarded connection path (host validation and IP pinning included), and
156167
* encrypts it only after the database answered. Driver errors are never
157168
* surfaced: a server can echo anything, including the login it rejected.
169+
* Input and verification failures throw `DatabaseConnectionVerificationError`;
170+
* an encryption failure propagates unchanged.
158171
*/
159172
export async function verifyAndEncryptDatabaseConnection(
160173
providerId: DatabaseConnectionProviderId,
@@ -163,13 +176,13 @@ export async function verifyAndEncryptDatabaseConnection(
163176
const spec = SPECS[providerId]
164177
const { label } = spec.descriptor
165178
if (Buffer.byteLength(serialized, 'utf8') > MAX_SERIALIZED_CONNECTION_BYTES) {
166-
throw new Error(`${label} connection must be at most 64 KiB`)
179+
throw new DatabaseConnectionVerificationError(`${label} connection must be at most 64 KiB`)
167180
}
168181
let parsed: ReturnType<DatabaseConnectionSpec['parse']>
169182
try {
170183
parsed = spec.parse(JSON.parse(serialized))
171184
} catch {
172-
throw new Error(`Invalid ${label} connection configuration`)
185+
throw new DatabaseConnectionVerificationError(`Invalid ${label} connection configuration`)
173186
}
174187
const { connection } = parsed
175188
try {
@@ -179,7 +192,7 @@ export async function verifyAndEncryptDatabaseConnection(
179192
throw new Error('Invalid verification response')
180193
}
181194
} catch {
182-
throw new Error(`Could not verify the ${label} connection`)
195+
throw new DatabaseConnectionVerificationError(`Could not verify the ${label} connection`)
183196
}
184197
const { encrypted } = await encryptSecret(JSON.stringify({ type: spec.secretType, connection }))
185198
return {

‎apps/sim/lib/credentials/service-account-secret.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ const {
1111
mockNormalizeDomain,
1212
mockClientCredentialMinter,
1313
mockVerifyAndEncryptOci,
14+
mockPostgresqlQuery,
1415
} = vi.hoisted(() => ({
1516
mockFetchSlackTeamId: vi.fn(),
1617
mockValidateAtlassian: vi.fn(),
1718
mockNormalizeDomain: vi.fn((raw: string) => raw.trim().toLowerCase()),
1819
mockClientCredentialMinter: vi.fn(),
1920
mockVerifyAndEncryptOci: vi.fn(),
21+
mockPostgresqlQuery: vi.fn(),
2022
}))
2123

2224
vi.mock('@/lib/core/security/encryption', () => encryptionMock)
@@ -59,6 +61,9 @@ vi.mock('@/lib/credentials/oci-api-key-service-account.server', () => ({
5961
},
6062
verifyAndEncryptOciApiKeyCredential: mockVerifyAndEncryptOci,
6163
}))
64+
vi.mock('@/lib/internal/postgresql/operations', () => ({
65+
executePostgresqlQuery: mockPostgresqlQuery,
66+
}))
6267
vi.mock('@/lib/api/contracts/credentials', () => ({
6368
serviceAccountJsonSchema: {
6469
safeParse: (value: string) => {
@@ -93,6 +98,7 @@ import {
9398
} from '@/lib/credentials/service-account-secret'
9499
import {
95100
ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID,
101+
POSTGRESQL_SERVICE_ACCOUNT_PROVIDER_ID,
96102
SLACK_CUSTOM_BOT_PROVIDER_ID,
97103
} from '@/lib/oauth/types'
98104

@@ -1010,3 +1016,35 @@ describe('a token provider whose region or host defaults to the one it always us
10101016
}
10111017
)
10121018
})
1019+
1020+
describe('a saved database connection', () => {
1021+
const serviceAccountJson = JSON.stringify({
1022+
host: 'db.example.com',
1023+
port: 5432,
1024+
database: 'app',
1025+
username: 'app_user',
1026+
password: 'secret',
1027+
})
1028+
1029+
it('asks the user to fix the connection when the database does not answer', async () => {
1030+
mockPostgresqlQuery.mockRejectedValue(new Error('ECONNREFUSED'))
1031+
1032+
await expect(
1033+
verifyAndBuildServiceAccountSecret(POSTGRESQL_SERVICE_ACCOUNT_PROVIDER_ID, {
1034+
serviceAccountJson,
1035+
})
1036+
).rejects.toBeInstanceOf(ServiceAccountSecretError)
1037+
})
1038+
1039+
it('keeps an encryption failure on a verified connection a server error', async () => {
1040+
mockPostgresqlQuery.mockResolvedValue({ rows: [{ sim_connection_ok: 1 }] })
1041+
mockEncryptSecret.mockRejectedValue(new Error('KMS unavailable'))
1042+
1043+
const saving = verifyAndBuildServiceAccountSecret(POSTGRESQL_SERVICE_ACCOUNT_PROVIDER_ID, {
1044+
serviceAccountJson,
1045+
})
1046+
1047+
await expect(saving).rejects.toThrow('KMS unavailable')
1048+
await expect(saving).rejects.not.toBeInstanceOf(ServiceAccountSecretError)
1049+
})
1050+
})

‎apps/sim/lib/credentials/service-account-secret.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import {
3030
DATABASE_CONNECTION_DESCRIPTORS,
3131
type DatabaseConnectionProviderId,
3232
} from '@/lib/credentials/database-connections/descriptors'
33-
import { verifyAndEncryptDatabaseConnection } from '@/lib/credentials/database-connections/server'
33+
import {
34+
DatabaseConnectionVerificationError,
35+
verifyAndEncryptDatabaseConnection,
36+
} from '@/lib/credentials/database-connections/server'
3437
import {
3538
DatabricksCredentialError,
3639
verifyAndEncryptDatabricksCredential,
@@ -558,7 +561,8 @@ function databaseConnectionSecretBuilder(
558561
let result
559562
try {
560563
result = await verifyAndEncryptDatabaseConnection(providerId, fields.serviceAccountJson)
561-
} catch {
564+
} catch (error) {
565+
if (!(error instanceof DatabaseConnectionVerificationError)) throw error
562566
throw new ServiceAccountSecretError(
563567
`Could not verify the ${label} connection. Check the connection details, network access, and database permissions.`
564568
)

0 commit comments

Comments
 (0)