-
-
Notifications
You must be signed in to change notification settings - Fork 18
Backend pg proxy #1719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backend pg proxy #1719
Changes from all commits
d0848c0
a257c97
9530d8a
35b9c3b
709b710
cb79f15
b4ec8e3
ba2b842
e0a3bb4
450f0dc
0d1a680
dc12ea4
86bc7e7
385da27
13ba6c7
bfa5e85
d921aa1
e0e5a0c
e464e21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
| import { IsNotEmpty, IsString } from 'class-validator'; | ||
|
|
||
| export class GetHostedConnectionCredentialsDto { | ||
| @ApiProperty() | ||
| @IsString() | ||
| @IsNotEmpty() | ||
| hostedDatabaseId: string; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
|
|
||
| export class HostedConnectionCredentialsRO { | ||
| @ApiProperty() | ||
| connectionId: string; | ||
|
|
||
| @ApiProperty() | ||
| host: string; | ||
|
|
||
| @ApiProperty() | ||
| port: number; | ||
|
|
||
| @ApiProperty() | ||
| database: string; | ||
|
|
||
| @ApiProperty() | ||
| username: string; | ||
|
|
||
| @ApiProperty() | ||
| password: string; | ||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Look for logging interceptors / middlewares that might serialize responses or DTOs.
rg -nP --type=ts -C3 '\b(LoggerInterceptor|intercept\s*\(|logResponse|res\.body|response\.body)\b'
# Confirm no toJSON/serializer strips sensitive fields yet.
rg -nP --type=ts -C2 '\bHostedConnectionCredentialsRO\b'Repository: rocket-admin/rocketadmin Length of output: 28104 🏁 Script executed: cat -n backend/src/microservices/saas-microservice/data-structures/hosted-connection-credentials.ro.tsRepository: rocket-admin/rocketadmin Length of output: 579 🏁 Script executed: # Check for serialization/redaction interceptors or utilities
rg -nP --type=ts -C3 '(ClassSerializerInterceptor|Exclude|Transform|toJSON|serializer)' backend/src/interceptors/Repository: rocket-admin/rocketadmin Length of output: 50 🏁 Script executed: # Check SentryInterceptor for password/sensitive field handling
cat -n backend/src/interceptors/sentry.interceptor.tsRepository: rocket-admin/rocketadmin Length of output: 1424 🏁 Script executed: # Look for password/sensitive field redaction lists or patterns
rg -nP --type=ts 'password|redact|sensitive|mask' backend/src/interceptors/Repository: rocket-admin/rocketadmin Length of output: 50 🏁 Script executed: # Check the use case implementation
cat -n backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.tsRepository: rocket-admin/rocketadmin Length of output: 1958 Add The
Without explicit redaction or documentation, future changes risk accidentally leaking credentials. 🤖 Prompt for AI Agents |
||
|
|
||
| @ApiProperty() | ||
| is_frozen: boolean; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,40 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Inject, Injectable, NotFoundException, Scope } from '@nestjs/common'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AbstractUseCase from '../../../common/abstract-use.case.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BaseType } from '../../../common/data-injection.tokens.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Messages } from '../../../exceptions/text/messages.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { GetHostedConnectionCredentialsDto } from '../data-structures/get-hosted-connection-credentials.dto.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { HostedConnectionCredentialsRO } from '../data-structures/hosted-connection-credentials.ro.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IGetHostedConnectionCredentials } from './saas-use-cases.interface.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Injectable({ scope: Scope.REQUEST }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class GetHostedConnectionCredentialsUseCase | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extends AbstractUseCase<GetHostedConnectionCredentialsDto, HostedConnectionCredentialsRO> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| implements IGetHostedConnectionCredentials | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Inject(BaseType.GLOBAL_DB_CONTEXT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected _dbContext: IGlobalDatabaseContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const connection = await this._dbContext.connectionRepository.findOne({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { id: inputData.hostedDatabaseId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+25
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> { | |
| const connection = await this._dbContext.connectionRepository.findOne({ | |
| where: { id: inputData.hostedDatabaseId }, | |
| }); | |
| private async findHostedConnectionWithDecryptedCredentials(hostedDatabaseId: string) { | |
| const connectionRepository = this._dbContext.connectionRepository as typeof this._dbContext.connectionRepository & { | |
| findOneById?: (id: string) => Promise<any>; | |
| findOneConnection?: (id: string) => Promise<any>; | |
| findAndDecryptConnection?: (id: string) => Promise<any>; | |
| }; | |
| if (typeof connectionRepository.findAndDecryptConnection === 'function') { | |
| return connectionRepository.findAndDecryptConnection(hostedDatabaseId); | |
| } | |
| if (typeof connectionRepository.findOneConnection === 'function') { | |
| return connectionRepository.findOneConnection(hostedDatabaseId); | |
| } | |
| if (typeof connectionRepository.findOneById === 'function') { | |
| return connectionRepository.findOneById(hostedDatabaseId); | |
| } | |
| throw new Error('Connection repository does not expose a decryption-aware lookup method.'); | |
| } | |
| protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> { | |
| const connection = await this.findHostedConnectionWithDecryptedCredentials(inputData.hostedDatabaseId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No tenant/company scoping on lookup.
connectionRepository.findOne({ where: { id: inputData.hostedDatabaseId } }) returns any connection by id, regardless of which company the caller belongs to. The DTO only carries hostedDatabaseId. Even though this route is behind the SaaS-to-SaaS JWT, a compromised or misbehaving caller (or a future non-SaaS caller) could enumerate UUIDs to exfiltrate credentials for any hosted DB in the system.
Recommend either: (a) require and validate a companyId in the DTO and filter where: { id, company: { id: companyId } }, or (b) explicitly filter to connections that are actually hosted (hosted: true / equivalent flag) so this path can't return credentials for customer-owned BYOD connections.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.ts`
around lines 22 - 28, The lookup in implementation
(get-hosted-connection-credentials.use.case.ts -> protected async
implementation) uses connectionRepository.findOne({ where: { id:
inputData.hostedDatabaseId } }) without scoping to tenant/company or hosted-only
connections; update the code and DTO to prevent cross-tenant exposure by either
(A) adding companyId to GetHostedConnectionCredentialsDto and changing the query
to connectionRepository.findOne({ where: { id: inputData.hostedDatabaseId,
company: { id: inputData.companyId } } }) with validation that the caller's
company matches, or (B) at minimum filter for hosted connections only by using
connectionRepository.findOne({ where: { id: inputData.hostedDatabaseId, hosted:
true } }) so only system-hosted DBs can be returned; apply the chosen change in
the implementation, adjust any DTO/validator usages, and keep error handling
(NotFoundException) the same if no record matches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1) See how hosted-db connection creation stores the password (encryption?).
fd -t f 'create-connection-for-hosted-db.use.case.ts' | xargs -I{} sed -n '1,220p' {}
# 2) Look for decryption helpers used elsewhere when returning connection credentials.
rg -nP -C2 '\b(decrypt|encrypt)\w*\s*\(' --type=ts -g 'backend/src/**'
# 3) See whether master/azure/connection encryption is applied on hosted connection writes.
rg -nP -C3 '(masterEncryption|azure_encryption|encryptData|decryptData)' --type=ts -g 'backend/src/**'Repository: rocket-admin/rocketadmin
Length of output: 50381
🏁 Script executed:
# Check ConnectionEntity definition for BeforeInsert/AfterLoad hooks
fd -t f 'connection.entity.ts' | xargs -I{} cat -n {} | head -150
# Check if there's connection-level encryption happening at entity level
rg -B5 -A5 'BeforeInsert|BeforeUpdate|AfterLoad' --type=ts -g 'backend/src/**/connection.entity.ts'
# Verify if masterEncryption should be true for hosted connections
rg -C5 'hosted.*masterEncryption|masterEncryption.*hosted' --type=ts -g 'backend/src/**'Repository: rocket-admin/rocketadmin
Length of output: 8372
🏁 Script executed:
# Get the complete BeforeInsert hook implementation
rg -A30 'encryptCredentialsAndGenerateNanoid' --type=ts -g 'backend/src/**/connection.entity.ts'
# Also check how decryptConnectionsCredentialsAsync works
rg -B5 -A15 'decryptConnectionsCredentialsAsync' --type=ts -g 'backend/src/**' | head -100Repository: rocket-admin/rocketadmin
Length of output: 16265
🏁 Script executed:
# Check the actual current implementation of get-hosted-connection-credentials.use.case.ts
cat -n backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.tsRepository: rocket-admin/rocketadmin
Length of output: 1958
Add missing decryption step before returning credentials.
The connection password is encrypted by ConnectionEntity's @BeforeInsert hook (via Encryptor.encryptData()), but this use case returns the encrypted value directly without decrypting. The API advertises "decrypted credentials" while returning ciphertext, causing authentication to fail silently.
Follow the pattern in UpdateHostedConnectionPasswordUseCase by calling decryptConnectionsCredentialsAsync() on the fetched connection before returning:
import { decryptConnectionCredentialsAsync } from '../../../entities/connection/utils/decrypt-connection-credentials-async.js';
protected async implementation(inputData: GetHostedConnectionCredentialsDto): Promise<HostedConnectionCredentialsRO> {
const connection = await this._dbContext.connectionRepository.findOne({
where: { id: inputData.hostedDatabaseId },
});
if (!connection) {
throw new NotFoundException(Messages.CONNECTION_NOT_FOUND);
}
await decryptConnectionCredentialsAsync(connection);
return { ... };
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@backend/src/microservices/saas-microservice/use-cases/get-hosted-connection-credentials.use.case.ts`
around lines 22 - 39, The implementation of
GetHostedConnectionCredentialsUseCase returns the encrypted password; fetch the
connection via _dbContext.connectionRepository.findOne (as already done), then
call decryptConnectionCredentialsAsync(connection) (the same helper used by
UpdateHostedConnectionPasswordUseCase) before constructing and returning the
HostedConnectionCredentialsRO so the password is decrypted; add the appropriate
import for decryptConnectionCredentialsAsync from
entities/connection/utils/decrypt-connection-credentials-async.js and keep the
existing NotFoundException(Messages.CONNECTION_NOT_FOUND) check.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,7 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi | |
| .send({ | ||
| companyId: companyId, | ||
| userId: userId, | ||
| hostedDatabaseId: faker.string.uuid(), | ||
| databaseName: 'postgres', | ||
| hostname: 'testPg-e2e-testing', | ||
| port: 5432, | ||
|
|
@@ -89,24 +90,14 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi | |
| .set('Content-Type', 'application/json') | ||
| .set('Accept', 'application/json'); | ||
|
|
||
| t.is(createHostedConnectionResult.status, 201); | ||
|
|
||
| const createdConnection = JSON.parse(createHostedConnectionResult.text); | ||
| const connectionId = createdConnection.id; | ||
| console.log('🚀 ~ createdConnection:', createdConnection); | ||
|
|
||
| t.is(createHostedConnectionResult.status, 201); | ||
| const connectionId = createdConnection.connectionId; | ||
|
|
||
| // Verify connection was created | ||
| t.truthy(connectionId); | ||
| t.is(createdConnection.type, 'postgres'); | ||
| t.is(createdConnection.database, 'postgres'); | ||
| t.is(createdConnection.host, 'testPg-e2e-testing'); | ||
| t.is(createdConnection.port, 5432); | ||
|
|
||
| // Verify admin group was created | ||
| t.truthy(createdConnection.groups); | ||
| t.is(createdConnection.groups.length, 1); | ||
| const adminGroup = createdConnection.groups[0]; | ||
| t.truthy(adminGroup.id); | ||
| t.is(adminGroup.isMain, true); | ||
|
|
||
| // Verify connection is accessible via connection groups endpoint | ||
| const groupsResponse = await request(app.getHttpServer()) | ||
|
|
@@ -119,6 +110,7 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi | |
| const groups = JSON.parse(groupsResponse.text); | ||
| t.is(groups.length, 1); | ||
| t.is(groups[0].accessLevel, AccessLevelEnum.edit); | ||
| const adminGroup = groups[0]; | ||
|
|
||
| // Verify tables endpoint works with this connection | ||
| const findTablesResponse = await request(app.getHttpServer()) | ||
|
|
@@ -132,8 +124,9 @@ test.serial(`${currentTest} should create a hosted postgres connection with admi | |
| t.true(Array.isArray(tables)); | ||
|
|
||
| // Verify user permissions - user should have full access | ||
| const groupId = adminGroup.group.id; | ||
| const permissionsResponse = await request(app.getHttpServer()) | ||
| .get(`/connection/permissions?connectionId=${connectionId}&groupId=${adminGroup.id}`) | ||
| .get(`/connection/permissions?connectionId=${connectionId}&groupId=${groupId}`) | ||
| .set('Content-Type', 'application/json') | ||
| .set('Cookie', token) | ||
| .set('Accept', 'application/json'); | ||
|
|
@@ -177,6 +170,7 @@ test.serial(`${currentTest} should return error when userId does not exist`, asy | |
| .send({ | ||
| companyId: faker.string.uuid(), | ||
| userId: faker.string.uuid(), | ||
| hostedDatabaseId: faker.string.uuid(), | ||
| databaseName: 'postgres', | ||
| hostname: 'testPg-e2e-testing', | ||
| port: 5432, | ||
|
|
@@ -187,6 +181,8 @@ test.serial(`${currentTest} should return error when userId does not exist`, asy | |
| .set('Content-Type', 'application/json') | ||
| .set('Accept', 'application/json'); | ||
|
|
||
| const responseBody = JSON.parse(result.text); | ||
| console.log('🚀 ~ responseBody:', responseBody); | ||
| t.is(result.status, 500); | ||
|
Comment on lines
+184
to
186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asserting HTTP 500 codifies a server-error response for an invalid user id. A missing/unknown Consider either fixing the upstream use case to throw 🤖 Prompt for AI Agents |
||
| } catch (e) { | ||
| console.error('Test error:', e); | ||
|
|
@@ -219,6 +215,7 @@ test.serial(`${currentTest} should delete a hosted connection`, async (t) => { | |
| .send({ | ||
| companyId: companyId, | ||
| userId: userId, | ||
| hostedDatabaseId: faker.string.uuid(), | ||
| databaseName: 'postgres', | ||
| hostname: 'testPg-e2e-testing', | ||
| port: 5432, | ||
|
|
@@ -229,9 +226,10 @@ test.serial(`${currentTest} should delete a hosted connection`, async (t) => { | |
| .set('Content-Type', 'application/json') | ||
| .set('Accept', 'application/json'); | ||
|
|
||
| t.is(createResult.status, 201); | ||
| const createdConnection = JSON.parse(createResult.text); | ||
| const connectionId = createdConnection.id; | ||
| console.log('🚀 ~ createdConnection:', createdConnection); | ||
| t.is(createResult.status, 201); | ||
| const connectionId = createdConnection.connectionId; | ||
|
|
||
| // Verify connection exists | ||
| const connectionsBeforeDelete = await request(app.getHttpServer()) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This DTO only accepts
hostedDatabaseId. Since the SaaS auth middleware validates only that a JWT is signed (no tenant scoping), an internal caller could fetch credentials for any connection ID. Consider requiringcompanyId(or other tenant identifier) and validating the connection belongs to that tenant before returning credentials.