Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,69 @@ export class CedarPermissionsService implements IUserAccessRepository {
return AccessLevelEnum.none;
}

async getUserConnectionAccessLevelsForMultipleConnections(
userId: string,
connectionIds: Array<string>,
): Promise<Map<string, AccessLevelEnum>> {
const result = new Map<string, AccessLevelEnum>();
if (connectionIds.length === 0) return result;

const allGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnections(connectionIds, userId);

const groupsByConnection = new Map<string, Array<GroupEntity>>();
for (const group of allGroups) {
const connId = group.connection?.id;
if (!connId) continue;
if (!groupsByConnection.has(connId)) {
groupsByConnection.set(connId, []);
}
groupsByConnection.get(connId).push(group);
}

for (const connectionId of connectionIds) {
const userGroups = groupsByConnection.get(connectionId);
if (!userGroups || userGroups.length === 0) {
result.set(connectionId, AccessLevelEnum.none);
continue;
}

const policies = userGroups.map((g) => g.cedarPolicy).filter(Boolean);
if (policies.length === 0) {
result.set(connectionId, AccessLevelEnum.none);
continue;
}

const entities = buildCedarEntities(userId, userGroups, connectionId);
if (
this.evaluatePolicies(
userId,
CedarAction.ConnectionEdit,
CedarResourceType.Connection,
connectionId,
policies,
entities,
)
) {
result.set(connectionId, AccessLevelEnum.edit);
} else if (
this.evaluatePolicies(
userId,
CedarAction.ConnectionRead,
CedarResourceType.Connection,
connectionId,
policies,
entities,
)
) {
result.set(connectionId, AccessLevelEnum.readonly);
} else {
result.set(connectionId, AccessLevelEnum.none);
}
}

return result;
}

async checkUserConnectionRead(cognitoUserName: string, connectionId: string): Promise<boolean> {
const ctx = await this.loadContext(connectionId, cognitoUserName);
if (!ctx) return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Constants } from '../../../helpers/constants/constants.js';
import { ConnectionEntity } from '../../connection/connection.entity.js';
import { decryptConnectionsCredentialsAsync } from '../../connection/utils/decrypt-connection-credentials-async.js';
import { CompanyInfoEntity } from '../company-info.entity.js';
import { ICompanyInfoRepository } from './company-info-repository.interface.js';

Expand All @@ -19,11 +20,15 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
},

async findOneCompanyInfoByUserIdWithConnections(userId: string): Promise<CompanyInfoEntity> {
return await this.createQueryBuilder('company_info')
const result = await this.createQueryBuilder('company_info')
.leftJoinAndSelect('company_info.users', 'users')
.leftJoinAndSelect('company_info.connections', 'connections')
.where('users.id = :userId', { userId })
.getOne();
if (result?.connections?.length) {
await decryptConnectionsCredentialsAsync(result.connections);
}
return result;
},

async findCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity> {
Expand Down Expand Up @@ -55,7 +60,7 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {

// returns groups and connections where user is invited
async findFullCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity> {
return await this.createQueryBuilder('company_info')
const result = await this.createQueryBuilder('company_info')
.leftJoinAndSelect('company_info.logo', 'logo')
.leftJoinAndSelect('company_info.favicon', 'favicon')
.leftJoinAndSelect('company_info.tab_title', 'tab_title')
Expand All @@ -68,6 +73,10 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
.leftJoinAndSelect('groups.users', 'groups_users')
.where('current_user.id = :userId', { userId })
.getOne();
if (result?.connections?.length) {
await decryptConnectionsCredentialsAsync(result.connections);
}
return result;
},

async findCompanyInfosByUserEmail(userEmail: string): Promise<CompanyInfoEntity[]> {
Expand All @@ -87,10 +96,12 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
.andWhere('connections.isTestConnection IS FALSE')
.andWhere('connections.is_frozen IS FALSE')
.getMany();
return foundCompaniesWithPaidConnections
const connections = foundCompaniesWithPaidConnections
.map((companyInfo: CompanyInfoEntity) => companyInfo.connections)
.filter(Boolean)
.flat();
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

async findCompanyFrozenPaidConnections(companyIds: Array<string>): Promise<Array<ConnectionEntity>> {
Expand All @@ -102,10 +113,12 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
.andWhere('connections.isTestConnection IS FALSE')
.andWhere('connections.is_frozen IS TRUE')
.getMany();
return foundCompaniesWithPaidConnections
const connections = foundCompaniesWithPaidConnections
.map((companyInfo: CompanyInfoEntity) => companyInfo.connections)
.filter(Boolean)
.flat();
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

async findCompanyWithLogo(companyId: string): Promise<CompanyInfoEntity> {
Expand Down
59 changes: 12 additions & 47 deletions backend/src/entities/connection/connection.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Expose } from 'class-transformer';
import { nanoid } from 'nanoid';
import {
AfterLoad,
BeforeInsert,
BeforeUpdate,
Column,
Expand All @@ -14,7 +13,6 @@ import {
Relation,
} from 'typeorm';
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
import { Constants } from '../../helpers/constants/constants.js';
import { Encryptor } from '../../helpers/encryption/encryptor.js';
import { isConnectionTypeAgent } from '../../helpers/index.js';
import { AgentEntity } from '../agent/agent.entity.js';
Expand Down Expand Up @@ -118,9 +116,18 @@ export class ConnectionEntity {
@Column({ default: null })
master_hash?: string | null;

/**
* Non-persisted flag indicating whether credentials are currently in decrypted state.
* Used by @BeforeUpdate to decide whether encryption is needed.
*/
credentialsDecrypted = false;

@BeforeUpdate()
updateTimestampEncryptCredentials(): void {
this.updatedAt = new Date();
if (!this.credentialsDecrypted) {
return;
}
if (!isConnectionTypeAgent(this.type)) {
this.host = Encryptor.encryptData(this.host);
this.database = Encryptor.encryptData(this.database);
Comment on lines 125 to 133

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BeforeUpdate now skips credential encryption entirely when credentialsDecrypted is false. This means any code path that sets credential fields (host/database/username/password/SSH/SSL fields) to plaintext without also setting credentialsDecrypted = true will persist those values unencrypted. Consider making encryption conditional on whether fields are already encrypted (format check) rather than relying solely on an external flag, or ensure all credential mutation paths set the flag explicitly.

Copilot uses AI. Check for mistakes.
Expand All @@ -138,6 +145,7 @@ export class ConnectionEntity {
this.cert = Encryptor.encryptData(this.cert);
}
}
this.credentialsDecrypted = false;
}

@BeforeInsert()
Expand Down Expand Up @@ -168,51 +176,8 @@ export class ConnectionEntity {
}
}

@AfterLoad()
decryptCredentials(): void {
if (this.isTestConnection) {
const testConnectionsArray = Constants.getTestConnectionsArr();
const foundTestConnectionByType = testConnectionsArray.find(
(testConnection) => testConnection.type === this.type,
);
if (foundTestConnectionByType) {
this.host = foundTestConnectionByType.host;
this.database = foundTestConnectionByType.database;
this.username = foundTestConnectionByType.username;
this.password = foundTestConnectionByType.password;
this.port = foundTestConnectionByType.port;
this.ssh = foundTestConnectionByType.ssh;
this.privateSSHKey = foundTestConnectionByType.privateSSHKey;
this.sshHost = foundTestConnectionByType.sshHost;
this.sshPort = foundTestConnectionByType.sshPort;
this.sshUsername = foundTestConnectionByType.sshUsername;
this.ssl = foundTestConnectionByType.ssl;
this.cert = foundTestConnectionByType.cert;
this.authSource = foundTestConnectionByType.authSource;
this.sid = foundTestConnectionByType.sid;
this.schema = foundTestConnectionByType.schema;
this.azure_encryption = foundTestConnectionByType.azure_encryption;
}
} else {
if (!isConnectionTypeAgent(this.type)) {
this.host = Encryptor.decryptData(this.host);
this.database = Encryptor.decryptData(this.database);
this.password = Encryptor.decryptData(this.password);
this.username = Encryptor.decryptData(this.username);
if (this.authSource) {
this.authSource = Encryptor.decryptData(this.authSource);
}
if (this.ssh) {
this.privateSSHKey = Encryptor.decryptData(this.privateSSHKey);
this.sshHost = Encryptor.decryptData(this.sshHost);
this.sshUsername = Encryptor.decryptData(this.sshUsername);
}
if (this.ssl && this.cert) {
this.cert = Encryptor.decryptData(this.cert);
}
}
}
}
// Decryption moved to async utility: decrypt-connection-credentials-async.ts
// All repository methods must call decryptConnectionCredentialsAsync() after loading.

@ManyToOne(
(_) => UserEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { Encryptor } from '../../../helpers/encryption/encryptor.js';
import { isConnectionTypeAgent } from '../../../helpers/index.js';
import { UserEntity } from '../../user/user.entity.js';
import { ConnectionEntity } from '../connection.entity.js';
import {
decryptConnectionCredentialsAsync,
decryptConnectionsCredentialsAsync,
} from '../utils/decrypt-connection-credentials-async.js';
import { isTestConnectionUtil } from '../utils/is-test-connection-util.js';
import { IConnectionRepository } from './connection.repository.interface.js';

Expand All @@ -26,6 +30,7 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
savedConnection.cert = this.decryptConnectionField(savedConnection.cert);
}
}
savedConnection.credentialsDecrypted = true;
return savedConnection;
},

Expand All @@ -38,7 +43,9 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
if (!includeTestConnections) {
connectionQb.andWhere('connection.isTestConnection = :isTest', { isTest: false });
}
return await connectionQb.getMany();
const connections = await connectionQb.getMany();
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

async findAllUserTestConnections(userId: string): Promise<Array<ConnectionEntity>> {
Expand All @@ -48,7 +55,9 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
.leftJoinAndSelect('connection.connection_properties', 'connection_properties')
.andWhere('user.id = :userId', { userId: userId })
.andWhere('connection.isTestConnection = :isTest', { isTest: true });
return await connectionQb.getMany();
const connections = await connectionQb.getMany();
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

async findAllUserNonTestsConnections(userId: string): Promise<Array<ConnectionEntity>> {
Expand Down Expand Up @@ -81,6 +90,7 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
connection.signing_key = Encryptor.generateRandomString(40);
await this.save(connection);
}
await decryptConnectionCredentialsAsync(connection);
return connection;
},

Expand All @@ -96,6 +106,7 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
connection.signing_key = Encryptor.generateRandomString(40);
await this.save(connection);
}
await decryptConnectionCredentialsAsync(connection);

if (connection.masterEncryption && !masterPwd) {
throw new Error(Messages.MASTER_PASSWORD_MISSING);
Expand All @@ -121,19 +132,24 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
const qb = this.createQueryBuilder('connection')
.leftJoinAndSelect('connection.groups', 'group')
.andWhere('connection.id = :connectionId', { connectionId: connectionId });
return await qb.getOne();
const connection = await qb.getOne();
if (connection) {
await decryptConnectionCredentialsAsync(connection);
}
return connection;
},

async getWorkedConnectionsInTwoWeeks(): Promise<Array<ConnectionEntity>> {
const freshNonTestConnectionsWithLogs = await this.createQueryBuilder('connection')
const connections = await this.createQueryBuilder('connection')
.leftJoinAndSelect('connection.author', 'author')
.leftJoin('connection.logs', 'logs')
.where('connection.createdAt > :date', { date: Constants.TWO_WEEKS_AGO() })
.andWhere('author.gclid IS NOT NULL')
.andWhere('connection.isTestConnection = :isTest', { isTest: false })
.andWhere('logs.id IS NOT NULL')
.getMany();
return freshNonTestConnectionsWithLogs;
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

async getConnectionByGroupIdWithCompanyAndUsersInCompany(groupId: string): Promise<ConnectionEntity | null> {
Expand All @@ -142,17 +158,29 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
.leftJoinAndSelect('connection.company', 'company')
.leftJoinAndSelect('company.users', 'user');
qb.andWhere('group.id = :groupId', { groupId: groupId });
return await qb.getOne();
const connection = await qb.getOne();
if (connection) {
await decryptConnectionCredentialsAsync(connection);
}
return connection;
},

async findOneById(connectionId: string): Promise<ConnectionEntity | null> {
return await this.findOne({ where: { id: connectionId } });
const connection = await this.findOne({ where: { id: connectionId } });
if (connection) {
await decryptConnectionCredentialsAsync(connection);
}
return connection;
},

async findOneAgentConnectionByToken(connectionToken: string): Promise<ConnectionEntity | null> {
const qb = this.createQueryBuilder('connection').leftJoinAndSelect('connection.agent', 'agent');
qb.andWhere('agent.token = :agentToken', { agentToken: connectionToken });
return await qb.getOne();
const connection = await qb.getOne();
if (connection) {
await decryptConnectionCredentialsAsync(connection);
}
return connection;
},

async isTestConnectionById(connectionId: string): Promise<boolean> {
Expand All @@ -179,13 +207,12 @@ export const customConnectionRepositoryExtension: IConnectionRepository &

async findAllCompanyUsersNonTestsConnections(companyId: string): Promise<Array<ConnectionEntity>> {
const connectionQb = this.createQueryBuilder('connection')
.leftJoin('connection.groups', 'group')
.leftJoin('group.users', 'user')
.leftJoin('user.company', 'company')
.leftJoinAndSelect('connection.connection_properties', 'connection_properties')
.where('connection.isTestConnection = :isTest', { isTest: false })
.andWhere('company.id = :companyId', { companyId: companyId });
return await connectionQb.getMany();
.andWhere('connection.companyId = :companyId', { companyId: companyId });
const connections = await connectionQb.getMany();
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

async freezeConnections(connectionsIds: Array<string>): Promise<void> {
Expand All @@ -210,7 +237,9 @@ export const customConnectionRepositoryExtension: IConnectionRepository &
.where('user.id = :userId', { userId: userId })
.andWhere('connection.isTestConnection = :isTest', { isTest: true })
.andWhere('connection.company IS NULL');
return await qb.getMany();
const connections = await qb.getMany();
await decryptConnectionsCredentialsAsync(connections);
return connections;
},

decryptConnectionField(field: string): string {
Expand Down
Loading
Loading