Skip to content

Commit d939e07

Browse files
authored
Merge branch 'main' into cedar-policy-editor
2 parents 418d633 + 1ac9111 commit d939e07

20 files changed

Lines changed: 3623 additions & 37 deletions

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ export enum UseCaseType {
114114
FREEZE_CONNECTIONS_IN_COMPANY = 'FREEZE_CONNECTIONS_IN_COMPANY',
115115
UNFREEZE_CONNECTIONS_IN_COMPANY = 'UNFREEZE_CONNECTIONS_IN_COMPANY',
116116
SAAS_REGISTER_USER_WITH_SAML = 'SAAS_REGISTER_USER_WITH_SAML',
117+
SAAS_CREATE_CONNECTION_FOR_HOSTED_DB = 'SAAS_CREATE_CONNECTION_FOR_HOSTED_DB',
118+
SAAS_DELETE_CONNECTION_FOR_HOSTED_DB = 'SAAS_DELETE_CONNECTION_FOR_HOSTED_DB',
117119

118120
INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',
119121
VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP',

backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ export async function migratePermissionsToCedar(dataSource: DataSource): Promise
88
const groupRepository = dataSource.getRepository(GroupEntity);
99
let migratedCount = 0;
1010

11+
// Migrate groups with no Cedar policy OR groups with old-format policies (using "principal in" instead of bare "principal")
1112
const groups = await groupRepository
1213
.createQueryBuilder('group')
1314
.leftJoinAndSelect('group.connection', 'connection')
1415
.leftJoinAndSelect('group.permissions', 'permission')
15-
.where('group.cedarPolicy IS NULL OR group.cedarPolicy = :empty', { empty: '' })
16+
.where('group.cedarPolicy IS NULL OR group.cedarPolicy = :empty OR group.cedarPolicy LIKE :oldFormat', {
17+
empty: '',
18+
oldFormat: '%principal in RocketAdmin::Group%',
19+
})
1620
.getMany();
1721

1822
for (const group of groups) {

backend/src/entities/demo-data/demo-data.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
22
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
33
import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js';
44
import { BaseType } from '../../common/data-injection.tokens.js';
5+
import { AccessLevelEnum } from '../../enums/access-level.enum.js';
56
import { FilterCriteriaEnum } from '../../enums/filter-criteria.enum.js';
67
import { TableActionEventEnum } from '../../enums/table-action-event-enum.js';
78
import { TableActionTypeEnum } from '../../enums/table-action-type.enum.js';
@@ -27,6 +28,7 @@ import { TableSettingsEntity } from '../table-settings/common-table-settings/tab
2728
import { buildNewTableSettingsEntity } from '../table-settings/common-table-settings/utils/build-new-table-settings-entity.js';
2829
import { buildConnectionEntitiesFromTestDtos } from '../user/utils/build-connection-entities-from-test-dtos.js';
2930
import { buildDefaultAdminGroups } from '../user/utils/build-default-admin-groups.js';
31+
import { generateCedarPolicyForGroup } from '../cedar-authorization/cedar-policy-generator.js';
3032
import { buildDefaultAdminPermissions } from '../user/utils/build-default-admin-permissions.js';
3133
import { CreateTableWidgetDs } from '../widget/application/data-sctructures/create-table-widgets.ds.js';
3234
import { buildNewTableWidgetEntity } from '../widget/utils/build-new-table-widget-entity.js';
@@ -77,6 +79,21 @@ export class DemoDataService {
7779
}),
7880
);
7981

