diff --git a/backend/.development.env b/backend/.development.env index 4426b8a24..a64fd3341 100755 --- a/backend/.development.env +++ b/backend/.development.env @@ -109,4 +109,6 @@ MYSQL_CONNECTION_SSH_PORT=22 MYSQL_CONNECTION_SSH_USERNAME=TEST_MYSQL_CONNECTION_SSH_USERNAME MYSQL_CONNECTION_SSH_KEY=TEST_MYSQL_CONNECTION_SSH_KEY -TEST_CONNECTIONS= \ No newline at end of file +TEST_CONNECTIONS= + +CEDAR_AUTHORIZATION_ENABLED='true' \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index a55f79b83..837755318 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,10 +25,11 @@ }, "dependencies": { "@amplitude/node": "1.10.2", - "@aws-sdk/client-bedrock-runtime": "^3.999.0", - "@aws-sdk/client-s3": "^3.999.0", - "@aws-sdk/lib-dynamodb": "^3.999.0", - "@aws-sdk/s3-request-presigner": "^3.999.0", + "@aws-sdk/client-bedrock-runtime": "^3.990.0", + "@aws-sdk/client-s3": "^3.990.0", + "@aws-sdk/lib-dynamodb": "^3.990.0", + "@aws-sdk/s3-request-presigner": "^3.990.0", + "@cedar-policy/cedar-wasm": "^4.9.0", "@electric-sql/pglite": "^0.3.15", "@faker-js/faker": "^10.3.0", "@langchain/aws": "^1.3.0", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 787057468..94211cae2 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -40,6 +40,7 @@ import { DashboardModule } from './entities/visualizations/dashboard/dashboards. import { PanelPositionModule } from './entities/visualizations/panel-position/panel-position.module.js'; import { PanelModule } from './entities/visualizations/panel/panel.module.js'; import { TableWidgetModule } from './entities/widget/table-widget.module.js'; +import { CedarAuthorizationModule } from './entities/cedar-authorization/cedar-authorization.module.js'; import { SaaSGatewayModule } from './microservices/gateways/saas-gateway.ts/saas-gateway.module.js'; import { SaasModule } from './microservices/saas-microservice/saas.module.js'; import { AppLoggerMiddleware } from './middlewares/logging-middleware/app-logger-middlewate.js'; @@ -59,6 +60,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js'; }, ], }), + CedarAuthorizationModule, AICoreModule, ConnectionModule, ConnectionPropertiesModule, diff --git a/backend/src/entities/cedar-authorization/cedar-action-map.ts b/backend/src/entities/cedar-authorization/cedar-action-map.ts new file mode 100644 index 000000000..9f1e99a14 --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-action-map.ts @@ -0,0 +1,28 @@ +export enum CedarAction { + ConnectionRead = 'connection:read', + ConnectionEdit = 'connection:edit', + GroupRead = 'group:read', + GroupEdit = 'group:edit', + TableRead = 'table:read', + TableAdd = 'table:add', + TableEdit = 'table:edit', + TableDelete = 'table:delete', +} + +export enum CedarResourceType { + Connection = 'RocketAdmin::Connection', + Group = 'RocketAdmin::Group', + Table = 'RocketAdmin::Table', +} + +export const CEDAR_ACTION_TYPE = 'RocketAdmin::Action'; +export const CEDAR_USER_TYPE = 'RocketAdmin::User'; +export const CEDAR_GROUP_TYPE = 'RocketAdmin::Group'; + +export interface CedarValidationRequest { + userId: string; + action: CedarAction; + connectionId?: string; + groupId?: string; + tableName?: string; +} diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.module.ts b/backend/src/entities/cedar-authorization/cedar-authorization.module.ts new file mode 100644 index 000000000..c69b22c2d --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-authorization.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { CedarAuthorizationService } from './cedar-authorization.service.js'; + +@Global() +@Module({ + providers: [CedarAuthorizationService], + exports: [CedarAuthorizationService], +}) +export class CedarAuthorizationModule {} diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts new file mode 100644 index 000000000..6f26c44c2 --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts @@ -0,0 +1,7 @@ +import { CedarValidationRequest } from './cedar-action-map.js'; + +export interface ICedarAuthorizationService { + isFeatureEnabled(): boolean; + validate(request: CedarValidationRequest): Promise; + invalidatePolicyCacheForConnection(connectionId: string): void; +} diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts new file mode 100644 index 000000000..c08782a1f --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts @@ -0,0 +1,154 @@ +import { HttpException, HttpStatus, Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { BaseType } from '../../common/data-injection.tokens.js'; +import { Messages } from '../../exceptions/text/messages.js'; +import { Cacher } from '../../helpers/cache/cacher.js'; +import { GroupEntity } from '../group/group.entity.js'; +import { groupCustomRepositoryExtension } from '../group/repository/group-custom-repository-extension.js'; +import { IGroupRepository } from '../group/repository/group.repository.interface.js'; +import { UserEntity } from '../user/user.entity.js'; +import { + CedarAction, + CedarResourceType, + CedarValidationRequest, + CEDAR_ACTION_TYPE, + CEDAR_USER_TYPE, +} from './cedar-action-map.js'; +import { ICedarAuthorizationService } from './cedar-authorization.service.interface.js'; +import { buildCedarEntities } from './cedar-entity-builder.js'; +import { CEDAR_SCHEMA } from './cedar-schema.js'; + +@Injectable() +export class CedarAuthorizationService implements ICedarAuthorizationService, OnModuleInit { + private cedarModule: typeof import('@cedar-policy/cedar-wasm/nodejs'); + private schema: Record; + private groupRepository: IGroupRepository; + private readonly logger = new Logger(CedarAuthorizationService.name); + + constructor( + @Inject(BaseType.DATA_SOURCE) + private readonly dataSource: DataSource, + ) {} + + async onModuleInit(): Promise { + if (!this.isFeatureEnabled()) return; + this.cedarModule = await import('@cedar-policy/cedar-wasm/nodejs'); + this.schema = CEDAR_SCHEMA as Record; + this.groupRepository = this.dataSource.getRepository(GroupEntity).extend(groupCustomRepositoryExtension); + this.logger.log('Cedar authorization service initialized'); + } + + isFeatureEnabled(): boolean { + return process.env.CEDAR_AUTHORIZATION_ENABLED === 'true'; + } + + async validate(request: CedarValidationRequest): Promise { + const { userId, action, groupId, tableName } = request; + let { connectionId } = request; + + const actionPrefix = action.split(':')[0]; + let resourceType: CedarResourceType; + let resourceId: string; + + switch (actionPrefix) { + case 'connection': + resourceType = CedarResourceType.Connection; + resourceId = connectionId; + break; + case 'group': + resourceType = CedarResourceType.Group; + connectionId = await this.getConnectionIdForGroup(groupId); + if (!connectionId) return false; + resourceId = groupId; + break; + case 'table': + resourceType = CedarResourceType.Table; + resourceId = `${connectionId}/${tableName}`; + break; + default: + return false; + } + + return this.evaluate(userId, connectionId, action, resourceType, resourceId, tableName); + } + + invalidatePolicyCacheForConnection(connectionId: string): void { + Cacher.invalidateCedarPolicyCache(connectionId); + } + + private async evaluate( + userId: string, + connectionId: string, + action: CedarAction, + resourceType: CedarResourceType, + resourceId: string, + tableName?: string, + ): Promise { + await this.assertUserNotSuspended(userId); + + const userGroups = await this.groupRepository.findAllUserGroupsInConnection(connectionId, userId); + if (userGroups.length === 0) return false; + + const policies = await this.loadPoliciesForConnection(connectionId); + if (!policies) return false; + + const entities = buildCedarEntities(userId, userGroups, connectionId, tableName); + + const call = { + principal: { type: CEDAR_USER_TYPE, id: userId }, + action: { type: CEDAR_ACTION_TYPE, id: action }, + resource: { type: resourceType as string, id: resourceId }, + context: {}, + policies: { staticPolicies: policies }, + entities: entities, + schema: this.schema, + }; + + const result = this.cedarModule.isAuthorized(call as Parameters[0]); + if (result.type === 'success') { + return result.response.decision === 'allow'; + } + + this.logger.warn(`Cedar authorization error: ${JSON.stringify(result.errors)}`); + return false; + } + + private async loadPoliciesForConnection(connectionId: string): Promise { + const cached = Cacher.getCedarPolicyCache(connectionId); + if (cached !== null) return cached; + + const groups = await this.groupRepository.findAllGroupsInConnection(connectionId); + const policyTexts = groups.map((g) => g.cedarPolicy).filter(Boolean); + + if (policyTexts.length === 0) return null; + + const combined = policyTexts.join('\n\n'); + Cacher.setCedarPolicyCache(connectionId, combined); + return combined; + } + + private async assertUserNotSuspended(userId: string): Promise { + const user = await this.dataSource.getRepository(UserEntity).findOne({ + where: { id: userId }, + select: ['id', 'suspended'], + }); + if (user?.suspended) { + throw new HttpException( + { + message: Messages.ACCOUNT_SUSPENDED, + }, + HttpStatus.FORBIDDEN, + ); + } + } + + private async getConnectionIdForGroup(groupId: string): Promise { + const group = await this.dataSource + .getRepository(GroupEntity) + .createQueryBuilder('group') + .leftJoinAndSelect('group.connection', 'connection') + .where('group.id = :groupId', { groupId }) + .getOne(); + return group?.connection?.id ?? null; + } +} diff --git a/backend/src/entities/cedar-authorization/cedar-entity-builder.ts b/backend/src/entities/cedar-authorization/cedar-entity-builder.ts new file mode 100644 index 000000000..5200fd777 --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-entity-builder.ts @@ -0,0 +1,53 @@ +import { GroupEntity } from '../group/group.entity.js'; + +export interface CedarEntityRecord { + uid: { type: string; id: string }; + attrs: Record; + parents: Array<{ type: string; id: string }>; +} + +export function buildCedarEntities( + userId: string, + userGroups: Array, + connectionId: string, + tableName?: string, +): Array { + const entities: Array = []; + + // User entity with group memberships + entities.push({ + uid: { type: 'RocketAdmin::User', id: userId }, + attrs: { suspended: false }, + parents: userGroups.map((g) => ({ type: 'RocketAdmin::Group', id: g.id })), + }); + + // Group entities + for (const group of userGroups) { + entities.push({ + uid: { type: 'RocketAdmin::Group', id: group.id }, + attrs: { + isMain: group.isMain, + connectionId: connectionId, + }, + parents: [], + }); + } + + // Connection entity + entities.push({ + uid: { type: 'RocketAdmin::Connection', id: connectionId }, + attrs: {}, + parents: [], + }); + + // Table entity (if table-level check) + if (tableName) { + entities.push({ + uid: { type: 'RocketAdmin::Table', id: `${connectionId}/${tableName}` }, + attrs: { connectionId: connectionId }, + parents: [{ type: 'RocketAdmin::Connection', id: connectionId }], + }); + } + + return entities; +} diff --git a/backend/src/entities/cedar-authorization/cedar-policy-generator.ts b/backend/src/entities/cedar-authorization/cedar-policy-generator.ts new file mode 100644 index 000000000..9472f4ddd --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-policy-generator.ts @@ -0,0 +1,81 @@ +import { AccessLevelEnum } from '../../enums/index.js'; +import { IComplexPermission } from '../permission/permission.interface.js'; + +export function generateCedarPolicyForGroup( + groupId: string, + connectionId: string, + isMain: boolean, + permissions: IComplexPermission, +): string { + const policies: Array = []; + const groupRef = `RocketAdmin::Group::"${groupId}"`; + const connectionRef = `RocketAdmin::Connection::"${connectionId}"`; + + if (isMain) { + policies.push( + `permit(\n principal in ${groupRef},\n action,\n resource\n);`, + ); + return policies.join('\n\n'); + } + + // Connection permissions + const connAccess = permissions.connection.accessLevel; + if (connAccess === AccessLevelEnum.edit) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`, + ); + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"connection:edit",\n resource == ${connectionRef}\n);`, + ); + } else if (connAccess === AccessLevelEnum.readonly) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`, + ); + } + + // Group permissions + const groupAccess = permissions.group.accessLevel; + const groupResourceRef = `RocketAdmin::Group::"${permissions.group.groupId}"`; + if (groupAccess === AccessLevelEnum.edit) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"group:read",\n resource == ${groupResourceRef}\n);`, + ); + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"group:edit",\n resource == ${groupResourceRef}\n);`, + ); + } else if (groupAccess === AccessLevelEnum.readonly) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"group:read",\n resource == ${groupResourceRef}\n);`, + ); + } + + // Table permissions + for (const table of permissions.tables) { + const tableRef = `RocketAdmin::Table::"${connectionId}/${table.tableName}"`; + const access = table.accessLevel; + + const hasAnyAccess = access.visibility || access.add || access.delete || access.edit; + if (hasAnyAccess) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:read",\n resource == ${tableRef}\n);`, + ); + } + if (access.add) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:add",\n resource == ${tableRef}\n);`, + ); + } + if (access.edit) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:edit",\n resource == ${tableRef}\n);`, + ); + } + if (access.delete) { + policies.push( + `permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:delete",\n resource == ${tableRef}\n);`, + ); + } + } + + return policies.join('\n\n'); +} diff --git a/backend/src/entities/cedar-authorization/cedar-schema.json b/backend/src/entities/cedar-authorization/cedar-schema.json new file mode 100644 index 000000000..c8aca55c7 --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-schema.json @@ -0,0 +1,91 @@ +{ + "RocketAdmin": { + "entityTypes": { + "User": { + "memberOfTypes": ["Group"], + "shape": { + "type": "Record", + "attributes": { + "suspended": { "type": "Boolean" } + } + } + }, + "Group": { + "memberOfTypes": [], + "shape": { + "type": "Record", + "attributes": { + "isMain": { "type": "Boolean" }, + "connectionId": { "type": "String" } + } + } + }, + "Connection": { + "memberOfTypes": [], + "shape": { + "type": "Record", + "attributes": {} + } + }, + "Table": { + "memberOfTypes": ["Connection"], + "shape": { + "type": "Record", + "attributes": { + "connectionId": { "type": "String" } + } + } + } + }, + "actions": { + "connection:read": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Connection"] + } + }, + "connection:edit": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Connection"] + } + }, + "group:read": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Group"] + } + }, + "group:edit": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Group"] + } + }, + "table:read": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Table"] + } + }, + "table:add": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Table"] + } + }, + "table:edit": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Table"] + } + }, + "table:delete": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Table"] + } + } + } + } +} diff --git a/backend/src/entities/cedar-authorization/cedar-schema.ts b/backend/src/entities/cedar-authorization/cedar-schema.ts new file mode 100644 index 000000000..d8452d09f --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-schema.ts @@ -0,0 +1,91 @@ +export const CEDAR_SCHEMA = { + RocketAdmin: { + entityTypes: { + User: { + memberOfTypes: ['Group'], + shape: { + type: 'Record', + attributes: { + suspended: { type: 'Boolean' }, + }, + }, + }, + Group: { + memberOfTypes: [], + shape: { + type: 'Record', + attributes: { + isMain: { type: 'Boolean' }, + connectionId: { type: 'String' }, + }, + }, + }, + Connection: { + memberOfTypes: [], + shape: { + type: 'Record', + attributes: {}, + }, + }, + Table: { + memberOfTypes: ['Connection'], + shape: { + type: 'Record', + attributes: { + connectionId: { type: 'String' }, + }, + }, + }, + }, + actions: { + 'connection:read': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Connection'], + }, + }, + 'connection:edit': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Connection'], + }, + }, + 'group:read': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Group'], + }, + }, + 'group:edit': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Group'], + }, + }, + 'table:read': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Table'], + }, + }, + 'table:add': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Table'], + }, + }, + 'table:edit': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Table'], + }, + }, + 'table:delete': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['Table'], + }, + }, + }, + }, +}; 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 new file mode 100644 index 000000000..77a6bfe44 --- /dev/null +++ b/backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts @@ -0,0 +1,65 @@ +import { DataSource } from 'typeorm'; +import { AccessLevelEnum, PermissionTypeEnum } from '../../../enums/index.js'; +import { ConnectionEntity } from '../../connection/connection.entity.js'; +import { GroupEntity } from '../../group/group.entity.js'; +import { PermissionEntity } from '../../permission/permission.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 connectionRepository = dataSource.getRepository(ConnectionEntity); + const groupRepository = dataSource.getRepository(GroupEntity); + const permissionRepository = dataSource.getRepository(PermissionEntity); + + const connections = await connectionRepository.find(); + let migratedCount = 0; + + for (const connection of connections) { + const groups = await groupRepository + .createQueryBuilder('group') + .leftJoinAndSelect('group.connection', 'connection') + .leftJoinAndSelect('group.permissions', 'permission') + .where('connection.id = :connectionId', { connectionId: connection.id }) + .getMany(); + + for (const group of groups) { + const permissions = group.permissions || []; + + 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(group.id, connection.id, group.isMain, complexPermission); + group.cedarPolicy = cedarPolicy; + await groupRepository.save(group); + migratedCount++; + } + } + + console.log(`Migrated Cedar policies for ${migratedCount} groups`); +} 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 9e14f404a..044cec44b 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 @@ -1,15 +1,20 @@ import { BadRequestException, Inject, Injectable, InternalServerErrorException, Scope } from '@nestjs/common'; import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; +import * as Sentry from '@sentry/node'; import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { Encryptor } from '../../../helpers/encryption/encryptor.js'; import { isConnectionTypeAgent, slackPostMessage } from '../../../helpers/index.js'; +import { SharedJobsService } from '../../shared-jobs/shared-jobs.service.js'; import { UserRoleEnum } from '../../user/enums/user-role.enum.js'; import { UserEntity } from '../../user/user.entity.js'; import { CreateConnectionDs } from '../application/data-structures/create-connection.ds.js'; import { CreatedConnectionDTO } from '../application/dto/created-connection.dto.js'; import { ConnectionEntity } from '../connection.entity.js'; +import { generateCedarPolicyForGroup } from '../../cedar-authorization/cedar-policy-generator.js'; +import { AccessLevelEnum } from '../../../enums/index.js'; import { buildConnectionEntity } from '../utils/build-connection-entity.js'; import { buildCreatedConnectionDs } from '../utils/build-created-connection.ds.js'; import { processAWSConnection } from '../utils/process-aws-connection.util.js'; @@ -24,6 +29,7 @@ export class CreateConnectionUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly sharedJobsService: SharedJobsService, ) { super(); } @@ -47,58 +53,90 @@ export class CreateConnectionUseCase await validateCreateConnectionData(createConnectionData); createConnectionData = await processAWSConnection(createConnectionData); + let isConnectionTestedSuccessfully: boolean = false; if (!isConnectionTypeAgent(createConnectionData.connection_parameters.type)) { const connectionParamsCopy = { ...createConnectionData.connection_parameters, }; const dao = getDataAccessObject(connectionParamsCopy); try { - await dao.testConnect(); + const testResult = await dao.testConnect(); + isConnectionTestedSuccessfully = testResult.result; } catch (e) { const text: string = e.message.toLowerCase(); + isConnectionTestedSuccessfully = false; if (text.includes('ssl required') || text.includes('ssl connection required')) { createConnectionData.connection_parameters.ssl = true; connectionParamsCopy.ssl = true; try { const updatedDao = getDataAccessObject(connectionParamsCopy); - await updatedDao.testConnect(); + const sslTestResult = await updatedDao.testConnect(); + isConnectionTestedSuccessfully = sslTestResult.result; } catch (_e) { + isConnectionTestedSuccessfully = false; createConnectionData.connection_parameters.ssl = false; connectionParamsCopy.ssl = false; } } } } - const createdConnection: ConnectionEntity = await buildConnectionEntity(createConnectionData, connectionAuthor); - const savedConnection: ConnectionEntity = - await this._dbContext.connectionRepository.saveNewConnection(createdConnection); + let connectionCopy: ConnectionEntity = null; + try { + const createdConnection: ConnectionEntity = await buildConnectionEntity(createConnectionData, connectionAuthor); + const savedConnection: ConnectionEntity = + await this._dbContext.connectionRepository.saveNewConnection(createdConnection); - let token: string; - if (isConnectionTypeAgent(savedConnection.type)) { - token = await this._dbContext.agentRepository.createNewAgentForConnectionAndReturnToken(savedConnection); - } - const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection( - savedConnection, - connectionAuthor, - ); - await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); - delete createdAdminGroup.connection; - await this._dbContext.userRepository.saveUserEntity(connectionAuthor); - createdConnection.groups = [createdAdminGroup]; - const foundUserCompany = await this._dbContext.companyInfoRepository.findOneCompanyInfoByUserIdWithConnections( - connectionAuthor.id, - ); - if (foundUserCompany) { - const connection = await this._dbContext.connectionRepository.findOne({ - where: { id: savedConnection.id }, - }); - connection.company = foundUserCompany; - await this._dbContext.connectionRepository.saveUpdatedConnection(connection); + connectionCopy = { ...savedConnection } as ConnectionEntity; + if (savedConnection.masterEncryption && masterPwd && !isConnectionTypeAgent(savedConnection.type)) { + connectionCopy = Encryptor.decryptConnectionCredentials(connectionCopy, masterPwd); + } + + let token: string; + if (isConnectionTypeAgent(savedConnection.type)) { + token = await this._dbContext.agentRepository.createNewAgentForConnectionAndReturnToken(savedConnection); + } + const createdAdminGroup = await this._dbContext.groupRepository.createdAdminGroupInConnection( + savedConnection, + connectionAuthor, + ); + await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup); + createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup( + createdAdminGroup.id, + savedConnection.id, + true, + { + connection: { connectionId: savedConnection.id, accessLevel: AccessLevelEnum.edit }, + group: { groupId: createdAdminGroup.id, accessLevel: AccessLevelEnum.edit }, + tables: [], + }, + ); + await this._dbContext.groupRepository.saveNewOrUpdatedGroup(createdAdminGroup); + delete createdAdminGroup.connection; + await this._dbContext.userRepository.saveUserEntity(connectionAuthor); + createdConnection.groups = [createdAdminGroup]; + const foundUserCompany = await this._dbContext.companyInfoRepository.findOneCompanyInfoByUserIdWithConnections( + connectionAuthor.id, + ); + if (foundUserCompany) { + const connection = await this._dbContext.connectionRepository.findOne({ + where: { id: savedConnection.id }, + }); + connection.company = foundUserCompany; + await this._dbContext.connectionRepository.saveUpdatedConnection(connection); + } + await slackPostMessage( + Messages.USER_CREATED_CONNECTION(connectionAuthor.email, createConnectionData.connection_parameters.type), + ); + const connectionRO = buildCreatedConnectionDs(savedConnection, token, masterPwd); + return connectionRO; + } finally { + if (isConnectionTestedSuccessfully && !isConnectionTypeAgent(connectionCopy.type)) { + // Fire-and-forget: run AI scan in background without blocking response + this.sharedJobsService.scanDatabaseAndCreateSettingsAndWidgetsWithAI(connectionCopy).catch((error) => { + console.error('Background AI scan failed:', error); + Sentry.captureException(error); + }); + } } - await slackPostMessage( - Messages.USER_CREATED_CONNECTION(connectionAuthor.email, createConnectionData.connection_parameters.type), - ); - const connectionRO = buildCreatedConnectionDs(savedConnection, token, masterPwd); - return connectionRO; } } diff --git a/backend/src/entities/connection/use-cases/create-group-in-connection.use.case.ts b/backend/src/entities/connection/use-cases/create-group-in-connection.use.case.ts index ed49bc123..68421e778 100644 --- a/backend/src/entities/connection/use-cases/create-group-in-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/create-group-in-connection.use.case.ts @@ -2,7 +2,10 @@ import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common'; import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; +import { AccessLevelEnum } from '../../../enums/index.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { Cacher } from '../../../helpers/cache/cacher.js'; +import { generateCedarPolicyForGroup } from '../../cedar-authorization/cedar-policy-generator.js'; import { FoundGroupResponseDto } from '../../group/dto/found-group-response.dto.js'; import { buildFoundGroupResponseDto } from '../../group/utils/biuld-found-group-response.dto.js'; import { CreateGroupInConnectionDs } from '../application/data-structures/create-group-in-connection.ds.js'; @@ -33,6 +36,18 @@ export class CreateGroupInConnectionUseCase const foundUser = await this._dbContext.userRepository.findOneUserById(cognitoUserName); const newGroupEntity = buildNewGroupEntityForConnectionWithUser(connectionToUpdate, foundUser, title); const savedGroup = await this._dbContext.groupRepository.saveNewOrUpdatedGroup(newGroupEntity); + savedGroup.cedarPolicy = generateCedarPolicyForGroup( + savedGroup.id, + connectionId, + false, + { + connection: { connectionId, accessLevel: AccessLevelEnum.none }, + group: { groupId: savedGroup.id, accessLevel: AccessLevelEnum.none }, + tables: [], + }, + ); + await this._dbContext.groupRepository.saveNewOrUpdatedGroup(savedGroup); + Cacher.invalidateCedarPolicyCache(connectionId); return buildFoundGroupResponseDto(savedGroup); } } diff --git a/backend/src/entities/group/group.entity.ts b/backend/src/entities/group/group.entity.ts index bd4f1a98c..dd024e455 100644 --- a/backend/src/entities/group/group.entity.ts +++ b/backend/src/entities/group/group.entity.ts @@ -33,6 +33,9 @@ export class GroupEntity { ) users?: Relation[]; + @Column({ type: 'text', default: null, nullable: true }) + cedarPolicy: string | null; + @ManyToOne( (_) => ConnectionEntity, (connection) => connection.groups, 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 4ac71ffe0..33c7a1cab 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 @@ -11,6 +11,8 @@ import { 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'; @@ -185,6 +187,13 @@ export class CreateOrUpdatePermissionsUseCase deletedPermissions, createdPermissions, ); + + // Generate and save Cedar policy for this group + const cedarPolicy = generateCedarPolicyForGroup(groupId, connectionId, groupToUpdate.isMain, resultPermissions); + groupToUpdate.cedarPolicy = cedarPolicy; + await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate); + Cacher.invalidateCedarPolicyCache(connectionId); + return resultPermissions; } } diff --git a/backend/src/guards/connection-edit.guard.ts b/backend/src/guards/connection-edit.guard.ts index 5ff244ec5..e3e1b567b 100644 --- a/backend/src/guards/connection-edit.guard.ts +++ b/backend/src/guards/connection-edit.guard.ts @@ -5,20 +5,26 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { ValidationHelper } from '../helpers/validators/validation-helper.js'; import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class ConnectionEditGuard implements CanActivate { + private readonly logger = new Logger(ConnectionEditGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -33,6 +39,31 @@ export class ConnectionEditGuard implements CanActivate { reject(new BadRequestException(Messages.CONNECTION_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.ConnectionEdit, + connectionId, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userConnectionEdit = false; try { userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit( diff --git a/backend/src/guards/connection-read.guard.ts b/backend/src/guards/connection-read.guard.ts index da9957cb6..2b76b71cb 100644 --- a/backend/src/guards/connection-read.guard.ts +++ b/backend/src/guards/connection-read.guard.ts @@ -5,20 +5,26 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { ValidationHelper } from '../helpers/validators/validation-helper.js'; import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class ConnectionReadGuard implements CanActivate { + private readonly logger = new Logger(ConnectionReadGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -33,6 +39,31 @@ export class ConnectionReadGuard implements CanActivate { reject(new BadRequestException(Messages.CONNECTION_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.ConnectionRead, + connectionId, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userConnectionRead = false; try { userConnectionRead = await this._dbContext.userAccessRepository.checkUserConnectionRead( diff --git a/backend/src/guards/group-edit.guard.ts b/backend/src/guards/group-edit.guard.ts index 4f61801eb..920399a4e 100644 --- a/backend/src/guards/group-edit.guard.ts +++ b/backend/src/guards/group-edit.guard.ts @@ -5,19 +5,25 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class GroupEditGuard implements CanActivate { + private readonly logger = new Logger(GroupEditGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -32,6 +38,31 @@ export class GroupEditGuard implements CanActivate { reject(new BadRequestException(Messages.GROUP_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.GroupEdit, + groupId, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userGroupEdit = false; try { userGroupEdit = await this._dbContext.userAccessRepository.checkUserGroupEdit(cognitoUserName, groupId); diff --git a/backend/src/guards/group-read.guard.ts b/backend/src/guards/group-read.guard.ts index bdc971833..f6718199e 100644 --- a/backend/src/guards/group-read.guard.ts +++ b/backend/src/guards/group-read.guard.ts @@ -5,19 +5,25 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class GroupReadGuard implements CanActivate { + private readonly logger = new Logger(GroupReadGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -32,6 +38,31 @@ export class GroupReadGuard implements CanActivate { reject(new BadRequestException(Messages.GROUP_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.GroupRead, + groupId, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userGroupRead = false; try { userGroupRead = await this._dbContext.userAccessRepository.checkUserGroupRead(cognitoUserName, groupId); diff --git a/backend/src/guards/table-add.guard.ts b/backend/src/guards/table-add.guard.ts index 0ce4a21bb..5dd02ca26 100644 --- a/backend/src/guards/table-add.guard.ts +++ b/backend/src/guards/table-add.guard.ts @@ -5,11 +5,14 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { getMasterPwd } from '../helpers/index.js'; import { ValidationHelper } from '../helpers/validators/validation-helper.js'; @@ -17,9 +20,12 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class TableAddGuard implements CanActivate { + private readonly logger = new Logger(TableAddGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -37,6 +43,32 @@ export class TableAddGuard implements CanActivate { reject(new BadRequestException(Messages.CONNECTION_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableAdd, + connectionId, + tableName, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userTableAdd = false; try { userTableAdd = await this._dbContext.userAccessRepository.checkTableAdd( diff --git a/backend/src/guards/table-delete.guard.ts b/backend/src/guards/table-delete.guard.ts index b2b3b14a5..cff05d2f6 100644 --- a/backend/src/guards/table-delete.guard.ts +++ b/backend/src/guards/table-delete.guard.ts @@ -5,11 +5,14 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { getMasterPwd } from '../helpers/index.js'; import { ValidationHelper } from '../helpers/validators/validation-helper.js'; @@ -17,9 +20,12 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class TableDeleteGuard implements CanActivate { + private readonly logger = new Logger(TableDeleteGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -37,6 +43,32 @@ export class TableDeleteGuard implements CanActivate { reject(new BadRequestException(Messages.CONNECTION_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableDelete, + connectionId, + tableName, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userTableDelete = false; try { userTableDelete = await this._dbContext.userAccessRepository.checkTableDelete( diff --git a/backend/src/guards/table-edit.guard.ts b/backend/src/guards/table-edit.guard.ts index f87f15dab..1f4342c8d 100644 --- a/backend/src/guards/table-edit.guard.ts +++ b/backend/src/guards/table-edit.guard.ts @@ -5,11 +5,14 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { getMasterPwd } from '../helpers/index.js'; import { ValidationHelper } from '../helpers/validators/validation-helper.js'; @@ -17,9 +20,12 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class TableEditGuard implements CanActivate { + private readonly logger = new Logger(TableEditGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -37,6 +43,32 @@ export class TableEditGuard implements CanActivate { reject(new BadRequestException(Messages.CONNECTION_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableEdit, + connectionId, + tableName, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userTableEdit = false; try { userTableEdit = await this._dbContext.userAccessRepository.checkTableEdit( diff --git a/backend/src/guards/table-read.guard.ts b/backend/src/guards/table-read.guard.ts index 8a7257bff..101e3dd66 100644 --- a/backend/src/guards/table-read.guard.ts +++ b/backend/src/guards/table-read.guard.ts @@ -5,11 +5,14 @@ import { ForbiddenException, Inject, Injectable, + Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { IRequestWithCognitoInfo } from '../authorization/index.js'; import { IGlobalDatabaseContext } from '../common/application/global-database-context.interface.js'; import { BaseType } from '../common/data-injection.tokens.js'; +import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js'; +import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js'; import { Messages } from '../exceptions/text/messages.js'; import { getMasterPwd } from '../helpers/index.js'; import { ValidationHelper } from '../helpers/validators/validation-helper.js'; @@ -17,9 +20,12 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class TableReadGuard implements CanActivate { + private readonly logger = new Logger(TableReadGuard.name); + constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarAuthService: CedarAuthorizationService, ) {} canActivate(context: ExecutionContext): boolean | Promise | Observable { @@ -37,6 +43,32 @@ export class TableReadGuard implements CanActivate { reject(new BadRequestException(Messages.CONNECTION_ID_MISSING)); return; } + + // Cedar-first authorization + if (this.cedarAuthService.isFeatureEnabled()) { + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableRead, + connectionId, + tableName, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); + return; + } catch (e) { + if (e instanceof ForbiddenException || e?.status === 403) { + reject(e); + return; + } + this.logger.error(`Cedar authorization error, falling back to legacy: ${e.message}`); + } + } + + // Legacy authorization fallback let userTableRead = false; try { userTableRead = await this._dbContext.userAccessRepository.improvedCheckTableRead( diff --git a/backend/src/helpers/cache/cacher.ts b/backend/src/helpers/cache/cacher.ts index 127393afb..3de7ba516 100644 --- a/backend/src/helpers/cache/cacher.ts +++ b/backend/src/helpers/cache/cacher.ts @@ -4,6 +4,7 @@ import { Constants } from '../constants/constants.js'; const invitationCache = new LRUCache(Constants.DEFAULT_INVITATION_CACHE_OPTIONS); const tableReadPermissionCache = new LRUCache(Constants.DEFAULT_TABLE_PERMISSIONS_CACHE_OPTIONS); +const cedarPolicyCache = new LRUCache(Constants.DEFAULT_CEDAR_POLICY_CACHE_OPTIONS); export class Cacher { public static setUserTableReadPermissionCache( @@ -65,8 +66,22 @@ export class Cacher { return userInvitations <= 10 && groupInvitations <= 10; } + public static getCedarPolicyCache(connectionId: string): string | null { + const cached = cedarPolicyCache.get(connectionId); + return cached !== undefined ? cached : null; + } + + public static setCedarPolicyCache(connectionId: string, policies: string): void { + cedarPolicyCache.set(connectionId, policies); + } + + public static invalidateCedarPolicyCache(connectionId: string): void { + cedarPolicyCache.delete(connectionId); + } + public static async clearAllCache(): Promise { await invitationCache.clear(); await tableReadPermissionCache.clear(); + cedarPolicyCache.clear(); } } diff --git a/backend/src/helpers/constants/constants.ts b/backend/src/helpers/constants/constants.ts index b66ac59bb..9162776a3 100644 --- a/backend/src/helpers/constants/constants.ts +++ b/backend/src/helpers/constants/constants.ts @@ -128,6 +128,11 @@ export const Constants = { ttl: 1000 * 10, }, + DEFAULT_CEDAR_POLICY_CACHE_OPTIONS: { + max: 500, + ttl: 1000 * 60 * 5, + }, + DEFAULT_FORWARD_IN_HOST: '127.0.0.1', AUTOCOMPLETE_ROW_LIMIT: 20, diff --git a/backend/src/migrations/1771545600000-AddCedarPolicyToGroup.ts b/backend/src/migrations/1771545600000-AddCedarPolicyToGroup.ts new file mode 100644 index 000000000..976edbb93 --- /dev/null +++ b/backend/src/migrations/1771545600000-AddCedarPolicyToGroup.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCedarPolicyToGroup1771545600000 implements MigrationInterface { + name = 'AddCedarPolicyToGroup1771545600000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "group" ADD COLUMN "cedarPolicy" text DEFAULT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "group" DROP COLUMN "cedarPolicy"`); + } +} diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-entity-builder.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-entity-builder.test.ts new file mode 100644 index 000000000..dd913cef3 --- /dev/null +++ b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-entity-builder.test.ts @@ -0,0 +1,85 @@ +import test from 'ava'; +import { buildCedarEntities } from '../../../src/entities/cedar-authorization/cedar-entity-builder.js'; +import { GroupEntity } from '../../../src/entities/group/group.entity.js'; + +const userId = 'test-user-id'; +const connectionId = 'test-connection-id'; + +function makeGroup(id: string, isMain: boolean): GroupEntity { + return { id, isMain } as unknown as GroupEntity; +} + +test('user entity has correct type, id, suspended=false, and group parents', (t) => { + const groups = [makeGroup('g1', false), makeGroup('g2', true)]; + const entities = buildCedarEntities(userId, groups, connectionId); + const userEntity = entities.find((e) => e.uid.type === 'RocketAdmin::User'); + t.truthy(userEntity); + t.is(userEntity.uid.id, userId); + t.is(userEntity.attrs.suspended, false); + t.is(userEntity.parents.length, 2); + t.deepEqual(userEntity.parents[0], { type: 'RocketAdmin::Group', id: 'g1' }); + t.deepEqual(userEntity.parents[1], { type: 'RocketAdmin::Group', id: 'g2' }); +}); + +test('group entities have correct type, isMain attribute, connectionId attribute, empty parents', (t) => { + const groups = [makeGroup('g1', false), makeGroup('g2', true)]; + const entities = buildCedarEntities(userId, groups, connectionId); + const groupEntities = entities.filter((e) => e.uid.type === 'RocketAdmin::Group'); + t.is(groupEntities.length, 2); + + const g1 = groupEntities.find((e) => e.uid.id === 'g1'); + t.is(g1.attrs.isMain, false); + t.is(g1.attrs.connectionId, connectionId); + t.deepEqual(g1.parents, []); + + const g2 = groupEntities.find((e) => e.uid.id === 'g2'); + t.is(g2.attrs.isMain, true); + t.is(g2.attrs.connectionId, connectionId); + t.deepEqual(g2.parents, []); +}); + +test('connection entity has correct type, id, empty attrs and parents', (t) => { + const entities = buildCedarEntities(userId, [makeGroup('g1', false)], connectionId); + const connEntity = entities.find((e) => e.uid.type === 'RocketAdmin::Connection'); + t.truthy(connEntity); + t.is(connEntity.uid.id, connectionId); + t.deepEqual(connEntity.attrs, {}); + t.deepEqual(connEntity.parents, []); +}); + +test('table entity created when tableName provided with correct id and parent', (t) => { + const tableName = 'users'; + const entities = buildCedarEntities(userId, [makeGroup('g1', false)], connectionId, tableName); + const tableEntity = entities.find((e) => e.uid.type === 'RocketAdmin::Table'); + t.truthy(tableEntity); + t.is(tableEntity.uid.id, `${connectionId}/${tableName}`); + t.is(tableEntity.attrs.connectionId, connectionId); + t.deepEqual(tableEntity.parents, [{ type: 'RocketAdmin::Connection', id: connectionId }]); +}); + +test('no table entity when tableName omitted', (t) => { + const entities = buildCedarEntities(userId, [makeGroup('g1', false)], connectionId); + const tableEntity = entities.find((e) => e.uid.type === 'RocketAdmin::Table'); + t.falsy(tableEntity); +}); + +test('correct total entity count for various inputs', (t) => { + // 1 user + 2 groups + 1 connection = 4 (no table) + const entities1 = buildCedarEntities(userId, [makeGroup('g1', false), makeGroup('g2', true)], connectionId); + t.is(entities1.length, 4); + + // 1 user + 1 group + 1 connection + 1 table = 4 + const entities2 = buildCedarEntities(userId, [makeGroup('g1', false)], connectionId, 'users'); + t.is(entities2.length, 4); + + // 1 user + 0 groups + 1 connection = 2 + const entities3 = buildCedarEntities(userId, [], connectionId); + t.is(entities3.length, 2); +}); + +test('empty groups array means user has no parents', (t) => { + const entities = buildCedarEntities(userId, [], connectionId); + const userEntity = entities.find((e) => e.uid.type === 'RocketAdmin::User'); + t.truthy(userEntity); + t.deepEqual(userEntity.parents, []); +}); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-permissions-e2e.test.ts new file mode 100644 index 000000000..c19c85a30 --- /dev/null +++ b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-permissions-e2e.test.ts @@ -0,0 +1,1384 @@ +/* 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 { ApplicationModule } from '../../../src/app.module.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 { createInitialTestUser } 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'; +import { + createConnectionAndInviteUserWithConnectionEditOnly, + createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection, + createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions, + createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions, +} from '../../utils/user-with-different-permissions-utils.js'; + +let app: INestApplication; +let _testUtils: TestUtils; +let currentTest: string; + +const mockFactory = new MockFactory(); + +test.before(async () => { + setSaasEnvVariable(); + process.env.CEDAR_AUTHORIZATION_ENABLED = '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(); + await createInitialTestUser(app); + 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); + } +}); + +//****************************** ADMIN GROUP (isMain=true) - FULL ACCESS VIA CEDAR ****************************** + +currentTest = 'GET /connections/'; + +test.serial(`${currentTest} should return connections where admin group user has edit access via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const findAll = await request(app.getHttpServer()) + .get('/connections') + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + + t.is(findAll.status, 200); + + const result = findAll.body.connections; + t.is(result.length, 1); + t.is(Object.hasOwn(result[0], 'connection'), true); + t.is(Object.hasOwn(result[0], 'accessLevel'), true); + t.is(result[0].accessLevel, AccessLevelEnum.edit); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'GET /connection/one/:slug'; + +test.serial(`${currentTest} should return a found connection for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const searchedConnectionId = testData.connections.firstId; + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${searchedConnectionId}`) + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + + const result = findOneResponce.body.connection; + t.is(result.type, 'postgres'); + t.is(Object.hasOwn(result, 'host'), true); + t.is(typeof result.port, 'number'); + t.is(Object.hasOwn(result, 'createdAt'), true); + t.is(Object.hasOwn(result, 'updatedAt'), true); + t.is(Object.hasOwn(result, 'password'), false); + } catch (error) { + console.error(error); + throw error; + } +}); + +test.serial( + `${currentTest} should return limited connection info when admin group user reads connection without permission via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const searchedConnectionId = testData.connections.secondId; + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${searchedConnectionId}`) + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + const findOneRO = JSON.parse(findOneResponce.text); + const connectionKeys: Array = Object.keys(findOneRO.connection); + for (const keyName of connectionKeys) { + t.is(Constants.CONNECTION_KEYS_NONE_PERMISSION.includes(keyName), true); + } + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /connection'; + +test.serial(`${currentTest} should return updated connection for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const updateConnection = mockFactory.generateUpdateConnectionDto(); + const updateConnectionResponse = await request(app.getHttpServer()) + .put(`/connection/${testData.connections.firstId}`) + .send(updateConnection) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateConnectionResponse.status, 200); + const result = updateConnectionResponse.body.connection; + t.is(Object.hasOwn(result, 'createdAt'), true); + t.is(Object.hasOwn(result, 'updatedAt'), true); + t.is(Object.hasOwn(result, 'password'), false); + } catch (error) { + console.error(error); + throw error; + } +}); + +test.serial( + `${currentTest} should throw an exception when admin group user tries to update connection without permission via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const updateConnection = mockFactory.generateUpdateConnectionDto(); + const updateConnectionResponse = await request(app.getHttpServer()) + .put(`/connection/${testData.connections.secondId}`) + .send(updateConnection) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateConnectionResponse.status, 403); + t.is(JSON.parse(updateConnectionResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'DELETE /connection/:slug'; + +test.serial(`${currentTest} should return delete result for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const response = await request(app.getHttpServer()) + .put(`/connection/delete/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(response.status, 200); + + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findOneResponce.status, 400); + const { message } = JSON.parse(findOneResponce.text); + t.is(message, Messages.CONNECTION_NOT_FOUND); + } catch (error) { + console.error(error); + throw error; + } +}); + +test.serial( + `${currentTest} should throw an exception when admin group user tries to delete connection without permission via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const response = await request(app.getHttpServer()) + .put(`/connection/delete/${testData.connections.secondId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(response.status, 403); + t.is(JSON.parse(response.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'POST /connection/group/:slug'; + +test.serial(`${currentTest} should return a created group for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const newGroup1 = mockFactory.generateCreateGroupDto1(); + const createGroupResponse = await request(app.getHttpServer()) + .post(`/connection/group/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .send(newGroup1) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(createGroupResponse.status, 201); + const result = JSON.parse(createGroupResponse.text); + t.is(result.title, newGroup1.title); + t.is(Object.hasOwn(result, 'users'), true); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'GET /connection/groups/:slug'; + +test.serial(`${currentTest} should return groups for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const response = await request(app.getHttpServer()) + .get(`/connection/groups/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(response.status, 200); + const result = JSON.parse(response.text); + t.is(result.length > 0, true); + t.is(result[0].accessLevel, AccessLevelEnum.edit); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'GET /connection/tables/:slug'; + +test.serial(`${currentTest} should return all tables for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const getTablesInConnection = await request(app.getHttpServer()) + .get(`/connection/tables/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesInConnection.status, 200); + const getTablesInConnectionRO = JSON.parse(getTablesInConnection.text); + t.is(getTablesInConnectionRO.length > 0, true); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'GET /table/rows/:slug'; + +test.serial(`${currentTest} should return found rows for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const getTableRows = await request(app.getHttpServer()) + .get( + `/table/rows/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 200); + const getTableRowsRO = JSON.parse(getTableRows.text); + t.is(Object.hasOwn(getTableRowsRO, 'rows'), true); + t.is(Object.hasOwn(getTableRowsRO, 'primaryColumns'), true); + t.is(Object.hasOwn(getTableRowsRO, 'pagination'), true); + t.is(Object.hasOwn(getTableRowsRO, 'structure'), true); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'POST /table/row/:slug'; + +test.serial(`${currentTest} should return added row for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 201); + const addRowInTableRO = JSON.parse(addRowInTable.text); + t.is(Object.hasOwn(addRowInTableRO.row, 'id'), true); + t.is(addRowInTableRO.row[testData.firstTableInfo.testTableColumnName], randomName); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'PUT /table/row/:slug'; + +test.serial(`${currentTest} should return updated row for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=1`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 200); + } catch (error) { + console.error(error); + throw error; + } +}); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial(`${currentTest} should return delete result for admin group user via Cedar`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const deleteRowInTable = await request(app.getHttpServer()) + .delete( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=19`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 200); + } catch (error) { + console.error(error); + throw error; + } +}); + +//****************************** CUSTOM GROUP: connection=readonly, group=edit ****************************** + +currentTest = 'GET /connections/'; + +test.serial( + `${currentTest} should return connections with readonly access for custom group user via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const findAll = await request(app.getHttpServer()) + .get('/connections') + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + + t.is(findAll.status, 200); + + const result = findAll.body.connections; + const targetConnection = result.find( + ({ connection }: any) => connection.id === testData.connections.firstId, + ); + t.is(Object.hasOwn(targetConnection, 'connection'), true); + t.is(Object.hasOwn(targetConnection, 'accessLevel'), true); + t.is(targetConnection.accessLevel, AccessLevelEnum.readonly); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /connection/one/:slug'; + +test.serial( + `${currentTest} should return a found connection for readonly custom group user via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${testData.connections.firstId}`) + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + const result = findOneResponce.body.connection; + t.is(Object.hasOwn(result, 'host'), true); + t.is(Object.hasOwn(result, 'createdAt'), true); + t.is(Object.hasOwn(result, 'updatedAt'), true); + t.is(Object.hasOwn(result, 'password'), false); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /connection'; + +test.serial( + `${currentTest} should throw an exception when readonly custom group user tries to update connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const updateConnection = mockFactory.generateUpdateConnectionDto(); + const updateConnectionResponse = await request(app.getHttpServer()) + .put(`/connection/${testData.connections.firstId}`) + .send(updateConnection) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateConnectionResponse.status, 403); + t.is(JSON.parse(updateConnectionResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'DELETE /connection/:slug'; + +test.serial( + `${currentTest} should throw an exception when readonly custom group user tries to delete connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const response = await request(app.getHttpServer()) + .put(`/connection/delete/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(response.status, 403); + t.is(JSON.parse(response.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /connection/groups/:slug'; + +test.serial( + `${currentTest} should return groups for custom group user with group edit permission via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const response = await request(app.getHttpServer()) + .get(`/connection/groups/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(response.status, 200); + const result = JSON.parse(response.text); + t.is(result.length > 0, true); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /connection/tables/:slug'; + +test.serial( + `${currentTest} should return tables for custom group user with table visibility via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const getTablesInConnection = await request(app.getHttpServer()) + .get(`/connection/tables/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesInConnection.status, 200); + const getTablesInConnectionRO = JSON.parse(getTablesInConnection.text); + t.is(getTablesInConnectionRO.length > 0, true); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /table/rows/:slug'; + +test.serial( + `${currentTest} should return found rows for custom group user with table visibility via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const getTableRows = await request(app.getHttpServer()) + .get( + `/table/rows/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 200); + const getTableRowsRO = JSON.parse(getTableRows.text); + t.is(Object.hasOwn(getTableRowsRO, 'rows'), true); + t.is(Object.hasOwn(getTableRowsRO, 'primaryColumns'), true); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should return added row for custom group user with table add permission via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 201); + const addRowInTableRO = JSON.parse(addRowInTable.text); + t.is(Object.hasOwn(addRowInTableRO.row, 'id'), true); + t.is(addRowInTableRO.row[testData.firstTableInfo.testTableColumnName], randomName); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should throw an exception when custom group user without edit permission tries to update row via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=2`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 403); + t.is(JSON.parse(updateRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should return delete result for custom group user with table delete permission via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const deleteRowInTable = await request(app.getHttpServer()) + .delete( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=19`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 200); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +//****************************** CUSTOM GROUP: connection=none, group=none, table-only perms ****************************** + +currentTest = 'GET /connection/one/:slug'; + +test.serial( + `${currentTest} should return limited connection info for table-only permissions group user via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${testData.connections.firstId}`) + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + const findOneRO = JSON.parse(findOneResponce.text); + const connectionKeys: Array = Object.keys(findOneRO.connection); + for (const keyName of connectionKeys) { + t.is(Constants.CONNECTION_KEYS_NONE_PERMISSION.includes(keyName), true); + } + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /connection'; + +test.serial( + `${currentTest} should throw an exception when table-only permissions user tries to update connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const updateConnection = mockFactory.generateUpdateConnectionDto(); + const updateConnectionResponse = await request(app.getHttpServer()) + .put(`/connection/${testData.connections.firstId}`) + .send(updateConnection) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateConnectionResponse.status, 403); + t.is(JSON.parse(updateConnectionResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'DELETE /connection/:slug'; + +test.serial( + `${currentTest} should throw an exception when table-only permissions user tries to delete connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const response = await request(app.getHttpServer()) + .put(`/connection/delete/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(response.status, 403); + t.is(JSON.parse(response.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /connection/tables/:slug'; + +test.serial( + `${currentTest} should return tables for table-only permissions user with table visibility via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const getTablesInConnection = await request(app.getHttpServer()) + .get(`/connection/tables/${testData.connections.firstId}`) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesInConnection.status, 200); + const getTablesInConnectionRO = JSON.parse(getTablesInConnection.text); + t.is(getTablesInConnectionRO.length > 0, true); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /table/rows/:slug'; + +test.serial( + `${currentTest} should return found rows for table-only permissions user with table visibility via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const getTableRows = await request(app.getHttpServer()) + .get( + `/table/rows/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 200); + const getTableRowsRO = JSON.parse(getTableRows.text); + t.is(Object.hasOwn(getTableRowsRO, 'rows'), true); + t.is(Object.hasOwn(getTableRowsRO, 'primaryColumns'), true); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should return added row for table-only permissions user with add=true via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 201); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should throw an exception when table-only permissions user without edit tries to update row via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=2`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 403); + t.is(JSON.parse(updateRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should return delete result for table-only permissions user with delete=true via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const deleteRowInTable = await request(app.getHttpServer()) + .delete( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=19`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 200); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +//****************************** CONNECTION:EDIT SCOPE — NO TABLE ACCESS ****************************** + +currentTest = 'GET /table/rows/:slug'; + +test.serial( + `${currentTest} should return 403 for connection:edit user with no table permissions via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const getTableRows = await request(app.getHttpServer()) + .get( + `/table/rows/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 403); + t.is(JSON.parse(getTableRows.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should return 403 for connection:edit user with no table permissions via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const addRowInTable = await request(app.getHttpServer()) + .post( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 403); + t.is(JSON.parse(addRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should return 403 for connection:edit user with no table permissions via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const updateRowInTable = await request(app.getHttpServer()) + .put( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=1`, + ) + .send({ + [testData.firstTableInfo.testTableColumnName]: randomName, + [testData.firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 403); + t.is(JSON.parse(updateRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should return 403 for connection:edit user with no table permissions via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const deleteRowInTable = await request(app.getHttpServer()) + .delete( + `/table/row/${testData.connections.firstId}?tableName=${testData.firstTableInfo.testTableName}&id=19`, + ) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 403); + t.is(JSON.parse(deleteRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'PUT /connection'; + +test.serial( + `${currentTest} should return 200 for connection:edit user updating connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const updateConnection = mockFactory.generateUpdateConnectionDto(); + const updateConnectionResponse = await request(app.getHttpServer()) + .put(`/connection/${testData.connections.firstId}`) + .send(updateConnection) + .set('Cookie', testData.users.simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateConnectionResponse.status, 200); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +currentTest = 'GET /connection/one/:slug'; + +test.serial( + `${currentTest} should return 200 for connection:edit user reading connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${testData.connections.firstId}`) + .set('Content-Type', 'application/json') + .set('Cookie', testData.users.simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + const result = findOneResponce.body.connection; + t.is(Object.hasOwn(result, 'host'), true); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +//****************************** PERMISSION UPDATE CACHE INVALIDATION ****************************** + +test.serial( + 'Upgrading table permissions takes effect under Cedar (cache invalidation)', + async (t) => { + try { + const connectionsId = { firstId: null, secondId: null, firstAdminGroupId: null }; + const connectionAdminUserInfo = await import('../../utils/register-user-and-return-user-info.js').then( + (m) => m.registerUserAndReturnUserInfo(app), + ); + const simpleUserRegisterInfo = await import('../../utils/register-user-and-return-user-info.js').then( + (m) => m.inviteUserInCompanyAndAcceptInvitation(connectionAdminUserInfo.token, undefined, app, undefined), + ); + const connectionAdminUserToken = connectionAdminUserInfo.token; + const simpleUserToken = simpleUserRegisterInfo.token; + + const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + const { createTestTable } = await import('../../utils/create-test-table.js'); + const firstTable = await createTestTable(newConnection); + const newGroup1 = mockFactory.generateCreateGroupDto1(); + + const createConnResp = await request(app.getHttpServer()) + .post('/connection') + .set('Cookie', connectionAdminUserToken) + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + connectionsId.firstId = JSON.parse(createConnResp.text).id; + + const createGroupResp = await request(app.getHttpServer()) + .post(`/connection/group/${connectionsId.firstId}`) + .set('Cookie', connectionAdminUserToken) + .send(newGroup1) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const groupId = JSON.parse(createGroupResp.text).id; + + // Set permissions with add=false + const permissionsNoAdd = { + connection: { connectionId: connectionsId.firstId, accessLevel: AccessLevelEnum.none }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [ + { + tableName: firstTable.testTableName, + accessLevel: { visibility: true, readonly: false, add: false, delete: false, edit: false }, + }, + ], + }; + + await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionsId.firstId}`) + .send({ permissions: permissionsNoAdd }) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Add user to group + await request(app.getHttpServer()) + .put('/group/user') + .set('Cookie', connectionAdminUserToken) + .send({ groupId, email: simpleUserRegisterInfo.email }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Verify user gets 403 on POST row + const addRowDenied = await request(app.getHttpServer()) + .post(`/table/row/${connectionsId.firstId}?tableName=${firstTable.testTableName}`) + .send({ + [firstTable.testTableColumnName]: faker.person.firstName(), + [firstTable.testTableSecondColumnName]: faker.internet.email(), + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowDenied.status, 403); + + // Upgrade permissions to add=true + const permissionsWithAdd = { + connection: { connectionId: connectionsId.firstId, accessLevel: AccessLevelEnum.none }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [ + { + tableName: firstTable.testTableName, + accessLevel: { visibility: true, readonly: false, add: true, delete: false, edit: false }, + }, + ], + }; + + await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionsId.firstId}`) + .send({ permissions: permissionsWithAdd }) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Verify user now gets 201 on POST row + const addRowAllowed = await request(app.getHttpServer()) + .post(`/table/row/${connectionsId.firstId}?tableName=${firstTable.testTableName}`) + .send({ + [firstTable.testTableColumnName]: faker.person.firstName(), + [firstTable.testTableSecondColumnName]: faker.internet.email(), + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowAllowed.status, 201); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +test.serial( + 'Downgrading connection permissions takes effect under Cedar (cache invalidation)', + async (t) => { + try { + const connectionAdminUserInfo = await import('../../utils/register-user-and-return-user-info.js').then( + (m) => m.registerUserAndReturnUserInfo(app), + ); + const simpleUserRegisterInfo = await import('../../utils/register-user-and-return-user-info.js').then( + (m) => m.inviteUserInCompanyAndAcceptInvitation(connectionAdminUserInfo.token, undefined, app, undefined), + ); + const connectionAdminUserToken = connectionAdminUserInfo.token; + const simpleUserToken = simpleUserRegisterInfo.token; + + const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + const newGroup1 = mockFactory.generateCreateGroupDto1(); + + const createConnResp = await request(app.getHttpServer()) + .post('/connection') + .set('Cookie', connectionAdminUserToken) + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connectionId = JSON.parse(createConnResp.text).id; + + const createGroupResp = await request(app.getHttpServer()) + .post(`/connection/group/${connectionId}`) + .set('Cookie', connectionAdminUserToken) + .send(newGroup1) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const groupId = JSON.parse(createGroupResp.text).id; + + // Set permissions with connection:edit + const permissionsEdit = { + connection: { connectionId, accessLevel: AccessLevelEnum.edit }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [], + }; + + await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionId}`) + .send({ permissions: permissionsEdit }) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Add user to group + await request(app.getHttpServer()) + .put('/group/user') + .set('Cookie', connectionAdminUserToken) + .send({ groupId, email: simpleUserRegisterInfo.email }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Verify user can PUT connection (200) + const updateConnection = mockFactory.generateUpdateConnectionDto(); + const updateAllowed = await request(app.getHttpServer()) + .put(`/connection/${connectionId}`) + .send(updateConnection) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateAllowed.status, 200); + + // Downgrade to readonly + const permissionsReadonly = { + connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [], + }; + + await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionId}`) + .send({ permissions: permissionsReadonly }) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Verify user gets 403 on PUT connection + const updateDenied = await request(app.getHttpServer()) + .put(`/connection/${connectionId}`) + .send(updateConnection) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateDenied.status, 403); + t.is(JSON.parse(updateDenied.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (error) { + console.error(error); + throw error; + } + }, +); + +//****************************** GROUP CREATION FLOW WITH CEDAR ****************************** + +test.serial( + 'Full group creation → permission assignment flow under Cedar', + async (t) => { + try { + const connectionAdminUserInfo = await import('../../utils/register-user-and-return-user-info.js').then( + (m) => m.registerUserAndReturnUserInfo(app), + ); + const simpleUserRegisterInfo = await import('../../utils/register-user-and-return-user-info.js').then( + (m) => m.inviteUserInCompanyAndAcceptInvitation(connectionAdminUserInfo.token, undefined, app, undefined), + ); + const connectionAdminUserToken = connectionAdminUserInfo.token; + const simpleUserToken = simpleUserRegisterInfo.token; + + const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + const { createTestTable } = await import('../../utils/create-test-table.js'); + const firstTable = await createTestTable(newConnection); + + // 1. Create connection (admin group gets wildcard Cedar policy) + const createConnResp = await request(app.getHttpServer()) + .post('/connection') + .set('Cookie', connectionAdminUserToken) + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createConnResp.status, 201); + const connectionId = JSON.parse(createConnResp.text).id; + + // 2. Admin creates new group + const newGroup1 = mockFactory.generateCreateGroupDto1(); + const createGroupResp = await request(app.getHttpServer()) + .post(`/connection/group/${connectionId}`) + .set('Cookie', connectionAdminUserToken) + .send(newGroup1) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createGroupResp.status, 201); + const groupId = JSON.parse(createGroupResp.text).id; + + // 3. Verify admin can still access everything (new group's empty policy doesn't break combined set) + const adminGetRows = await request(app.getHttpServer()) + .get(`/table/rows/${connectionId}?tableName=${firstTable.testTableName}`) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(adminGetRows.status, 200); + + // 4. Add user to new group (no permissions set yet) + await request(app.getHttpServer()) + .put('/group/user') + .set('Cookie', connectionAdminUserToken) + .send({ groupId, email: simpleUserRegisterInfo.email }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // 5. Verify user gets 403 on all operations (no permissions) + const userGetRows = await request(app.getHttpServer()) + .get(`/table/rows/${connectionId}?tableName=${firstTable.testTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userGetRows.status, 403); + + const userAddRow = await request(app.getHttpServer()) + .post(`/table/row/${connectionId}?tableName=${firstTable.testTableName}`) + .send({ + [firstTable.testTableColumnName]: faker.person.firstName(), + [firstTable.testTableSecondColumnName]: faker.internet.email(), + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userAddRow.status, 403); + + const userUpdateConn = await request(app.getHttpServer()) + .put(`/connection/${connectionId}`) + .send(mockFactory.generateUpdateConnectionDto()) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userUpdateConn.status, 403); + + // 6. Set permissions on the group + const permissions = { + connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [ + { + tableName: firstTable.testTableName, + accessLevel: { visibility: true, readonly: false, add: true, delete: false, edit: false }, + }, + ], + }; + + await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionId}`) + .send({ permissions }) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // 7. Verify user can now access resources per the new permissions + const userGetRowsAfter = await request(app.getHttpServer()) + .get(`/table/rows/${connectionId}?tableName=${firstTable.testTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userGetRowsAfter.status, 200); + + const userAddRowAfter = await request(app.getHttpServer()) + .post(`/table/row/${connectionId}?tableName=${firstTable.testTableName}`) + .send({ + [firstTable.testTableColumnName]: faker.person.firstName(), + [firstTable.testTableSecondColumnName]: faker.internet.email(), + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userAddRowAfter.status, 201); + + // connection:readonly means read works but edit fails + const userGetConn = await request(app.getHttpServer()) + .get(`/connection/one/${connectionId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userGetConn.status, 200); + + const userUpdateConnAfter = await request(app.getHttpServer()) + .put(`/connection/${connectionId}`) + .send(mockFactory.generateUpdateConnectionDto()) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(userUpdateConnAfter.status, 403); + } catch (error) { + console.error(error); + throw error; + } + }, +); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-generator.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-generator.test.ts new file mode 100644 index 000000000..544aac706 --- /dev/null +++ b/backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-generator.test.ts @@ -0,0 +1,234 @@ +import test from 'ava'; +import { generateCedarPolicyForGroup } from '../../../src/entities/cedar-authorization/cedar-policy-generator.js'; +import { AccessLevelEnum } from '../../../src/enums/index.js'; +import { IComplexPermission } from '../../../src/entities/permission/permission.interface.js'; + +const groupId = 'test-group-id'; +const connectionId = 'test-connection-id'; + +function makePermissions(overrides: Partial = {}): IComplexPermission { + return { + connection: { connectionId, accessLevel: AccessLevelEnum.none }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [], + ...overrides, + }; +} + +test('isMain=true generates a single wildcard permit', (t) => { + const result = generateCedarPolicyForGroup(groupId, connectionId, true, makePermissions()); + t.true(result.includes('principal in RocketAdmin::Group::"test-group-id"')); + t.true(result.includes('action,')); + t.true(result.includes('resource')); + // Should be a single policy + const permits = result.match(/permit\(/g); + t.is(permits.length, 1); +}); + +test('connection:edit generates ONLY connection:read + connection:edit (not wildcard)', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + connection: { connectionId, accessLevel: AccessLevelEnum.edit }, + }), + ); + t.true(result.includes('action == RocketAdmin::Action::"connection:read"')); + t.true(result.includes('action == RocketAdmin::Action::"connection:edit"')); + // Must NOT contain wildcard `action,` on its own line (which would grant table access) + t.false(result.includes('action,\n resource\n')); + // Must NOT contain table actions + t.false(result.includes('table:read')); + t.false(result.includes('table:add')); + t.false(result.includes('table:edit')); + t.false(result.includes('table:delete')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 2); +}); + +test('connection:readonly generates only connection:read', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + connection: { connectionId, accessLevel: AccessLevelEnum.readonly }, + }), + ); + t.true(result.includes('action == RocketAdmin::Action::"connection:read"')); + t.false(result.includes('connection:edit')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 1); +}); + +test('connection:none generates no connection policies', (t) => { + const result = generateCedarPolicyForGroup(groupId, connectionId, false, makePermissions()); + t.false(result.includes('connection:read')); + t.false(result.includes('connection:edit')); +}); + +test('group:edit generates group:read + group:edit', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + group: { groupId, accessLevel: AccessLevelEnum.edit }, + }), + ); + t.true(result.includes('action == RocketAdmin::Action::"group:read"')); + t.true(result.includes('action == RocketAdmin::Action::"group:edit"')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 2); +}); + +test('group:readonly generates only group:read', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + group: { groupId, accessLevel: AccessLevelEnum.readonly }, + }), + ); + t.true(result.includes('action == RocketAdmin::Action::"group:read"')); + t.false(result.includes('group:edit')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 1); +}); + +test('table with visibility=true only generates only table:read', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + tables: [ + { + tableName: 'users', + accessLevel: { visibility: true, readonly: false, add: false, delete: false, edit: false }, + }, + ], + }), + ); + t.true(result.includes('action == RocketAdmin::Action::"table:read"')); + t.false(result.includes('table:add')); + t.false(result.includes('table:edit')); + t.false(result.includes('table:delete')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 1); +}); + +test('table with all flags true generates table:read + table:add + table:edit + table:delete', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + tables: [ + { + tableName: 'users', + accessLevel: { visibility: true, readonly: false, add: true, delete: true, edit: true }, + }, + ], + }), + ); + t.true(result.includes('table:read')); + t.true(result.includes('table:add')); + t.true(result.includes('table:edit')); + t.true(result.includes('table:delete')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 4); +}); + +test('table with add=true only generates table:read + table:add (hasAnyAccess triggers table:read)', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + tables: [ + { + tableName: 'users', + accessLevel: { visibility: false, readonly: false, add: true, delete: false, edit: false }, + }, + ], + }), + ); + t.true(result.includes('table:read')); + t.true(result.includes('table:add')); + t.false(result.includes('table:edit')); + t.false(result.includes('table:delete')); + const permits = result.match(/permit\(/g); + t.is(permits.length, 2); +}); + +test('table with all flags false generates no policies for that table', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + tables: [ + { + tableName: 'users', + accessLevel: { visibility: false, readonly: false, add: false, delete: false, edit: false }, + }, + ], + }), + ); + t.false(result.includes('table:')); + t.is(result, ''); +}); + +test('all none + no tables returns empty string', (t) => { + const result = generateCedarPolicyForGroup(groupId, connectionId, false, makePermissions()); + t.is(result, ''); +}); + +test('multiple tables generate separate policies per table with correct resource refs', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + tables: [ + { + tableName: 'users', + accessLevel: { visibility: true, readonly: false, add: false, delete: false, edit: false }, + }, + { + tableName: 'orders', + accessLevel: { visibility: true, readonly: false, add: true, delete: false, edit: false }, + }, + ], + }), + ); + t.true(result.includes(`RocketAdmin::Table::"${connectionId}/users"`)); + t.true(result.includes(`RocketAdmin::Table::"${connectionId}/orders"`)); + // users: table:read only; orders: table:read + table:add + const permits = result.match(/permit\(/g); + t.is(permits.length, 3); +}); + +test('resource ref format validation', (t) => { + const result = generateCedarPolicyForGroup( + groupId, + connectionId, + false, + makePermissions({ + connection: { connectionId, accessLevel: AccessLevelEnum.edit }, + group: { groupId, accessLevel: AccessLevelEnum.edit }, + tables: [ + { + tableName: 'users', + accessLevel: { visibility: true, readonly: false, add: false, delete: false, edit: false }, + }, + ], + }), + ); + t.true(result.includes(`RocketAdmin::Group::"${groupId}"`)); + t.true(result.includes(`RocketAdmin::Connection::"${connectionId}"`)); + t.true(result.includes(`RocketAdmin::Table::"${connectionId}/users"`)); +}); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-group-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-group-e2e.test.ts index f5916d8c2..a04ee01cd 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-group-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-group-e2e.test.ts @@ -218,9 +218,9 @@ test.serial(`${currentTest} should throw an error when group id is not real`, as .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findAllUsersInGroup.status, 400); + t.is(findAllUsersInGroup.status, 403); const { message } = JSON.parse(findAllUsersInGroup.text); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -497,9 +497,9 @@ test.serial(`${currentTest} should throw an error when groupId is incorrect`, as .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(addUserInGroup.status, 400); + t.is(addUserInGroup.status, 403); const { message } = JSON.parse(addUserInGroup.text); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -692,9 +692,9 @@ test.serial(`${currentTest} should return throw an exception when groupId is inc .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(deleteResult.status, 400); + t.is(deleteResult.status, 403); const { message } = JSON.parse(deleteResult.text); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -1128,8 +1128,8 @@ test.serial(`${currentTest} should throw an error, when group id is incorrect`, .set('Accept', 'application/json'); const { message } = JSON.parse(removeUserFromGroup.text); - t.is(removeUserFromGroup.status, 400); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(removeUserFromGroup.status, 403); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-settings-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-settings-e2e.test.ts index 29c669c97..65eb1c636 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-settings-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-settings-e2e.test.ts @@ -88,6 +88,7 @@ test.serial(`${currentTest} should throw an exception when tableName is missing` t.is(findSettingsRO.message, Messages.TABLE_NAME_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -116,6 +117,7 @@ test.serial(`${currentTest} should throw an exception when connectionId is missi t.is(findSettingsRO.message, Messages.CONNECTION_ID_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -147,6 +149,7 @@ test.serial( t.deepEqual(findSettingsRO, {}); } catch (e) { console.error(e); + throw e; } }, ); @@ -222,6 +225,7 @@ test.serial(`${currentTest} should return connection settings object`, async (t) t.is(findSettingsRO.connection_id, connectionId); } catch (e) { console.error(e); + throw e; } }); @@ -297,6 +301,7 @@ test.serial(`${currentTest} should return created table settings`, async (t) => t.is(findSettingsRO.connection_id, connectionId); } catch (e) { console.error(e); + throw e; } }); @@ -354,6 +359,7 @@ test.serial(`${currentTest} should throw exception when tableName is missing`, a t.is(createTableSettingsRO.message, Messages.TABLE_NAME_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -395,6 +401,7 @@ test.serial(`${currentTest} should throw exception when connectionId is missing` t.is(createTableSettingsRO.message, Messages.CONNECTION_ID_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -451,6 +458,7 @@ test.serial(`${currentTest} should throw exception when search_fields is not an t.is(createTableSettingsRO.message, 'The field "search_fields" must be an array'); } catch (e) { console.error(e); + throw e; } }); @@ -507,6 +515,7 @@ test.serial(`${currentTest} should throw exception when excluded_fields is not a t.is(createTableSettingsRO.message, 'The field "excluded_fields" must be an array'); } catch (e) { console.error(e); + throw e; } }); @@ -563,6 +572,7 @@ test.serial(`${currentTest} should throw exception when sortable_by is not an ar t.is(createTableSettingsRO.message, 'The field "sortable_by" must be an array'); } catch (e) { console.error(e); + throw e; } }); @@ -622,6 +632,7 @@ test.serial( t.is(createTableSettingsRO.message, 'There are no such fields: testField - in the table "connection"'); } catch (e) { console.error(e); + throw e; } }, ); @@ -681,6 +692,7 @@ test.serial( t.is(createTableSettingsRO.message, 'There are no such fields: testField - in the table "connection"'); } catch (e) { console.error(e); + throw e; } }, ); @@ -724,6 +736,7 @@ test.serial( t.is(createTableSettingsRO.message, 'There are no such fields: testField - in the table "connection"'); } catch (e) { console.error(e); + throw e; } }, ); @@ -783,6 +796,7 @@ test.serial( t.is(createTableSettingsRO.message, 'There are no such fields: testField - in the table "connection"'); } catch (e) { console.error(e); + throw e; } }, ); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts index 3e827482d..bec751ce1 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-admin-permissions-e2e.test.ts @@ -902,8 +902,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (error) { console.error(error); throw error; @@ -1018,7 +1018,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (error) { console.error(error); throw error; @@ -1183,7 +1183,7 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteUserInGroupRO = JSON.parse(deleteUserInGroupResponse.text); - t.is(deleteUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (error) { console.error(error); throw error; diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-group-edit-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-group-edit-permissions-e2e.test.ts index daa8c304b..f4d338b85 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-user-group-edit-permissions-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-group-edit-permissions-e2e.test.ts @@ -120,6 +120,7 @@ test.serial(`${currentTest} should return connections, where second user have ac t.is(Object.hasOwn(result[0].connection, 'author'), false); } catch (e) { console.error(e); + throw e; } }); @@ -162,6 +163,7 @@ test.serial(`${currentTest} should return a found connection`, async (t) => { t.is(Object.hasOwn(result, 'author'), false); } catch (e) { console.error(e); + throw e; } }); @@ -193,6 +195,7 @@ test.serial( t.is(Object.hasOwn(findOneRO, 'host'), false); } catch (e) { console.error(e); + throw e; } }, ); @@ -222,6 +225,7 @@ test.serial(`${currentTest} should throw exception you do not have permission`, t.is(JSON.parse(updateConnectionResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -249,6 +253,7 @@ test.serial( t.is(JSON.parse(updateConnectionResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -286,6 +291,7 @@ test.serial(`${currentTest} should throw an exception do not have permissions`, t.is(JSON.parse(findOneResponce.text).connection.id, connections.firstId); } catch (e) { console.error(e); + throw e; } }); @@ -321,6 +327,7 @@ test.serial( t.is(findOneResponce.status, 200); } catch (e) { console.error(e); + throw e; } }, ); @@ -350,6 +357,7 @@ test.serial(`${currentTest} should throw an exception don not have permission`, t.is(JSON.parse(createGroupResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -377,6 +385,7 @@ test.serial( t.is(JSON.parse(createGroupResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -426,6 +435,7 @@ test.serial(`${currentTest} should return connection without deleted group resul t.is(JSON.parse(response.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -470,6 +480,7 @@ test.serial( t.is(JSON.parse(response.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -519,6 +530,7 @@ test.serial(`${currentTest} should groups in connection`, async (t) => { t.is(index >= 0, false); } catch (e) { console.error(e); + throw e; } }); @@ -555,6 +567,7 @@ test.serial( t.is(result.length, 0); } catch (e) { console.error(e); + throw e; } }, ); @@ -612,6 +625,7 @@ test.serial(`${currentTest} should return permissions object for current group i t.is(tables[tableIndex].accessLevel.edit, tablePermissions.edit); } catch (e) { console.error(e); + throw e; } }); @@ -669,69 +683,69 @@ test.serial(`${currentTest} should return permissions object for current group i t.is(tables[foundTableIndex].accessLevel.edit, tablePermissions.edit); } catch (e) { console.error(e); + throw e; } }); -test.serial( - `${currentTest} should return permissions object for current group in current connection for current user`, - async (t) => { - try { - const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); - const { - connections, - firstTableInfo, - groups, - permissions, - secondTableInfo, - users: { adminUserToken, simpleUserToken }, - } = testData; +test.serial(`${currentTest} should return permissions object for current group in current connection for current user`, async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const { + connections, + firstTableInfo, + groups, + permissions, + secondTableInfo, + users: { adminUserToken, simpleUserToken }, + } = testData; - const getGroupsResponse = await request(app.getHttpServer()) - .get(`/connection/groups/${connections.secondId}`) - .set('Cookie', adminUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); - t.is(getGroupsResponse.status, 200); - const getGroupsRO = JSON.parse(getGroupsResponse.text); + const getGroupsResponse = await request(app.getHttpServer()) + .get(`/connection/groups/${connections.secondId}`) + .set('Cookie', adminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getGroupsResponse.status, 200); + const getGroupsRO = JSON.parse(getGroupsResponse.text); - const groupId = getGroupsRO[0].group.id; + const groupId = getGroupsRO[0].group.id; - const response = await request(app.getHttpServer()) - .get(`/connection/user/permissions?connectionId=${connections.secondId}&groupId=${groupId}`) - .set('Cookie', simpleUserToken) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json'); + const response = await request(app.getHttpServer()) + .get(`/connection/user/permissions?connectionId=${connections.secondId}&groupId=${groupId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); - t.is(response.status, 200); - const result = JSON.parse(response.text); + t.is(response.status, 200); + const result = JSON.parse(response.text); + console.log('🚀 ~ result:', result); - t.is(Object.hasOwn(result, 'connection'), true); - t.is(Object.hasOwn(result, 'group'), true); - t.is(Object.hasOwn(result, 'tables'), true); - t.is(typeof result.connection, 'object'); - t.is(typeof result.group, 'object'); - t.is(result.connection.connectionId, connections.secondId); - t.is(result.group.groupId, groupId); - t.is(result.connection.accessLevel, AccessLevelEnum.none); - t.is(result.group.accessLevel, AccessLevelEnum.none); - t.is(typeof result.tables, 'object'); - - const { tables } = result; - const foundTableIndex = tables.findIndex((table) => table.tableName === firstTableInfo.testTableName); - t.is(tables.length > 0, true); - t.is(typeof tables[0], 'object'); - t.is(Object.hasOwn(tables[foundTableIndex], 'accessLevel'), true); - t.is(tables[foundTableIndex].accessLevel.visibility, false); - t.is(tables[foundTableIndex].accessLevel.readonly, false); - t.is(tables[foundTableIndex].accessLevel.add, false); - t.is(tables[foundTableIndex].accessLevel.delete, false); - t.is(tables[foundTableIndex].accessLevel.edit, false); - t.is(tables[foundTableIndex].accessLevel.edit, false); - } catch (e) { - console.error(e); - } - }, -); + t.is(Object.hasOwn(result, 'connection'), true); + t.is(Object.hasOwn(result, 'group'), true); + t.is(Object.hasOwn(result, 'tables'), true); + t.is(typeof result.connection, 'object'); + t.is(typeof result.group, 'object'); + t.is(result.connection.connectionId, connections.secondId); + t.is(result.group.groupId, groupId); + t.is(result.connection.accessLevel, AccessLevelEnum.none); + t.is(result.group.accessLevel, AccessLevelEnum.none); + t.is(typeof result.tables, 'object'); + + const { tables } = result; + const foundTableIndex = tables.findIndex((table) => table.tableName === secondTableInfo.testTableName); + t.is(tables.length > 0, true); + t.is(typeof tables[0], 'object'); + t.is(Object.hasOwn(tables[foundTableIndex], 'accessLevel'), true); + t.is(tables[foundTableIndex].accessLevel.visibility, false); + t.is(tables[foundTableIndex].accessLevel.readonly, false); + t.is(tables[foundTableIndex].accessLevel.add, false); + t.is(tables[foundTableIndex].accessLevel.delete, false); + t.is(tables[foundTableIndex].accessLevel.edit, false); + t.is(tables[foundTableIndex].accessLevel.edit, false); + } catch (e) { + console.error(e); + throw e; + } +}); //****************************** GROUP CONTROLLER ******************************// @@ -761,6 +775,7 @@ test.serial(`${currentTest} should return found groups with current user`, async t.is(Object.hasOwn(groups[0].group, 'isMain'), true); } catch (e) { console.error(e); + throw e; } }); @@ -801,6 +816,7 @@ test.serial(`${currentTest} it should return users in group`, async (t) => { t.is(Object.hasOwn(getUsersRO[0], 'email'), true); } catch (e) { console.error(e); + throw e; } }); @@ -836,6 +852,7 @@ test.serial( t.is(getUsersRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -884,6 +901,7 @@ test.serial(`${currentTest} should return group with added user`, async (t) => { t.is(users[2].id === users[0].id, false); } catch (e) { console.error(e); + throw e; } }); @@ -919,6 +937,7 @@ test.serial(`${currentTest} should throw exception, when user email not passed i // t.is(addUserInGroupRO.message, ErrorsMessages.VALIDATION_FAILED); } catch (e) { console.error(e); + throw e; } }); @@ -955,6 +974,7 @@ test.serial(`${currentTest} should throw exception, when group id not passed in t.is(addUserInGroupRO.message, Messages.GROUP_ID_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -987,10 +1007,11 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1025,6 +1046,7 @@ test.serial(`${currentTest} should delete result after group deletion`, async (t t.is(deleteGroupRO.isMain, false); } catch (e) { console.error(e); + throw e; } }); @@ -1060,6 +1082,7 @@ test.serial(`${currentTest} should throw an exception when you try delete admin t.is(deleteGroupRO.message, Messages.CANT_DELETE_ADMIN_GROUP); } catch (e) { console.error(e); + throw e; } }); @@ -1092,6 +1115,7 @@ test.serial(`${currentTest} should throw an exception when group id not passed i t.is(deleteGroupResponse.status, 404); } catch (e) { console.error(e); + throw e; } }); @@ -1121,9 +1145,10 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1179,6 +1204,7 @@ test.serial(`${currentTest} should return group without deleted user`, async (t) t.is(users[0].id === users[1].id, false); } catch (e) { console.error(e); + throw e; } }); @@ -1218,6 +1244,7 @@ test.serial(`${currentTest} should throw exception, when user email not passed i // t.is(deleteUserInGroupRO.message, ErrorsMessages.VALIDATION_FAILED); } catch (e) { console.error(e); + throw e; } }); @@ -1257,6 +1284,7 @@ test.serial(`${currentTest} should throw exception, when group id not passed in t.is(deleteUserInGroupRO.message, Messages.GROUP_ID_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -1294,9 +1322,10 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteUserInGroupRO = JSON.parse(deleteUserInGroupResponse.text); - t.is(deleteUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1359,6 +1388,7 @@ test.serial(`${currentTest} should throw an exception do not have permission`, a t.is(createOrUpdatePermissionRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1419,6 +1449,7 @@ test.serial( t.is(createOrUpdatePermissionResponse.status, 403); } catch (e) { console.error(e); + throw e; } }, ); @@ -1468,6 +1499,7 @@ test.serial(`${currentTest} should throw an exception, when you try change admin t.is(createOrUpdatePermissionRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1507,6 +1539,7 @@ test.serial(`${currentTest} should return all tables in connection`, async (t) = t.is(add, tablePermissions.add); } catch (e) { console.error(e); + throw e; } }); @@ -1529,6 +1562,7 @@ test.serial(`${currentTest} should throw an exception, when connection id not pa t.is(getTablesInConnection.status, 404); } catch (e) { console.error(e); + throw e; } }); @@ -1556,6 +1590,7 @@ test.serial( t.is(getTablesInConnectionRO.message, Messages.CONNECTION_NOT_FOUND); } catch (e) { console.error(e); + throw e; } }, ); @@ -1590,6 +1625,7 @@ test.serial(`${currentTest} should return found rows from table`, async (t) => { t.is(foreignKeys.length, 0); } catch (e) { console.error(e); + throw e; } }); @@ -1612,6 +1648,7 @@ test.serial(`${currentTest} should throw an exception when connection id not pas t.is(getTablesRows.status, 404); } catch (e) { console.error(e); + throw e; } }); @@ -1637,6 +1674,7 @@ test.serial(`${currentTest} should throw an exception when connection id passed t.is(getTablesInConnectionRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1662,6 +1700,7 @@ test.serial(`${currentTest} should throw an exception when table name passed in t.is(getTablesInConnectionRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1697,6 +1736,7 @@ test.serial(`${currentTest} should return table structure`, async (t) => { t.is(foreignKeys.length, 0); } catch (e) { console.error(e); + throw e; } }); @@ -1720,6 +1760,7 @@ test.serial(`${currentTest} should throw an exception when connection id not pas t.is(getTablesStructure.status, 404); } catch (e) { console.error(e); + throw e; } }); @@ -1746,6 +1787,7 @@ test.serial(`${currentTest} should throw an exception when connection id passed t.is(getTablesStructureRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1770,6 +1812,7 @@ test.serial(`${currentTest} should throw an exception when table name not passed t.is(getTablesStructureRO.message, Messages.TABLE_NAME_MISSING); } catch (e) { console.error(e); + throw e; } }); @@ -1795,6 +1838,7 @@ test.serial(`${currentTest} should throw an exception when table name passed in t.is(getTablesStructureRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1841,6 +1885,7 @@ test.serial(`${currentTest} should return added row`, async (t) => { t.is(Object.hasOwn(addRowInTableRO, 'readonly_fields'), true); } catch (e) { console.error(e); + throw e; } }); @@ -1875,6 +1920,7 @@ test.serial(`${currentTest} should throw an exception when connection id passed t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1909,6 +1955,7 @@ test.serial(`${currentTest} should throw an exception when table name passed in t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -1948,6 +1995,7 @@ test.serial( t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -1983,6 +2031,7 @@ test.serial(`${currentTest} should throw an exception when connection id passed t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2017,6 +2066,7 @@ test.serial(`${currentTest} should throw an exception when table name passed in t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2042,6 +2092,7 @@ test.serial(`${currentTest} should return delete result`, async (t) => { t.is(deleteRowInTable.status, 200); } catch (e) { console.error(e); + throw e; } }); @@ -2067,6 +2118,7 @@ test.serial(`${currentTest} should throw an exception when connection id passed t.is(deleteRowInTable.status, 403); } catch (e) { console.error(e); + throw e; } }); @@ -2091,6 +2143,7 @@ test.serial(`${currentTest} should throw an exception when table name passed in t.is(deleteRowInTabbleRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2123,6 +2176,7 @@ test.serial(`${currentTest} should return row`, async (t) => { t.is(Object.hasOwn(getRowInTableRO, 'readonly_fields'), true); } catch (e) { console.error(e); + throw e; } }); @@ -2147,6 +2201,7 @@ test.serial(`${currentTest} should throw an exception when connection id passed t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2171,6 +2226,7 @@ test.serial(`${currentTest} should throw an exception when table name passed in t.is(addRowInTableRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2225,6 +2281,7 @@ test.serial(`${currentTest} should return all found logs in connection'`, async t.is(Object.hasOwn(getRowInTableRO.logs[0], 'connection_id'), true); } catch (e) { console.error(e); + throw e; } }); @@ -2324,6 +2381,7 @@ test.skip(`${currentTest} should return empty table settings when it was not cre t.is(JSON.stringify(getTableSettingsRO), JSON.stringify({})); } catch (e) { console.error(e); + throw e; } }); @@ -2378,6 +2436,7 @@ test.serial(`${currentTest} 'should return table settings when it was created`, t.is(getTableSettingsRO.connection_id, connections.firstId); } catch (e) { console.error(e); + throw e; } }); @@ -2426,6 +2485,7 @@ test.serial( t.is(getTableSettingsRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -2466,6 +2526,7 @@ test.serial(`${currentTest} should throw an exception do not have permission`, a t.is(JSON.parse(createTableSettingsResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2507,6 +2568,7 @@ test.serial( t.is(createTableSettingsRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -2566,6 +2628,7 @@ test.serial(`${currentTest} should throw an exception do not have permission`, a t.is(JSON.parse(updateTableSettingsResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2603,9 +2666,9 @@ test.serial( t.is(createTableSettingsResponse.status, 201); const updateTableSettingsDTO = mockFactory.generateTableSettings( - connections.firstId, - firstTableInfo.testTableName, - [firstTableInfo.testTableSecondColumnName], + connections.secondId, + secondTableInfo.testTableName, + [secondTableInfo.testTableSecondColumnName], ['id'], ['updated_at'], ['created_at'], @@ -2615,7 +2678,7 @@ test.serial( ); const updateTableSettingsResponse = await request(app.getHttpServer()) - .put(`/settings?connectionId=${connections.secondId}&tableName=${secondTableInfo.testTableName}}}`) + .put(`/settings?connectionId=${connections.secondId}&tableName=${secondTableInfo.testTableName}`) .send(updateTableSettingsDTO) .set('Cookie', simpleUserToken) .set('Content-Type', 'application/json') @@ -2624,6 +2687,7 @@ test.serial( t.is(JSON.parse(updateTableSettingsResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -2671,6 +2735,7 @@ test.serial(`${currentTest} should return array without deleted table settings`, t.is(JSON.parse(deleteTableSettingsResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2717,6 +2782,7 @@ test.serial( t.is(deleteTableSettingsRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); @@ -2747,6 +2813,7 @@ test.serial(`${currentTest} should return empty widgets array when widgets not c t.is(getTableWidgetsRO.length, 0); } catch (e) { console.error(e); + throw e; } }); @@ -2808,6 +2875,7 @@ test.serial(`${currentTest} should return array of table widgets for table`, asy t.is(compareTableWidgetsArrays(getTableStructureRO.table_widgets, newTableWidgets), true); } catch (e) { console.error(e); + throw e; } }); @@ -2850,6 +2918,7 @@ test.serial( t.pass(); } catch (e) { console.error(e); + throw e; } }, ); @@ -2884,6 +2953,7 @@ test.serial(`${currentTest} should throw an exception do not have permissions`, t.is(createTableWidgetRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }); @@ -2916,6 +2986,7 @@ test.serial( t.is(createTableWidgetRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); + throw e; } }, ); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-table-different-group-connection-readonly-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-table-different-group-connection-readonly-permissions-e2e.test.ts index d56592902..379ede015 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-user-table-different-group-connection-readonly-permissions-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-table-different-group-connection-readonly-permissions-e2e.test.ts @@ -976,8 +976,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -1118,7 +1118,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts index e3862b7ea..d273221a4 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-user-with-table-only-permissions-e2e.test.ts @@ -929,8 +929,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -1067,7 +1067,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; diff --git a/backend/test/ava-tests/saas-tests/group-e2e.test.ts b/backend/test/ava-tests/saas-tests/group-e2e.test.ts index 1cb1524f0..c57b042d7 100644 --- a/backend/test/ava-tests/saas-tests/group-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/group-e2e.test.ts @@ -206,9 +206,9 @@ test.serial(`${currentTest} should throw an error when group id is not real`, as .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findAllUsersInGroup.status, 400); + t.is(findAllUsersInGroup.status, 403); const { message } = JSON.parse(findAllUsersInGroup.text); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -485,9 +485,9 @@ test.serial(`${currentTest} should throw an error when groupId is incorrect`, as .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(addUserInGroup.status, 400); + t.is(addUserInGroup.status, 403); const { message } = JSON.parse(addUserInGroup.text); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -680,9 +680,9 @@ test.serial(`${currentTest} should return throw an exception when groupId is inc .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(deleteResult.status, 400); + t.is(deleteResult.status, 403); const { message } = JSON.parse(deleteResult.text); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -1116,8 +1116,8 @@ test.serial(`${currentTest} should throw an error, when group id is incorrect`, .set('Accept', 'application/json'); const { message } = JSON.parse(removeUserFromGroup.text); - t.is(removeUserFromGroup.status, 400); - t.is(message, Messages.CONNECTION_NOT_FOUND); + t.is(removeUserFromGroup.status, 403); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; diff --git a/backend/test/ava-tests/saas-tests/saas-cedar-permissions-e2e.test.ts b/backend/test/ava-tests/saas-tests/saas-cedar-permissions-e2e.test.ts new file mode 100644 index 000000000..b9fb5a7cf --- /dev/null +++ b/backend/test/ava-tests/saas-tests/saas-cedar-permissions-e2e.test.ts @@ -0,0 +1,1104 @@ +/* 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 { ApplicationModule } from '../../../src/app.module.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 { TestUtils } from '../../utils/test.utils.js'; +import { createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions } from '../../utils/user-with-different-permissions-utils.js'; + +let app: INestApplication; +let _testUtils: TestUtils; +let currentTest: string; + +const mockFactory = new MockFactory(); +const newConnectionToPostgres = mockFactory.generateConnectionToTestPostgresDBInDocker(); +const updateConnection = mockFactory.generateUpdateConnectionDto(); + +test.before(async () => { + process.env.CEDAR_AUTHORIZATION_ENABLED = '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; + await app.close(); + } catch (e) { + console.error('After tests error ' + e); + } +}); + +//****************************** DEFAULT PERMISSIONS: connection=readonly, group=readonly ****************************** +//****************************** table: visibility=true, readonly=false, add=true, delete=true, edit=false ****************************** + +currentTest = 'GET /connections/'; + +test.serial( + `${currentTest} should return connections with readonly access for user in readonly group via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + firstTableInfo, + groups, + permissions, + secondTableInfo, + users: { adminUserToken, simpleUserToken }, + } = testData; + const findAll = await request(app.getHttpServer()) + .get('/connections') + .set('Content-Type', 'application/json') + .set('Cookie', simpleUserToken) + .set('Accept', 'application/json'); + + t.is(findAll.status, 200); + + const result = findAll.body.connections; + const targetConnection = result.find( + ({ connection }: any) => connection.id === connections.firstId, + ); + t.is(Object.hasOwn(targetConnection, 'connection'), true); + t.is(Object.hasOwn(targetConnection, 'accessLevel'), true); + t.is(targetConnection.accessLevel, AccessLevelEnum.readonly); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'GET /connection/one/:slug'; + +test.serial(`${currentTest} should return a found connection for readonly group user via Cedar`, async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${connections.firstId}`) + .set('Content-Type', 'application/json') + .set('Cookie', simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + + const result = findOneResponce.body.connection; + t.is(result.type, 'postgres'); + t.is(Object.hasOwn(result, 'host'), true); + t.is(typeof result.port, 'number'); + t.is(Object.hasOwn(result, 'createdAt'), true); + t.is(Object.hasOwn(result, 'updatedAt'), true); + t.is(Object.hasOwn(result, 'password'), false); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `${currentTest} should return limited connection info when user has no permission on second connection via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const findOneResponce = await request(app.getHttpServer()) + .get(`/connection/one/${connections.secondId}`) + .set('Content-Type', 'application/json') + .set('Cookie', simpleUserToken) + .set('Accept', 'application/json'); + t.is(findOneResponce.status, 200); + const findOneRO = JSON.parse(findOneResponce.text); + t.is(Object.hasOwn(findOneRO, 'host'), false); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'PUT /connection'; + +test.serial( + `${currentTest} should throw exception when readonly user tries to update connection via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const updateConnectionResponse = await request(app.getHttpServer()) + .put(`/connection/${connections.firstId}`) + .send(updateConnection) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateConnectionResponse.status, 403); + t.is(JSON.parse(updateConnectionResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'DELETE /connection/:slug'; + +test.serial( + `${currentTest} should throw exception when readonly user tries to delete connection via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const response = await request(app.getHttpServer()) + .put(`/connection/delete/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(response.status, 403); + t.is(JSON.parse(response.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'POST /connection/group/:slug'; + +test.serial( + `${currentTest} should throw exception when readonly group user tries to create group via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + users: { simpleUserToken }, + } = testData; + const newGroup1 = mockFactory.generateCreateGroupDto1(); + + const createGroupResponse = await request(app.getHttpServer()) + .post(`/connection/group/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .send(newGroup1) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(createGroupResponse.status, 403); + t.is(JSON.parse(createGroupResponse.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'GET /connection/groups/:slug'; + +test.serial(`${currentTest} should return groups for readonly group user via Cedar`, async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const response = await request(app.getHttpServer()) + .get(`/connection/groups/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(response.status, 200); + const result = JSON.parse(response.text); + t.is(result.length > 0, true); + t.is(result[0].accessLevel, AccessLevelEnum.readonly); + + const adminGroupIndex = result.findIndex((el: any) => el.group.title === 'Admin'); + t.is(adminGroupIndex >= 0, false); + } catch (e) { + console.error(e); + throw e; + } +}); + +currentTest = 'GET /connection/tables/:slug'; + +test.serial(`${currentTest} should return all tables for readonly group user via Cedar`, async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions(app); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const getTablesInConnection = await request(app.getHttpServer()) + .get(`/connection/tables/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesInConnection.status, 200); + const getTablesInConnectionRO = JSON.parse(getTablesInConnection.text); + t.is(getTablesInConnectionRO.length > 0, true); + const tableIndex = getTablesInConnectionRO.findIndex( + (table: any) => table.table === firstTableInfo.testTableName, + ); + t.is(typeof getTablesInConnectionRO[tableIndex].permissions, 'object'); + } catch (e) { + console.error(e); + throw e; + } +}); + +currentTest = 'GET /table/rows/:slug'; + +test.serial(`${currentTest} should return found rows for readonly group user via Cedar`, async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions(app); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const getTableRows = await request(app.getHttpServer()) + .get(`/table/rows/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 200); + const getTableRowsRO = JSON.parse(getTableRows.text); + const { rows, primaryColumns, pagination, structure, foreignKeys } = getTableRowsRO; + t.is(rows.length, Constants.DEFAULT_PAGINATION.perPage); + t.is(primaryColumns.length, 1); + t.is(structure.length, 5); + } catch (e) { + console.error(e); + throw e; + } +}); + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should return added row for user with add=true permission via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 201); + const addRowInTableRO = JSON.parse(addRowInTable.text); + t.is(Object.hasOwn(addRowInTableRO.row, 'id'), true); + t.is(addRowInTableRO.row[firstTableInfo.testTableColumnName], randomName); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when user with edit=false tries to update row via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + name: randomName, + email: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 403); + t.is(JSON.parse(updateRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should return delete result for user with delete=true permission via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const deleteRowInTable = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=19`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 200); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** CUSTOM TABLE PERMISSIONS: add=false ****************************** + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when user with add=false tries to add row via Cedar`, + async (t) => { + try { + const permissionNoAdd = { + visibility: true, + readonly: false, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNoAdd, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 403); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** CUSTOM TABLE PERMISSIONS: edit=true ****************************** + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should return updated row for user with edit=true permission via Cedar`, + async (t) => { + try { + const permissionWithEdit = { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionWithEdit, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 200); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** CUSTOM TABLE PERMISSIONS: delete=false ****************************** + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when user with delete=false tries to delete row via Cedar`, + async (t) => { + try { + const permissionNoDelete = { + visibility: true, + readonly: false, + add: true, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNoDelete, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const deleteRowInTable = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=19`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 403); + t.is(JSON.parse(deleteRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** CUSTOM TABLE PERMISSIONS: visibility=false ****************************** + +currentTest = 'GET /table/rows/:slug'; + +test.serial( + `${currentTest} should throw exception when user with visibility=false tries to read table rows via Cedar`, + async (t) => { + try { + const permissionNoVisibility = { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNoVisibility, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const getTableRows = await request(app.getHttpServer()) + .get(`/table/rows/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 403); + t.is(JSON.parse(getTableRows.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** CUSTOM TABLE PERMISSIONS: all=true (full table access) ****************************** + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should return added row for user with all table permissions true via Cedar`, + async (t) => { + try { + const permissionAll = { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionAll, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 201); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should return updated row for user with all table permissions true via Cedar`, + async (t) => { + try { + const permissionAll = { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionAll, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 200); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should return delete result for user with all table permissions true via Cedar`, + async (t) => { + try { + const permissionAll = { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionAll, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const deleteRowInTable = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=19`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 200); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** CUSTOM TABLE PERMISSIONS: all=false (no table access) ****************************** + +currentTest = 'GET /table/rows/:slug'; + +test.serial( + `${currentTest} should throw exception when user with all table permissions false tries to read rows via Cedar`, + async (t) => { + try { + const permissionNone = { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNone, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const getTableRows = await request(app.getHttpServer()) + .get(`/table/rows/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 403); + t.is(JSON.parse(getTableRows.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when user with all table permissions false tries to add row via Cedar`, + async (t) => { + try { + const permissionNone = { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNone, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 403); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when user with all table permissions false tries to update row via Cedar`, + async (t) => { + try { + const permissionNone = { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNone, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRowInTable.status, 403); + t.is(JSON.parse(updateRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when user with all table permissions false tries to delete row via Cedar`, + async (t) => { + try { + const permissionNone = { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionNone, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const deleteRowInTable = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=19`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRowInTable.status, 403); + t.is(JSON.parse(deleteRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** FAKE CONNECTION/TABLE - AUTHORIZATION EDGE CASES ****************************** + +currentTest = 'GET /table/rows/:slug'; + +test.serial( + `${currentTest} should throw exception when connection id is fake via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + firstTableInfo, + users: { simpleUserToken }, + } = testData; + const fakeId = faker.string.uuid(); + const getTableRows = await request(app.getHttpServer()) + .get(`/table/rows/${fakeId}?tableName=${firstTableInfo.testTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 403); + t.is(JSON.parse(getTableRows.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} should throw exception when table name is fake via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + users: { simpleUserToken }, + } = testData; + const fakeTableName = `${faker.lorem.words(1)}_${faker.string.uuid()}`; + const getTableRows = await request(app.getHttpServer()) + .get(`/table/rows/${connections.firstId}?tableName=${fakeTableName}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRows.status, 403); + t.is(JSON.parse(getTableRows.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'POST /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when connection id is fake via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + firstTableInfo, + users: { simpleUserToken }, + } = testData; + const fakeConnectionId = faker.string.uuid(); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${fakeConnectionId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: randomName, + [firstTableInfo.testTableSecondColumnName]: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTable.status, 403); + t.is(JSON.parse(addRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'PUT /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when connection id is fake via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + firstTableInfo, + users: { simpleUserToken }, + } = testData; + const fakeConnectionId = faker.string.uuid(); + const randomName = faker.person.firstName(); + const randomEmail = faker.internet.email(); + const created_at = new Date(); + const updated_at = new Date(); + const updateRowInTable = await request(app.getHttpServer()) + .put(`/table/row/${fakeConnectionId}?tableName=${firstTableInfo.testTableName}&id=1`) + .send({ + name: randomName, + email: randomEmail, + created_at: created_at, + updated_at: updated_at, + }) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(JSON.parse(updateRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial( + `${currentTest} should throw exception when connection id is fake via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + firstTableInfo, + users: { simpleUserToken }, + } = testData; + const fakeConnectionId = faker.string.uuid(); + const deleteRowInTable = await request(app.getHttpServer()) + .delete(`/table/row/${fakeConnectionId}?tableName=${firstTableInfo.testTableName}&id=1`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(JSON.parse(deleteRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + t.is(deleteRowInTable.status, 403); + } catch (e) { + console.error(e); + throw e; + } + }, +); diff --git a/backend/test/ava-tests/saas-tests/user-admin-permissions-e2e.test.ts b/backend/test/ava-tests/saas-tests/user-admin-permissions-e2e.test.ts index 4c88b529a..e622400dd 100644 --- a/backend/test/ava-tests/saas-tests/user-admin-permissions-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/user-admin-permissions-e2e.test.ts @@ -898,8 +898,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (error) { console.error(error); throw error; @@ -1014,7 +1014,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (error) { console.error(error); throw error; @@ -1179,7 +1179,7 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteUserInGroupRO = JSON.parse(deleteUserInGroupResponse.text); - t.is(deleteUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (error) { console.error(error); throw error; diff --git a/backend/test/ava-tests/saas-tests/user-group-edit-permissions-e2e.test.ts b/backend/test/ava-tests/saas-tests/user-group-edit-permissions-e2e.test.ts index e781e08de..e6416a07b 100644 --- a/backend/test/ava-tests/saas-tests/user-group-edit-permissions-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/user-group-edit-permissions-e2e.test.ts @@ -713,7 +713,7 @@ test.serial( t.is(typeof result.tables, 'object'); const { tables } = result; - const foundTableIndex = tables.findIndex((table) => table.tableName === firstTableInfo.testTableName); + const foundTableIndex = tables.findIndex((table) => table.tableName === secondTableInfo.testTableName); t.is(tables.length > 0, true); t.is(typeof tables[0], 'object'); t.is(Object.hasOwn(tables[foundTableIndex], 'accessLevel'), true); @@ -983,8 +983,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); } @@ -1117,7 +1117,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); } @@ -1310,7 +1310,7 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteUserInGroupRO = JSON.parse(deleteUserInGroupResponse.text); - t.is(deleteUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); } @@ -2470,7 +2470,7 @@ test.serial(`${currentTest} should throw an exception do not have permission`, a ); const createTableSettingsResponse = await request(app.getHttpServer()) - .post(`/settings?connectionId=${connections.firstId}&tableName=${firstTableInfo.testTableName}}`) + .post(`/settings?connectionId=${connections.firstId}&tableName=${firstTableInfo.testTableName}`) .send(createTableSettingsDTO) .set('Cookie', simpleUserToken) .set('Content-Type', 'application/json') @@ -2509,7 +2509,7 @@ test.serial( ); const createTableSettingsResponse = await request(app.getHttpServer()) - .post(`/settings?connectionId=${connections.secondId}&tableName=${secondTableInfo.testTableName}}`) + .post(`/settings?connectionId=${connections.secondId}&tableName=${secondTableInfo.testTableName}`) .send(createTableSettingsDTO) .set('Cookie', simpleUserToken) .set('Content-Type', 'application/json') @@ -2570,7 +2570,7 @@ test.serial(`${currentTest} should throw an exception do not have permission`, a ); const updateTableSettingsResponse = await request(app.getHttpServer()) - .put(`/settings?connectionId=${connections.firstId}&tableName=${firstTableInfo.testTableName}}`) + .put(`/settings?connectionId=${connections.firstId}&tableName=${firstTableInfo.testTableName}`) .send(updateTableSettingsDTO) .set('Cookie', simpleUserToken) .set('Content-Type', 'application/json') @@ -2616,9 +2616,9 @@ test.serial( t.is(createTableSettingsResponse.status, 201); const updateTableSettingsDTO = mockFactory.generateTableSettings( - connections.firstId, - firstTableInfo.testTableName, - [firstTableInfo.testTableSecondColumnName], + connections.secondId, + secondTableInfo.testTableName, + [secondTableInfo.testTableSecondColumnName], ['id'], ['updated_at'], ['created_at'], @@ -2628,7 +2628,7 @@ test.serial( ); const updateTableSettingsResponse = await request(app.getHttpServer()) - .put(`/settings?connectionId=${connections.secondId}&tableName=${secondTableInfo.testTableName}}}`) + .put(`/settings?connectionId=${connections.secondId}&tableName=${secondTableInfo.testTableName}`) .send(updateTableSettingsDTO) .set('Cookie', simpleUserToken) .set('Content-Type', 'application/json') diff --git a/backend/test/ava-tests/saas-tests/user-table-different-group-connection-readonly-permissions-e2e.test.ts b/backend/test/ava-tests/saas-tests/user-table-different-group-connection-readonly-permissions-e2e.test.ts index c6537696e..dc23c60a3 100644 --- a/backend/test/ava-tests/saas-tests/user-table-different-group-connection-readonly-permissions-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/user-table-different-group-connection-readonly-permissions-e2e.test.ts @@ -971,8 +971,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -1113,7 +1113,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; diff --git a/backend/test/ava-tests/saas-tests/user-with-table-only-permissions-e2e.test.ts b/backend/test/ava-tests/saas-tests/user-with-table-only-permissions-e2e.test.ts index 957223730..d4a39c1c7 100644 --- a/backend/test/ava-tests/saas-tests/user-with-table-only-permissions-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/user-with-table-only-permissions-e2e.test.ts @@ -923,8 +923,8 @@ test.serial(`${currentTest} should throw exception, when group id passed in requ .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const addUserInGroupRO = JSON.parse(addUserInGroupResponse.text); - t.is(addUserInGroupResponse.status, 400); - t.is(addUserInGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(addUserInGroupResponse.status, 403); + t.is(addUserInGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; @@ -1061,7 +1061,7 @@ test.serial(`${currentTest} should throw an exception when group id passed in re .set('Content-Type', 'application/json') .set('Accept', 'application/json'); const deleteGroupRO = JSON.parse(deleteGroupResponse.text); - t.is(deleteGroupRO.message, Messages.CONNECTION_NOT_FOUND); + t.is(deleteGroupRO.message, Messages.DONT_HAVE_PERMISSIONS); } catch (e) { console.error(e); throw e; diff --git a/backend/test/utils/user-with-different-permissions-utils.ts b/backend/test/utils/user-with-different-permissions-utils.ts index 657eae10d..569a2e4b4 100644 --- a/backend/test/utils/user-with-different-permissions-utils.ts +++ b/backend/test/utils/user-with-different-permissions-utils.ts @@ -533,6 +533,126 @@ export async function createConnectionsAndInviteNewUserInAdminGroupOfFirstConnec }; } +export async function createConnectionAndInviteUserWithConnectionEditOnly( + app: INestApplication, +): Promise { + const connectionsId = { + firstId: null, + secondId: null, + firstAdminGroupId: null, + }; + + const mockFactory = new MockFactory(); + const connectionAdminUserInfo = await registerUserAndReturnUserInfo(app); + const simpleUserRegisterInfo = await inviteUserInCompanyAndAcceptInvitation( + connectionAdminUserInfo.token, + undefined, + app, + undefined, + ); + const connectionAdminUserToken = connectionAdminUserInfo.token; + const simpleUserToken = simpleUserRegisterInfo.token; + + const newConnection = mockFactory.generateConnectionToTestPostgresDBInDocker(); + const newConnection2 = mockFactory.generateConnectionToTestMySQLDBInDocker(); + const newGroup1 = mockFactory.generateCreateGroupDto1(); + const firstTable = await createTestTable(newConnection); + const secondTable = await createTestTable(newConnection2); + + const createFirstConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .set('Cookie', connectionAdminUserToken) + .send(newConnection) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createFirstConnectionRO = JSON.parse(createFirstConnectionResponse.text); + connectionsId.firstId = createFirstConnectionRO.id; + + const getGroupsResponse = await request(app.getHttpServer()) + .get(`/connection/groups/${connectionsId.firstId}`) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const getGroupsRO = JSON.parse(getGroupsResponse.text); + + const firstAdminGroupId = getGroupsRO[0].group.id; + + const createSecondConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .set('Cookie', connectionAdminUserToken) + .send(newConnection2) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createSecondConnectionRO = JSON.parse(createSecondConnectionResponse.text); + connectionsId.secondId = createSecondConnectionRO.id; + const email = simpleUserRegisterInfo.email; + + const createGroupResponse = await request(app.getHttpServer()) + .post(`/connection/group/${connectionsId.firstId}`) + .set('Cookie', connectionAdminUserToken) + .send(newGroup1) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const groupId = JSON.parse(createGroupResponse.text).id; + + const permissions = { + connection: { + connectionId: connectionsId.firstId, + accessLevel: AccessLevelEnum.edit, + }, + group: { + groupId: groupId, + accessLevel: AccessLevelEnum.none, + }, + tables: [], + }; + + const _createOrUpdatePermissionResponse = await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionsId.firstId}`) + .send({ permissions }) + .set('Cookie', connectionAdminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + await request(app.getHttpServer()) + .put('/group/user') + .set('Cookie', connectionAdminUserToken) + .send({ groupId, email }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + return { + firstTableInfo: firstTable, + secondTableInfo: secondTable, + permissions: { + table: { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }, + }, + connections: { + firstId: connectionsId.firstId, + secondId: connectionsId.secondId, + }, + groups: { + firstAdminGroupId: firstAdminGroupId, + secondAdminGroupId: null, + createdGroupId: groupId, + }, + users: { + adminUserToken: connectionAdminUserToken, + simpleUserToken: simpleUserToken, + simpleUserEmail: simpleUserRegisterInfo.email, + adminUserEmail: connectionAdminUserInfo.email, + adminUserPassword: connectionAdminUserInfo.password, + simpleUserPassword: simpleUserRegisterInfo.password, + }, + }; +} + interface IUserDifferentTableOnlyPermissionsFooData { firstTableInfo: CreatedTableInfo; secondTableInfo: CreatedTableInfo; diff --git a/yarn.lock b/yarn.lock index 56c2d3bad..8fd2b4701 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,7 +267,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-bedrock-runtime@npm:^3.989.0, @aws-sdk/client-bedrock-runtime@npm:^3.999.0": +"@aws-sdk/client-bedrock-runtime@npm:^3.989.0, @aws-sdk/client-bedrock-runtime@npm:^3.990.0": version: 3.999.0 resolution: "@aws-sdk/client-bedrock-runtime@npm:3.999.0" dependencies: @@ -419,7 +419,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-s3@npm:^3.999.0": +"@aws-sdk/client-s3@npm:^3.990.0": version: 3.999.0 resolution: "@aws-sdk/client-s3@npm:3.999.0" dependencies: @@ -884,7 +884,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/lib-dynamodb@npm:^3.999.0": +"@aws-sdk/lib-dynamodb@npm:^3.990.0, @aws-sdk/lib-dynamodb@npm:^3.999.0": version: 3.999.0 resolution: "@aws-sdk/lib-dynamodb@npm:3.999.0" dependencies: @@ -1259,7 +1259,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/s3-request-presigner@npm:^3.999.0": +"@aws-sdk/s3-request-presigner@npm:^3.990.0": version: 3.999.0 resolution: "@aws-sdk/s3-request-presigner@npm:3.999.0" dependencies: @@ -1819,6 +1819,13 @@ __metadata: languageName: node linkType: hard +"@cedar-policy/cedar-wasm@npm:^4.9.0": + version: 4.9.0 + resolution: "@cedar-policy/cedar-wasm@npm:4.9.0" + checksum: 431046ae3f1802fc7163c11cb0c42856f876f2c89a200f43570f25249c2c647658ef024d33292b4c0331c4fee8855ac9d9f3e91afe9dd78d69a86fcf65956afc + languageName: node + linkType: hard + "@cfworker/json-schema@npm:^4.0.2": version: 4.1.1 resolution: "@cfworker/json-schema@npm:4.1.1" @@ -5872,10 +5879,11 @@ __metadata: dependencies: "@amplitude/node": 1.10.2 "@ava/typescript": 6.0.0 - "@aws-sdk/client-bedrock-runtime": ^3.999.0 - "@aws-sdk/client-s3": ^3.999.0 - "@aws-sdk/lib-dynamodb": ^3.999.0 - "@aws-sdk/s3-request-presigner": ^3.999.0 + "@aws-sdk/client-bedrock-runtime": ^3.990.0 + "@aws-sdk/client-s3": ^3.990.0 + "@aws-sdk/lib-dynamodb": ^3.990.0 + "@aws-sdk/s3-request-presigner": ^3.990.0 + "@cedar-policy/cedar-wasm": ^4.9.0 "@electric-sql/pglite": ^0.3.15 "@faker-js/faker": ^10.3.0 "@langchain/aws": ^1.3.0