Skip to content

Commit 5de6a06

Browse files
guguclaude
andcommitted
Merge branch 'main' into migrate-cedar-policy-groups-to-signals
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 8c4397c + f3b07e8 commit 5de6a06

87 files changed

Lines changed: 1911 additions & 621 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/common/data-injection.tokens.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export enum UseCaseType {
4343
REFRESH_CONNECTION_AGENT_TOKEN = 'REFRESH_CONNECTION_AGENT_TOKEN',
4444
VALIDATE_CONNECTION_MASTER_PASSWORD = 'VALIDATE_CONNECTION_MASTER_PASSWORD',
4545
UNFREEZE_CONNECTION = 'UNFREEZE_CONNECTION',
46+
UPDATE_CONNECTION_TITLE = 'UPDATE_CONNECTION_TITLE',
4647

4748
FIND_ALL_USER_GROUPS = 'FIND_ALL_USER_GROUPS',
4849
INVITE_USER_IN_GROUP = 'INVITE_USER_IN_GROUP',
@@ -117,6 +118,7 @@ export enum UseCaseType {
117118
SAAS_CREATE_CONNECTION_FOR_HOSTED_DB = 'SAAS_CREATE_CONNECTION_FOR_HOSTED_DB',
118119
SAAS_DELETE_CONNECTION_FOR_HOSTED_DB = 'SAAS_DELETE_CONNECTION_FOR_HOSTED_DB',
119120
SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD = 'SAAS_UPDATE_HOSTED_CONNECTION_PASSWORD',
121+
SAAS_GET_CONNECTIONS_INFO_BY_IDS = 'SAAS_GET_CONNECTIONS_INFO_BY_IDS',
120122

121123
INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
122124
VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',

backend/src/entities/cedar-authorization/cedar-permissions.service.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1+
import * as cedarWasm from '@cedar-policy/cedar-wasm/nodejs';
12
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
3+
import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js';
4+
import { BaseType } from '../../common/data-injection.tokens.js';
25
import { AccessLevelEnum } from '../../enums/index.js';
36
import { Messages } from '../../exceptions/text/messages.js';
47
import { Cacher } from '../../helpers/cache/cacher.js';
5-
import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js';
6-
import { BaseType } from '../../common/data-injection.tokens.js';
78
import { GroupEntity } from '../group/group.entity.js';
89
import { ITablePermissionData } from '../permission/permission.interface.js';
9-
import { CedarAction, CedarResourceType, CEDAR_ACTION_TYPE, CEDAR_USER_TYPE } from './cedar-action-map.js';
10+
import { IUserAccessRepository } from '../user-access/repository/user-access.repository.interface.js';
11+
import { CEDAR_ACTION_TYPE, CEDAR_USER_TYPE, CedarAction, CedarResourceType } from './cedar-action-map.js';
1012
import { buildCedarEntities } from './cedar-entity-builder.js';
1113
import { CEDAR_SCHEMA } from './cedar-schema.js';
12-
import * as cedarWasm from '@cedar-policy/cedar-wasm/nodejs';
13-
import { IUserAccessRepository } from '../user-access/repository/user-access.repository.interface.js';
1414

1515
interface EvalContext {
1616
userGroups: Array<GroupEntity>;
@@ -58,6 +58,69 @@ export class CedarPermissionsService implements IUserAccessRepository {
5858
return AccessLevelEnum.none;
5959
}
6060

61+
async getUserConnectionAccessLevelsForMultipleConnections(
62+
userId: string,
63+
connectionIds: Array<string>,
64+
): Promise<Map<string, AccessLevelEnum>> {
65+
const result = new Map<string, AccessLevelEnum>();
66+
if (connectionIds.length === 0) return result;
67+
68+
const allGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnections(connectionIds, userId);
69+
70+
const groupsByConnection = new Map<string, Array<GroupEntity>>();
71+
for (const group of allGroups) {
72+
const connId = group.connection?.id;
73+
if (!connId) continue;
74+
if (!groupsByConnection.has(connId)) {
75+
groupsByConnection.set(connId, []);
76+
}
77+
groupsByConnection.get(connId).push(group);
78+
}
79+
80+
for (const connectionId of connectionIds) {
81+
const userGroups = groupsByConnection.get(connectionId);
82+
if (!userGroups || userGroups.length === 0) {
83+
result.set(connectionId, AccessLevelEnum.none);
84+
continue;
85+
}
86+
87+
const policies = userGroups.map((g) => g.cedarPolicy).filter(Boolean);
88+
if (policies.length === 0) {
89+
result.set(connectionId, AccessLevelEnum.none);
90+
continue;
91+
}
92+
93+
const entities = buildCedarEntities(userId, userGroups, connectionId);
94+
if (
95+
this.evaluatePolicies(
96+
userId,
97+
CedarAction.ConnectionEdit,
98+
CedarResourceType.Connection,
99+
connectionId,
100+
policies,
101+
entities,
102+
)
103+
) {
104+
result.set(connectionId, AccessLevelEnum.edit);
105+
} else if (
106+
this.evaluatePolicies(
107+
userId,
108+
CedarAction.ConnectionRead,
109+
CedarResourceType.Connection,
110+
connectionId,
111+
policies,
112+
entities,
113+
)
114+
) {
115+
result.set(connectionId, AccessLevelEnum.readonly);
116+
} else {
117+
result.set(connectionId, AccessLevelEnum.none);
118+
}
119+
}
120+
121+
return result;
122+
}
123+
61124
async checkUserConnectionRead(cognitoUserName: string, connectionId: string): Promise<boolean> {
62125
const ctx = await this.loadContext(connectionId, cognitoUserName);
63126
if (!ctx) return false;
@@ -372,5 +435,4 @@ export class CedarPermissionsService implements IUserAccessRepository {
372435

373436
return { userGroups, policies };
374437
}
375-
376438
}

backend/src/entities/company-info/repository/company-info-custom-repository.extension.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Constants } from '../../../helpers/constants/constants.js';
22
import { ConnectionEntity } from '../../connection/connection.entity.js';
3+
import { decryptConnectionsCredentialsAsync } from '../../connection/utils/decrypt-connection-credentials-async.js';
34
import { CompanyInfoEntity } from '../company-info.entity.js';
45
import { ICompanyInfoRepository } from './company-info-repository.interface.js';
56

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

2122
async findOneCompanyInfoByUserIdWithConnections(userId: string): Promise<CompanyInfoEntity> {
22-
return await this.createQueryBuilder('company_info')
23+
const result = await this.createQueryBuilder('company_info')
2324
.leftJoinAndSelect('company_info.users', 'users')
2425
.leftJoinAndSelect('company_info.connections', 'connections')
2526
.where('users.id = :userId', { userId })
2627
.getOne();
28+
if (result?.connections?.length) {
29+
await decryptConnectionsCredentialsAsync(result.connections);
30+
}
31+
return result;
2732
},
2833

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

5661
// returns groups and connections where user is invited
5762
async findFullCompanyInfoByUserId(userId: string): Promise<CompanyInfoEntity> {
58-
return await this.createQueryBuilder('company_info')
63+
const result = await this.createQueryBuilder('company_info')
5964
.leftJoinAndSelect('company_info.logo', 'logo')
6065
.leftJoinAndSelect('company_info.favicon', 'favicon')
6166
.leftJoinAndSelect('company_info.tab_title', 'tab_title')
@@ -68,6 +73,10 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
6873
.leftJoinAndSelect('groups.users', 'groups_users')
6974
.where('current_user.id = :userId', { userId })
7075
.getOne();
76+
if (result?.connections?.length) {
77+
await decryptConnectionsCredentialsAsync(result.connections);
78+
}
79+
return result;
7180
},
7281

7382
async findCompanyInfosByUserEmail(userEmail: string): Promise<CompanyInfoEntity[]> {
@@ -87,10 +96,12 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
8796
.andWhere('connections.isTestConnection IS FALSE')
8897
.andWhere('connections.is_frozen IS FALSE')
8998
.getMany();
90-
return foundCompaniesWithPaidConnections
99+
const connections = foundCompaniesWithPaidConnections
91100
.map((companyInfo: CompanyInfoEntity) => companyInfo.connections)
92101
.filter(Boolean)
93102
.flat();
103+
await decryptConnectionsCredentialsAsync(connections);
104+
return connections;
94105
},
95106

96107
async findCompanyFrozenPaidConnections(companyIds: Array<string>): Promise<Array<ConnectionEntity>> {
@@ -102,10 +113,12 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = {
102113
.andWhere('connections.isTestConnection IS FALSE')
103114
.andWhere('connections.is_frozen IS TRUE')
104115
.getMany();
105-
return foundCompaniesWithPaidConnections
116+
const connections = foundCompaniesWithPaidConnections
106117
.map((companyInfo: CompanyInfoEntity) => companyInfo.connections)
107118
.filter(Boolean)
108119
.flat();
120+
await decryptConnectionsCredentialsAsync(connections);
121+
return connections;
109122
},
110123

111124
async findCompanyWithLogo(companyId: string): Promise<CompanyInfoEntity> {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class UpdateConnectionTitleDs {
2+
connectionId: string;
3+
userId: string;
4+
title: string;
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class UpdateConnectionTitleDto {
5+
@ApiProperty()
6+
@IsString()
7+
@IsNotEmpty()
8+
title: string;
9+
}

backend/src/entities/connection/connection.controller.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { GetGroupsInConnectionDs } from './application/data-structures/get-group
4141
import { GetPermissionsInConnectionDs } from './application/data-structures/get-permissions-in-connection.ds.js';
4242
import { RestoredConnectionDs } from './application/data-structures/restored-connection.ds.js';
4343
import { UpdateConnectionDs } from './application/data-structures/update-connection.ds.js';
44+
import { UpdateConnectionTitleDs } from './application/data-structures/update-connection-title.ds.js';
4445
import { UpdateMasterPasswordDs } from './application/data-structures/update-master-password.ds.js';
4546
import { ValidateConnectionMasterPasswordDs } from './application/data-structures/validate-connection-master-password.ds.js';
4647
import { CreateConnectionDto } from './application/dto/create-connection.dto.js';
@@ -51,6 +52,7 @@ import { DeleteGroupFromConnectionDTO } from './application/dto/delete-group-fro
5152
import { FoundUserGroupsInConnectionDTO } from './application/dto/found-user-groups-in-connection.dto.js';
5253
import { ConnectionTokenResponseDTO } from './application/dto/new-connection-token-response.dto.js';
5354
import { TestConnectionResponseDTO } from './application/dto/test-connection-response.dto.js';
55+
import { UpdateConnectionTitleDto } from './application/dto/update-connection-title.dto.js';
5456
import { UpdateMasterPasswordRequestBodyDto } from './application/dto/update-master-password-request-body.dto.js';
5557
import { UpdatedConnectionResponseDTO } from './application/dto/updated-connection-response.dto.js';
5658
import { ValidationResultRo } from './application/dto/validation-result.ro.js';
@@ -69,6 +71,7 @@ import {
6971
ITestConnection,
7072
IUnfreezeConnection,
7173
IUpdateConnection,
74+
IUpdateConnectionTitle,
7275
IUpdateMasterPassword,
7376
IValidateConnectionMasterPassword,
7477
IValidateConnectionToken,
@@ -120,6 +123,8 @@ export class ConnectionController {
120123
private readonly validateConnectionMasterPasswordUseCase: IValidateConnectionMasterPassword,
121124
@Inject(UseCaseType.UNFREEZE_CONNECTION)
122125
private readonly unfreezeConnectionUseCase: IUnfreezeConnection,
126+
@Inject(UseCaseType.UPDATE_CONNECTION_TITLE)
127+
private readonly updateConnectionTitleUseCase: IUpdateConnectionTitle,
123128
@Inject(BaseType.GLOBAL_DB_CONTEXT)
124129
protected _dbContext: IGlobalDatabaseContext,
125130
private readonly amplitudeService: AmplitudeService,
@@ -685,4 +690,29 @@ export class ConnectionController {
685690
}
686691
return await this.unfreezeConnectionUseCase.execute({ connectionId, userId }, InTransactionEnum.ON);
687692
}
693+
694+
@ApiOperation({ summary: 'Update connection title' })
695+
@ApiBody({ type: UpdateConnectionTitleDto })
696+
@ApiResponse({
697+
status: 200,
698+
type: SuccessResponse,
699+
description: 'Connection title was updated.',
700+
})
701+
@UseGuards(ConnectionEditGuard)
702+
@Put('/connection/title/:connectionId')
703+
async updateConnectionTitle(
704+
@Body() titleData: UpdateConnectionTitleDto,
705+
@SlugUuid('connectionId') connectionId: string,
706+
@UserId() userId: string,
707+
): Promise<SuccessResponse> {
708+
if (!connectionId) {
709+
throw new BadRequestException(Messages.CONNECTION_ID_MISSING);
710+
}
711+
const inputData: UpdateConnectionTitleDs = {
712+
connectionId,
713+
userId,
714+
title: titleData.title,
715+
};
716+
return await this.updateConnectionTitleUseCase.execute(inputData, InTransactionEnum.ON);
717+
}
688718
}

backend/src/entities/connection/connection.entity.ts

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
12
import { Expose } from 'class-transformer';
23
import { nanoid } from 'nanoid';
34
import {
4-
AfterLoad,
55
BeforeInsert,
66
BeforeUpdate,
77
Column,
@@ -13,8 +13,6 @@ import {
1313
PrimaryColumn,
1414
Relation,
1515
} from 'typeorm';
16-
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
17-
import { Constants } from '../../helpers/constants/constants.js';
1816
import { Encryptor } from '../../helpers/encryption/encryptor.js';
1917
import { isConnectionTypeAgent } from '../../helpers/index.js';
2018
import { AgentEntity } from '../agent/agent.entity.js';
@@ -118,9 +116,18 @@ export class ConnectionEntity {
118116
@Column({ default: null })
119117
master_hash?: string | null;
120118

119+
/**
120+
* Non-persisted flag indicating whether credentials are currently in decrypted state.
121+
* Used by @BeforeUpdate to decide whether encryption is needed.
122+
*/
123+
credentialsDecrypted = false;
124+
121125
@BeforeUpdate()
122126
updateTimestampEncryptCredentials(): void {
123127
this.updatedAt = new Date();
128+
if (!this.credentialsDecrypted) {
129+
return;
130+
}
124131
if (!isConnectionTypeAgent(this.type)) {
125132
this.host = Encryptor.encryptData(this.host);
126133
this.database = Encryptor.encryptData(this.database);
@@ -138,6 +145,7 @@ export class ConnectionEntity {
138145
this.cert = Encryptor.encryptData(this.cert);
139146
}
140147
}
148+
this.credentialsDecrypted = false;
141149
}
142150

143151
@BeforeInsert()
@@ -168,51 +176,8 @@ export class ConnectionEntity {
168176
}
169177
}
170178

171-
@AfterLoad()
172-
decryptCredentials(): void {
173-
if (this.isTestConnection) {
174-
const testConnectionsArray = Constants.getTestConnectionsArr();
175-
const foundTestConnectionByType = testConnectionsArray.find(
176-
(testConnection) => testConnection.type === this.type,
177-
);
178-
if (foundTestConnectionByType) {
179-
this.host = foundTestConnectionByType.host;
180-
this.database = foundTestConnectionByType.database;
181-
this.username = foundTestConnectionByType.username;
182-
this.password = foundTestConnectionByType.password;
183-
this.port = foundTestConnectionByType.port;
184-
this.ssh = foundTestConnectionByType.ssh;
185-
this.privateSSHKey = foundTestConnectionByType.privateSSHKey;
186-
this.sshHost = foundTestConnectionByType.sshHost;
187-
this.sshPort = foundTestConnectionByType.sshPort;
188-
this.sshUsername = foundTestConnectionByType.sshUsername;
189-
this.ssl = foundTestConnectionByType.ssl;
190-
this.cert = foundTestConnectionByType.cert;
191-
this.authSource = foundTestConnectionByType.authSource;
192-
this.sid = foundTestConnectionByType.sid;
193-
this.schema = foundTestConnectionByType.schema;
194-
this.azure_encryption = foundTestConnectionByType.azure_encryption;
195-
}
196-
} else {
197-
if (!isConnectionTypeAgent(this.type)) {
198-
this.host = Encryptor.decryptData(this.host);
199-
this.database = Encryptor.decryptData(this.database);
200-
this.password = Encryptor.decryptData(this.password);
201-
this.username = Encryptor.decryptData(this.username);
202-
if (this.authSource) {
203-
this.authSource = Encryptor.decryptData(this.authSource);
204-
}
205-
if (this.ssh) {
206-
this.privateSSHKey = Encryptor.decryptData(this.privateSSHKey);
207-
this.sshHost = Encryptor.decryptData(this.sshHost);
208-
this.sshUsername = Encryptor.decryptData(this.sshUsername);
209-
}
210-
if (this.ssl && this.cert) {
211-
this.cert = Encryptor.decryptData(this.cert);
212-
}
213-
}
214-
}
215-
}
179+
// Decryption moved to async utility: decrypt-connection-credentials-async.ts
180+
// All repository methods must call decryptConnectionCredentialsAsync() after loading.
216181

217182
@ManyToOne(
218183
(_) => UserEntity,

backend/src/entities/connection/connection.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { TestConnectionUseCase } from './use-cases/test-connection.use.case.js';
3333
import { UnfreezeConnectionUseCase } from './use-cases/unfreeze-connection.use.case.js';
3434
import { UpdateConnectionUseCase } from './use-cases/update-connection.use.case.js';
3535
import { UpdateConnectionMasterPasswordUseCase } from './use-cases/update-connection-master-password.use.case.js';
36+
import { UpdateConnectionTitleUseCase } from './use-cases/update-connection-title.use.case.js';
3637
import { ValidateConnectionMasterPasswordUseCase } from './use-cases/validate-connection-master-password.use.case.js';
3738
import { ValidateConnectionTokenUseCase } from './use-cases/validate-connection-token.use.case.js';
3839

@@ -130,6 +131,10 @@ import { ValidateConnectionTokenUseCase } from './use-cases/validate-connection-
130131
provide: UseCaseType.UNFREEZE_CONNECTION,
131132
useClass: UnfreezeConnectionUseCase,
132133
},
134+
{
135+
provide: UseCaseType.UPDATE_CONNECTION_TITLE,
136+
useClass: UpdateConnectionTitleUseCase,
137+
},
133138
],
134139
controllers: [ConnectionController],
135140
})
@@ -156,6 +161,7 @@ export class ConnectionModule implements NestModule {
156161
{ path: '/connection/token/refresh/:connectionId', method: RequestMethod.GET },
157162
{ path: '/connection/masterpwd/verify/:connectionId', method: RequestMethod.GET },
158163
{ path: '/connection/unfreeze/:connectionId', method: RequestMethod.PUT },
164+
{ path: '/connection/title/:connectionId', method: RequestMethod.PUT },
159165
)
160166
.apply(AuthWithApiMiddleware)
161167
.forRoutes({ path: 'connections', method: RequestMethod.GET });

0 commit comments

Comments
 (0)