82+
await Promise.all(
83+
createdTestGroups.map(async (group: GroupEntity) => {
84+
const connectionId = group.connection?.id;
85+
if (!connectionId) return;
86+
group.cedarPolicy = generateCedarPolicyForGroup(connectionId, group.isMain, {
87+
connection: { connectionId, accessLevel: AccessLevelEnum.edit },
88+
group: { groupId: group.id, accessLevel: AccessLevelEnum.edit },
89+
tables: [],
90+
});
91+
delete group.permissions;
92+
delete group.users;
93+
await this._dbContext.groupRepository.saveNewOrUpdatedGroup(group);
94+
}),
95+
);
96+
8097
if (!isTest()) {
8198
const createdPostgresConnection = createdTestConnections.find(
8299
(connection) => connection.type === ConnectionTypesEnum.postgres,

backend/src/helpers/constants/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export const Constants = {
225225
if (!isSaaS || isSaaS !== 'true') {
226226
return [];
227227
}
228+
228229
const testConnections: Array<CreateConnectionDto> = Constants.getTestConnectionsFromDSN() || [];
229230
if (!testConnections.length) {
230231
testConnections.push(
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsNumber, IsString, IsUUID, Max, Min } from 'class-validator';
3+
4+
export class CreateConnectionForHostedDbDto {
5+
@ApiProperty({
6+
description: 'Company ID',
7+
example: '123e4567-e89b-12d3-a456-426614174000',
8+
})
9+
@IsString()
10+
@IsNotEmpty()
11+
@IsUUID()
12+
companyId: string;
13+
14+
@ApiProperty({
15+
description: 'User ID',
16+
example: '123e4567-e89b-12d3-a456-426614174000',
17+
})
18+
@IsString()
19+
@IsNotEmpty()
20+
@IsUUID()
21+
userId: string;
22+
23+
@ApiProperty({
24+
description: 'Database name',
25+
example: 'my_database',
26+
})
27+
@IsString()
28+
@IsNotEmpty()
29+
databaseName: string;
30+
31+
@ApiProperty({
32+
description: 'Database hostname',
33+
example: 'localhost',
34+
})
35+
@IsString()
36+
@IsNotEmpty()
37+
hostname: string;
38+
39+
@ApiProperty({
40+
description: 'Database port',
41+
example: 5432,
42+
})
43+
@IsNotEmpty()
44+
@IsNumber()
45+
@Max(65535)
46+
@Min(1)
47+
port: number;
48+
49+
@ApiProperty({
50+
description: 'Database username',
51+
example: 'db_user',
52+
})
53+
@IsString()
54+
@IsNotEmpty()
55+
username: string;
56+
57+
@ApiProperty({
58+
description: 'Database password',
59+
example: 'secure_password',
60+
})
61+
@IsString()
62+
@IsNotEmpty()
63+
password: string;
64+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
3+
4+
export class DeleteConnectionForHostedDbDto {
5+
@ApiProperty({
6+
description: 'Company ID',
7+
example: '123e4567-e89b-12d3-a456-426614174000',
8+
})
9+
@IsNotEmpty()
10+
@IsString()
11+
@IsUUID()
12+
companyId: string;
13+
14+
@ApiProperty({
15+
description: 'Hosted db entity ID',
16+
example: '123e4567-e89b-12d3-a456-426614174000',
17+
})
18+
@IsNotEmpty()
19+
@IsString()
20+
@IsUUID()
21+
hostedDatabaseId: string;
22+
23+
@ApiProperty({
24+
description: 'Database name',
25+
example: 'my_database',
26+
})
27+
databaseName: string;
28+
}

backend/src/microservices/saas-microservice/saas.controller.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { SaasSAMLUserRegisterDS } from './data-structures/saas-saml-user-registe
3131
import { SaasRegisterUserWithGoogleDS } from './data-structures/sass-register-user-with-google.js';
3232
import {
3333
ICompanyRegistration,
34+
ICreateConnectionForHostedDb,
35+
IDeleteConnectionForHostedDb,
3436
IFreezeConnectionsInCompany,
3537
IGetUserInfo,
3638
ILoginUserWithGitHub,
@@ -44,6 +46,9 @@ import {
4446
ISuspendUsers,
4547
ISuspendUsersOverLimit,
4648
} from './use-cases/saas-use-cases.interface.js';
49+
import { CreatedConnectionDTO } from '../../entities/connection/application/dto/created-connection.dto.js';
50+
import { CreateConnectionForHostedDbDto } from './data-structures/create-connecttion-for-selfhosted-db.dto.js';
51+
import { DeleteConnectionForHostedDbDto } from './data-structures/delete-connection-for-hosted-db.dto.js';
4752

4853
@UseInterceptors(SentryInterceptor)
4954
@SkipThrottle()
@@ -82,6 +87,10 @@ export class SaasController {
8287
private readonly freezeConnectionsInCompanyUseCase: IFreezeConnectionsInCompany,
8388
@Inject(UseCaseType.UNFREEZE_CONNECTIONS_IN_COMPANY)
8489
private readonly unfreezeConnectionsInCompanyUseCase: IFreezeConnectionsInCompany,
90+
@Inject(UseCaseType.SAAS_CREATE_CONNECTION_FOR_HOSTED_DB)
91+
private readonly createConnectionForHostedDbUseCase: ICreateConnectionForHostedDb,
92+
@Inject(UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB)
93+
private readonly deleteConnectionForHostedDbUseCase: IDeleteConnectionForHostedDb,
8594
) {}
8695

8796
@ApiOperation({ summary: 'Company registered webhook' })
@@ -274,4 +283,30 @@ export class SaasController {
274283
samlAttributes,
275284
});
276285
}
286+
287+
@ApiOperation({ summary: 'Created connection of hosted database' })
288+
@ApiBody({ type: CreateConnectionForHostedDbDto })
289+
@ApiResponse({
290+
status: 201,
291+
type: CreatedConnectionDTO,
292+
})
293+
@Post('/connection/hosted')
294+
async createConnectionForHostedDb(
295+
@Body() connectionData: CreateConnectionForHostedDbDto,
296+
): Promise<CreatedConnectionDTO> {
297+
return await this.createConnectionForHostedDbUseCase.execute(connectionData);
298+
}
299+
300+
@ApiOperation({ summary: 'Delete connection of hosted database' })
301+
@ApiBody({ type: DeleteConnectionForHostedDbDto })
302+
@ApiResponse({
303+
status: 201,
304+
type: CreatedConnectionDTO,
305+
})
306+
@Post('connection/hosted/delete')
307+
async deleteConnectionForHostedDb(
308+
@Body() deleteConnectionData: DeleteConnectionForHostedDbDto,
309+
): Promise<CreatedConnectionDTO> {
310+
return await this.deleteConnectionForHostedDbUseCase.execute(deleteConnectionData);
311+
}
277312
}

backend/src/microservices/saas-microservice/saas.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { SaaSRegisterUserWIthSamlUseCase } from './use-cases/register-user-with-
2020
import { SaasUsualRegisterUseCase } from './use-cases/saas-usual-register-user.use.case.js';
2121
import { SuspendUsersUseCase } from './use-cases/suspend-users.use.case.js';
2222
import { SuspendUsersOverLimitUseCase } from './use-cases/suspend-users-over-limit.use.case.js';
23+
import { CreateConnectionForHostedDbUseCase } from './use-cases/create-connection-for-hosted-db.use.case.js';
24+
import { DeleteConnectionForHostedDbUseCase } from './use-cases/delete-connection-for-hosted-db.use.case.js';
2325
import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connections-in-company-use.case.js';
2426

2527
@Module({
@@ -85,6 +87,14 @@ import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connec
8587
provide: UseCaseType.SAAS_SUSPEND_USERS_OVER_LIMIT,
8688
useClass: SuspendUsersOverLimitUseCase,
8789
},
90+
{
91+
provide: UseCaseType.SAAS_CREATE_CONNECTION_FOR_HOSTED_DB,
92+
useClass: CreateConnectionForHostedDbUseCase,
93+
},
94+
{
95+
provide: UseCaseType.SAAS_DELETE_CONNECTION_FOR_HOSTED_DB,
96+
useClass: DeleteConnectionForHostedDbUseCase,
97+
},
8898
SignInAuditService,
8999
],
90100
controllers: [SaasController],
@@ -108,6 +118,8 @@ export class SaasModule {
108118
{ path: 'saas/company/freeze-connections', method: RequestMethod.PUT },
109119
{ path: 'saas/company/unfreeze-connections', method: RequestMethod.PUT },
110120
{ path: 'saas/user/saml/login', method: RequestMethod.POST },
121+
{ path: 'saas/connection/hosted', method: RequestMethod.POST },
122+
{ path: 'saas/connection/hosted/delete', method: RequestMethod.POST },
111123
);
112124
}
113125
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Inject, Injectable, InternalServerErrorException, Scope } from '@nestjs/common';
2+
import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js';
3+
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
4+
import AbstractUseCase from '../../../common/abstract-use.case.js';
5+
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
6+
import { BaseType } from '../../../common/data-injection.tokens.js';
7+
import { Messages } from '../../../exceptions/text/messages.js';
8+
import { slackPostMessage } from '../../../helpers/index.js';
9+
import { AccessLevelEnum } from '../../../enums/index.js';
10+
import { generateCedarPolicyForGroup } from '../../../entities/cedar-authorization/cedar-policy-generator.js';
11+
import { CreatedConnectionDTO } from '../../../entities/connection/application/dto/created-connection.dto.js';
12+
import { ConnectionEntity } from '../../../entities/connection/connection.entity.js';
13+
import { readSslCertificate } from '../../../entities/connection/ssl-certificate/read-certificate.js';
14+
import { buildCreatedConnectionDs } from '../../../entities/connection/utils/build-created-connection.ds.js';
15+
import { CreateConnectionForHostedDbDto } from '../data-structures/create-connecttion-for-selfhosted-db.dto.js';
16+
import { ICreateConnectionForHostedDb } from './saas-use-cases.interface.js';
17+
18+
@Injectable({ scope: Scope.REQUEST })
19+
export class CreateConnectionForHostedDbUseCase
20+
extends AbstractUseCase<CreateConnectionForHostedDbDto, CreatedConnectionDTO>
21+
implements ICreateConnectionForHostedDb
22+
{
23+
constructor(
24+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
25+
protected _dbContext: IGlobalDatabaseContext,
26+
) {
27+
super();
28+
}
29+
30+
protected async implementation(inputData: CreateConnectionForHostedDbDto): Promise<CreatedConnectionDTO> {
31+
const { companyId, userId, databaseName, hostname, port, username, password } = inputData;
32+
33+
const connectionAuthor = await this._dbContext.userRepository.findOneUserById(userId);
34+
if (!connectionAuthor) {
35+
throw new InternalServerErrorException(Messages.USER_NOT_FOUND);
36+
}
37+
38+
await slackPostMessage(
39+
Messages.USER_TRY_CREATE_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres),
40+
);
41+
42+
const cert = await readSslCertificate();
43+
44+
const connectionParams = {
45+
type: ConnectionTypesEnum.postgres,
46+
host: hostname,
47+
port: port,
48+
username: username,
49+
password: password,
50+
database: databaseName,
51+
schema: null,
52+
sid: null,
53+
ssh: false,
54+
privateSSHKey: null,
55+
sshHost: null,
56+
sshPort: null,
57+
sshUsername: null,
58+
ssl: true,
59+
cert: cert,
60+
azure_encryption: false,
61+
authSource: null,
62+
dataCenter: null,
63+
};
64+
65+
const dao = getDataAccessObject(connectionParams);
66+
await dao.testConnect();
67+
68+
const connection = new ConnectionEntity();
69+
connection.type = ConnectionTypesEnum.postgres;
70+
connection.host = hostname;
71+
connection.port = port;
72+
connection.username = username;
73+
connection.password = password;
74+
connection.database = databaseName;
75+
connection.ssl = true;
76+
connection.cert = cert;
77+
connection.ssh = false;
78+
connection.azure_encryption = false;
79+
connection.masterEncryption = false;
80+
connection.author = connectionAuthor;
81+
82+
const savedConnection = await this._dbContext.connectionRepository.saveNewConnection(connection);
83+
84+
const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection(
85+
savedConnection,
86+
connectionAuthor,
87+
);
88+
await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup);
89+
createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup(
90+
savedConnection.id,
91+
true,
92+
{
93+
connection: { connectionId: savedConnection.id, accessLevel: AccessLevelEnum.edit },
94+
group: { groupId: createdAdminGroup.id, accessLevel: AccessLevelEnum.edit },
95+
tables: [],
96+
},
97+
);
98+
await this._dbContext.groupRepository.saveNewOrUpdatedGroup(createdAdminGroup);
99+
delete createdAdminGroup.connection;
100+
await this._dbContext.userRepository.saveUserEntity(connectionAuthor);
101+
savedConnection.groups = [createdAdminGroup];
102+
103+
const foundCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByCompanyIdWithoutConnections(companyId);
104+
if (foundCompany) {
105+
const connectionToUpdate = await this._dbContext.connectionRepository.findOne({
106+
where: { id: savedConnection.id },
107+
});
108+
connectionToUpdate.company = foundCompany;
109+
await this._dbContext.connectionRepository.saveUpdatedConnection(connectionToUpdate);
110+
}
111+
112+
await slackPostMessage(
113+
Messages.USER_CREATED_CONNECTION(connectionAuthor.email, ConnectionTypesEnum.postgres),
114+
);
115+
116+
const connectionRO = buildCreatedConnectionDs(savedConnection, null, null);
117+
return connectionRO;
118+
}
119+
}

0 commit comments

Comments
 (0)