From b2c17c1a225f4eb874357355846ee62740c386bd Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 20 Mar 2026 12:58:07 +0000 Subject: [PATCH 1/5] refactor: remove unused permission methods and clean up repository interfaces --- .../permission-custom-repository-extension.ts | 42 -- .../permission.repository.interface.ts | 5 - ...user-access-custom-repository-extension.ts | 473 ------------------ 3 files changed, 520 deletions(-) delete mode 100644 backend/src/entities/user-access/repository/user-access-custom-repository-extension.ts diff --git a/backend/src/entities/permission/repository/permission-custom-repository-extension.ts b/backend/src/entities/permission/repository/permission-custom-repository-extension.ts index f663aa429..df9df4121 100644 --- a/backend/src/entities/permission/repository/permission-custom-repository-extension.ts +++ b/backend/src/entities/permission/repository/permission-custom-repository-extension.ts @@ -1,7 +1,6 @@ import { AccessLevelEnum } from '../../../enums/access-level.enum.js'; import { PermissionTypeEnum } from '../../../enums/permission-type.enum.js'; import { GroupEntity } from '../../group/group.entity.js'; -import { TablePermissionDs } from '../application/data-structures/create-permissions.ds.js'; import { PermissionEntity } from '../permission.entity.js'; export const permissionCustomRepositoryExtension = { @@ -75,33 +74,6 @@ export const permissionCustomRepositoryExtension = { return groupAccessLevel; }, - async getGroupPermissionsForTable( - connectionId: string, - groupId: string, - tableName: string, - ): Promise { - const tableQb = this.createQueryBuilder('permission') - .leftJoinAndSelect('permission.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Table, - }) - .andWhere('permission.tableName = :tableName', { tableName: tableName }); - const tablePermissions: Array = await tableQb.getMany(); - return { - tableName: tableName, - accessLevel: { - add: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.add), - delete: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.delete), - edit: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.edit), - readonly: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.readonly), - visibility: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.visibility), - }, - }; - }, - async getGroupPermissionsForAllTables(connectionId: string, groupId: string): Promise> { const tableQb = this.createQueryBuilder('permission') .leftJoinAndSelect('permission.groups', 'group') @@ -138,20 +110,6 @@ export const permissionCustomRepositoryExtension = { return await groupQb.getOne(); }, - async getAllUserPermissionsForAllTablesInConnection( - userId: string, - connectionId: string, - ): Promise> { - const qb = this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('user.id = :userId', { userId: userId }) - .andWhere('permission.type = :permissionType', { permissionType: PermissionTypeEnum.Table }); - return await qb.getMany(); - }, - async removePermissionEntity(permission: PermissionEntity): Promise { return await this.remove(permission); }, diff --git a/backend/src/entities/permission/repository/permission.repository.interface.ts b/backend/src/entities/permission/repository/permission.repository.interface.ts index 0a206980a..458d208de 100644 --- a/backend/src/entities/permission/repository/permission.repository.interface.ts +++ b/backend/src/entities/permission/repository/permission.repository.interface.ts @@ -1,6 +1,5 @@ import { AccessLevelEnum } from '../../../enums/index.js'; import { GroupEntity } from '../../group/group.entity.js'; -import { TablePermissionDs } from '../application/data-structures/create-permissions.ds.js'; import { PermissionEntity } from '../permission.entity.js'; export interface IPermissionRepository { @@ -12,8 +11,6 @@ export interface IPermissionRepository { getGroupPermissionsForGroup(connectionId: string, groupId: string): Promise; - getGroupPermissionsForTable(connectionId: string, groupId: string, tableName: string): Promise; - getGroupPermissionsForAllTables(connectionId: string, groupId: string): Promise>; getPermissionEntityForConnection(connectionId: string, groupId: string): Promise; @@ -21,6 +18,4 @@ export interface IPermissionRepository { getPermissionEntityForGroup(connectionId: string, groupId: string): Promise; removePermissionEntity(permission: PermissionEntity): Promise; - - getAllUserPermissionsForAllTablesInConnection(userId: string, connectionId: string): Promise>; } diff --git a/backend/src/entities/user-access/repository/user-access-custom-repository-extension.ts b/backend/src/entities/user-access/repository/user-access-custom-repository-extension.ts deleted file mode 100644 index e2dc2c03b..000000000 --- a/backend/src/entities/user-access/repository/user-access-custom-repository-extension.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; -import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; -import { Messages } from '../../../exceptions/text/messages.js'; -import { Cacher } from '../../../helpers/cache/cacher.js'; -import { ConnectionEntity } from '../../connection/connection.entity.js'; -import { PermissionEntity } from '../../permission/permission.entity.js'; -import { ITablePermissionData } from '../../permission/permission.interface.js'; -import { UserEntity } from '../../user/user.entity.js'; -import { IUserAccessRepository } from './user-access.repository.interface.js'; - -export const userAccessCustomReposiotoryExtension: IUserAccessRepository = { - async getUserConnectionAccessLevel(cognitoUserName: string, connectionId: string): Promise { - const qb = this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND user.suspended = :isSuspended AND permission.type = :permissionType', - { - connectionId, - cognitoUserName, - isSuspended: false, - permissionType: PermissionTypeEnum.Connection, - }, - ); - - const resultPermissions = await qb.getMany(); - - if (resultPermissions.length === 0) { - const isUserSuspended = !!(await this.manager - .getRepository(UserEntity) - .createQueryBuilder('user') - .where('user.id = :cognitoUserName AND user.suspended = :isSuspended', { - cognitoUserName, - isSuspended: true, - }) - .getCount()); - - if (isUserSuspended) { - throw new HttpException( - { - message: Messages.ACCOUNT_SUSPENDED, - }, - HttpStatus.FORBIDDEN, - ); - } - return AccessLevelEnum.none; - } - - for (const permission of resultPermissions) { - if (permission.accessLevel === AccessLevelEnum.edit) { - return AccessLevelEnum.edit; - } - } - - for (const permission of resultPermissions) { - if (permission.accessLevel === AccessLevelEnum.readonly) { - return AccessLevelEnum.readonly; - } - } - return AccessLevelEnum.none; - }, - - async checkUserConnectionRead(cognitoUserName: string, connectionId: string): Promise { - const connectionAccessLevel = await this.getUserConnectionAccessLevel(cognitoUserName, connectionId); - return connectionAccessLevel === AccessLevelEnum.edit || connectionAccessLevel === AccessLevelEnum.readonly; - }, - - async checkUserConnectionEdit(cognitoUserName: string, connectionId: string): Promise { - const connectionAccessLevel = await this.getUserConnectionAccessLevel(cognitoUserName, connectionId); - return connectionAccessLevel === AccessLevelEnum.edit; - }, - - async getGroupAccessLevel(cognitoUserName: string, groupId: string): Promise { - const connectionId = await this.getConnectionId(groupId); - - const connectionEdit = !!(await this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND user.suspended = :isSuspended AND permission.type = :permissionType AND permission.accessLevel = :accessLevel', - { - connectionId, - cognitoUserName, - isSuspended: false, - permissionType: PermissionTypeEnum.Connection, - accessLevel: AccessLevelEnum.edit, - }, - ) - .getCount()); - - if (connectionEdit) { - return AccessLevelEnum.edit; - } - const qb = this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND user.suspended = :isSuspended AND permission.type = :permissionType AND group.id = :groupId', - { - connectionId, - cognitoUserName, - isSuspended: false, - permissionType: PermissionTypeEnum.Group, - groupId, - }, - ); - - const resultPermissions = await qb.getMany(); - - if (resultPermissions.length === 0) { - const isUserSuspended = !!(await this.manager - .getRepository(UserEntity) - .createQueryBuilder('user') - .where('user.id = :cognitoUserName AND user.suspended = :isSuspended', { - cognitoUserName, - isSuspended: true, - }) - .getCount()); - - if (isUserSuspended) { - throw new HttpException( - { - message: Messages.ACCOUNT_SUSPENDED, - }, - HttpStatus.FORBIDDEN, - ); - } - return AccessLevelEnum.none; - } - - const connectionAccessLevels = resultPermissions.map((permission: PermissionEntity) => { - return permission.accessLevel.toLowerCase(); - }); - - if (connectionAccessLevels.includes(AccessLevelEnum.edit)) { - return AccessLevelEnum.edit; - } - - if (connectionAccessLevels.includes(AccessLevelEnum.readonly)) { - return AccessLevelEnum.readonly; - } - - return AccessLevelEnum.none; - }, - - async checkUserGroupRead(cognitoUserName: string, groupId: string): Promise { - const userGroupAccessLevel = await this.getGroupAccessLevel(cognitoUserName, groupId); - return userGroupAccessLevel === AccessLevelEnum.edit || userGroupAccessLevel === AccessLevelEnum.readonly; - }, - - async checkUserGroupEdit(cognitoUserName: string, groupId: string): Promise { - const userGroupAccessLevel = await this.getGroupAccessLevel(cognitoUserName, groupId); - return userGroupAccessLevel === AccessLevelEnum.edit; - }, - - async getUserTablePermissions( - cognitoUserName: string, - connectionId: string, - tableName: string, - _masterPwd: string, - ): Promise { - const connectionEdit = !!(await this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND user.suspended = :isSuspended AND permission.type = :permissionType AND permission.accessLevel = :accessLevel', - { - connectionId, - cognitoUserName, - isSuspended: false, - permissionType: PermissionTypeEnum.Connection, - accessLevel: AccessLevelEnum.edit, - }, - ) - .getCount()); - - if (connectionEdit) { - return { - tableName: tableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }; - } - - const qb = this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND user.suspended = :isSuspended AND permission.type = :permissionType AND permission.tableName = :tableName', - { - connectionId, - cognitoUserName, - isSuspended: false, - permissionType: PermissionTypeEnum.Table, - tableName, - }, - ); - - const resultPermissions: Array = await qb.getMany(); - - if (resultPermissions.length === 0) { - const isUserSuspended = !!(await this.manager - .getRepository(UserEntity) - .createQueryBuilder('user') - .where('user.id = :cognitoUserName AND user.suspended = :isSuspended', { - cognitoUserName, - isSuspended: true, - }) - .getCount()); - - if (isUserSuspended) { - throw new HttpException( - { - message: Messages.ACCOUNT_SUSPENDED, - }, - HttpStatus.FORBIDDEN, - ); - } - } - - const accessLevels = { - visibility: false, - readonly: false, - add: false, - delete: false, - edit: false, - }; - - for (const permission of resultPermissions) { - switch (permission.accessLevel) { - case AccessLevelEnum.visibility: - accessLevels.visibility = true; - break; - case AccessLevelEnum.readonly: - accessLevels.readonly = true; - break; - case AccessLevelEnum.add: - accessLevels.add = true; - break; - case AccessLevelEnum.delete: - accessLevels.delete = true; - break; - case AccessLevelEnum.edit: - accessLevels.edit = true; - break; - } - } - - return { - tableName: tableName, - accessLevel: accessLevels, - }; - }, - - async getUserPermissionsForAvailableTables( - cognitoUserName: string, - connectionId: string, - tableNames: Array, - ): Promise> { - const connectionEdit = !!(await this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND permission.type = :permissionType AND permission.accessLevel = :accessLevel', - { - connectionId, - cognitoUserName, - permissionType: PermissionTypeEnum.Connection, - accessLevel: AccessLevelEnum.edit, - }, - ) - .getCount()); - - const tablesWithPermissionsArr = []; - if (connectionEdit) { - for (const tableName of tableNames) { - tablesWithPermissionsArr.push({ - tableName: tableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }); - } - return tablesWithPermissionsArr; - } - - const qb = this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where( - 'connection.id = :connectionId AND user.id = :cognitoUserName AND user.suspended = :isSuspended AND permission.type = :permissionType', - { - connectionId, - cognitoUserName, - isSuspended: false, - permissionType: PermissionTypeEnum.Table, - }, - ); - - const allTablePermissions: Array = await qb.getMany(); - - if (allTablePermissions.length === 0) { - const isUserSuspended = !!(await this.manager - .getRepository(UserEntity) - .createQueryBuilder('user') - .where('user.id = :cognitoUserName AND user.suspended = :isSuspended', { - cognitoUserName, - isSuspended: true, - }) - .getCount()); - - if (isUserSuspended) { - throw new HttpException( - { - message: Messages.ACCOUNT_SUSPENDED, - }, - HttpStatus.FORBIDDEN, - ); - } - } - - const tablesAndAccessLevels = new Map>(); - - for (const tableName of tableNames) { - if (tableName !== '__proto__') { - tablesAndAccessLevels.set(tableName, new Set()); - } - } - - for (const permission of allTablePermissions) { - const { tableName, accessLevel } = permission; - if (tablesAndAccessLevels.has(tableName)) { - tablesAndAccessLevels.get(tableName)!.add(accessLevel as AccessLevelEnum); - } - } - - for (const [tableName, accessLevels] of tablesAndAccessLevels) { - const accessLevelObj = { - visibility: accessLevels.has(AccessLevelEnum.visibility), - readonly: accessLevels.has(AccessLevelEnum.readonly), - add: accessLevels.has(AccessLevelEnum.add), - delete: accessLevels.has(AccessLevelEnum.delete), - edit: accessLevels.has(AccessLevelEnum.edit), - }; - - tablesWithPermissionsArr.push({ - tableName, - accessLevel: accessLevelObj, - }); - } - - return tablesWithPermissionsArr.filter((tableWithPermission: ITablePermissionData) => { - return tableWithPermission.accessLevel.visibility; - }); - }, - - async checkTableRead( - cognitoUserName: string, - connectionId: string, - tableName: string, - masterPwd: string, - ): Promise { - const { accessLevel } = await this.getUserTablePermissions(cognitoUserName, connectionId, tableName, masterPwd); - return accessLevel.visibility || accessLevel.add || accessLevel.delete || accessLevel.edit; - }, - - async checkTableAdd( - cognitoUserName: string, - connectionId: string, - tableName: string, - masterPwd: string, - ): Promise { - const { accessLevel } = await this.getUserTablePermissions(cognitoUserName, connectionId, tableName, masterPwd); - return accessLevel.visibility && accessLevel.add; - }, - - async checkTableDelete( - cognitoUserName: string, - connectionId: string, - tableName: string, - masterPwd: string, - ): Promise { - const { accessLevel } = await this.getUserTablePermissions(cognitoUserName, connectionId, tableName, masterPwd); - return accessLevel.visibility && accessLevel.delete; - }, - - async checkTableEdit( - cognitoUserName: string, - connectionId: string, - tableName: string, - masterPwd: string, - ): Promise { - const { accessLevel } = await this.getUserTablePermissions(cognitoUserName, connectionId, tableName, masterPwd); - return accessLevel.visibility && accessLevel.edit; - }, - - async getConnectionId(groupId: string): Promise { - const connectionRepository = this.manager.getRepository(ConnectionEntity); - const connection = await connectionRepository - .createQueryBuilder('connection') - .leftJoin('connection.groups', 'group') - .where('group.id = :id', { id: groupId }) - .getOne(); - - if (!connection) { - throw new HttpException({ message: Messages.CONNECTION_NOT_FOUND }, HttpStatus.BAD_REQUEST); - } - - return connection.id; - }, - - async improvedCheckTableRead(userId: string, connectionId: string, tableName: string): Promise { - const cachedReadPermission: boolean | null = Cacher.getUserTableReadPermissionCache( - userId, - connectionId, - tableName, - ); - if (cachedReadPermission !== null) { - return cachedReadPermission; - } - - const qb = this.createQueryBuilder('permission') - .leftJoin('permission.groups', 'group') - .leftJoin('group.users', 'user') - .leftJoin('group.connection', 'connection') - .where('connection.id = :connectionId AND user.id = :userId AND user.suspended = :isSuspended', { - connectionId, - userId, - isSuspended: false, - }); - - const allUserPermissions: Array = await qb.getMany(); - - if (allUserPermissions.length === 0) { - Cacher.setUserTableReadPermissionCache(userId, connectionId, tableName, false); - return false; - } - - const hasReadPermission = allUserPermissions.some( - (permission) => - (permission.type === PermissionTypeEnum.Connection && permission.accessLevel === AccessLevelEnum.edit) || - (permission.type === PermissionTypeEnum.Table && - permission.tableName === tableName && - [AccessLevelEnum.visibility, AccessLevelEnum.add, AccessLevelEnum.delete, AccessLevelEnum.edit].includes( - permission.accessLevel as AccessLevelEnum, - )), - ); - - if (hasReadPermission) { - Cacher.setUserTableReadPermissionCache(userId, connectionId, tableName, true); - return true; - } - - Cacher.setUserTableReadPermissionCache(userId, connectionId, tableName, false); - return false; - }, -}; From e03bf6d7172b0fbba5c77f5d51e3438ac4e096c9 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 20 Mar 2026 18:01:39 +0000 Subject: [PATCH 2/5] refactor: streamline permission handling and remove deprecated methods --- .../cedar-authorization.service.ts | 74 ---------- .../cedar-policy-parser.ts | 5 +- .../use-cases/create-connection.use.case.ts | 1 - .../find-all-connections.use.case.ts | 15 +- ...ssions-for-group-in-connection.use.case.ts | 53 ++++--- .../entities/demo-data/demo-data.service.ts | 11 -- .../create-or-update-permissions.use.case.ts | 136 ++---------------- .../utils/build-final-tables-permissions.ts | 50 ------- .../build-new-permission-entity-connection.ts | 9 -- .../build-new-permission-entity-group.ts | 9 -- .../utils/build-default-admin-permissions.ts | 25 ---- ...reate-connection-for-hosted-db.use.case.ts | 1 - .../non-saas-cedar-policy-parser.test.ts | 2 +- .../non-saas-cedar-save-policy-e2e.test.ts | 2 +- ...cedar-migration-existing-users-e2e.test.ts | 22 +-- .../saas-cedar-save-policy-e2e.test.ts | 2 +- 16 files changed, 72 insertions(+), 345 deletions(-) delete mode 100644 backend/src/entities/permission/utils/build-final-tables-permissions.ts delete mode 100644 backend/src/entities/permission/utils/build-new-permission-entity-connection.ts delete mode 100644 backend/src/entities/permission/utils/build-new-permission-entity-group.ts delete mode 100644 backend/src/entities/user/utils/build-default-admin-permissions.ts diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts index a8f6303a4..4c05429d6 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts @@ -1,12 +1,10 @@ import { HttpException, HttpStatus, Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { AccessLevelEnum, PermissionTypeEnum } from '../../enums/index.js'; import { Messages } from '../../exceptions/text/messages.js'; import { Cacher } from '../../helpers/cache/cacher.js'; import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js'; import { BaseType } from '../../common/data-injection.tokens.js'; import { GroupEntity } from '../group/group.entity.js'; import { IComplexPermission } from '../permission/permission.interface.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { CedarAction, CedarResourceType, @@ -106,8 +104,6 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On const classicalPermissions = parseCedarPolicyToClassicalPermissions(cedarPolicy, connectionId, groupId); - await this.syncClassicalPermissions(group, classicalPermissions); - group.cedarPolicy = cedarPolicy; await this.globalDbContext.groupRepository.saveNewOrUpdatedGroup(group); Cacher.invalidateCedarPolicyCache(connectionId); @@ -316,74 +312,4 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On } } - private async syncClassicalPermissions(group: GroupEntity, permissions: IComplexPermission): Promise { - if (group.permissions && group.permissions.length > 0) { - for (const perm of group.permissions) { - await this.globalDbContext.permissionRepository.removePermissionEntity(perm); - } - } - group.permissions = []; - - if (permissions.connection.accessLevel !== AccessLevelEnum.none) { - const connPerm = new PermissionEntity(); - connPerm.type = PermissionTypeEnum.Connection; - connPerm.accessLevel = permissions.connection.accessLevel; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(connPerm); - group.permissions.push(saved); - } - - if (permissions.group.accessLevel !== AccessLevelEnum.none) { - const groupPerm = new PermissionEntity(); - groupPerm.type = PermissionTypeEnum.Group; - groupPerm.accessLevel = permissions.group.accessLevel; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(groupPerm); - group.permissions.push(saved); - } - - for (const table of permissions.tables) { - const access = table.accessLevel; - if (access.visibility) { - const perm = new PermissionEntity(); - perm.type = PermissionTypeEnum.Table; - perm.accessLevel = AccessLevelEnum.visibility; - perm.tableName = table.tableName; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(perm); - group.permissions.push(saved); - } - if (access.readonly) { - const perm = new PermissionEntity(); - perm.type = PermissionTypeEnum.Table; - perm.accessLevel = AccessLevelEnum.readonly; - perm.tableName = table.tableName; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(perm); - group.permissions.push(saved); - } - if (access.add) { - const perm = new PermissionEntity(); - perm.type = PermissionTypeEnum.Table; - perm.accessLevel = AccessLevelEnum.add; - perm.tableName = table.tableName; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(perm); - group.permissions.push(saved); - } - if (access.edit) { - const perm = new PermissionEntity(); - perm.type = PermissionTypeEnum.Table; - perm.accessLevel = AccessLevelEnum.edit; - perm.tableName = table.tableName; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(perm); - group.permissions.push(saved); - } - if (access.delete) { - const perm = new PermissionEntity(); - perm.type = PermissionTypeEnum.Table; - perm.accessLevel = AccessLevelEnum.delete; - perm.tableName = table.tableName; - const saved = await this.globalDbContext.permissionRepository.saveNewOrUpdatedPermission(perm); - group.permissions.push(saved); - } - } - - await this.globalDbContext.groupRepository.saveNewOrUpdatedGroup(group); - } } diff --git a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts index 7e08cfd2f..a852fe070 100644 --- a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts +++ b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts @@ -79,6 +79,10 @@ export function parseCedarPolicyToClassicalPermissions( } result.tables = Array.from(tableMap.values()); + for (const table of result.tables) { + const a = table.accessLevel; + a.readonly = a.visibility && !a.add && !a.edit && !a.delete; + } result.dashboards = Array.from(dashboardMap.values()); return result; @@ -209,7 +213,6 @@ function applyTableAction(entry: ITablePermissionData, action: string): void { switch (action) { case 'table:read': entry.accessLevel.visibility = true; - entry.accessLevel.readonly = true; break; case 'table:add': entry.accessLevel.add = true; diff --git a/backend/src/entities/connection/use-cases/create-connection.use.case.ts b/backend/src/entities/connection/use-cases/create-connection.use.case.ts index c62257c4c..95f352e7e 100644 --- a/backend/src/entities/connection/use-cases/create-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/create-connection.use.case.ts @@ -99,7 +99,6 @@ export class CreateConnectionUseCase savedConnection, connectionAuthor, ); - await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup( savedConnection.id, true, diff --git a/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts b/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts index b1e5ad45e..98933df6c 100644 --- a/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts +++ b/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts @@ -8,14 +8,13 @@ import { isSaaS } from '../../../helpers/app/is-saas.js'; import { Constants } from '../../../helpers/constants/constants.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; +import { generateCedarPolicyForGroup } from '../../cedar-authorization/cedar-policy-generator.js'; import { GroupEntity } from '../../group/group.entity.js'; -import { PermissionEntity } from '../../permission/permission.entity.js'; import { CreateUserDs } from '../../user/application/data-structures/create-user.ds.js'; import { FindUserDs } from '../../user/application/data-structures/find-user.ds.js'; import { UserRoleEnum } from '../../user/enums/user-role.enum.js'; import { buildConnectionEntitiesFromTestDtos } from '../../user/utils/build-connection-entities-from-test-dtos.js'; import { buildDefaultAdminGroups } from '../../user/utils/build-default-admin-groups.js'; -import { buildDefaultAdminPermissions } from '../../user/utils/build-default-admin-permissions.js'; import { FoundConnectionsDs } from '../application/data-structures/found-connections.ds.js'; import { ConnectionEntity } from '../connection.entity.js'; import { buildFoundConnectionDs } from '../utils/build-found-connection.ds.js'; @@ -76,10 +75,16 @@ export class FindAllConnectionsUseCase return await this._dbContext.groupRepository.saveNewOrUpdatedGroup(group); }), ); - const testPermissionsEntities = buildDefaultAdminPermissions(createdTestGroups); await Promise.all( - testPermissionsEntities.map(async (permission: PermissionEntity) => { - await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(permission); + createdTestGroups.map(async (group: GroupEntity) => { + const connectionId = group.connection?.id; + if (!connectionId) return; + group.cedarPolicy = generateCedarPolicyForGroup(connectionId, group.isMain, { + connection: { connectionId, accessLevel: AccessLevelEnum.edit }, + group: { groupId: group.id, accessLevel: AccessLevelEnum.edit }, + tables: [], + }); + await this._dbContext.groupRepository.saveNewOrUpdatedGroup(group); }), ); allFoundUserTestConnections.push(...createdTestConnections); diff --git a/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts b/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts index 32962d25e..6cb54316e 100644 --- a/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts @@ -5,6 +5,7 @@ import { IGlobalDatabaseContext } from '../../../common/application/global-datab import { BaseType } from '../../../common/data-injection.tokens.js'; import { AccessLevelEnum } from '../../../enums/access-level.enum.js'; import { TablePermissionDs } from '../../permission/application/data-structures/create-permissions.ds.js'; +import { parseCedarPolicyToClassicalPermissions } from '../../cedar-authorization/cedar-policy-parser.js'; import { FoundPermissionsInConnectionDs } from '../application/data-structures/found-permissions-in-connection.ds.js'; import { GetPermissionsInConnectionDs } from '../application/data-structures/get-permissions-in-connection.ds.js'; import { IGetPermissionsForGroupInConnection } from './use-cases.interfaces.js'; @@ -22,14 +23,25 @@ export class GetPermissionsForGroupInConnectionUseCase } protected async implementation(inputData: GetPermissionsInConnectionDs): Promise { - const groupPermissionForConnection = await this._dbContext.permissionRepository.getGroupPermissionForConnection( - inputData.connectionId, - inputData.groupId, - ); - const groupPermissionForGroup = await this._dbContext.permissionRepository.getGroupPermissionsForGroup( - inputData.connectionId, - inputData.groupId, - ); + const group = await this._dbContext.groupRepository.findGroupWithPermissionsById(inputData.groupId); + + let connectionAccessLevel = AccessLevelEnum.none; + let groupAccessLevel = AccessLevelEnum.none; + const tablePermissionsMap = new Map(); + + if (group?.cedarPolicy) { + const parsed = parseCedarPolicyToClassicalPermissions( + group.cedarPolicy, + inputData.connectionId, + inputData.groupId, + ); + connectionAccessLevel = parsed.connection.accessLevel; + groupAccessLevel = parsed.group.accessLevel; + for (const table of parsed.tables) { + tablePermissionsMap.set(table.tableName, table); + } + } + const connection = await this._dbContext.connectionRepository.findAndDecryptConnection( inputData.connectionId, inputData.masterPwd, @@ -37,35 +49,34 @@ export class GetPermissionsForGroupInConnectionUseCase const dao = getDataAccessObject(connection); const tables: Array = (await dao.getTablesFromDB()).map((table) => table.tableName); - const allTablePermissions = await this._dbContext.permissionRepository.getGroupPermissionsForAllTables( - inputData.connectionId, - inputData.groupId, - ); - const tablesWithAccessLevels: Array = tables.map((tableName) => { - const tablePermissions = allTablePermissions.filter((p) => p.tableName === tableName); + const existing = tablePermissionsMap.get(tableName); + if (existing) { + return existing; + } return { tableName, accessLevel: { - add: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.add), - delete: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.delete), - edit: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.edit), - readonly: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.readonly), - visibility: !!tablePermissions.find((el) => el.accessLevel === AccessLevelEnum.visibility), + add: false, + delete: false, + edit: false, + readonly: false, + visibility: false, }, }; }); + const allTableSettingsInConnection = await this._dbContext.tableSettingsRepository.findTableSettingsInConnection( inputData.connectionId, ); return { connection: { connectionId: inputData.connectionId, - accessLevel: groupPermissionForConnection, + accessLevel: connectionAccessLevel, }, group: { groupId: inputData.groupId, - accessLevel: groupPermissionForGroup, + accessLevel: groupAccessLevel, }, tables: tablesWithAccessLevels.map((table) => { const tableSettings = allTableSettingsInConnection.find( diff --git a/backend/src/entities/demo-data/demo-data.service.ts b/backend/src/entities/demo-data/demo-data.service.ts index 75082a80c..b3c546ec8 100644 --- a/backend/src/entities/demo-data/demo-data.service.ts +++ b/backend/src/entities/demo-data/demo-data.service.ts @@ -14,7 +14,6 @@ import { CreateConnectionPropertiesDs } from '../connection-properties/applicati import { ConnectionPropertiesEntity } from '../connection-properties/connection-properties.entity.js'; import { buildConnectionPropertiesEntity } from '../connection-properties/utils/build-connection-properties-entity.js'; import { GroupEntity } from '../group/group.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { buildActionEventWithRule } from '../table-actions/table-action-events-module/utils/build-action-event-with-rule.util.js'; import { CreateRuleDataDs, @@ -29,7 +28,6 @@ import { buildNewTableSettingsEntity } from '../table-settings/common-table-sett import { buildConnectionEntitiesFromTestDtos } from '../user/utils/build-connection-entities-from-test-dtos.js'; import { buildDefaultAdminGroups } from '../user/utils/build-default-admin-groups.js'; import { generateCedarPolicyForGroup } from '../cedar-authorization/cedar-policy-generator.js'; -import { buildDefaultAdminPermissions } from '../user/utils/build-default-admin-permissions.js'; import { CreateTableWidgetDs } from '../widget/application/data-sctructures/create-table-widgets.ds.js'; import { buildNewTableWidgetEntity } from '../widget/utils/build-new-table-widget-entity.js'; @@ -72,13 +70,6 @@ export class DemoDataService { return await this._dbContext.groupRepository.saveNewOrUpdatedGroup(group); }), ); - const testPermissionsEntities = buildDefaultAdminPermissions(createdTestGroups); - await Promise.all( - testPermissionsEntities.map(async (permission: PermissionEntity) => { - await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(permission); - }), - ); - await Promise.all( createdTestGroups.map(async (group: GroupEntity) => { const connectionId = group.connection?.id; @@ -88,8 +79,6 @@ export class DemoDataService { group: { groupId: group.id, accessLevel: AccessLevelEnum.edit }, tables: [], }); - delete group.permissions; - delete group.users; await this._dbContext.groupRepository.saveNewOrUpdatedGroup(group); }), ); diff --git a/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts b/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts index 5ff56d044..6868c70bc 100644 --- a/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts +++ b/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts @@ -2,20 +2,13 @@ import { HttpException, HttpStatus, Inject, Injectable } 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 { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; import { Messages } from '../../../exceptions/text/messages.js'; -import { isObjectPropertyExists } from '../../../helpers/validators/is-object-property-exists-validator.js'; import { CreatePermissionsDs, PermissionsDs, - TablePermissionDs, } from '../application/data-structures/create-permissions.ds.js'; -import { PermissionEntity } from '../permission.entity.js'; import { generateCedarPolicyForGroup } from '../../cedar-authorization/cedar-policy-generator.js'; import { Cacher } from '../../../helpers/cache/cacher.js'; -import { buildFinalTablesPermissions } from '../utils/build-final-tables-permissions.js'; -import { buildNewPermissionEntityConnection } from '../utils/build-new-permission-entity-connection.js'; -import { buildNewPermissionEntityGroup } from '../utils/build-new-permission-entity-group.js'; import { ICreateOrUpdatePermissions } from './permissions-use-cases.interface.js'; @Injectable() @@ -31,18 +24,6 @@ export class CreateOrUpdatePermissionsUseCase } protected async implementation(inputData: CreatePermissionsDs): Promise { - const resultPermissions: PermissionsDs = { - connection: { - accessLevel: AccessLevelEnum.none, - connectionId: inputData.permissions.connection.connectionId, - }, - group: { - accessLevel: AccessLevelEnum.none, - groupId: inputData.permissions.group.groupId, - }, - tables: [], - }; - const { groupId, permissions: { @@ -77,116 +58,17 @@ export class CreateOrUpdatePermissionsUseCase ); } - const [currentConnectionPermission, currentGroupPermission]: Array = await Promise.all([ - (async () => { - return await this._dbContext.permissionRepository.getPermissionEntityForConnection(connectionId, groupId); - })(), - (async () => { - return await this._dbContext.permissionRepository.getPermissionEntityForGroup(connectionId, groupId); - })(), - ]); - const allTablePermissionsInGroup = await this._dbContext.permissionRepository.getGroupPermissionsForAllTables( - connectionId, - groupId, - ); - // *** CONNECTION PERMISSION - if (currentConnectionPermission) { - const updatedPermissionData = { - type: PermissionTypeEnum.Connection, + const resultPermissions: PermissionsDs = { + connection: { accessLevel: inputData.permissions.connection.accessLevel, - tableName: '', - groupId: groupId, - }; - const updated = Object.assign(currentConnectionPermission, updatedPermissionData); - await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(updated); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); - resultPermissions.connection.accessLevel = updated.accessLevel; - } else { - const newPermission = buildNewPermissionEntityConnection(inputData.permissions.connection.accessLevel); - const savedPermission = await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(newPermission); - groupToUpdate.permissions.push(savedPermission); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); - resultPermissions.connection.accessLevel = newPermission.accessLevel as AccessLevelEnum; - } - - // *** GROUP PERMISSION - if (currentGroupPermission) { - const updatedPermissionData = { - type: PermissionTypeEnum.Group, + connectionId: inputData.permissions.connection.connectionId, + }, + group: { accessLevel: inputData.permissions.group.accessLevel, - tableName: '', - groupId: groupId, - }; - const updated = Object.assign(currentGroupPermission, updatedPermissionData); - await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(updated); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); - resultPermissions.group.accessLevel = updated.accessLevel; - } else { - const newPermission = buildNewPermissionEntityGroup(inputData.permissions.group.accessLevel); - const savedPermission = await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(newPermission); - groupToUpdate.permissions.push(savedPermission); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); - resultPermissions.group.accessLevel = newPermission.accessLevel as AccessLevelEnum; - } - - // *** TABLES PERMISSIONS - const tablePermissions = inputData.permissions.tables; - // delete falsy permissions - const deletedPermissions: Array = []; - await Promise.all( - tablePermissions.map(async (tablePermission: TablePermissionDs) => { - const { accessLevel, tableName } = tablePermission; - for (const key in accessLevel) { - // has own property check added to avoid object injection - // eslint-disable-next-line security/detect-object-injection - if (isObjectPropertyExists(accessLevel, key) && !accessLevel[key]) { - const permissionIndex = groupToUpdate.permissions.findIndex( - (permission: PermissionEntity) => permission.accessLevel === key && tableName === permission.tableName, - ); - if (permissionIndex >= 0) { - const permissionInGroup = groupToUpdate.permissions.at(permissionIndex); - const deletedPermission = - await this._dbContext.permissionRepository.removePermissionEntity(permissionInGroup); - deletedPermissions.push(deletedPermission); - } - } - } - }), - ); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); - - //create truthy permissions - const createdPermissions: Array = []; - await Promise.all( - tablePermissions.map(async (tablePermission: TablePermissionDs) => { - const { accessLevel, tableName } = tablePermission; - for (const key in accessLevel) { - // has own property check added to avoid object injection - // eslint-disable-next-line security/detect-object-injection - if (isObjectPropertyExists(accessLevel, key) && accessLevel[key]) { - const permissionIndex = groupToUpdate.permissions.findIndex( - (permission: PermissionEntity) => permission.accessLevel === key && tableName === permission.tableName, - ); - if (permissionIndex < 0) { - const permissionEntity = new PermissionEntity(); - permissionEntity.type = PermissionTypeEnum.Table; - permissionEntity.accessLevel = key; - permissionEntity.tableName = tableName; - groupToUpdate.permissions.push(permissionEntity); - const createdPermission = - await this._dbContext.permissionRepository.saveNewOrUpdatedPermission(permissionEntity); - createdPermissions.push(createdPermission); - } - } - } - }), - ); - await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); - resultPermissions.tables = buildFinalTablesPermissions( - allTablePermissionsInGroup, - deletedPermissions, - createdPermissions, - ); + groupId: inputData.permissions.group.groupId, + }, + tables: inputData.permissions.tables, + }; // Generate and save Cedar policy for this group const cedarPolicy = generateCedarPolicyForGroup(connectionId, groupToUpdate.isMain, resultPermissions); diff --git a/backend/src/entities/permission/utils/build-final-tables-permissions.ts b/backend/src/entities/permission/utils/build-final-tables-permissions.ts deleted file mode 100644 index 2bec19262..000000000 --- a/backend/src/entities/permission/utils/build-final-tables-permissions.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AccessLevelEnum } from '../../../enums/index.js'; -import { getUniqArrayStrings } from '../../../helpers/index.js'; -import { TablePermissionDs } from '../application/data-structures/create-permissions.ds.js'; -import { PermissionEntity } from '../permission.entity.js'; - -export function buildFinalTablesPermissions( - permissionsOnStart: Array, - deletedPermissions: Array, - createdPermissions: Array, -): Array { - const filteredPermissions: Array = permissionsOnStart.filter((permission: PermissionEntity) => { - return !deletedPermissions.find((deletedPermission) => - comparePermissionsByProperties(deletedPermission, permission), - ); - }); - - const totalPermissions: Array = filteredPermissions.concat(createdPermissions); - let tableNames: Array = totalPermissions.map((permission) => permission.tableName); - tableNames = getUniqArrayStrings(tableNames); - return tableNames.map((tableName): TablePermissionDs => { - return { - accessLevel: { - add: !!totalPermissions.find( - (permission) => permission.tableName === tableName && permission.accessLevel === AccessLevelEnum.add, - ), - delete: !!totalPermissions.find( - (permission) => permission.tableName === tableName && permission.accessLevel === AccessLevelEnum.delete, - ), - edit: !!totalPermissions.find( - (permission) => permission.tableName === tableName && permission.accessLevel === AccessLevelEnum.edit, - ), - readonly: !!totalPermissions.find( - (permission) => permission.tableName === tableName && permission.accessLevel === AccessLevelEnum.readonly, - ), - visibility: !!totalPermissions.find( - (permission) => permission.tableName === tableName && permission.accessLevel === AccessLevelEnum.visibility, - ), - }, - tableName: tableName, - }; - }); -} - -function comparePermissionsByProperties(f_permission: PermissionEntity, s_permission: PermissionEntity): boolean { - return ( - f_permission.accessLevel === s_permission.accessLevel && - f_permission.tableName === s_permission.tableName && - f_permission.type === s_permission.type - ); -} diff --git a/backend/src/entities/permission/utils/build-new-permission-entity-connection.ts b/backend/src/entities/permission/utils/build-new-permission-entity-connection.ts deleted file mode 100644 index 18b49ac7f..000000000 --- a/backend/src/entities/permission/utils/build-new-permission-entity-connection.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; -import { PermissionEntity } from '../permission.entity.js'; - -export function buildNewPermissionEntityConnection(accessLevel: AccessLevelEnum): PermissionEntity { - const newPermission = new PermissionEntity(); - newPermission.type = PermissionTypeEnum.Connection; - newPermission.accessLevel = accessLevel; - return newPermission; -} diff --git a/backend/src/entities/permission/utils/build-new-permission-entity-group.ts b/backend/src/entities/permission/utils/build-new-permission-entity-group.ts deleted file mode 100644 index b760d7651..000000000 --- a/backend/src/entities/permission/utils/build-new-permission-entity-group.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; -import { PermissionEntity } from '../permission.entity.js'; - -export function buildNewPermissionEntityGroup(accessLevel: AccessLevelEnum): PermissionEntity { - const newPermission = new PermissionEntity(); - newPermission.type = PermissionTypeEnum.Group; - newPermission.accessLevel = accessLevel; - return newPermission; -} diff --git a/backend/src/entities/user/utils/build-default-admin-permissions.ts b/backend/src/entities/user/utils/build-default-admin-permissions.ts deleted file mode 100644 index 3c16fac93..000000000 --- a/backend/src/entities/user/utils/build-default-admin-permissions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; -import { GroupEntity } from '../../group/group.entity.js'; -import { PermissionEntity } from '../../permission/permission.entity.js'; - -export function buildDefaultAdminPermissions(groups: Array): Array { - const permissions: Array = []; - for (const group of groups) { - const connectionAdminPermission = new PermissionEntity(); - connectionAdminPermission.type = PermissionTypeEnum.Connection; - connectionAdminPermission.accessLevel = AccessLevelEnum.edit; - connectionAdminPermission.groups = []; - connectionAdminPermission.groups.push(group); - - permissions.push(connectionAdminPermission); - - const groupAdminPermission = new PermissionEntity(); - groupAdminPermission.type = PermissionTypeEnum.Group; - groupAdminPermission.accessLevel = AccessLevelEnum.edit; - groupAdminPermission.groups = []; - groupAdminPermission.groups.push(group); - - permissions.push(groupAdminPermission); - } - return permissions; -} diff --git a/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts b/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts index 202bacf20..517b1670d 100644 --- a/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts +++ b/backend/src/microservices/saas-microservice/use-cases/create-connection-for-hosted-db.use.case.ts @@ -85,7 +85,6 @@ export class CreateConnectionForHostedDbUseCase savedConnection, connectionAuthor, ); - await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup( savedConnection.id, true, diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-parser.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-parser.test.ts index 8289bb42d..93a7e902c 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-parser.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-parser.test.ts @@ -65,7 +65,7 @@ test('parses table:read + table:add + table:edit + table:delete into full access const result = parseCedarPolicyToClassicalPermissions(policy, connectionId, groupId); t.is(result.tables.length, 1); t.is(result.tables[0].accessLevel.visibility, true); - t.is(result.tables[0].accessLevel.readonly, true); + t.is(result.tables[0].accessLevel.readonly, false); t.is(result.tables[0].accessLevel.add, true); t.is(result.tables[0].accessLevel.edit, true); t.is(result.tables[0].accessLevel.delete, true); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-save-policy-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-save-policy-e2e.test.ts index 91aaa0a54..38c4b4dbc 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-save-policy-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-save-policy-e2e.test.ts @@ -142,7 +142,7 @@ test.serial( t.is(body.classicalPermissions.tables.length, 1); t.is(body.classicalPermissions.tables[0].tableName, tableName); t.is(body.classicalPermissions.tables[0].accessLevel.visibility, true); - t.is(body.classicalPermissions.tables[0].accessLevel.readonly, true); + t.is(body.classicalPermissions.tables[0].accessLevel.readonly, false); t.is(body.classicalPermissions.tables[0].accessLevel.add, true); t.is(body.classicalPermissions.tables[0].accessLevel.edit, true); t.is(body.classicalPermissions.tables[0].accessLevel.delete, true); diff --git a/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts b/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts index 56d726bf4..4add21929 100644 --- a/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts @@ -20,7 +20,6 @@ import { ConnectionEntity } from '../../../src/entities/connection/connection.en import { UserRoleEnum } from '../../../src/entities/user/enums/user-role.enum.js'; import { generateGwtToken } from '../../../src/entities/user/utils/generate-gwt-token.js'; import { buildDefaultAdminGroups } from '../../../src/entities/user/utils/build-default-admin-groups.js'; -import { buildDefaultAdminPermissions } from '../../../src/entities/user/utils/build-default-admin-permissions.js'; import { generateCedarPolicyForGroup } from '../../../src/entities/cedar-authorization/cedar-policy-generator.js'; import { AccessLevelEnum, PermissionTypeEnum } from '../../../src/enums/index.js'; import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; @@ -1549,13 +1548,20 @@ test.serial( }), ); - // Step 3: buildDefaultAdminPermissions (same as DemoDataService) - const testPermissionsEntities = buildDefaultAdminPermissions(createdGroups); - await Promise.all( - testPermissionsEntities.map(async (permission: PermissionEntity) => { - await permissionRepository.save(permission); - }), - ); + // Step 3: Create old-style permission entities (simulating pre-Cedar state) + for (const group of createdGroups) { + const connPerm = new PermissionEntity(); + connPerm.type = PermissionTypeEnum.Connection; + connPerm.accessLevel = AccessLevelEnum.edit; + connPerm.groups = [group]; + await permissionRepository.save(connPerm); + + const groupPerm = new PermissionEntity(); + groupPerm.type = PermissionTypeEnum.Group; + groupPerm.accessLevel = AccessLevelEnum.edit; + groupPerm.groups = [group]; + await permissionRepository.save(groupPerm); + } // Step 4: Generate Cedar policies and re-save groups (same as DemoDataService fix) // This is the critical step: the fix in DemoDataService deletes group.permissions diff --git a/backend/test/ava-tests/saas-tests/saas-cedar-save-policy-e2e.test.ts b/backend/test/ava-tests/saas-tests/saas-cedar-save-policy-e2e.test.ts index 5c126c10f..467094afa 100644 --- a/backend/test/ava-tests/saas-tests/saas-cedar-save-policy-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/saas-cedar-save-policy-e2e.test.ts @@ -137,7 +137,7 @@ test.serial( t.is(body.classicalPermissions.tables.length, 1); t.is(body.classicalPermissions.tables[0].tableName, tableName); t.is(body.classicalPermissions.tables[0].accessLevel.visibility, true); - t.is(body.classicalPermissions.tables[0].accessLevel.readonly, true); + t.is(body.classicalPermissions.tables[0].accessLevel.readonly, false); t.is(body.classicalPermissions.tables[0].accessLevel.add, true); t.is(body.classicalPermissions.tables[0].accessLevel.edit, true); t.is(body.classicalPermissions.tables[0].accessLevel.delete, true); From 606c407c2429a296f0d804c91c22d5288019670b Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Mon, 23 Mar 2026 10:59:06 +0000 Subject: [PATCH 3/5] remove old permissions entity --- .../global-database-context.interface.ts | 2 - .../application/global-database-context.ts | 11 - .../scripts/migrate-permissions-to-cedar.ts | 24 +- .../company-info/company-info.module.ts | 2 - .../entities/connection/connection.module.ts | 2 - ...w-group-entity-for-connection-with-user.ts | 1 - .../custom-field/custom-field.module.ts | 2 - backend/src/entities/group/group.entity.ts | 10 - backend/src/entities/group/group.module.ts | 2 - .../group-custom-repository-extension.ts | 44 +- .../entities/permission/permission.entity.ts | 24 - .../entities/permission/permission.module.ts | 2 - .../permission-custom-repository-extension.ts | 116 -- .../permission.repository.interface.ts | 21 - .../table-settings.module.ts | 2 - backend/src/entities/table/table.module.ts | 2 - backend/src/entities/user/user.module.ts | 2 - .../user/utils/build-default-admin-groups.ts | 1 - .../1771545700000-RemovePermissionTables.ts | 25 + ...cedar-migration-existing-users-e2e.test.ts | 1588 ---------------- ...cedar-migration-existing-users-e2e.test.ts | 1664 ----------------- backend/test/mock.factory.ts | 3 - 22 files changed, 73 insertions(+), 3477 deletions(-) delete mode 100644 backend/src/entities/permission/permission.entity.ts delete mode 100644 backend/src/entities/permission/repository/permission-custom-repository-extension.ts delete mode 100644 backend/src/entities/permission/repository/permission.repository.interface.ts create mode 100644 backend/src/migrations/1771545700000-RemovePermissionTables.ts delete mode 100644 backend/test/ava-tests/non-saas-tests/non-saas-cedar-migration-existing-users-e2e.test.ts delete mode 100644 backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts diff --git a/backend/src/common/application/global-database-context.interface.ts b/backend/src/common/application/global-database-context.interface.ts index 9d12fde0c..4b523951b 100644 --- a/backend/src/common/application/global-database-context.interface.ts +++ b/backend/src/common/application/global-database-context.interface.ts @@ -23,7 +23,6 @@ import { ICustomFieldsRepository } from '../../entities/custom-field/repository/ import { IEmailVerificationRepository } from '../../entities/email/repository/email-verification.repository.interface.js'; import { IGroupRepository } from '../../entities/group/repository/group.repository.interface.js'; import { ILogOutRepository } from '../../entities/log-out/repository/log-out-repository.interface.js'; -import { IPermissionRepository } from '../../entities/permission/repository/permission.repository.interface.js'; import { ISecretAccessLogRepository } from '../../entities/secret-access-log/repository/secret-access-log-repository.interface.js'; import { SecretAccessLogEntity } from '../../entities/secret-access-log/secret-access-log.entity.js'; import { ActionEventsEntity } from '../../entities/table-actions/table-action-events-module/action-event.entity.js'; @@ -70,7 +69,6 @@ export interface IGlobalDatabaseContext extends IDatabaseContext { userRepository: Repository & IUserRepository; connectionRepository: Repository & IConnectionRepository; groupRepository: IGroupRepository; - permissionRepository: IPermissionRepository; tableSettingsRepository: Repository & ITableSettingsRepository; agentRepository: IAgentRepository; emailVerificationRepository: IEmailVerificationRepository; diff --git a/backend/src/common/application/global-database-context.ts b/backend/src/common/application/global-database-context.ts index 5899e73b3..df13c3468 100644 --- a/backend/src/common/application/global-database-context.ts +++ b/backend/src/common/application/global-database-context.ts @@ -42,9 +42,6 @@ import { groupCustomRepositoryExtension } from '../../entities/group/repository/ import { LogOutEntity } from '../../entities/log-out/log-out.entity.js'; import { logOutCustomRepositoryExtension } from '../../entities/log-out/repository/log-out-custom-repository-extension.js'; import { ILogOutRepository } from '../../entities/log-out/repository/log-out-repository.interface.js'; -import { PermissionEntity } from '../../entities/permission/permission.entity.js'; -import { IPermissionRepository } from '../../entities/permission/repository/permission.repository.interface.js'; -import { permissionCustomRepositoryExtension } from '../../entities/permission/repository/permission-custom-repository-extension.js'; import { secretAccessLogRepositoryExtension } from '../../entities/secret-access-log/repository/secret-access-log-repository.extension.js'; import { ISecretAccessLogRepository } from '../../entities/secret-access-log/repository/secret-access-log-repository.interface.js'; import { SecretAccessLogEntity } from '../../entities/secret-access-log/secret-access-log.entity.js'; @@ -123,7 +120,6 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { private _userRepository: Repository & IUserRepository; private _connectionRepository: Repository & IConnectionRepository; private _groupRepository: IGroupRepository; - private _permissionRepository: IPermissionRepository; private _tableSettingsRepository: Repository & ITableSettingsRepository; private _agentRepository: IAgentRepository; private _emailVerificationRepository: IEmailVerificationRepository; @@ -175,9 +171,6 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { .getRepository(ConnectionEntity) .extend(customConnectionRepositoryExtension); this._groupRepository = this.appDataSource.getRepository(GroupEntity).extend(groupCustomRepositoryExtension); - this._permissionRepository = this.appDataSource - .getRepository(PermissionEntity) - .extend(permissionCustomRepositoryExtension); this._tableSettingsRepository = this.appDataSource .getRepository(TableSettingsEntity) .extend(tableSettingsCustomRepositoryExtension); @@ -285,10 +278,6 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { return this._groupRepository; } - public get permissionRepository(): IPermissionRepository { - return this._permissionRepository; - } - public get tableSettingsRepository(): Repository & ITableSettingsRepository { return this._tableSettingsRepository; } diff --git a/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts b/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts index 8e7d65361..02af012e5 100644 --- a/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts +++ b/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts @@ -8,11 +8,23 @@ export async function migratePermissionsToCedar(dataSource: DataSource): Promise const groupRepository = dataSource.getRepository(GroupEntity); let migratedCount = 0; - // Migrate groups with no Cedar policy OR groups with old-format policies (using "principal in" instead of bare "principal") + const permissionTableExists = await dataSource + .query( + `SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'permission' + ) AS "exists"`, + ) + .then((rows: Array<{ exists: boolean }>) => rows[0]?.exists === true); + + if (!permissionTableExists) { + console.log('Permission tables already removed — skipping legacy migration'); + return; + } + const groups = await groupRepository .createQueryBuilder('group') .leftJoinAndSelect('group.connection', 'connection') - .leftJoinAndSelect('group.permissions', 'permission') .where('group.cedarPolicy IS NULL OR group.cedarPolicy = :empty OR group.cedarPolicy LIKE :oldFormat', { empty: '', oldFormat: '%principal in RocketAdmin::Group%', @@ -23,7 +35,13 @@ export async function migratePermissionsToCedar(dataSource: DataSource): Promise const connection = group.connection; if (!connection) continue; - const permissions = group.permissions || []; + const permissions: Array<{ type: string; accessLevel: string; tableName: string }> = await dataSource.query( + `SELECT p.type, p."accessLevel", p."tableName" + FROM permission p + INNER JOIN permission_groups_group pg ON pg."permissionId" = p.id + WHERE pg."groupId" = $1`, + [group.id], + ); const connectionPermission = permissions.find((p) => p.type === PermissionTypeEnum.Connection); const groupPermission = permissions.find((p) => p.type === PermissionTypeEnum.Group); diff --git a/backend/src/entities/company-info/company-info.module.ts b/backend/src/entities/company-info/company-info.module.ts index 1c4348344..ff7042c6b 100644 --- a/backend/src/entities/company-info/company-info.module.ts +++ b/backend/src/entities/company-info/company-info.module.ts @@ -9,7 +9,6 @@ import { ConnectionPropertiesEntity } from '../connection-properties/connection- import { CustomFieldsEntity } from '../custom-field/custom-fields.entity.js'; import { GroupEntity } from '../group/group.entity.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { TableLogsEntity } from '../table-logs/table-logs.entity.js'; import { TableSettingsEntity } from '../table-settings/common-table-settings/table-settings.entity.js'; import { UserEntity } from '../user/user.entity.js'; @@ -50,7 +49,6 @@ import { VerifyInviteUserInCompanyAndConnectionGroupUseCase } from './use-cases/ ConnectionEntity, UserEntity, GroupEntity, - PermissionEntity, TableSettingsEntity, TableLogsEntity, CustomFieldsEntity, diff --git a/backend/src/entities/connection/connection.module.ts b/backend/src/entities/connection/connection.module.ts index cc1adfe95..bf4d87b1e 100644 --- a/backend/src/entities/connection/connection.module.ts +++ b/backend/src/entities/connection/connection.module.ts @@ -10,7 +10,6 @@ import { ConnectionPropertiesEntity } from '../connection-properties/connection- import { CustomFieldsEntity } from '../custom-field/custom-fields.entity.js'; import { GroupEntity } from '../group/group.entity.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { TableLogsEntity } from '../table-logs/table-logs.entity.js'; import { TableSettingsEntity } from '../table-settings/common-table-settings/table-settings.entity.js'; import { UserEntity } from '../user/user.entity.js'; @@ -43,7 +42,6 @@ import { ValidateConnectionTokenUseCase } from './use-cases/validate-connection- ConnectionEntity, UserEntity, GroupEntity, - PermissionEntity, TableSettingsEntity, TableLogsEntity, CustomFieldsEntity, diff --git a/backend/src/entities/connection/utils/build-new-group-entity-for-connection-with-user.ts b/backend/src/entities/connection/utils/build-new-group-entity-for-connection-with-user.ts index 4453e5869..e050cd907 100644 --- a/backend/src/entities/connection/utils/build-new-group-entity-for-connection-with-user.ts +++ b/backend/src/entities/connection/utils/build-new-group-entity-for-connection-with-user.ts @@ -9,7 +9,6 @@ export function buildNewGroupEntityForConnectionWithUser( ): GroupEntity { const newGroup = new GroupEntity(); newGroup.title = groupTitle ? groupTitle : 'New group'; - newGroup.permissions = []; newGroup.users = [user]; newGroup.connection = connection; newGroup.isMain = false; diff --git a/backend/src/entities/custom-field/custom-field.module.ts b/backend/src/entities/custom-field/custom-field.module.ts index 565f0149e..20598280d 100644 --- a/backend/src/entities/custom-field/custom-field.module.ts +++ b/backend/src/entities/custom-field/custom-field.module.ts @@ -8,7 +8,6 @@ import { ConnectionEntity } from '../connection/connection.entity.js'; import { ConnectionPropertiesEntity } from '../connection-properties/connection-properties.entity.js'; import { GroupEntity } from '../group/group.entity.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { TableLogsEntity } from '../table-logs/table-logs.entity.js'; import { TableSettingsEntity } from '../table-settings/common-table-settings/table-settings.entity.js'; import { UserEntity } from '../user/user.entity.js'; @@ -27,7 +26,6 @@ import { UpdateCustomFieldUseCase } from './use-cases/update-custom-field.use.ca ConnectionEntity, CustomFieldsEntity, GroupEntity, - PermissionEntity, TableLogsEntity, TableSettingsEntity, TableWidgetEntity, diff --git a/backend/src/entities/group/group.entity.ts b/backend/src/entities/group/group.entity.ts index dd024e455..658c52b36 100644 --- a/backend/src/entities/group/group.entity.ts +++ b/backend/src/entities/group/group.entity.ts @@ -1,6 +1,5 @@ import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Relation, Unique } from 'typeorm'; import { ConnectionEntity } from '../connection/connection.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { UserEntity } from '../user/user.entity.js'; @Entity('group') @@ -15,15 +14,6 @@ export class GroupEntity { @Column({ default: false, type: 'boolean' }) isMain: boolean; - @ManyToMany( - (_) => PermissionEntity, - (permission) => permission.groups, - { - onDelete: 'CASCADE', - }, - ) - permissions?: Relation[]; - @ManyToMany( (_) => UserEntity, (user) => user.groups, diff --git a/backend/src/entities/group/group.module.ts b/backend/src/entities/group/group.module.ts index 31efe8d32..06eb82f92 100644 --- a/backend/src/entities/group/group.module.ts +++ b/backend/src/entities/group/group.module.ts @@ -6,7 +6,6 @@ import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js'; import { AgentModule } from '../agent/agent.module.js'; import { ConnectionEntity } from '../connection/connection.entity.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { TableSettingsEntity } from '../table-settings/common-table-settings/table-settings.entity.js'; import { UserEntity } from '../user/user.entity.js'; import { UserModule } from '../user/user.module.js'; @@ -24,7 +23,6 @@ import { UpdateGroupTitleUseCase } from './use-cases/update-group-title.use.case TypeOrmModule.forFeature([ ConnectionEntity, GroupEntity, - PermissionEntity, UserEntity, TableSettingsEntity, LogOutEntity, diff --git a/backend/src/entities/group/repository/group-custom-repository-extension.ts b/backend/src/entities/group/repository/group-custom-repository-extension.ts index a40ccf09f..7d4321896 100644 --- a/backend/src/entities/group/repository/group-custom-repository-extension.ts +++ b/backend/src/entities/group/repository/group-custom-repository-extension.ts @@ -1,4 +1,3 @@ -import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { UserEntity } from '../../user/user.entity.js'; import { GroupEntity } from '../group.entity.js'; @@ -71,27 +70,38 @@ export const groupCustomRepositoryExtension: IGroupRepository = { }, async findGroupWithPermissionsById(groupId: string): Promise { - const qb = this.createQueryBuilder('group') - .leftJoinAndSelect('group.permissions', 'permission') - .andWhere('group.id = :id', { id: groupId }); + const qb = this.createQueryBuilder('group').andWhere('group.id = :id', { id: groupId }); return await qb.getOne(); }, async findAllUsersInGroupsWhereUserIsAdmin(userId: string, connectionId: string): Promise> { - const userQb = this.manager + // Find groups where the user is a member, in the given connection, + // and the Cedar policy grants group:edit access (wildcard or explicit) + const adminGroupIds: Array<{ id: string }> = await this.createQueryBuilder('group') + .select('group.id', 'id') + .leftJoin('group.connection', 'connection') + .leftJoin('group.users', 'user') + .where('user.id = :userId', { userId }) + .andWhere('connection.id = :connectionId', { connectionId }) + .andWhere( + '(group.isMain = true OR group.cedarPolicy LIKE :wildcardPattern OR group.cedarPolicy LIKE :groupEditPattern)', + { + wildcardPattern: '%principal, action, resource%', + groupEditPattern: '%group:edit%', + }, + ) + .getRawMany(); + + if (adminGroupIds.length === 0) { + return []; + } + + const groupIds = adminGroupIds.map((g) => g.id); + return await this.manager .getRepository(UserEntity) .createQueryBuilder('user') - .leftJoinAndSelect('user.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .leftJoinAndSelect('group.permissions', 'permission') - .andWhere('user.id = :userId', { userId: userId }) - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Group, - }) - .andWhere('permission.accessLevel = :permissionAccessLevel', { - permissionAccessLevel: AccessLevelEnum.edit, - }); - return await userQb.getMany(); + .leftJoin('user.groups', 'group') + .where('group.id IN (:...groupIds)', { groupIds }) + .getMany(); }, }; diff --git a/backend/src/entities/permission/permission.entity.ts b/backend/src/entities/permission/permission.entity.ts deleted file mode 100644 index da99651c2..000000000 --- a/backend/src/entities/permission/permission.entity.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn, Relation } from 'typeorm'; -import { GroupEntity } from '../group/group.entity.js'; - -@Entity('permission') -export class PermissionEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - type: string; - - @Column() - accessLevel: string; - - @Column({ default: '' }) - tableName?: string; //todo need reworking to required - - @ManyToMany( - () => GroupEntity, - (group) => group.permissions, - ) - @JoinTable() - groups: Relation[]; -} diff --git a/backend/src/entities/permission/permission.module.ts b/backend/src/entities/permission/permission.module.ts index f4cf4e254..8e2229be9 100644 --- a/backend/src/entities/permission/permission.module.ts +++ b/backend/src/entities/permission/permission.module.ts @@ -15,7 +15,6 @@ import { TableSettingsEntity } from '../table-settings/common-table-settings/tab import { UserEntity } from '../user/user.entity.js'; import { TableWidgetEntity } from '../widget/table-widget.entity.js'; import { PermissionController } from './permission.controller.js'; -import { PermissionEntity } from './permission.entity.js'; import { CreateOrUpdatePermissionsUseCase } from './use-cases/create-or-update-permissions.use.case.js'; @Module({ @@ -24,7 +23,6 @@ import { CreateOrUpdatePermissionsUseCase } from './use-cases/create-or-update-p ConnectionEntity, CustomFieldsEntity, GroupEntity, - PermissionEntity, TableLogsEntity, TableSettingsEntity, TableWidgetEntity, diff --git a/backend/src/entities/permission/repository/permission-custom-repository-extension.ts b/backend/src/entities/permission/repository/permission-custom-repository-extension.ts deleted file mode 100644 index df9df4121..000000000 --- a/backend/src/entities/permission/repository/permission-custom-repository-extension.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { AccessLevelEnum } from '../../../enums/access-level.enum.js'; -import { PermissionTypeEnum } from '../../../enums/permission-type.enum.js'; -import { GroupEntity } from '../../group/group.entity.js'; -import { PermissionEntity } from '../permission.entity.js'; - -export const permissionCustomRepositoryExtension = { - async saveNewOrUpdatedPermission(permissionData: PermissionEntity): Promise { - return await this.save(permissionData); - }, - - async createdDefaultAdminPermissionsInGroup(group: GroupEntity): Promise> { - const connectionAdminPermission = new PermissionEntity(); - connectionAdminPermission.groups = [group]; - connectionAdminPermission.type = PermissionTypeEnum.Connection; - connectionAdminPermission.accessLevel = AccessLevelEnum.edit; - const createdConnectionAdminPermission = await this.save(connectionAdminPermission); - const groupAdminPermission = new PermissionEntity(); - groupAdminPermission.groups = [group]; - groupAdminPermission.type = PermissionTypeEnum.Group; - groupAdminPermission.accessLevel = AccessLevelEnum.edit; - const createdGroupAdminPermission = await this.save(groupAdminPermission); - return [createdConnectionAdminPermission, createdGroupAdminPermission]; - }, - - async getGroupPermissionForConnection(connectionId: string, groupId: string): Promise { - const connectionQb = this.createQueryBuilder('permission') - .leftJoinAndSelect('permission.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Connection, - }); - const connectionPermission: PermissionEntity = await connectionQb.getOne(); - let connectionAccessLevel: AccessLevelEnum = AccessLevelEnum.none; - if (connectionPermission) { - switch (connectionPermission.accessLevel.toLowerCase()) { - case AccessLevelEnum.edit: - connectionAccessLevel = AccessLevelEnum.edit; - break; - case AccessLevelEnum.readonly: - connectionAccessLevel = AccessLevelEnum.readonly; - break; - default: - break; - } - } - return connectionAccessLevel; - }, - - async getGroupPermissionsForGroup(connectionId: string, groupId: string): Promise { - const groupQb = this.createQueryBuilder('permission') - .leftJoinAndSelect('permission.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Group, - }); - const groupPermission: PermissionEntity = await groupQb.getOne(); - let groupAccessLevel: AccessLevelEnum = AccessLevelEnum.none; - if (groupPermission) { - switch (groupPermission.accessLevel.toLowerCase()) { - case AccessLevelEnum.edit: - groupAccessLevel = AccessLevelEnum.edit; - break; - case AccessLevelEnum.readonly: - groupAccessLevel = AccessLevelEnum.readonly; - break; - default: - break; - } - } - return groupAccessLevel; - }, - - async getGroupPermissionsForAllTables(connectionId: string, groupId: string): Promise> { - const tableQb = this.createQueryBuilder('permission') - .leftJoinAndSelect('permission.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Table, - }); - return await tableQb.getMany(); - }, - - async getPermissionEntityForConnection(connectionId: string, groupId: string): Promise { - const connectionQb = this.createQueryBuilder('permission') - .leftJoinAndSelect('permission.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Connection, - }); - return await connectionQb.getOne(); - }, - - async getPermissionEntityForGroup(connectionId: string, groupId: string): Promise { - const groupQb = this.createQueryBuilder('permission') - .leftJoinAndSelect('permission.groups', 'group') - .leftJoinAndSelect('group.connection', 'connection') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('permission.type = :permissionType', { - permissionType: PermissionTypeEnum.Group, - }); - return await groupQb.getOne(); - }, - - async removePermissionEntity(permission: PermissionEntity): Promise { - return await this.remove(permission); - }, -}; diff --git a/backend/src/entities/permission/repository/permission.repository.interface.ts b/backend/src/entities/permission/repository/permission.repository.interface.ts deleted file mode 100644 index 458d208de..000000000 --- a/backend/src/entities/permission/repository/permission.repository.interface.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AccessLevelEnum } from '../../../enums/index.js'; -import { GroupEntity } from '../../group/group.entity.js'; -import { PermissionEntity } from '../permission.entity.js'; - -export interface IPermissionRepository { - saveNewOrUpdatedPermission(permissionData: PermissionEntity): Promise; - - createdDefaultAdminPermissionsInGroup(group: GroupEntity): Promise>; - - getGroupPermissionForConnection(connectionId: string, groupId: string): Promise; - - getGroupPermissionsForGroup(connectionId: string, groupId: string): Promise; - - getGroupPermissionsForAllTables(connectionId: string, groupId: string): Promise>; - - getPermissionEntityForConnection(connectionId: string, groupId: string): Promise; - - getPermissionEntityForGroup(connectionId: string, groupId: string): Promise; - - removePermissionEntity(permission: PermissionEntity): Promise; -} diff --git a/backend/src/entities/table-settings/common-table-settings/table-settings.module.ts b/backend/src/entities/table-settings/common-table-settings/table-settings.module.ts index d76a50416..549aa0431 100644 --- a/backend/src/entities/table-settings/common-table-settings/table-settings.module.ts +++ b/backend/src/entities/table-settings/common-table-settings/table-settings.module.ts @@ -9,7 +9,6 @@ import { ConnectionPropertiesEntity } from '../../connection-properties/connecti import { CustomFieldsEntity } from '../../custom-field/custom-fields.entity.js'; import { GroupEntity } from '../../group/group.entity.js'; import { LogOutEntity } from '../../log-out/log-out.entity.js'; -import { PermissionEntity } from '../../permission/permission.entity.js'; import { TableLogsEntity } from '../../table-logs/table-logs.entity.js'; import { UserEntity } from '../../user/user.entity.js'; import { UserModule } from '../../user/user.module.js'; @@ -27,7 +26,6 @@ import { UpdateTableSettingsUseCase } from './use-cases/update-table-settings.us ConnectionEntity, CustomFieldsEntity, GroupEntity, - PermissionEntity, TableLogsEntity, TableSettingsEntity, TableWidgetEntity, diff --git a/backend/src/entities/table/table.module.ts b/backend/src/entities/table/table.module.ts index 0dc1470ab..75ad84d90 100644 --- a/backend/src/entities/table/table.module.ts +++ b/backend/src/entities/table/table.module.ts @@ -10,7 +10,6 @@ import { ConnectionPropertiesEntity } from '../connection-properties/connection- import { CustomFieldsEntity } from '../custom-field/custom-fields.entity.js'; import { GroupEntity } from '../group/group.entity.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { TableLogsEntity } from '../table-logs/table-logs.entity.js'; import { TableSettingsEntity } from '../table-settings/common-table-settings/table-settings.entity.js'; import { UserEntity } from '../user/user.entity.js'; @@ -36,7 +35,6 @@ import { UpdateRowInTableUseCase } from './use-cases/update-row-in-table.use.cas ConnectionEntity, CustomFieldsEntity, GroupEntity, - PermissionEntity, TableLogsEntity, TableSettingsEntity, TableWidgetEntity, diff --git a/backend/src/entities/user/user.module.ts b/backend/src/entities/user/user.module.ts index 6961ffd70..7ecde9708 100644 --- a/backend/src/entities/user/user.module.ts +++ b/backend/src/entities/user/user.module.ts @@ -12,7 +12,6 @@ import { ConnectionPropertiesEntity } from '../connection-properties/connection- import { CustomFieldsEntity } from '../custom-field/custom-fields.entity.js'; import { GroupEntity } from '../group/group.entity.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; -import { PermissionEntity } from '../permission/permission.entity.js'; import { TableLogsEntity } from '../table-logs/table-logs.entity.js'; import { TableSettingsEntity } from '../table-settings/common-table-settings/table-settings.entity.js'; import { SignInAuditEntity } from '../user-sign-in-audit/sign-in-audit.entity.js'; @@ -47,7 +46,6 @@ import { UserHelperService } from './user-helper.service.js'; ConnectionEntity, CustomFieldsEntity, GroupEntity, - PermissionEntity, TableLogsEntity, TableSettingsEntity, TableWidgetEntity, diff --git a/backend/src/entities/user/utils/build-default-admin-groups.ts b/backend/src/entities/user/utils/build-default-admin-groups.ts index e4844273c..97ce9f9ca 100644 --- a/backend/src/entities/user/utils/build-default-admin-groups.ts +++ b/backend/src/entities/user/utils/build-default-admin-groups.ts @@ -10,7 +10,6 @@ export function buildDefaultAdminGroups(user: UserEntity, connections: Array { + await queryRunner.query(`DROP TABLE IF EXISTS "permission_groups_group"`); + await queryRunner.query(`DROP TABLE IF EXISTS "permission"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "permission" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying NOT NULL, "accessLevel" character varying NOT NULL, "tableName" character varying NOT NULL DEFAULT '', CONSTRAINT "PK_3b8b97af9d9d8807e41e6f48362" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE TABLE "permission_groups_group" ("permissionId" uuid NOT NULL, "groupId" uuid NOT NULL, CONSTRAINT "PK_permission_groups_group" PRIMARY KEY ("permissionId", "groupId"))`, + ); + await queryRunner.query( + `ALTER TABLE "permission_groups_group" ADD CONSTRAINT "FK_permission_groups_permissionId" FOREIGN KEY ("permissionId") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "permission_groups_group" ADD CONSTRAINT "FK_permission_groups_groupId" FOREIGN KEY ("groupId") REFERENCES "group"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } +} diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-migration-existing-users-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-migration-existing-users-e2e.test.ts deleted file mode 100644 index 5f0681f43..000000000 --- a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-migration-existing-users-e2e.test.ts +++ /dev/null @@ -1,1588 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable security/detect-object-injection */ -import { faker } from '@faker-js/faker'; -import { INestApplication, ValidationPipe } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import test from 'ava'; -import { ValidationError } from 'class-validator'; -import cookieParser from 'cookie-parser'; -import request from 'supertest'; -import { DataSource } from 'typeorm'; -import { ApplicationModule } from '../../../src/app.module.js'; -import { BaseType } from '../../../src/common/data-injection.tokens.js'; -import { migratePermissionsToCedar } from '../../../src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.js'; -import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; -import { AccessLevelEnum } from '../../../src/enums/index.js'; -import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; -import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; -import { Messages } from '../../../src/exceptions/text/messages.js'; -import { Cacher } from '../../../src/helpers/cache/cacher.js'; -import { Constants } from '../../../src/helpers/constants/constants.js'; -import { DatabaseModule } from '../../../src/shared/database/database.module.js'; -import { DatabaseService } from '../../../src/shared/database/database.service.js'; -import { MockFactory } from '../../mock.factory.js'; -import { createTestTable } from '../../utils/create-test-table.js'; -import { - inviteUserInCompanyAndAcceptInvitation, - registerUserAndReturnUserInfo, -} from '../../utils/register-user-and-return-user-info.js'; -import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js'; -import { TestUtils } from '../../utils/test.utils.js'; - -let app: INestApplication; -let _testUtils: TestUtils; -let currentTest: string; - -const mockFactory = new MockFactory(); - -test.before(async () => { - // Start WITHOUT Cedar enabled - simulating the old permission system - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - setSaasEnvVariable(); - const moduleFixture = await Test.createTestingModule({ - imports: [ApplicationModule, DatabaseModule], - providers: [DatabaseService, TestUtils], - }).compile(); - app = moduleFixture.createNestApplication(); - _testUtils = moduleFixture.get(TestUtils); - - app.use(cookieParser()); - app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); - app.useGlobalPipes( - new ValidationPipe({ - exceptionFactory(validationErrors: ValidationError[] = []) { - return new ValidationException(validationErrors); - }, - }), - ); - await app.init(); - app.getHttpServer().listen(0); -}); - -test.after(async () => { - try { - await Cacher.clearAllCache(); - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - await app.close(); - } catch (e) { - console.error('After tests error ' + e); - } -}); - -//****************************** CEDAR MIGRATION: existing users retain permissions after migration ****************************** - -currentTest = 'Cedar migration - admin user'; - -test.serial( - `${currentTest} should retain full access for admin user (isMain group) after Cedar migration`, - async (t) => { - try { - // Step 1: Create user and connection WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Step 2: Verify classical permissions work (admin can read tables) - const getTablesBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify admin user still has full access via Cedar - const getTablesAfterMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesAfterMigration.status, 200); - - // Verify admin can read table rows - const getTableRows = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTableRows.status, 200); - - // Verify admin can add rows - const randomName = faker.person.firstName(); - const randomEmail = faker.internet.email(); - const addRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName, - [testTable.testTableSecondColumnName]: randomEmail, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowResponse.status, 201); - - // Verify admin can update connection - const updateConnection = mockFactory.generateUpdateConnectionDto(); - const updateConnectionResponse = await request(app.getHttpServer()) - .put(`/connection/${connectionId}`) - .send(updateConnection) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateConnectionResponse.status, 200); - - // Verify admin can read groups - const getGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getGroupsResponse.status, 200); - const groups = JSON.parse(getGroupsResponse.text); - t.is(groups.length > 0, true); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - non-admin user with readonly permissions'; - -test.serial( - `${currentTest} should retain readonly access for non-admin user after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - admin creates connection, group, invites user with readonly permissions - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a non-admin group - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - const groupId = JSON.parse(createGroupResponse.text).id; - - // Set readonly permissions via classical system - const permissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: groupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: true, - add: false, - delete: false, - edit: false, - }, - }, - ], - }; - - const setPermissionsResponse = await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(setPermissionsResponse.status, 200); - - // Add user to group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify classical permissions work - const getTablesBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesBeforeMigration.status, 200); - - const getRowsBeforeMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify user retains readonly access via Cedar - const getConnectionsAfterMigration = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionsAfterMigration.status, 200); - const connections = getConnectionsAfterMigration.body.connections; - const targetConnection = connections.find( - ({ connection }: any) => connection.id === connectionId, - ); - t.is(targetConnection.accessLevel, AccessLevelEnum.readonly); - - const getTablesAfterMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesAfterMigration.status, 200); - - const getRowsAfterMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsAfterMigration.status, 200); - - // Verify readonly user still CANNOT update connection - const updateConnection = mockFactory.generateUpdateConnectionDto(); - const updateConnectionResponse = await request(app.getHttpServer()) - .put(`/connection/${connectionId}`) - .send(updateConnection) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateConnectionResponse.status, 403); - - // Verify readonly user still CANNOT add rows - const randomName = faker.person.firstName(); - const randomEmail = faker.internet.email(); - const addRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName, - [testTable.testTableSecondColumnName]: randomEmail, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowResponse.status, 403); - - // Verify readonly user still CANNOT delete rows - const deleteRowResponse = await request(app.getHttpServer()) - .delete(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=1`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(deleteRowResponse.status, 403); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - non-admin user with edit permissions'; - -test.serial( - `${currentTest} should retain table edit permissions for non-admin user after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a non-admin group with full table edit permissions - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - const groupId = JSON.parse(createGroupResponse.text).id; - - // Set edit permissions - const permissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: groupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Add user to group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify classical permissions work - user can add rows - const randomName1 = faker.person.firstName(); - const randomEmail1 = faker.internet.email(); - const addRowBeforeMigration = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName1, - [testTable.testTableSecondColumnName]: randomEmail1, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowBeforeMigration.status, 201); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify all table operations work via Cedar - - // Can read table rows - const getRowsAfterMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsAfterMigration.status, 200); - - // Can add rows - const randomName2 = faker.person.firstName(); - const randomEmail2 = faker.internet.email(); - const addRowAfterMigration = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName2, - [testTable.testTableSecondColumnName]: randomEmail2, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowAfterMigration.status, 201); - const addedRowId = JSON.parse(addRowAfterMigration.text).row.id; - - // Can update rows - const randomName3 = faker.person.firstName(); - const randomEmail3 = faker.internet.email(); - const updateRowAfterMigration = await request(app.getHttpServer()) - .put(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=${addedRowId}`) - .send({ - [testTable.testTableColumnName]: randomName3, - [testTable.testTableSecondColumnName]: randomEmail3, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateRowAfterMigration.status, 200); - - // Can delete rows - const deleteRowAfterMigration = await request(app.getHttpServer()) - .delete(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=${addedRowId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(deleteRowAfterMigration.status, 200); - - // Verify connection is still readonly (cannot update connection) - const updateConnection = mockFactory.generateUpdateConnectionDto(); - const updateConnectionResponse = await request(app.getHttpServer()) - .put(`/connection/${connectionId}`) - .send(updateConnection) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateConnectionResponse.status, 403); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - user with connection edit permissions'; - -test.serial( - `${currentTest} should retain connection edit access for non-admin user after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a non-admin group with connection edit permissions - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - const groupId = JSON.parse(createGroupResponse.text).id; - - // Set connection edit permissions - const permissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.edit, - }, - group: { - groupId: groupId, - accessLevel: AccessLevelEnum.none, - }, - tables: [], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Add user to group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify classical permissions work - const getConnectionBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/one/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify user retains connection read access via Cedar - const getConnectionAfterMigration = await request(app.getHttpServer()) - .get(`/connection/one/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionAfterMigration.status, 200); - - // Verify connection appears in connections list - const getConnectionsAfterMigration = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionsAfterMigration.status, 200); - const connections = getConnectionsAfterMigration.body.connections; - const targetConnection = connections.find( - ({ connection }: any) => connection.id === connectionId, - ); - t.truthy(targetConnection); - t.is(targetConnection.accessLevel, AccessLevelEnum.edit); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - user added to admin group of existing connection'; - -test.serial( - `${currentTest} should retain admin access for user added to admin group after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Get admin group (isMain=true) - const getGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groups = JSON.parse(getGroupsResponse.text); - const adminGroupId = groups[0].group.id; - - // Add user to admin group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: adminGroupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify user has admin access via classical permissions - const getTablesBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify admin group member retains full access via Cedar - - // Can read tables - const getTablesAfterMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesAfterMigration.status, 200); - - // Can read table rows - const getRowsAfterMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsAfterMigration.status, 200); - - // Can add rows - const randomName = faker.person.firstName(); - const randomEmail = faker.internet.email(); - const addRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName, - [testTable.testTableSecondColumnName]: randomEmail, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowResponse.status, 201); - - // Can create new group (admin privilege) - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', simpleUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - multiple users with different permission levels'; - -test.serial( - `${currentTest} should retain correct permissions for multiple users after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const readonlyUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const readonlyUserToken = readonlyUserInfo.token; - - const editUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const editUserToken = editUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create readonly group - const readonlyGroup = mockFactory.generateCreateGroupDto1(); - const createReadonlyGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(readonlyGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const readonlyGroupId = JSON.parse(createReadonlyGroupResponse.text).id; - - const readonlyPermissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: readonlyGroupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: true, - add: false, - delete: false, - edit: false, - }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${readonlyGroupId}?connectionId=${connectionId}`) - .send({ permissions: readonlyPermissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: readonlyGroupId, email: readonlyUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Create edit group - const editGroup = { title: `edit_group_${faker.string.uuid()}` }; - const createEditGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(editGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const editGroupId = JSON.parse(createEditGroupResponse.text).id; - - const editPermissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: editGroupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${editGroupId}?connectionId=${connectionId}`) - .send({ permissions: editPermissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: editGroupId, email: editUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Verify readonly user permissions - const readonlyGetRows = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', readonlyUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(readonlyGetRows.status, 200); - - // Readonly user CANNOT add - const readonlyAddRow = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: faker.person.firstName(), - [testTable.testTableSecondColumnName]: faker.internet.email(), - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', readonlyUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(readonlyAddRow.status, 403); - - // Step 5: Verify edit user permissions - const editGetRows = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', editUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(editGetRows.status, 200); - - // Edit user CAN add - const editAddRow = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: faker.person.firstName(), - [testTable.testTableSecondColumnName]: faker.internet.email(), - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', editUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(editAddRow.status, 201); - - // Edit user CAN delete - const addedRowId = JSON.parse(editAddRow.text).row.id; - const editDeleteRow = await request(app.getHttpServer()) - .delete(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=${addedRowId}`) - .set('Cookie', editUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(editDeleteRow.status, 200); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -//****************************** CEDAR MIGRATION: connections and groups listing ****************************** - -currentTest = 'Cedar migration - GET /connections'; - -test.serial( - `${currentTest} should return all user connections with correct access levels after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - create two connections - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection1 = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const newConnection2 = mockFactory.generateConnectionToTestMySQLDBInDocker(); - const testTable1 = await createTestTable(newConnection1); - const testTable2 = await createTestTable(newConnection2); - - const createConn1Response = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection1) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConn1Response.status, 201); - const connectionId1 = JSON.parse(createConn1Response.text).id; - - const createConn2Response = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection2) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConn2Response.status, 201); - const connectionId2 = JSON.parse(createConn2Response.text).id; - - // Create group with readonly permissions on connection 1 - const newGroup1 = mockFactory.generateCreateGroupDto1(); - const createGroup1Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId1}`) - .set('Cookie', adminUserToken) - .send(newGroup1) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groupId1 = JSON.parse(createGroup1Response.text).id; - - const permissions1 = { - connection: { connectionId: connectionId1, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: groupId1, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTable1.testTableName, - accessLevel: { visibility: true, readonly: true, add: false, delete: false, edit: false }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId1}?connectionId=${connectionId1}`) - .send({ permissions: permissions1 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: groupId1, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Create group with edit permissions on connection 2 - const newGroup2 = { title: `group2_${faker.string.uuid()}` }; - const createGroup2Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId2}`) - .set('Cookie', adminUserToken) - .send(newGroup2) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groupId2 = JSON.parse(createGroup2Response.text).id; - - const permissions2 = { - connection: { connectionId: connectionId2, accessLevel: AccessLevelEnum.edit }, - group: { groupId: groupId2, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTable2.testTableName, - accessLevel: { visibility: true, readonly: false, add: true, delete: true, edit: true }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId2}?connectionId=${connectionId2}`) - .send({ permissions: permissions2 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: groupId2, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Verify GET /connections returns both connections with correct access levels - const getConnectionsResponse = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionsResponse.status, 200); - - const connections = getConnectionsResponse.body.connections; - const conn1 = connections.find(({ connection }: any) => connection.id === connectionId1); - const conn2 = connections.find(({ connection }: any) => connection.id === connectionId2); - - t.truthy(conn1, 'Connection 1 should be present in the list'); - t.is(conn1.accessLevel, AccessLevelEnum.readonly); - - t.truthy(conn2, 'Connection 2 should be present in the list'); - t.is(conn2.accessLevel, AccessLevelEnum.edit); - - // Step 5: Verify admin user also sees all connections - const adminGetConnectionsResponse = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGetConnectionsResponse.status, 200); - - const adminConnections = adminGetConnectionsResponse.body.connections; - const adminConn1 = adminConnections.find(({ connection }: any) => connection.id === connectionId1); - const adminConn2 = adminConnections.find(({ connection }: any) => connection.id === connectionId2); - t.truthy(adminConn1); - t.truthy(adminConn2); - t.is(adminConn1.accessLevel, AccessLevelEnum.edit); - t.is(adminConn2.accessLevel, AccessLevelEnum.edit); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - GET /connection/groups/:slug'; - -test.serial( - `${currentTest} should return groups for connection with correct access levels after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Get admin group id - const getAdminGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const adminGroups = JSON.parse(getAdminGroupsResponse.text); - const adminGroupId = adminGroups[0].group.id; - - // Create two custom groups - const customGroup1 = mockFactory.generateCreateGroupDto1(); - const createCustomGroup1Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(customGroup1) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const customGroupId1 = JSON.parse(createCustomGroup1Response.text).id; - - const customGroup2 = { title: `custom_group_2_${faker.string.uuid()}` }; - const createCustomGroup2Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(customGroup2) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const customGroupId2 = JSON.parse(createCustomGroup2Response.text).id; - - // Set permissions for group 1 - readonly - const perms1 = { - connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: customGroupId1, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { visibility: true, readonly: true, add: false, delete: false, edit: false }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${customGroupId1}?connectionId=${connectionId}`) - .send({ permissions: perms1 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Set permissions for group 2 - edit - const perms2 = { - connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: customGroupId2, accessLevel: AccessLevelEnum.edit }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { visibility: true, readonly: false, add: true, delete: true, edit: true }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${customGroupId2}?connectionId=${connectionId}`) - .send({ permissions: perms2 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Add simple user to group 1 - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: customGroupId1, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Admin user should see all groups (admin + 2 custom) - const adminGetGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGetGroupsResponse.status, 200); - const adminVisibleGroups = JSON.parse(adminGetGroupsResponse.text); - t.is(adminVisibleGroups.length >= 3, true, 'Admin should see at least 3 groups (admin + 2 custom)'); - - // Step 5: Simple user should see groups but NOT the admin group - const simpleGetGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(simpleGetGroupsResponse.status, 200); - const simpleVisibleGroups = JSON.parse(simpleGetGroupsResponse.text); - t.is(simpleVisibleGroups.length > 0, true, 'Simple user should see at least one group'); - - // Simple user should not see the admin group - const adminGroupVisible = simpleVisibleGroups.find((g: any) => g.group.id === adminGroupId); - t.falsy(adminGroupVisible, 'Simple user should not see admin group'); - - // Simple user should see the group they belong to with readonly access level - const userGroup = simpleVisibleGroups.find((g: any) => g.group.id === customGroupId1); - t.truthy(userGroup, 'Simple user should see their own group'); - t.is(userGroup.accessLevel, AccessLevelEnum.readonly); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration - GET /connection/one/:slug'; - -test.serial( - `${currentTest} should return connection details after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a group and add user with readonly permissions - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groupId = JSON.parse(createGroupResponse.text).id; - - const permissions = { - connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId, accessLevel: AccessLevelEnum.readonly }, - tables: [], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Admin can get full connection details - const adminGetConnectionResponse = await request(app.getHttpServer()) - .get(`/connection/one/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGetConnectionResponse.status, 200); - const adminConnectionResult = adminGetConnectionResponse.body.connection; - t.is(adminConnectionResult.type, 'postgres'); - t.is(Object.hasOwn(adminConnectionResult, 'host'), true); - t.is(typeof adminConnectionResult.port, 'number'); - - // Step 5: Simple user can get connection details (readonly) - const simpleGetConnectionResponse = await request(app.getHttpServer()) - .get(`/connection/one/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(simpleGetConnectionResponse.status, 200); - const simpleConnectionResult = simpleGetConnectionResponse.body.connection; - t.is(simpleConnectionResult.type, 'postgres'); - t.is(Object.hasOwn(simpleConnectionResult, 'host'), true); - t.is(Object.hasOwn(simpleConnectionResult, 'password'), false); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -//****************************** CEDAR MIGRATION: demo connections, groups, and users access ****************************** - -currentTest = 'Cedar migration - demo connections access'; - -test.serial( - `${currentTest} should allow access to demo-like connections, their groups, and group users after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - simulate demo connections scenario - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - // Invite two users into the company - const user1Info = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const user1Token = user1Info.token; - - const user2Info = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const user2Token = user2Info.token; - - // Create multiple connections (simulating demo/test connections) - const postgresConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const mysqlConnection = mockFactory.generateConnectionToTestMySQLDBInDocker(); - const testTablePostgres = await createTestTable(postgresConnection); - const testTableMysql = await createTestTable(mysqlConnection); - - const createPostgresResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(postgresConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createPostgresResponse.status, 201); - const postgresConnectionId = JSON.parse(createPostgresResponse.text).id; - - const createMysqlResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(mysqlConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createMysqlResponse.status, 201); - const mysqlConnectionId = JSON.parse(createMysqlResponse.text).id; - - // Get admin group ids for both connections - const getPostgresGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${postgresConnectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const postgresGroupsList = JSON.parse(getPostgresGroupsResponse.text); - const postgresAdminGroupId = postgresGroupsList.find((g: any) => g.group.isMain).group.id; - - const getMysqlGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${mysqlConnectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const mysqlGroupsList = JSON.parse(getMysqlGroupsResponse.text); - const mysqlAdminGroupId = mysqlGroupsList.find((g: any) => g.group.isMain).group.id; - - // Add user1 to admin group of postgres connection - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: postgresAdminGroupId, email: user1Info.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Create a custom group on postgres connection for user2 with readonly - const customGroup = mockFactory.generateCreateGroupDto1(); - const createCustomGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${postgresConnectionId}`) - .set('Cookie', adminUserToken) - .send(customGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const customGroupId = JSON.parse(createCustomGroupResponse.text).id; - - const customPerms = { - connection: { connectionId: postgresConnectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: customGroupId, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTablePostgres.testTableName, - accessLevel: { visibility: true, readonly: true, add: false, delete: false, edit: false }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${customGroupId}?connectionId=${postgresConnectionId}`) - .send({ permissions: customPerms }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: customGroupId, email: user2Info.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Fetch all connections as admin - const adminConnectionsResponse = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminConnectionsResponse.status, 200); - const adminConnections = adminConnectionsResponse.body.connections; - const foundPostgres = adminConnections.find(({ connection }: any) => connection.id === postgresConnectionId); - const foundMysql = adminConnections.find(({ connection }: any) => connection.id === mysqlConnectionId); - t.truthy(foundPostgres, 'Admin should see postgres connection'); - t.truthy(foundMysql, 'Admin should see mysql connection'); - - // Step 5: Pick postgres connection - get its groups - const postgresGroupsAfterMigration = await request(app.getHttpServer()) - .get(`/connection/groups/${postgresConnectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(postgresGroupsAfterMigration.status, 200); - const postgresGroups = JSON.parse(postgresGroupsAfterMigration.text); - t.is(postgresGroups.length >= 2, true, 'Should have at least admin group + custom group'); - - // Find the admin group and custom group - const adminGroup = postgresGroups.find((g: any) => g.group.id === postgresAdminGroupId); - const customGroupResult = postgresGroups.find((g: any) => g.group.id === customGroupId); - t.truthy(adminGroup, 'Admin group should be present'); - t.truthy(customGroupResult, 'Custom group should be present'); - - // Step 6: Get users from admin group - const adminGroupUsersResponse = await request(app.getHttpServer()) - .get(`/group/users/${postgresAdminGroupId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGroupUsersResponse.status, 200); - const adminGroupUsers = JSON.parse(adminGroupUsersResponse.text); - t.is(adminGroupUsers.length >= 2, true, 'Admin group should have at least admin + user1'); - const user1InAdminGroup = adminGroupUsers.find( - (u: any) => u.email.toLowerCase() === user1Info.email.toLowerCase(), - ); - t.truthy(user1InAdminGroup, 'User1 should be in admin group'); - - // Step 7: Get users from custom group - const customGroupUsersResponse = await request(app.getHttpServer()) - .get(`/group/users/${customGroupId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(customGroupUsersResponse.status, 200); - const customGroupUsers = JSON.parse(customGroupUsersResponse.text); - t.is(customGroupUsers.length >= 1, true, 'Custom group should have at least user2'); - const user2InCustomGroup = customGroupUsers.find( - (u: any) => u.email.toLowerCase() === user2Info.email.toLowerCase(), - ); - t.truthy(user2InCustomGroup, 'User2 should be in custom group'); - - // Step 8: Verify user1 (admin group) can access tables on postgres connection - const user1TablesResponse = await request(app.getHttpServer()) - .get(`/connection/tables/${postgresConnectionId}`) - .set('Cookie', user1Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user1TablesResponse.status, 200); - - // Step 9: Verify user2 (custom readonly group) can read but not add rows - const user2GetRowsResponse = await request(app.getHttpServer()) - .get(`/table/rows/${postgresConnectionId}?tableName=${testTablePostgres.testTableName}`) - .set('Cookie', user2Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user2GetRowsResponse.status, 200); - - const user2AddRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${postgresConnectionId}?tableName=${testTablePostgres.testTableName}`) - .send({ - [testTablePostgres.testTableColumnName]: faker.person.firstName(), - [testTablePostgres.testTableSecondColumnName]: faker.internet.email(), - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', user2Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user2AddRowResponse.status, 403); - - // Step 10: Verify user2 can also see groups (via group:read permission) - const user2GroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${postgresConnectionId}`) - .set('Cookie', user2Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user2GroupsResponse.status, 200); - const user2Groups = JSON.parse(user2GroupsResponse.text); - t.is(user2Groups.length > 0, true, 'User2 should see at least their group'); - - // User2 should not see admin group - const user2SeesAdmin = user2Groups.find((g: any) => g.group.id === postgresAdminGroupId); - t.falsy(user2SeesAdmin, 'User2 should not see admin group'); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); diff --git a/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts b/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts deleted file mode 100644 index 4add21929..000000000 --- a/backend/test/ava-tests/saas-tests/saas-cedar-migration-existing-users-e2e.test.ts +++ /dev/null @@ -1,1664 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable security/detect-object-injection */ -import { faker } from '@faker-js/faker'; -import { INestApplication, ValidationPipe } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; -import test from 'ava'; -import { ValidationError } from 'class-validator'; -import cookieParser from 'cookie-parser'; -import request from 'supertest'; -import { DataSource } from 'typeorm'; -import { ApplicationModule } from '../../../src/app.module.js'; -import { BaseType } from '../../../src/common/data-injection.tokens.js'; -import { migratePermissionsToCedar } from '../../../src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.js'; -import { GroupEntity } from '../../../src/entities/group/group.entity.js'; -import { PermissionEntity } from '../../../src/entities/permission/permission.entity.js'; -import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; -import { UserEntity } from '../../../src/entities/user/user.entity.js'; -import { CompanyInfoEntity } from '../../../src/entities/company-info/company-info.entity.js'; -import { ConnectionEntity } from '../../../src/entities/connection/connection.entity.js'; -import { UserRoleEnum } from '../../../src/entities/user/enums/user-role.enum.js'; -import { generateGwtToken } from '../../../src/entities/user/utils/generate-gwt-token.js'; -import { buildDefaultAdminGroups } from '../../../src/entities/user/utils/build-default-admin-groups.js'; -import { generateCedarPolicyForGroup } from '../../../src/entities/cedar-authorization/cedar-policy-generator.js'; -import { AccessLevelEnum, PermissionTypeEnum } from '../../../src/enums/index.js'; -import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; -import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; -import { Messages } from '../../../src/exceptions/text/messages.js'; -import { Cacher } from '../../../src/helpers/cache/cacher.js'; -import { Constants } from '../../../src/helpers/constants/constants.js'; -import { DatabaseModule } from '../../../src/shared/database/database.module.js'; -import { DatabaseService } from '../../../src/shared/database/database.service.js'; -import { MockFactory } from '../../mock.factory.js'; -import { createTestTable } from '../../utils/create-test-table.js'; -import { - inviteUserInCompanyAndAcceptInvitation, - registerUserAndReturnUserInfo, -} from '../../utils/register-user-and-return-user-info.js'; -import { setSaasEnvVariable } from '../../utils/set-saas-env-variable.js'; -import { TestUtils } from '../../utils/test.utils.js'; - -let app: INestApplication; -let _testUtils: TestUtils; -let currentTest: string; - -const mockFactory = new MockFactory(); - -test.before(async () => { - // Start WITHOUT Cedar enabled - simulating the old permission system - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - setSaasEnvVariable(true); - const moduleFixture = await Test.createTestingModule({ - imports: [ApplicationModule, DatabaseModule], - providers: [DatabaseService, TestUtils], - }).compile(); - app = moduleFixture.createNestApplication(); - _testUtils = moduleFixture.get(TestUtils); - - app.use(cookieParser()); - app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); - app.useGlobalPipes( - new ValidationPipe({ - exceptionFactory(validationErrors: ValidationError[] = []) { - return new ValidationException(validationErrors); - }, - }), - ); - await app.init(); - app.getHttpServer().listen(0); -}); - -test.after(async () => { - try { - await Cacher.clearAllCache(); - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - setSaasEnvVariable(false); - await app.close(); - } catch (e) { - console.error('After tests error ' + e); - } -}); - -//****************************** CEDAR MIGRATION: existing SaaS users retain permissions after migration ****************************** - -currentTest = 'Cedar migration SaaS - admin user'; - -test.serial( - `${currentTest} should retain full access for admin user (isMain group) after Cedar migration`, - async (t) => { - try { - // Step 1: Create user and connection WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Step 2: Verify classical permissions work - const getTablesBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify admin user still has full access via Cedar - const getTablesAfterMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesAfterMigration.status, 200); - - // Verify admin can read table rows - const getTableRows = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTableRows.status, 200); - - // Verify admin can add rows - const randomName = faker.person.firstName(); - const randomEmail = faker.internet.email(); - const addRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName, - [testTable.testTableSecondColumnName]: randomEmail, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowResponse.status, 201); - - // Verify admin can update connection - const updateConnection = mockFactory.generateUpdateConnectionDto(); - const updateConnectionResponse = await request(app.getHttpServer()) - .put(`/connection/${connectionId}`) - .send(updateConnection) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateConnectionResponse.status, 200); - - // Verify admin can read groups - const getGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getGroupsResponse.status, 200); - const groups = JSON.parse(getGroupsResponse.text); - t.is(groups.length > 0, true); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration SaaS - non-admin user with readonly permissions'; - -test.serial( - `${currentTest} should retain readonly access for non-admin user after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a non-admin group - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - const groupId = JSON.parse(createGroupResponse.text).id; - - // Set readonly permissions via classical system - const permissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: groupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: true, - add: false, - delete: false, - edit: false, - }, - }, - ], - }; - - const setPermissionsResponse = await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(setPermissionsResponse.status, 200); - - // Add user to group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify classical permissions work - const getTablesBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesBeforeMigration.status, 200); - - const getRowsBeforeMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify user retains readonly access via Cedar - const getConnectionsAfterMigration = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionsAfterMigration.status, 200); - const connections = getConnectionsAfterMigration.body.connections; - const targetConnection = connections.find( - ({ connection }: any) => connection.id === connectionId, - ); - t.is(targetConnection.accessLevel, AccessLevelEnum.readonly); - - const getTablesAfterMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesAfterMigration.status, 200); - - const getRowsAfterMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsAfterMigration.status, 200); - - // Verify readonly user still CANNOT update connection - const updateConnection = mockFactory.generateUpdateConnectionDto(); - const updateConnectionResponse = await request(app.getHttpServer()) - .put(`/connection/${connectionId}`) - .send(updateConnection) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateConnectionResponse.status, 403); - - // Verify readonly user still CANNOT add rows - const randomName = faker.person.firstName(); - const randomEmail = faker.internet.email(); - const addRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName, - [testTable.testTableSecondColumnName]: randomEmail, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowResponse.status, 403); - - // Verify readonly user still CANNOT delete rows - const deleteRowResponse = await request(app.getHttpServer()) - .delete(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=1`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(deleteRowResponse.status, 403); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration SaaS - non-admin user with edit permissions'; - -test.serial( - `${currentTest} should retain table edit permissions for non-admin user after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a non-admin group with full table edit permissions - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - const groupId = JSON.parse(createGroupResponse.text).id; - - // Set edit permissions - const permissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: groupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Add user to group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify classical permissions work - user can add rows - const randomName1 = faker.person.firstName(); - const randomEmail1 = faker.internet.email(); - const addRowBeforeMigration = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName1, - [testTable.testTableSecondColumnName]: randomEmail1, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowBeforeMigration.status, 201); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify all table operations work via Cedar - - // Can read table rows - const getRowsAfterMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsAfterMigration.status, 200); - - // Can add rows - const randomName2 = faker.person.firstName(); - const randomEmail2 = faker.internet.email(); - const addRowAfterMigration = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName2, - [testTable.testTableSecondColumnName]: randomEmail2, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowAfterMigration.status, 201); - const addedRowId = JSON.parse(addRowAfterMigration.text).row.id; - - // Can update rows - const randomName3 = faker.person.firstName(); - const randomEmail3 = faker.internet.email(); - const updateRowAfterMigration = await request(app.getHttpServer()) - .put(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=${addedRowId}`) - .send({ - [testTable.testTableColumnName]: randomName3, - [testTable.testTableSecondColumnName]: randomEmail3, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateRowAfterMigration.status, 200); - - // Can delete rows - const deleteRowAfterMigration = await request(app.getHttpServer()) - .delete(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=${addedRowId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(deleteRowAfterMigration.status, 200); - - // Verify connection is still readonly (cannot update connection) - const updateConnection = mockFactory.generateUpdateConnectionDto(); - const updateConnectionResponse = await request(app.getHttpServer()) - .put(`/connection/${connectionId}`) - .send(updateConnection) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(updateConnectionResponse.status, 403); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration SaaS - user added to admin group of existing connection'; - -test.serial( - `${currentTest} should retain admin access for user added to admin group after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Get admin group (isMain=true) - const getGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groups = JSON.parse(getGroupsResponse.text); - const adminGroupId = groups[0].group.id; - - // Add user to admin group - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: adminGroupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Verify user has admin access via classical permissions - const getTablesBeforeMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesBeforeMigration.status, 200); - - // Step 3: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 4: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 5: Verify admin group member retains full access via Cedar - - // Can read tables - const getTablesAfterMigration = await request(app.getHttpServer()) - .get(`/connection/tables/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getTablesAfterMigration.status, 200); - - // Can read table rows - const getRowsAfterMigration = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getRowsAfterMigration.status, 200); - - // Can add rows - const randomName = faker.person.firstName(); - const randomEmail = faker.internet.email(); - const addRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: randomName, - [testTable.testTableSecondColumnName]: randomEmail, - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(addRowResponse.status, 201); - - // Can create new group (admin privilege) - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', simpleUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createGroupResponse.status, 201); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration SaaS - multiple users with different permission levels'; - -test.serial( - `${currentTest} should retain correct permissions for multiple users after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const readonlyUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const readonlyUserToken = readonlyUserInfo.token; - - const editUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const editUserToken = editUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create readonly group - const readonlyGroup = mockFactory.generateCreateGroupDto1(); - const createReadonlyGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(readonlyGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const readonlyGroupId = JSON.parse(createReadonlyGroupResponse.text).id; - - const readonlyPermissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: readonlyGroupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: true, - add: false, - delete: false, - edit: false, - }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${readonlyGroupId}?connectionId=${connectionId}`) - .send({ permissions: readonlyPermissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: readonlyGroupId, email: readonlyUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Create edit group - const editGroup = { title: `edit_group_${faker.string.uuid()}` }; - const createEditGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(editGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const editGroupId = JSON.parse(createEditGroupResponse.text).id; - - const editPermissions = { - connection: { - connectionId: connectionId, - accessLevel: AccessLevelEnum.readonly, - }, - group: { - groupId: editGroupId, - accessLevel: AccessLevelEnum.readonly, - }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${editGroupId}?connectionId=${connectionId}`) - .send({ permissions: editPermissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: editGroupId, email: editUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Verify readonly user permissions - const readonlyGetRows = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', readonlyUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(readonlyGetRows.status, 200); - - // Readonly user CANNOT add - const readonlyAddRow = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: faker.person.firstName(), - [testTable.testTableSecondColumnName]: faker.internet.email(), - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', readonlyUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(readonlyAddRow.status, 403); - - // Step 5: Verify edit user permissions - const editGetRows = await request(app.getHttpServer()) - .get(`/table/rows/${connectionId}?tableName=${testTable.testTableName}`) - .set('Cookie', editUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(editGetRows.status, 200); - - // Edit user CAN add - const editAddRow = await request(app.getHttpServer()) - .post(`/table/row/${connectionId}?tableName=${testTable.testTableName}`) - .send({ - [testTable.testTableColumnName]: faker.person.firstName(), - [testTable.testTableSecondColumnName]: faker.internet.email(), - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', editUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(editAddRow.status, 201); - - // Edit user CAN delete - const addedRowId = JSON.parse(editAddRow.text).row.id; - const editDeleteRow = await request(app.getHttpServer()) - .delete(`/table/row/${connectionId}?tableName=${testTable.testTableName}&id=${addedRowId}`) - .set('Cookie', editUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(editDeleteRow.status, 200); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -//****************************** CEDAR MIGRATION: connections and groups listing ****************************** - -currentTest = 'Cedar migration SaaS - GET /connections'; - -test.serial( - `${currentTest} should return all user connections with correct access levels after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - create two connections - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection1 = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const newConnection2 = mockFactory.generateConnectionToTestMySQLDBInDocker(); - const testTable1 = await createTestTable(newConnection1); - const testTable2 = await createTestTable(newConnection2); - - const createConn1Response = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection1) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConn1Response.status, 201); - const connectionId1 = JSON.parse(createConn1Response.text).id; - - const createConn2Response = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection2) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConn2Response.status, 201); - const connectionId2 = JSON.parse(createConn2Response.text).id; - - // Create group with readonly permissions on connection 1 - const newGroup1 = mockFactory.generateCreateGroupDto1(); - const createGroup1Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId1}`) - .set('Cookie', adminUserToken) - .send(newGroup1) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groupId1 = JSON.parse(createGroup1Response.text).id; - - const permissions1 = { - connection: { connectionId: connectionId1, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: groupId1, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTable1.testTableName, - accessLevel: { visibility: true, readonly: true, add: false, delete: false, edit: false }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId1}?connectionId=${connectionId1}`) - .send({ permissions: permissions1 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: groupId1, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Create group with edit permissions on connection 2 - const newGroup2 = { title: `group2_${faker.string.uuid()}` }; - const createGroup2Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId2}`) - .set('Cookie', adminUserToken) - .send(newGroup2) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groupId2 = JSON.parse(createGroup2Response.text).id; - - const permissions2 = { - connection: { connectionId: connectionId2, accessLevel: AccessLevelEnum.edit }, - group: { groupId: groupId2, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTable2.testTableName, - accessLevel: { visibility: true, readonly: false, add: true, delete: true, edit: true }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId2}?connectionId=${connectionId2}`) - .send({ permissions: permissions2 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: groupId2, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Verify GET /connections returns both connections with correct access levels - const getConnectionsResponse = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getConnectionsResponse.status, 200); - - const connections = getConnectionsResponse.body.connections; - const conn1 = connections.find(({ connection }: any) => connection.id === connectionId1); - const conn2 = connections.find(({ connection }: any) => connection.id === connectionId2); - - t.truthy(conn1, 'Connection 1 should be present in the list'); - t.is(conn1.accessLevel, AccessLevelEnum.readonly); - - t.truthy(conn2, 'Connection 2 should be present in the list'); - t.is(conn2.accessLevel, AccessLevelEnum.edit); - - // Step 5: Verify admin user also sees all connections - const adminGetConnectionsResponse = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGetConnectionsResponse.status, 200); - - const adminConnections = adminGetConnectionsResponse.body.connections; - const adminConn1 = adminConnections.find(({ connection }: any) => connection.id === connectionId1); - const adminConn2 = adminConnections.find(({ connection }: any) => connection.id === connectionId2); - t.truthy(adminConn1); - t.truthy(adminConn2); - t.is(adminConn1.accessLevel, AccessLevelEnum.edit); - t.is(adminConn2.accessLevel, AccessLevelEnum.edit); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration SaaS - GET /connection/groups/:slug'; - -test.serial( - `${currentTest} should return groups for connection with correct access levels after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const testTable = await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Get admin group id - const getAdminGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const adminGroups = JSON.parse(getAdminGroupsResponse.text); - const adminGroupId = adminGroups[0].group.id; - - // Create two custom groups - const customGroup1 = mockFactory.generateCreateGroupDto1(); - const createCustomGroup1Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(customGroup1) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const customGroupId1 = JSON.parse(createCustomGroup1Response.text).id; - - const customGroup2 = { title: `custom_group_2_${faker.string.uuid()}` }; - const createCustomGroup2Response = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(customGroup2) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const customGroupId2 = JSON.parse(createCustomGroup2Response.text).id; - - // Set permissions for group 1 - readonly - const perms1 = { - connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: customGroupId1, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { visibility: true, readonly: true, add: false, delete: false, edit: false }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${customGroupId1}?connectionId=${connectionId}`) - .send({ permissions: perms1 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Set permissions for group 2 - edit - const perms2 = { - connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: customGroupId2, accessLevel: AccessLevelEnum.edit }, - tables: [ - { - tableName: testTable.testTableName, - accessLevel: { visibility: true, readonly: false, add: true, delete: true, edit: true }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${customGroupId2}?connectionId=${connectionId}`) - .send({ permissions: perms2 }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Add simple user to group 1 - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: customGroupId1, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Admin user should see all groups (admin + 2 custom) - const adminGetGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGetGroupsResponse.status, 200); - const adminVisibleGroups = JSON.parse(adminGetGroupsResponse.text); - t.is(adminVisibleGroups.length >= 3, true, 'Admin should see at least 3 groups (admin + 2 custom)'); - - // Step 5: Simple user should see groups but NOT the admin group - const simpleGetGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(simpleGetGroupsResponse.status, 200); - const simpleVisibleGroups = JSON.parse(simpleGetGroupsResponse.text); - t.is(simpleVisibleGroups.length > 0, true, 'Simple user should see at least one group'); - - // Simple user should not see the admin group - const adminGroupVisible = simpleVisibleGroups.find((g: any) => g.group.id === adminGroupId); - t.falsy(adminGroupVisible, 'Simple user should not see admin group'); - - // Simple user should see the group they belong to with readonly access level - const userGroup = simpleVisibleGroups.find((g: any) => g.group.id === customGroupId1); - t.truthy(userGroup, 'Simple user should see their own group'); - t.is(userGroup.accessLevel, AccessLevelEnum.readonly); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -currentTest = 'Cedar migration SaaS - GET /connection/one/:slug'; - -test.serial( - `${currentTest} should return connection details after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - const simpleUserInfo = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const simpleUserToken = simpleUserInfo.token; - - const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - await createTestTable(newConnection); - - const createConnectionResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(newConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createConnectionResponse.status, 201); - const connectionId = JSON.parse(createConnectionResponse.text).id; - - // Create a group and add user with readonly permissions - const newGroup = mockFactory.generateCreateGroupDto1(); - const createGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${connectionId}`) - .set('Cookie', adminUserToken) - .send(newGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const groupId = JSON.parse(createGroupResponse.text).id; - - const permissions = { - connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId, accessLevel: AccessLevelEnum.readonly }, - tables: [], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${groupId}?connectionId=${connectionId}`) - .send({ permissions }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId, email: simpleUserInfo.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Admin can get full connection details - const adminGetConnectionResponse = await request(app.getHttpServer()) - .get(`/connection/one/${connectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGetConnectionResponse.status, 200); - const adminConnectionResult = adminGetConnectionResponse.body.connection; - t.is(adminConnectionResult.type, 'postgres'); - t.is(Object.hasOwn(adminConnectionResult, 'host'), true); - t.is(typeof adminConnectionResult.port, 'number'); - - // Step 5: Simple user can get connection details (readonly) - const simpleGetConnectionResponse = await request(app.getHttpServer()) - .get(`/connection/one/${connectionId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(simpleGetConnectionResponse.status, 200); - const simpleConnectionResult = simpleGetConnectionResponse.body.connection; - t.is(simpleConnectionResult.type, 'postgres'); - t.is(Object.hasOwn(simpleConnectionResult, 'host'), true); - t.is(Object.hasOwn(simpleConnectionResult, 'password'), false); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -//****************************** CEDAR MIGRATION: demo connections, groups, and users access ****************************** - -currentTest = 'Cedar migration SaaS - demo connections access'; - -test.serial( - `${currentTest} should allow access to demo-like connections, their groups, and group users after Cedar migration`, - async (t) => { - try { - // Step 1: Set up WITHOUT Cedar - simulate demo connections scenario - const adminUserInfo = await registerUserAndReturnUserInfo(app); - const adminUserToken = adminUserInfo.token; - - // Invite two users into the company - const user1Info = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const user1Token = user1Info.token; - - const user2Info = await inviteUserInCompanyAndAcceptInvitation( - adminUserToken, - undefined, - app, - undefined, - ); - const user2Token = user2Info.token; - - // Create multiple connections (simulating demo/test connections) - const postgresConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); - const mysqlConnection = mockFactory.generateConnectionToTestMySQLDBInDocker(); - const testTablePostgres = await createTestTable(postgresConnection); - const testTableMysql = await createTestTable(mysqlConnection); - - const createPostgresResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(postgresConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createPostgresResponse.status, 201); - const postgresConnectionId = JSON.parse(createPostgresResponse.text).id; - - const createMysqlResponse = await request(app.getHttpServer()) - .post('/connection') - .set('Cookie', adminUserToken) - .send(mysqlConnection) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(createMysqlResponse.status, 201); - const mysqlConnectionId = JSON.parse(createMysqlResponse.text).id; - - // Get admin group ids for both connections - const getPostgresGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${postgresConnectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const postgresGroupsList = JSON.parse(getPostgresGroupsResponse.text); - const postgresAdminGroupId = postgresGroupsList.find((g: any) => g.group.isMain).group.id; - - const getMysqlGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${mysqlConnectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const mysqlGroupsList = JSON.parse(getMysqlGroupsResponse.text); - const mysqlAdminGroupId = mysqlGroupsList.find((g: any) => g.group.isMain).group.id; - - // Add user1 to admin group of postgres connection - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: postgresAdminGroupId, email: user1Info.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Create a custom group on postgres connection for user2 with readonly - const customGroup = mockFactory.generateCreateGroupDto1(); - const createCustomGroupResponse = await request(app.getHttpServer()) - .post(`/connection/group/${postgresConnectionId}`) - .set('Cookie', adminUserToken) - .send(customGroup) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - const customGroupId = JSON.parse(createCustomGroupResponse.text).id; - - const customPerms = { - connection: { connectionId: postgresConnectionId, accessLevel: AccessLevelEnum.readonly }, - group: { groupId: customGroupId, accessLevel: AccessLevelEnum.readonly }, - tables: [ - { - tableName: testTablePostgres.testTableName, - accessLevel: { visibility: true, readonly: true, add: false, delete: false, edit: false }, - }, - ], - }; - - await request(app.getHttpServer()) - .put(`/permissions/${customGroupId}?connectionId=${postgresConnectionId}`) - .send({ permissions: customPerms }) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - await request(app.getHttpServer()) - .put('/group/user') - .set('Cookie', adminUserToken) - .send({ groupId: customGroupId, email: user2Info.email }) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - - // Step 2: Run Cedar migration - const dataSource = app.get(BaseType.DATA_SOURCE); - await migratePermissionsToCedar(dataSource); - - // Step 3: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 4: Fetch all connections as admin - const adminConnectionsResponse = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminConnectionsResponse.status, 200); - const adminConnections = adminConnectionsResponse.body.connections; - const foundPostgres = adminConnections.find(({ connection }: any) => connection.id === postgresConnectionId); - const foundMysql = adminConnections.find(({ connection }: any) => connection.id === mysqlConnectionId); - t.truthy(foundPostgres, 'Admin should see postgres connection'); - t.truthy(foundMysql, 'Admin should see mysql connection'); - - // Step 5: Pick postgres connection - get its groups - const postgresGroupsAfterMigration = await request(app.getHttpServer()) - .get(`/connection/groups/${postgresConnectionId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(postgresGroupsAfterMigration.status, 200); - const postgresGroups = JSON.parse(postgresGroupsAfterMigration.text); - t.is(postgresGroups.length >= 2, true, 'Should have at least admin group + custom group'); - - // Find the admin group and custom group - const adminGroup = postgresGroups.find((g: any) => g.group.id === postgresAdminGroupId); - const customGroupResult = postgresGroups.find((g: any) => g.group.id === customGroupId); - t.truthy(adminGroup, 'Admin group should be present'); - t.truthy(customGroupResult, 'Custom group should be present'); - - // Step 6: Get users from admin group - const adminGroupUsersResponse = await request(app.getHttpServer()) - .get(`/group/users/${postgresAdminGroupId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(adminGroupUsersResponse.status, 200); - const adminGroupUsers = JSON.parse(adminGroupUsersResponse.text); - t.is(adminGroupUsers.length >= 2, true, 'Admin group should have at least admin + user1'); - const user1InAdminGroup = adminGroupUsers.find( - (u: any) => u.email.toLowerCase() === user1Info.email.toLowerCase(), - ); - t.truthy(user1InAdminGroup, 'User1 should be in admin group'); - - // Step 7: Get users from custom group - const customGroupUsersResponse = await request(app.getHttpServer()) - .get(`/group/users/${customGroupId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(customGroupUsersResponse.status, 200); - const customGroupUsers = JSON.parse(customGroupUsersResponse.text); - t.is(customGroupUsers.length >= 1, true, 'Custom group should have at least user2'); - const user2InCustomGroup = customGroupUsers.find( - (u: any) => u.email.toLowerCase() === user2Info.email.toLowerCase(), - ); - t.truthy(user2InCustomGroup, 'User2 should be in custom group'); - - // Step 8: Verify user1 (admin group) can access tables on postgres connection - const user1TablesResponse = await request(app.getHttpServer()) - .get(`/connection/tables/${postgresConnectionId}`) - .set('Cookie', user1Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user1TablesResponse.status, 200); - - // Step 9: Verify user2 (custom readonly group) can read but not add rows - const user2GetRowsResponse = await request(app.getHttpServer()) - .get(`/table/rows/${postgresConnectionId}?tableName=${testTablePostgres.testTableName}`) - .set('Cookie', user2Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user2GetRowsResponse.status, 200); - - const user2AddRowResponse = await request(app.getHttpServer()) - .post(`/table/row/${postgresConnectionId}?tableName=${testTablePostgres.testTableName}`) - .send({ - [testTablePostgres.testTableColumnName]: faker.person.firstName(), - [testTablePostgres.testTableSecondColumnName]: faker.internet.email(), - created_at: new Date(), - updated_at: new Date(), - }) - .set('Cookie', user2Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user2AddRowResponse.status, 403); - - // Step 10: Verify user2 can also see groups (via group:read permission) - const user2GroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${postgresConnectionId}`) - .set('Cookie', user2Token) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(user2GroupsResponse.status, 200); - const user2Groups = JSON.parse(user2GroupsResponse.text); - t.is(user2Groups.length > 0, true, 'User2 should see at least their group'); - - // User2 should not see admin group - const user2SeesAdmin = user2Groups.find((g: any) => g.group.id === postgresAdminGroupId); - t.falsy(user2SeesAdmin, 'User2 should not see admin group'); - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); - -//****************************** CEDAR MIGRATION: demo-style connections (buildDefaultAdminGroups + buildDefaultAdminPermissions flow) ****************************** - -currentTest = 'Cedar migration SaaS - demo-style connections created via buildDefaultAdminGroups/Permissions'; - -test.serial( - `${currentTest} should retain access after Cedar migration when permissions are created via the same flow as DemoDataService`, - async (t) => { - try { - // This test replicates the exact same code flow that DemoDataService.createDemoData() uses: - // 1. Create connection - // 2. buildDefaultAdminGroups → save groups (with permissions: []) - // 3. buildDefaultAdminPermissions → save permissions (with permission.groups = [group]) - // 4. Generate Cedar policies and re-save groups (the step that previously wiped the join table) - // 5. NULL out Cedar policies to simulate old pre-fix state - // 6. Run migration and verify access levels - const dataSource = app.get(BaseType.DATA_SOURCE); - const userRepository = dataSource.getRepository(UserEntity); - const companyRepository = dataSource.getRepository(CompanyInfoEntity); - const connectionRepository = dataSource.getRepository(ConnectionEntity); - const groupRepository = dataSource.getRepository(GroupEntity); - const permissionRepository = dataSource.getRepository(PermissionEntity); - - const email = `demo_test_${faker.string.uuid()}@test.local`.toLowerCase(); - const password = '#r@dY^e&7R4b5Ib@31iE4xbn'; - - const company = companyRepository.create({ - id: faker.string.uuid(), - name: `demo_company_${faker.string.uuid()}`, - }); - const savedCompany = await companyRepository.save(company); - - const user = userRepository.create({ - email, - password, - isActive: true, - company: savedCompany, - role: UserRoleEnum.ADMIN, - }); - const savedUser = await userRepository.save(user); - - const tokenData = generateGwtToken(savedUser, []); - const userToken = `${Constants.JWT_COOKIE_KEY_NAME}=${tokenData.token}`; - - // Step 1: Create a connection (same as DemoDataService but without isTestConnection to avoid test-connection pruning) - const connectionEntity = connectionRepository.create({ - title: 'Demo-style Postgres', - type: 'postgres' as any, - host: 'testPg-e2e-testing', - port: 5432, - username: 'postgres', - password: '123', - database: 'postgres', - masterEncryption: false, - isTestConnection: false, - author: savedUser, - }); - const savedConnection = await connectionRepository.save(connectionEntity); - - // Step 2: buildDefaultAdminGroups (same as DemoDataService) - const testGroupsEntities = buildDefaultAdminGroups(savedUser, [savedConnection]); - const createdGroups = await Promise.all( - testGroupsEntities.map(async (group: GroupEntity) => { - return await groupRepository.save(group); - }), - ); - - // Step 3: Create old-style permission entities (simulating pre-Cedar state) - for (const group of createdGroups) { - const connPerm = new PermissionEntity(); - connPerm.type = PermissionTypeEnum.Connection; - connPerm.accessLevel = AccessLevelEnum.edit; - connPerm.groups = [group]; - await permissionRepository.save(connPerm); - - const groupPerm = new PermissionEntity(); - groupPerm.type = PermissionTypeEnum.Group; - groupPerm.accessLevel = AccessLevelEnum.edit; - groupPerm.groups = [group]; - await permissionRepository.save(groupPerm); - } - - // Step 4: Generate Cedar policies and re-save groups (same as DemoDataService fix) - // This is the critical step: the fix in DemoDataService deletes group.permissions - // before re-saving, otherwise TypeORM syncs the empty array and wipes the join table - await Promise.all( - createdGroups.map(async (group: GroupEntity) => { - const connectionId = group.connection?.id; - if (!connectionId) return; - group.cedarPolicy = generateCedarPolicyForGroup(connectionId, group.isMain, { - connection: { connectionId, accessLevel: AccessLevelEnum.edit }, - group: { groupId: group.id, accessLevel: AccessLevelEnum.edit }, - tables: [], - }); - delete group.permissions; - delete group.users; - await groupRepository.save(group); - }), - ); - - // Step 5: Verify permissions are still linked to groups (the fix should preserve them) - for (const group of createdGroups) { - const loadedGroup = await groupRepository - .createQueryBuilder('group') - .leftJoinAndSelect('group.permissions', 'permission') - .where('group.id = :id', { id: group.id }) - .getOne(); - t.is(loadedGroup.permissions.length, 2, `Group ${group.id} should have 2 permissions (Connection + Group)`); - const connectionPerm = loadedGroup.permissions.find((p) => p.type === PermissionTypeEnum.Connection); - const groupPerm = loadedGroup.permissions.find((p) => p.type === PermissionTypeEnum.Group); - t.truthy(connectionPerm, 'Should have Connection permission'); - t.truthy(groupPerm, 'Should have Group permission'); - t.is(connectionPerm.accessLevel, AccessLevelEnum.edit, 'Connection permission should be edit'); - t.is(groupPerm.accessLevel, AccessLevelEnum.edit, 'Group permission should be edit'); - } - - // Step 6: NULL out Cedar policies to simulate old pre-fix state - for (const group of createdGroups) { - await groupRepository.update(group.id, { cedarPolicy: null }); - } - - // Step 7: Run Cedar migration - await migratePermissionsToCedar(dataSource); - - // Step 8: Enable Cedar - process.env.CEDAR_AUTHORIZATION_ENABLED = 'true'; - await Cacher.clearAllCache(); - - // Step 9: Verify migration regenerated correct Cedar policies - for (const group of createdGroups) { - const migratedGroup = await groupRepository.findOne({ where: { id: group.id } }); - t.truthy(migratedGroup.cedarPolicy, `Group ${group.id} should have a Cedar policy after migration`); - t.truthy( - migratedGroup.cedarPolicy.length > 0, - `Group ${group.id} Cedar policy should not be empty`, - ); - } - - // Step 10: Verify user can still access the connection via API with Cedar enabled - const connectionsAfterMigration = await request(app.getHttpServer()) - .get('/connections') - .set('Cookie', userToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(connectionsAfterMigration.status, 200); - const connections = connectionsAfterMigration.body.connections; - const demoConnection = connections.find( - ({ connection }) => connection.id === savedConnection.id, - ); - t.truthy(demoConnection, 'Demo-style connection should be visible after Cedar migration'); - t.is(demoConnection.accessLevel, AccessLevelEnum.edit, 'Admin should have edit access after Cedar migration'); - - // Step 11: Verify can get groups for the connection - const groupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${savedConnection.id}`) - .set('Cookie', userToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(groupsResponse.status, 200); - const groupsData = JSON.parse(groupsResponse.text); - t.is(groupsData.length > 0, true, 'Connection should have at least one group'); - - // Step 12: Verify can get group users - for (const groupData of groupsData) { - const groupUsersResponse = await request(app.getHttpServer()) - .get(`/group/users/${groupData.group.id}`) - .set('Cookie', userToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(groupUsersResponse.status, 200, `Should be able to get users for group ${groupData.group.id}`); - const groupUsers = JSON.parse(groupUsersResponse.text); - t.is(groupUsers.length > 0, true, `Group ${groupData.group.id} should have at least one user`); - } - } catch (e) { - console.error(e); - throw e; - } finally { - delete process.env.CEDAR_AUTHORIZATION_ENABLED; - } - }, -); diff --git a/backend/test/mock.factory.ts b/backend/test/mock.factory.ts index 4906ae847..7a0144251 100644 --- a/backend/test/mock.factory.ts +++ b/backend/test/mock.factory.ts @@ -512,12 +512,9 @@ export class MockFactory { 'group', 'tableSettings', 'tableLogs', - 'permission', - 'permission_groups_group', 'user_groups_group', 'user', 'connection', - 'group_permissions_permission', 'group_users_user', 'customFields', 'agent', From 8f2044de2517956c88302c39b9250e4009e0845e Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Mon, 23 Mar 2026 12:52:38 +0000 Subject: [PATCH 4/5] remove old permissions code --- .../cedar-authorization.service.ts | 2 +- ...ssions-for-group-in-connection.use.case.ts | 2 +- .../group-custom-repository-extension.ts | 2 +- .../repository/group.repository.interface.ts | 2 +- .../create-or-update-permissions.use.case.ts | 2 +- backend/test/mock.factory.ts | 29 +------------------ 6 files changed, 6 insertions(+), 33 deletions(-) diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts index 4c05429d6..5cdafd917 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts @@ -85,7 +85,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On ): Promise<{ cedarPolicy: string; classicalPermissions: IComplexPermission }> { this.validateCedarPolicyText(cedarPolicy); - const group = await this.globalDbContext.groupRepository.findGroupWithPermissionsById(groupId); + const group = await this.globalDbContext.groupRepository.findGroupById(groupId); if (!group) { throw new HttpException({ message: Messages.GROUP_NOT_FOUND }, HttpStatus.BAD_REQUEST); } diff --git a/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts b/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts index 6cb54316e..b0ccc8e13 100644 --- a/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/get-permissions-for-group-in-connection.use.case.ts @@ -23,7 +23,7 @@ export class GetPermissionsForGroupInConnectionUseCase } protected async implementation(inputData: GetPermissionsInConnectionDs): Promise { - const group = await this._dbContext.groupRepository.findGroupWithPermissionsById(inputData.groupId); + const group = await this._dbContext.groupRepository.findGroupById(inputData.groupId); let connectionAccessLevel = AccessLevelEnum.none; let groupAccessLevel = AccessLevelEnum.none; diff --git a/backend/src/entities/group/repository/group-custom-repository-extension.ts b/backend/src/entities/group/repository/group-custom-repository-extension.ts index 7d4321896..48df0bee7 100644 --- a/backend/src/entities/group/repository/group-custom-repository-extension.ts +++ b/backend/src/entities/group/repository/group-custom-repository-extension.ts @@ -69,7 +69,7 @@ export const groupCustomRepositoryExtension: IGroupRepository = { return await qb.getMany(); }, - async findGroupWithPermissionsById(groupId: string): Promise { + async findGroupById(groupId: string): Promise { const qb = this.createQueryBuilder('group').andWhere('group.id = :id', { id: groupId }); return await qb.getOne(); }, diff --git a/backend/src/entities/group/repository/group.repository.interface.ts b/backend/src/entities/group/repository/group.repository.interface.ts index 899334bc9..4b3dc7390 100644 --- a/backend/src/entities/group/repository/group.repository.interface.ts +++ b/backend/src/entities/group/repository/group.repository.interface.ts @@ -21,7 +21,7 @@ export interface IGroupRepository { findAllUsersInGroup(groupId: string): Promise>; - findGroupWithPermissionsById(groupId: string): Promise; + findGroupById(groupId: string): Promise; findAllUsersInGroupsWhereUserIsAdmin(userId: string, connectionId: string): Promise>; } diff --git a/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts b/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts index 6868c70bc..c68fb7669 100644 --- a/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts +++ b/backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts @@ -40,7 +40,7 @@ export class CreateOrUpdatePermissionsUseCase HttpStatus.BAD_REQUEST, ); } - const groupToUpdate = await this._dbContext.groupRepository.findGroupWithPermissionsById(groupId); + const groupToUpdate = await this._dbContext.groupRepository.findGroupById(groupId); if (!groupToUpdate) { throw new HttpException( { diff --git a/backend/test/mock.factory.ts b/backend/test/mock.factory.ts index 7a0144251..4bf4ab616 100644 --- a/backend/test/mock.factory.ts +++ b/backend/test/mock.factory.ts @@ -8,7 +8,7 @@ import { CreateConnectionPropertiesDto } from '../src/entities/connection-proper import { CreateTableActionDTO } from '../src/entities/table-actions/table-actions-module/dto/create-table-action.dto.js'; import { TableActionEntity } from '../src/entities/table-actions/table-actions-module/table-action.entity.js'; import { CreateTableWidgetDto } from '../src/entities/widget/dto/index.js'; -import { AccessLevelEnum, PermissionTypeEnum, QueryOrderingEnum, WidgetTypeEnum } from '../src/enums/index.js'; +import { AccessLevelEnum, QueryOrderingEnum, WidgetTypeEnum } from '../src/enums/index.js'; import { TestConstants } from './mocks/test-constants.js'; class CreateGroupDto { @@ -425,33 +425,6 @@ export class MockFactory { return permissionsArr; } - generateCreatePermissionDTOgroup(groupId) { - return { - type: PermissionTypeEnum.Group, - accessLevel: AccessLevelEnum.edit, - tableName: '', - groupId: groupId, - }; - } - - generateCreatePermissionDTOconnection(groupId) { - return { - type: PermissionTypeEnum.Connection, - accessLevel: AccessLevelEnum.edit, - tableName: '', - groupId: groupId, - }; - } - - generateCreatePermissionDTOtable(groupId: string, tableName: string) { - return { - type: PermissionTypeEnum.Table, - accessLevel: AccessLevelEnum.edit, - tableName: tableName, - groupId: groupId, - }; - } - generatePermissions( connectionId: string, _groupId: string, From 0d43b8802a934d9f4fa8c6a3edb30ecdbdc7c51a Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Mon, 23 Mar 2026 14:11:39 +0000 Subject: [PATCH 5/5] refactor: streamline CedarAuthorizationService and remove legacy permission migration script --- .../cedar-authorization.service.ts | 17 ++-- .../scripts/migrate-permissions-to-cedar.ts | 82 ------------------- .../entities/permission/permission.module.ts | 5 -- backend/src/enums/index.ts | 1 - backend/src/enums/permission-type.enum.ts | 5 -- backend/src/main.ts | 5 -- 6 files changed, 5 insertions(+), 110 deletions(-) delete mode 100644 backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts delete mode 100644 backend/src/enums/permission-type.enum.ts diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts index 5cdafd917..7edce909b 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts @@ -85,14 +85,12 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On ): Promise<{ cedarPolicy: string; classicalPermissions: IComplexPermission }> { this.validateCedarPolicyText(cedarPolicy); - const group = await this.globalDbContext.groupRepository.findGroupById(groupId); + const group = await this.globalDbContext.groupRepository.findGroupByIdWithConnectionAndUsers(groupId); if (!group) { throw new HttpException({ message: Messages.GROUP_NOT_FOUND }, HttpStatus.BAD_REQUEST); } - const groupWithConnection = await this.globalDbContext.groupRepository.findGroupByIdWithConnectionAndUsers(groupId); - - if (groupWithConnection?.connection?.id !== connectionId) { + if (group.connection?.id !== connectionId) { throw new HttpException({ message: Messages.GROUP_NOT_FROM_THIS_CONNECTION }, HttpStatus.BAD_REQUEST); } @@ -177,7 +175,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On const userGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnection(connectionId, userId); if (userGroups.length === 0) return false; - const groupPolicies = await this.loadPoliciesPerGroup(connectionId, userGroups); + const groupPolicies = this.loadPoliciesPerGroup(userGroups); if (groupPolicies.length === 0) return false; const entities = buildCedarEntities(userId, userGroups, connectionId, tableName, dashboardId); @@ -206,13 +204,8 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On return false; } - private async loadPoliciesPerGroup(connectionId: string, userGroups: Array): Promise { - const groups = await this.globalDbContext.groupRepository.findAllGroupsInConnection(connectionId); - const userGroupIdSet = new Set(userGroups.map((g) => g.id)); - return groups - .filter((g) => userGroupIdSet.has(g.id)) - .map((g) => g.cedarPolicy) - .filter(Boolean); + private loadPoliciesPerGroup(userGroups: Array): string[] { + return userGroups.map((g) => g.cedarPolicy).filter(Boolean); } private async assertUserNotSuspended(userId: string): Promise { diff --git a/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts b/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts deleted file mode 100644 index 02af012e5..000000000 --- a/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DataSource } from 'typeorm'; -import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; -import { GroupEntity } from '../../group/group.entity.js'; -import { IComplexPermission, ITablePermissionData } from '../../permission/permission.interface.js'; -import { generateCedarPolicyForGroup } from '../cedar-policy-generator.js'; - -export async function migratePermissionsToCedar(dataSource: DataSource): Promise { - const groupRepository = dataSource.getRepository(GroupEntity); - let migratedCount = 0; - - const permissionTableExists = await dataSource - .query( - `SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'permission' - ) AS "exists"`, - ) - .then((rows: Array<{ exists: boolean }>) => rows[0]?.exists === true); - - if (!permissionTableExists) { - console.log('Permission tables already removed — skipping legacy migration'); - return; - } - - const groups = await groupRepository - .createQueryBuilder('group') - .leftJoinAndSelect('group.connection', 'connection') - .where('group.cedarPolicy IS NULL OR group.cedarPolicy = :empty OR group.cedarPolicy LIKE :oldFormat', { - empty: '', - oldFormat: '%principal in RocketAdmin::Group%', - }) - .getMany(); - - for (const group of groups) { - const connection = group.connection; - if (!connection) continue; - - const permissions: Array<{ type: string; accessLevel: string; tableName: string }> = await dataSource.query( - `SELECT p.type, p."accessLevel", p."tableName" - FROM permission p - INNER JOIN permission_groups_group pg ON pg."permissionId" = p.id - WHERE pg."groupId" = $1`, - [group.id], - ); - - const connectionPermission = permissions.find((p) => p.type === PermissionTypeEnum.Connection); - const groupPermission = permissions.find((p) => p.type === PermissionTypeEnum.Group); - const tablePermissions = permissions.filter((p) => p.type === PermissionTypeEnum.Table); - - const tableMap = new Map(); - for (const tp of tablePermissions) { - const existing = tableMap.get(tp.tableName) || { - tableName: tp.tableName, - accessLevel: { visibility: false, readonly: false, add: false, delete: false, edit: false }, - }; - const level = tp.accessLevel as keyof ITablePermissionData['accessLevel']; - if (level in existing.accessLevel) { - existing.accessLevel[level] = true; - } - tableMap.set(tp.tableName, existing); - } - - const complexPermission: IComplexPermission = { - connection: { - connectionId: connection.id, - accessLevel: (connectionPermission?.accessLevel as AccessLevelEnum) || AccessLevelEnum.none, - }, - group: { - groupId: group.id, - accessLevel: (groupPermission?.accessLevel as AccessLevelEnum) || AccessLevelEnum.none, - }, - tables: Array.from(tableMap.values()), - }; - - const cedarPolicy = generateCedarPolicyForGroup(connection.id, group.isMain, complexPermission); - group.cedarPolicy = cedarPolicy; - await groupRepository.save(group); - migratedCount++; - } - - console.log(`Migrated Cedar policies for ${migratedCount} groups (skipped groups with existing policies)`); -} diff --git a/backend/src/entities/permission/permission.module.ts b/backend/src/entities/permission/permission.module.ts index 8e2229be9..15db6ef31 100644 --- a/backend/src/entities/permission/permission.module.ts +++ b/backend/src/entities/permission/permission.module.ts @@ -51,11 +51,6 @@ export class PermissionModule implements NestModule { consumer .apply(AuthMiddleware) .forRoutes( - { path: 'permissions/all', method: RequestMethod.GET }, - { path: 'permission/:slug', method: RequestMethod.POST }, - { path: 'permission/group/:slug', method: RequestMethod.POST }, - { path: 'permission/group/:slug', method: RequestMethod.PUT }, - { path: 'permissions/:slug', method: RequestMethod.POST }, { path: 'permissions/:slug', method: RequestMethod.PUT }, ); } diff --git a/backend/src/enums/index.ts b/backend/src/enums/index.ts index 917246f4f..8c7ba978b 100644 --- a/backend/src/enums/index.ts +++ b/backend/src/enums/index.ts @@ -6,7 +6,6 @@ export { FilterCriteriaEnum } from './filter-criteria.enum.js'; export { InTransactionEnum } from './in-transaction.enum.js'; export { LogOperationTypeEnum } from './log-operation-type.enum.js'; export { OperationResultStatusEnum } from './operation-result-status.enum.js'; -export { PermissionTypeEnum } from './permission-type.enum.js'; export { QueryOrderingEnum } from './query-ordering.enum.js'; export { SubscriptionLevelEnum } from './subscription-level.enum.js'; export { TableActionTypeEnum } from './table-action-type.enum.js'; diff --git a/backend/src/enums/permission-type.enum.ts b/backend/src/enums/permission-type.enum.ts deleted file mode 100644 index da8600af7..000000000 --- a/backend/src/enums/permission-type.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum PermissionTypeEnum { - Group = 'Group', - Connection = 'Connection', - Table = 'Table', -} diff --git a/backend/src/main.ts b/backend/src/main.ts index 0c7c29980..72fc56809 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -7,9 +7,7 @@ import bodyParser from 'body-parser'; import { ValidationError } from 'class-validator'; import cookieParser from 'cookie-parser'; import helmet from 'helmet'; -import { DataSource } from 'typeorm'; import { ApplicationModule } from './app.module.js'; -import { migratePermissionsToCedar } from './entities/cedar-authorization/scripts/migrate-permissions-to-cedar.js'; import { WinstonLogger } from './entities/logging/winston-logger.js'; import { AllExceptionsFilter } from './exceptions/all-exceptions.filter.js'; import { ValidationException } from './exceptions/custom-exceptions/validation-exception.js'; @@ -84,9 +82,6 @@ async function bootstrap() { }), ); - const dataSource = app.get(DataSource); - await migratePermissionsToCedar(dataSource); - await app.listen(3000); } catch (e) { console.error(`Failed to initialize, due to ${e}`);