From 2f89f2241316ec8aed0bef7a307ac08ed5dc5c67 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 21 May 2026 14:46:14 +0000 Subject: [PATCH] added RocketAdmin::ActionEvent permissions --- .../cedar-authorization/cedar-action-map.ts | 5 + .../cedar-authorization.service.ts | 29 +- .../cedar-entity-builder.ts | 13 +- .../cedar-permissions.service.ts | 60 ++- .../cedar-policy-generator.ts | 18 + .../cedar-policy-parser.ts | 56 ++- .../cedar-authorization/cedar-schema.json | 16 + .../cedar-authorization/cedar-schema.ts | 16 + .../data-structures/create-permissions.ds.ts | 32 ++ .../permission/permission.interface.ts | 12 + .../create-or-update-permissions.use.case.ts | 8 +- ...tion-events-custom-repository.extension.ts | 9 + ...tion-events-custom-repository.interface.ts | 2 + .../action-rules.controller.ts | 3 +- .../activate-actions-in-rule.use.case.ts | 8 +- .../src/guards/action-event-trigger.guard.ts | 59 +++ .../non-saas-table-cassandra.e2e.test.ts | 2 +- .../non-saas-table-ibmdb2-e2e.test.ts | 2 +- .../non-saas-table-mongodb-e2e.test.ts | 2 +- .../non-saas-table-mssql-e2e.test.ts | 2 +- .../non-saas-table-mssql-schema-e2e.test.ts | 2 +- .../non-saas-table-mysql-e2e.test.ts | 2 +- .../non-saas-table-oracledb-e2e.test.ts | 2 +- ...non-saas-table-oracledb-schema-e2e.test.ts | 2 +- ...-saas-table-postgres-encrypted-e2e.test.ts | 2 +- ...non-saas-table-postgres-schema-e2e.test.ts | 2 +- .../non-saas-table-redis-e2e.test.ts | 2 +- .../saas-tests/action-rules-e2e.test.ts | 363 +++++++++++++++++- .../ava-tests/saas-tests/api-key-e2e.test.ts | 2 +- .../connection-properties-e2e.test.ts | 2 +- .../table-cassandra-agent.e2e.test.ts | 2 +- .../saas-tests/table-cassandra.e2e.test.ts | 2 +- .../table-clickhouse-agent-e2e.test.ts | 2 +- .../saas-tests/table-clickhouse-e2e.test.ts | 2 +- .../saas-tests/table-dynamodb-e2e.test.ts | 2 +- .../table-elasticsearch-e2e.test.ts | 2 +- .../saas-tests/table-ibmdb2-agent-e2e.test.ts | 2 +- .../saas-tests/table-ibmdb2-e2e.test.ts | 2 +- .../table-mongodb-agent-e2e.test.ts | 2 +- .../saas-tests/table-mongodb-e2e.test.ts | 2 +- .../saas-tests/table-mssql-agent-e2e.test.ts | 2 +- .../saas-tests/table-mssql-e2e.test.ts | 2 +- .../saas-tests/table-mssql-schema-e2e.test.ts | 2 +- .../saas-tests/table-mysql-agent-e2e.test.ts | 2 +- .../saas-tests/table-mysql-e2e.test.ts | 2 +- .../saas-tests/table-oracle-agent-e2e.test.ts | 2 +- .../saas-tests/table-oracledb-e2e.test.ts | 2 +- .../table-oracledb-schema-e2e.test.ts | 2 +- .../table-postgres-agent-e2e.test.ts | 2 +- .../saas-tests/table-postgres-e2e.test.ts | 2 +- .../table-postgres-encrypted-e2e.test.ts | 2 +- .../table-postgres-schema-e2e.test.ts | 2 +- .../saas-tests/table-redis-agent-e2e.test.ts | 2 +- .../saas-tests/table-redis-e2e.test.ts | 2 +- 54 files changed, 724 insertions(+), 59 deletions(-) create mode 100644 backend/src/guards/action-event-trigger.guard.ts diff --git a/backend/src/entities/cedar-authorization/cedar-action-map.ts b/backend/src/entities/cedar-authorization/cedar-action-map.ts index c15962229..b161ba42b 100644 --- a/backend/src/entities/cedar-authorization/cedar-action-map.ts +++ b/backend/src/entities/cedar-authorization/cedar-action-map.ts @@ -9,6 +9,7 @@ export enum CedarAction { TableEdit = 'table:edit', TableDelete = 'table:delete', TableAiRequest = 'table:ai-request', + ActionEventTrigger = 'actionEvent:trigger', DashboardRead = 'dashboard:read', DashboardCreate = 'dashboard:create', DashboardEdit = 'dashboard:edit', @@ -23,6 +24,7 @@ export enum CedarResourceType { Connection = 'RocketAdmin::Connection', Group = 'RocketAdmin::Group', Table = 'RocketAdmin::Table', + ActionEvent = 'RocketAdmin::ActionEvent', Dashboard = 'RocketAdmin::Dashboard', Panel = 'RocketAdmin::Panel', } @@ -31,12 +33,15 @@ export const CEDAR_ACTION_TYPE = 'RocketAdmin::Action'; export const CEDAR_USER_TYPE = 'RocketAdmin::User'; export const CEDAR_GROUP_TYPE = 'RocketAdmin::Group'; +export const ACTION_EVENT_PROBE_ID = '__probe__'; + export interface CedarValidationRequest { userId: string; action: CedarAction; connectionId?: string; groupId?: string; tableName?: string; + actionEventId?: string; dashboardId?: string; panelId?: string; } diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts index 035671198..e51521ce9 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts @@ -34,7 +34,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On } async validate(request: CedarValidationRequest): Promise { - const { userId, action, groupId, tableName, dashboardId, panelId } = request; + const { userId, action, groupId, tableName, dashboardId, panelId, actionEventId } = request; let { connectionId } = request; const actionPrefix = action.split(':')[0]; @@ -56,6 +56,22 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On resourceType = CedarResourceType.Table; resourceId = `${connectionId}/${tableName}`; break; + case 'actionEvent': { + if (!tableName || !actionEventId) return false; + resourceType = CedarResourceType.ActionEvent; + resourceId = `${connectionId}/${tableName}/${actionEventId}`; + return this.evaluate( + userId, + connectionId, + action, + resourceType, + resourceId, + tableName, + undefined, + undefined, + actionEventId, + ); + } case 'dashboard': { resourceType = CedarResourceType.Dashboard; const needsSentinel = action === CedarAction.DashboardCreate || !dashboardId; @@ -195,6 +211,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On tableName?: string, dashboardId?: string, panelId?: string, + actionEventId?: string, ): Promise { await this.assertUserNotSuspended(userId); @@ -204,7 +221,15 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On const groupPolicies = this.loadPoliciesPerGroup(userGroups); if (groupPolicies.length === 0) return false; - const entities = buildCedarEntities(userId, userGroups, connectionId, tableName, dashboardId, panelId); + const entities = buildCedarEntities( + userId, + userGroups, + connectionId, + tableName, + dashboardId, + panelId, + actionEventId, + ); for (const policy of groupPolicies) { const call = { diff --git a/backend/src/entities/cedar-authorization/cedar-entity-builder.ts b/backend/src/entities/cedar-authorization/cedar-entity-builder.ts index 08f8d53b3..ce511a264 100644 --- a/backend/src/entities/cedar-authorization/cedar-entity-builder.ts +++ b/backend/src/entities/cedar-authorization/cedar-entity-builder.ts @@ -13,6 +13,7 @@ export function buildCedarEntities( tableName?: string, dashboardId?: string, panelId?: string, + actionEventId?: string, ): Array { const entities: Array = []; @@ -42,7 +43,7 @@ export function buildCedarEntities( parents: [], }); - // Table entity (if table-level check) + // Table entity (if table-level check, or as parent for an ActionEvent) if (tableName) { entities.push({ uid: { type: 'RocketAdmin::Table', id: `${connectionId}/${tableName}` }, @@ -51,6 +52,16 @@ export function buildCedarEntities( }); } + // ActionEvent entity, parented by its Table — required so `resource in Table::"..."` + // policies authorize triggering specific events without naming each event. + if (actionEventId && tableName) { + entities.push({ + uid: { type: 'RocketAdmin::ActionEvent', id: `${connectionId}/${tableName}/${actionEventId}` }, + attrs: { connectionId: connectionId, tableName: tableName }, + parents: [{ type: 'RocketAdmin::Table', id: `${connectionId}/${tableName}` }], + }); + } + if (dashboardId) { entities.push({ uid: { type: 'RocketAdmin::Dashboard', id: `${connectionId}/${dashboardId}` }, diff --git a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts index db35ae54f..69edf0ba3 100644 --- a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts @@ -8,7 +8,13 @@ import { Cacher } from '../../helpers/cache/cacher.js'; import { GroupEntity } from '../group/group.entity.js'; import { ITablePermissionData } from '../permission/permission.interface.js'; import { IUserAccessRepository } from '../user-access/repository/user-access.repository.interface.js'; -import { CEDAR_ACTION_TYPE, CEDAR_USER_TYPE, CedarAction, CedarResourceType } from './cedar-action-map.js'; +import { + ACTION_EVENT_PROBE_ID, + CEDAR_ACTION_TYPE, + CEDAR_USER_TYPE, + CedarAction, + CedarResourceType, +} from './cedar-action-map.js'; import { buildCedarEntities } from './cedar-entity-builder.js'; import { CEDAR_SCHEMA } from './cedar-schema.js'; @@ -222,6 +228,7 @@ export class CedarPermissionsService implements IUserAccessRepository { delete: false, edit: false, aiRequest: false, + triggerCustomAction: false, }, }; } @@ -327,6 +334,35 @@ export class CedarPermissionsService implements IUserAccessRepository { ); } + async checkActionEventTrigger( + cognitoUserName: string, + connectionId: string, + tableName: string, + actionEventId: string, + _masterPwd?: string, + ): Promise { + const ctx = await this.loadContext(connectionId, cognitoUserName); + if (!ctx) return false; + + const entities = buildCedarEntities( + cognitoUserName, + ctx.userGroups, + connectionId, + tableName, + undefined, + undefined, + actionEventId, + ); + return this.evaluatePolicies( + cognitoUserName, + CedarAction.ActionEventTrigger, + CedarResourceType.ActionEvent, + `${connectionId}/${tableName}/${actionEventId}`, + ctx.policies, + entities, + ); + } + async getConnectionId(groupId: string): Promise { const group = await this.globalDbContext.groupRepository.findGroupByIdWithConnectionAndUsers(groupId); if (!group?.connection?.id) { @@ -404,6 +440,27 @@ export class CedarPermissionsService implements IUserAccessRepository { ctx.policies, entities, ); + // "Blanket trigger on this table" — only `permit(... resource in Table::"...")` policies + // match this synthetic probe event. Per-event grants (resource == ActionEvent::"...x") + // won't match the probe id, so the table-level flag stays false unless the user truly + // has table-wide trigger. + const probeEntities = buildCedarEntities( + userId, + ctx.userGroups, + connectionId, + tableName, + undefined, + undefined, + ACTION_EVENT_PROBE_ID, + ); + const canTriggerAnyCustomAction = this.evaluatePolicies( + userId, + CedarAction.ActionEventTrigger, + CedarResourceType.ActionEvent, + `${connectionId}/${tableName}/${ACTION_EVENT_PROBE_ID}`, + ctx.policies, + probeEntities, + ); return { tableName, @@ -414,6 +471,7 @@ export class CedarPermissionsService implements IUserAccessRepository { delete: canDelete, edit: canEdit, aiRequest: canAiRequest, + triggerCustomAction: canTriggerAnyCustomAction, }, }; } diff --git a/backend/src/entities/cedar-authorization/cedar-policy-generator.ts b/backend/src/entities/cedar-authorization/cedar-policy-generator.ts index 433eb48c5..b35a3d1ac 100644 --- a/backend/src/entities/cedar-authorization/cedar-policy-generator.ts +++ b/backend/src/entities/cedar-authorization/cedar-policy-generator.ts @@ -132,6 +132,8 @@ export function generateCedarPolicyForGroup( const tableRef = `RocketAdmin::Table::"${connectionId}/${table.tableName}"`; const access = table.accessLevel; + // triggerCustomAction is intentionally excluded from hasAnyAccess: triggering custom + // action events is a side-effect-only capability and does not imply table visibility. const hasAnyAccess = access.visibility || access.add || access.delete || access.edit; if (hasAnyAccess) { policies.push( @@ -158,6 +160,22 @@ export function generateCedarPolicyForGroup( `permit(\n principal,\n action == RocketAdmin::Action::"table:ai-request",\n resource == ${tableRef}\n);`, ); } + if (access.triggerCustomAction) { + // Blanket: any action event whose parent is this table is permitted. + policies.push( + `permit(\n principal,\n action == RocketAdmin::Action::"actionEvent:trigger",\n resource in ${tableRef}\n);`, + ); + } + } + + if (permissions.actionEvents) { + for (const event of permissions.actionEvents) { + if (!event.accessLevel?.trigger) continue; + const eventRef = `RocketAdmin::ActionEvent::"${connectionId}/${event.tableName}/${event.eventId}"`; + policies.push( + `permit(\n principal,\n action == RocketAdmin::Action::"actionEvent:trigger",\n resource == ${eventRef}\n);`, + ); + } } return policies.join('\n\n'); diff --git a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts index b6605eef9..710ce3bb9 100644 --- a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts +++ b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts @@ -1,5 +1,6 @@ import { AccessLevelEnum } from '../../enums/access-level.enum.js'; import { + IActionEventPermissionData, IComplexPermission, IDashboardPermissionData, IPanelPermissionData, @@ -11,6 +12,7 @@ interface ParsedPermitStatement { resourceType: string | null; resourceId: string | null; isWildcard: boolean; + isInRelation: boolean; } export function parseCedarPolicyToClassicalPermissions( @@ -24,11 +26,13 @@ export function parseCedarPolicyToClassicalPermissions( connection: { connectionId, accessLevel: AccessLevelEnum.none }, group: { groupId, accessLevel: AccessLevelEnum.none }, tables: [], + actionEvents: [], dashboards: [], panels: [], }; const tableMap = new Map(); + const actionEventMap = new Map(); const dashboardMap = new Map(); const panelMap = new Map(); @@ -74,6 +78,28 @@ export function parseCedarPolicyToClassicalPermissions( applyTableAction(tableEntry, permit.action); break; } + case 'actionEvent:trigger': { + if (permit.resourceType === 'RocketAdmin::Table' && permit.isInRelation) { + // Blanket: trigger any event on this table + const tableName = extractTableName(permit.resourceId, connectionId); + if (!tableName) break; + const tableEntry = getOrCreateTableEntry(tableMap, tableName); + tableEntry.accessLevel.triggerCustomAction = true; + } else if (permit.resourceType === 'RocketAdmin::ActionEvent') { + // Per-event grant + const parts = extractActionEventResource(permit.resourceId, connectionId); + if (!parts) break; + const key = `${parts.tableName}/${parts.eventId}`; + if (!actionEventMap.has(key)) { + actionEventMap.set(key, { + eventId: parts.eventId, + tableName: parts.tableName, + accessLevel: { trigger: true }, + }); + } + } + break; + } case 'dashboard:read': case 'dashboard:create': case 'dashboard:edit': @@ -102,6 +128,7 @@ export function parseCedarPolicyToClassicalPermissions( const a = table.accessLevel; a.readonly = a.visibility && !a.add && !a.edit && !a.delete; } + result.actionEvents = Array.from(actionEventMap.values()); result.dashboards = Array.from(dashboardMap.values()); result.panels = Array.from(panelMap.values()); @@ -173,6 +200,7 @@ function parsePermitBody(body: string): ParsedPermitStatement { resourceType: null, resourceId: null, isWildcard: false, + isInRelation: false, }; const actionMatch = body.match(/action\s*==\s*RocketAdmin::Action::"([^"]+)"/); @@ -185,10 +213,15 @@ function parsePermitBody(body: string): ParsedPermitStatement { } } - const resourceMatch = body.match(/resource\s*==\s*(RocketAdmin::\w+)::"([^"]+)"/); - if (resourceMatch) { - result.resourceType = resourceMatch[1]; - result.resourceId = resourceMatch[2]; + const resourceEqMatch = body.match(/resource\s*==\s*(RocketAdmin::\w+)::"([^"]+)"/); + const resourceInMatch = body.match(/resource\s+in\s+(RocketAdmin::\w+)::"([^"]+)"/); + if (resourceEqMatch) { + result.resourceType = resourceEqMatch[1]; + result.resourceId = resourceEqMatch[2]; + } else if (resourceInMatch) { + result.resourceType = resourceInMatch[1]; + result.resourceId = resourceInMatch[2]; + result.isInRelation = true; } else { const resourceClause = body.match(/,\s*(resource)\s*$/m); if (resourceClause && !result.action) { @@ -229,6 +262,7 @@ function getOrCreateTableEntry(map: Map, tableName delete: false, edit: false, aiRequest: false, + triggerCustomAction: false, }, }; map.set(tableName, entry); @@ -256,6 +290,20 @@ function applyTableAction(entry: ITablePermissionData, action: string): void { } } +function extractActionEventResource( + resourceId: string | null, + connectionId: string, +): { tableName: string; eventId: string } | null { + if (!resourceId) return null; + const prefix = `${connectionId}/`; + const stripped = resourceId.startsWith(prefix) ? resourceId.slice(prefix.length) : resourceId; + const slash = stripped.indexOf('/'); + if (slash <= 0 || slash === stripped.length - 1) return null; + const tableName = stripped.slice(0, slash); + const eventId = stripped.slice(slash + 1); + return { tableName, eventId }; +} + function getOrCreateDashboardEntry( map: Map, dashboardId: string, diff --git a/backend/src/entities/cedar-authorization/cedar-schema.json b/backend/src/entities/cedar-authorization/cedar-schema.json index 16e7a7d87..0460d3ce8 100644 --- a/backend/src/entities/cedar-authorization/cedar-schema.json +++ b/backend/src/entities/cedar-authorization/cedar-schema.json @@ -36,6 +36,16 @@ } } }, + "ActionEvent": { + "memberOfTypes": ["Table"], + "shape": { + "type": "Record", + "attributes": { + "connectionId": { "type": "String" }, + "tableName": { "type": "String" } + } + } + }, "Dashboard": { "memberOfTypes": ["Connection"], "shape": { @@ -107,6 +117,12 @@ "resourceTypes": ["Table"] } }, + "actionEvent:trigger": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["ActionEvent"] + } + }, "dashboard:read": { "appliesTo": { "principalTypes": ["User"], diff --git a/backend/src/entities/cedar-authorization/cedar-schema.ts b/backend/src/entities/cedar-authorization/cedar-schema.ts index 4d1ef726c..c6aae8d28 100644 --- a/backend/src/entities/cedar-authorization/cedar-schema.ts +++ b/backend/src/entities/cedar-authorization/cedar-schema.ts @@ -36,6 +36,16 @@ export const CEDAR_SCHEMA = { }, }, }, + ActionEvent: { + memberOfTypes: ['Table'], + shape: { + type: 'Record', + attributes: { + connectionId: { type: 'String' }, + tableName: { type: 'String' }, + }, + }, + }, Dashboard: { memberOfTypes: ['Connection'], shape: { @@ -116,6 +126,12 @@ export const CEDAR_SCHEMA = { resourceTypes: ['Table'], }, }, + 'actionEvent:trigger': { + appliesTo: { + principalTypes: ['User'], + resourceTypes: ['ActionEvent'], + }, + }, 'dashboard:read': { appliesTo: { principalTypes: ['User'], diff --git a/backend/src/entities/permission/application/data-structures/create-permissions.ds.ts b/backend/src/entities/permission/application/data-structures/create-permissions.ds.ts index 402eec090..666eb1c1c 100644 --- a/backend/src/entities/permission/application/data-structures/create-permissions.ds.ts +++ b/backend/src/entities/permission/application/data-structures/create-permissions.ds.ts @@ -19,6 +19,7 @@ export class PermissionsDs { groupId: string; }; tables: Array; + actionEvents?: Array; } export class TableAccessLevelsDs { @@ -46,6 +47,11 @@ export class TableAccessLevelsDs { @IsOptional() @IsBoolean() aiRequest?: boolean; + + @ApiProperty({ required: false }) + @IsOptional() + @IsBoolean() + triggerCustomAction?: boolean; } export class TablePermissionDs { @@ -79,6 +85,26 @@ export class ConnectionPermissionDs { accessLevel: AccessLevelEnum; } +export class ActionEventAccessLevelsDs { + @ApiProperty() + @IsBoolean() + trigger: boolean; +} + +export class ActionEventPermissionDs { + @ApiProperty() + @IsString() + eventId: string; + + @ApiProperty() + @IsString() + tableName: string; + + @ApiProperty({ type: ActionEventAccessLevelsDs }) + @ValidateNested() + accessLevel: ActionEventAccessLevelsDs; +} + export class ComplexPermissionDs { @ApiProperty({ type: ConnectionPermissionDs }) @ValidateNested() @@ -92,4 +118,10 @@ export class ComplexPermissionDs { @IsArray() @ValidateNested({ each: true }) tables: Array; + + @ApiProperty({ required: false, isArray: true, type: ActionEventPermissionDs }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + actionEvents?: Array; } diff --git a/backend/src/entities/permission/permission.interface.ts b/backend/src/entities/permission/permission.interface.ts index 58bf5735d..4255ba772 100644 --- a/backend/src/entities/permission/permission.interface.ts +++ b/backend/src/entities/permission/permission.interface.ts @@ -4,6 +4,7 @@ export interface IComplexPermission { connection: IConnectionPermissionData; group: IGroupPermissionData; tables: Array; + actionEvents?: Array; dashboards?: Array; panels?: Array; } @@ -25,6 +26,7 @@ export interface ITableAccessLevel { delete: boolean; edit: boolean; aiRequest?: boolean; + triggerCustomAction?: boolean; } export interface ITablePermissionData { @@ -36,6 +38,16 @@ export interface ITableAndViewPermissionData extends ITablePermissionData { isView: boolean; } +export interface IActionEventAccessLevel { + trigger: boolean; +} + +export interface IActionEventPermissionData { + eventId: string; + tableName: string; + accessLevel: IActionEventAccessLevel; +} + export interface IDashboardAccessLevel { read: boolean; create: boolean; 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 c68fb7669..20a9ed4aa 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 @@ -3,12 +3,9 @@ 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 { - CreatePermissionsDs, - PermissionsDs, -} from '../application/data-structures/create-permissions.ds.js'; -import { generateCedarPolicyForGroup } from '../../cedar-authorization/cedar-policy-generator.js'; import { Cacher } from '../../../helpers/cache/cacher.js'; +import { generateCedarPolicyForGroup } from '../../cedar-authorization/cedar-policy-generator.js'; +import { CreatePermissionsDs, PermissionsDs } from '../application/data-structures/create-permissions.ds.js'; import { ICreateOrUpdatePermissions } from './permissions-use-cases.interface.js'; @Injectable() @@ -68,6 +65,7 @@ export class CreateOrUpdatePermissionsUseCase groupId: inputData.permissions.group.groupId, }, tables: inputData.permissions.tables, + actionEvents: inputData.permissions.actionEvents, }; // Generate and save Cedar policy for this group diff --git a/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.extension.ts b/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.extension.ts index 60d4c84cc..475bfc224 100644 --- a/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.extension.ts +++ b/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.extension.ts @@ -16,4 +16,13 @@ export const actionEventsCustomRepositoryExtension: IActionEventsRepository = { .andWhere('action_events.event = :event', { event: TableActionEventEnum.CUSTOM }) .getMany(); }, + + async findEventByIdInConnection(eventId: string, connectionId: string): Promise { + return await this.createQueryBuilder('action_events') + .leftJoinAndSelect('action_events.action_rule', 'action_rule') + .leftJoin('action_rule.connection', 'connection') + .where('action_events.id = :eventId', { eventId }) + .andWhere('connection.id = :connectionId', { connectionId }) + .getOne(); + }, }; diff --git a/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.interface.ts b/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.interface.ts index 979454729..dd0e9abc3 100644 --- a/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.interface.ts +++ b/backend/src/entities/table-actions/table-action-events-module/repository/action-events-custom-repository.interface.ts @@ -4,4 +4,6 @@ export interface IActionEventsRepository { saveNewOrUpdatedActionEvent(event: ActionEventsEntity): Promise; findCustomEventsForTable(connectionId: string, tableName: string): Promise>; + + findEventByIdInConnection(eventId: string, connectionId: string): Promise; } diff --git a/backend/src/entities/table-actions/table-action-rules-module/action-rules.controller.ts b/backend/src/entities/table-actions/table-action-rules-module/action-rules.controller.ts index 026b76874..f48af2642 100644 --- a/backend/src/entities/table-actions/table-action-rules-module/action-rules.controller.ts +++ b/backend/src/entities/table-actions/table-action-rules-module/action-rules.controller.ts @@ -21,6 +21,7 @@ import { UserId } from '../../../decorators/user-id.decorator.js'; import { InTransactionEnum } from '../../../enums/in-transaction.enum.js'; import { TableActionEventEnum } from '../../../enums/table-action-event-enum.js'; import { Messages } from '../../../exceptions/text/messages.js'; +import { ActionEventTriggerGuard } from '../../../guards/action-event-trigger.guard.js'; import { ConnectionEditGuard } from '../../../guards/connection-edit.guard.js'; import { ConnectionReadGuard } from '../../../guards/connection-read.guard.js'; import { validateStringWithEnum } from '../../../helpers/validators/validate-string-with-enum.js'; @@ -239,7 +240,7 @@ export class ActionRulesController { isArray: true, }) @ApiBody({ type: Object }) - // @UseGuards(ConnectionReadGuard) + @UseGuards(ActionEventTriggerGuard) @Post('/event/actions/activate/:eventId/:connectionId') async activateTableActionsInRule( @SlugUuid('connectionId') connectionId: string, diff --git a/backend/src/entities/table-actions/table-action-rules-module/use-cases/activate-actions-in-rule.use.case.ts b/backend/src/entities/table-actions/table-action-rules-module/use-cases/activate-actions-in-rule.use.case.ts index e03ded93e..9c1e35810 100644 --- a/backend/src/entities/table-actions/table-action-rules-module/use-cases/activate-actions-in-rule.use.case.ts +++ b/backend/src/entities/table-actions/table-action-rules-module/use-cases/activate-actions-in-rule.use.case.ts @@ -1,11 +1,10 @@ -import { ForbiddenException, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import AbstractUseCase from '../../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../../common/data-injection.tokens.js'; import { LogOperationTypeEnum } from '../../../../enums/log-operation-type.enum.js'; import { OperationResultStatusEnum } from '../../../../enums/operation-result-status.enum.js'; import { Messages } from '../../../../exceptions/text/messages.js'; -import { CedarPermissionsService } from '../../../cedar-authorization/cedar-permissions.service.js'; import { TableLogsService } from '../../../table-logs/table-logs.service.js'; import { TableActionActivationService } from '../../table-actions-module/table-action-activation.service.js'; import { ActivateEventActionsDS } from '../application/data-structures/activate-rule-actions.ds.js'; @@ -22,7 +21,6 @@ export class ActivateActionsInEventUseCase protected _dbContext: IGlobalDatabaseContext, private tableLogsService: TableLogsService, private tableActionActivationService: TableActionActivationService, - private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -46,10 +44,6 @@ export class ActivateActionsInEventUseCase ); } const tableName = foundActionsWithCustomEvents[0].action_rule.table_name; - const canUserReadTable = await this.cedarPermissions.checkTableRead(userId, connectionId, tableName, masterPwd); - if (!canUserReadTable) { - throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS); - } const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, diff --git a/backend/src/guards/action-event-trigger.guard.ts b/backend/src/guards/action-event-trigger.guard.ts new file mode 100644 index 000000000..4a5ae1c60 --- /dev/null +++ b/backend/src/guards/action-event-trigger.guard.ts @@ -0,0 +1,59 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + ForbiddenException, + Inject, + Injectable, +} from '@nestjs/common'; +import { IRequestWithCognitoInfo } from '../authorization/cognito-decoded.interface.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 ActionEventTriggerGuard implements CanActivate { + constructor( + private readonly cedarAuthService: CedarAuthorizationService, + @Inject(BaseType.GLOBAL_DB_CONTEXT) + private readonly globalDbContext: IGlobalDatabaseContext, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request: IRequestWithCognitoInfo = context.switchToHttp().getRequest(); + const cognitoUserName = request.decoded.sub; + const connectionId: string = request.params?.connectionId; + const eventId: string = request.params?.eventId; + + if (!connectionId || (!validateUuidByRegex(connectionId) && !ValidationHelper.isValidNanoId(connectionId))) { + throw new BadRequestException(Messages.CONNECTION_ID_MISSING); + } + if (!eventId || !validateUuidByRegex(eventId)) { + throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS); + } + + const actionEvent = await this.globalDbContext.actionEventsRepository.findEventByIdInConnection( + eventId, + connectionId, + ); + if (!actionEvent || !actionEvent.action_rule?.table_name) { + throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS); + } + + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.ActionEventTrigger, + connectionId, + tableName: actionEvent.action_rule.table_name, + actionEventId: eventId, + }); + if (!allowed) { + throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS); + } + return true; + } +} diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts index 5d4dc9835..d80dac00c 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-cassandra.e2e.test.ts @@ -110,7 +110,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts index 01bea55cd..3dcc81792 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-ibmdb2-e2e.test.ts @@ -110,7 +110,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts index 8c749b809..8dcbbdcde 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts @@ -109,7 +109,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-e2e.test.ts index 87f179c4b..cb7cf27db 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-e2e.test.ts @@ -105,7 +105,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-schema-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-schema-e2e.test.ts index f9f59b1ea..e508aadda 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-schema-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mssql-schema-e2e.test.ts @@ -106,7 +106,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts index 3de807f4e..333fb10a8 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts @@ -105,7 +105,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-e2e.test.ts index dd9465988..61f56c564 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-e2e.test.ts @@ -107,7 +107,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-schema-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-schema-e2e.test.ts index fad166d78..fd329633a 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-schema-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-oracledb-schema-e2e.test.ts @@ -107,7 +107,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-encrypted-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-encrypted-e2e.test.ts index d6f031567..7168dfa5d 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-encrypted-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-encrypted-e2e.test.ts @@ -107,7 +107,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-schema-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-schema-e2e.test.ts index 3033efbbc..a5b9da456 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-schema-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-postgres-schema-e2e.test.ts @@ -105,7 +105,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts index db8fec11f..c1a18d664 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts @@ -109,7 +109,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/action-rules-e2e.test.ts b/backend/test/ava-tests/saas-tests/action-rules-e2e.test.ts index f75dc3180..fbc259558 100644 --- a/backend/test/ava-tests/saas-tests/action-rules-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/action-rules-e2e.test.ts @@ -20,16 +20,21 @@ import { } from '../../../src/entities/table-actions/table-action-rules-module/application/dto/found-action-rules-with-actions-and-events.dto.js'; import { FoundTableActionRulesRoDTO } from '../../../src/entities/table-actions/table-action-rules-module/application/dto/found-table-action-rules.ro.dto.js'; import { UpdateTableActionRuleBodyDTO } from '../../../src/entities/table-actions/table-action-rules-module/application/dto/update-action-rule-with-actions-and-events.dto.js'; +import { AccessLevelEnum } from '../../../src/enums/access-level.enum.js'; import { TableActionEventEnum } from '../../../src/enums/table-action-event-enum.js'; import { TableActionMethodEnum } from '../../../src/enums/table-action-method-enum.js'; import { TableActionTypeEnum } from '../../../src/enums/table-action-type.enum.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 { DatabaseModule } from '../../../src/shared/database/database.module.js'; import { DatabaseService } from '../../../src/shared/database/database.service.js'; import { MockFactory } from '../../mock.factory.js'; -import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js'; +import { + inviteUserInCompanyAndAcceptInvitation, + registerUserAndReturnUserInfo, +} from '../../utils/register-user-and-return-user-info.js'; import { TestUtils } from '../../utils/test.utils.js'; const mockFactory = new MockFactory(); @@ -1416,3 +1421,359 @@ test.serial(`${currentTest} should log custom action event title in operation_cu scope.done(); }); + +async function bootstrapTriggerPermissionFixture(): Promise<{ + ownerToken: string; + inviteeToken: string; + connectionId: string; + eventId: string; + ruleId: string; + groupId: string; + fakeUrl: string; +}> { + await resetPostgresTestDB(); + const owner = await registerUserAndReturnUserInfo(app); + const invitee = await inviteUserInCompanyAndAcceptInvitation(owner.token, 'USER', app, undefined); + + const createConnectionResult = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('Cookie', owner.token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const connection = JSON.parse(createConnectionResult.text); + t_is201(createConnectionResult.status); + + const fakeUrl = 'http://www.example.com'; + const ruleDto: CreateTableActionRuleBodyDTO = { + title: 'Trigger permission rule', + table_name: testTableName, + events: [ + { + type: TableActionTypeEnum.single, + event: TableActionEventEnum.CUSTOM, + title: 'Permission test event', + icon: 'test-icon', + require_confirmation: false, + }, + ], + table_actions: [ + { + url: fakeUrl, + method: TableActionMethodEnum.URL, + slack_url: undefined, + emails: [], + }, + ], + }; + const ruleResult = await request(app.getHttpServer()) + .post(`/action/rule/${connection.id}`) + .send(ruleDto) + .set('Cookie', owner.token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const rule: FoundActionRulesWithActionsAndEventsDTO = JSON.parse(ruleResult.text); + t_is201(ruleResult.status); + + const newGroup = new MockFactory().generateCreateGroupDto1(); + const createGroupResult = await request(app.getHttpServer()) + .post(`/connection/group/${connection.id}`) + .send(newGroup) + .set('Cookie', owner.token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const groupId = JSON.parse(createGroupResult.text).id; + + await request(app.getHttpServer()) + .put('/group/user') + .set('Cookie', owner.token) + .send({ groupId, email: invitee.email }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + return { + ownerToken: owner.token, + inviteeToken: invitee.token, + connectionId: connection.id, + eventId: rule.events[0].id, + ruleId: rule.id, + groupId, + fakeUrl, + }; +} + +function t_is201(status: number): void { + if (status > 201) { + throw new Error(`Expected 2xx, got ${status}`); + } +} + +async function setTablePermissions( + ownerToken: string, + connectionId: string, + groupId: string, + tableName: string, + tableAccessLevel: Record, + actionEvents: Array<{ eventId: string; tableName: string; accessLevel: { trigger: boolean } }> = [], +): Promise { + const permissions = { + connection: { connectionId, accessLevel: AccessLevelEnum.none }, + group: { groupId, accessLevel: AccessLevelEnum.none }, + tables: [{ tableName, accessLevel: tableAccessLevel }], + actionEvents, + }; + const res = await request(app.getHttpServer()) + .put(`/permissions/${groupId}?connectionId=${connectionId}`) + .send({ permissions }) + .set('Cookie', ownerToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + return res.status; +} + +currentTest = 'POST /event/actions/activate/:eventId/:connectionId - trigger permission guard'; + +test.serial(`${currentTest} owner can still trigger custom actions`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + const scope = nock(fx.fakeUrl).post('/').reply(201, { status: 201 }); + + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.ownerToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 201); + const ro: ActivatedTableActionsDTO = JSON.parse(res.text); + t.is(Object.hasOwn(ro, 'activationResults'), true); + scope.done(); +}); + +test.serial(`${currentTest} invitee with triggerCustomAction=true can trigger`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + const setStatus = await setTablePermissions(fx.ownerToken, fx.connectionId, fx.groupId, testTableName, { + visibility: true, + readonly: true, + add: false, + delete: false, + edit: false, + triggerCustomAction: true, + }); + t.true(setStatus < 300, `set permissions failed: ${setStatus}`); + + const scope = nock(fx.fakeUrl).post('/').reply(201, { status: 201 }); + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 201); + const ro: ActivatedTableActionsDTO = JSON.parse(res.text); + t.is(Object.hasOwn(ro, 'activationResults'), true); + scope.done(); +}); + +test.serial(`${currentTest} invitee with triggerCustomAction=false but other perms is denied`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + const setStatus = await setTablePermissions(fx.ownerToken, fx.connectionId, fx.groupId, testTableName, { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + triggerCustomAction: false, + }); + t.true(setStatus < 300, `set permissions failed: ${setStatus}`); + + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 403); + t.is(JSON.parse(res.text).message, Messages.DONT_HAVE_PERMISSIONS); +}); + +test.serial(`${currentTest} invitee with no permissions on the table is denied`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 403); + t.is(JSON.parse(res.text).message, Messages.DONT_HAVE_PERMISSIONS); +}); + +test.serial(`${currentTest} eventId from a different connection is rejected`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + const ownerSecond = await registerUserAndReturnUserInfo(app); + const createConn2 = await request(app.getHttpServer()) + .post('/connection') + .send(newConnection) + .set('Cookie', ownerSecond.token) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const conn2 = JSON.parse(createConn2.text); + t.is(createConn2.status, 201); + + // Use the first event id against the SECOND connection id — guard's lookup should miss + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${conn2.id}`) + .set('Cookie', ownerSecond.token) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 403); + t.is(JSON.parse(res.text).message, Messages.DONT_HAVE_PERMISSIONS); +}); + +test.serial(`${currentTest} per-event grant allows triggering that specific event`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + const setStatus = await setTablePermissions( + fx.ownerToken, + fx.connectionId, + fx.groupId, + testTableName, + { + visibility: true, + readonly: true, + add: false, + delete: false, + edit: false, + triggerCustomAction: false, + }, + [{ eventId: fx.eventId, tableName: testTableName, accessLevel: { trigger: true } }], + ); + t.true(setStatus < 300, `set permissions failed: ${setStatus}`); + + const scope = nock(fx.fakeUrl).post('/').reply(201, { status: 201 }); + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 201); + scope.done(); +}); + +test.serial(`${currentTest} per-event grant does not leak to other events on same table`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + + // Add a second custom event on the same rule by updating it + const fetchRule = await request(app.getHttpServer()) + .get(`/action/rule/${fx.ruleId}/${fx.connectionId}`) + .set('Cookie', fx.ownerToken); + const existingRule: FoundActionRulesWithActionsAndEventsDTO = JSON.parse(fetchRule.text); + + const updateDto: UpdateTableActionRuleBodyDTO = { + title: existingRule.title, + table_name: existingRule.table_name, + events: [ + { + id: existingRule.events[0].id, + type: existingRule.events[0].type, + event: existingRule.events[0].event, + title: existingRule.events[0].title, + icon: existingRule.events[0].icon, + require_confirmation: existingRule.events[0].require_confirmation, + }, + { + type: TableActionTypeEnum.single, + event: TableActionEventEnum.CUSTOM, + title: 'Second event', + icon: 'second-icon', + require_confirmation: false, + }, + ], + table_actions: existingRule.table_actions.map((a) => ({ + id: a.id, + url: a.url ?? undefined, + method: a.method, + slack_url: a.slack_url ?? undefined, + emails: a.emails ?? undefined, + })), + }; + const updateRes = await request(app.getHttpServer()) + .put(`/action/rule/${fx.ruleId}/${fx.connectionId}`) + .send(updateDto) + .set('Cookie', fx.ownerToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(updateRes.status, 200); + const updatedRule: FoundActionRulesWithActionsAndEventsDTO = JSON.parse(updateRes.text); + const secondEvent = updatedRule.events.find((e) => e.id !== fx.eventId); + t.truthy(secondEvent); + + // Grant trigger only on the FIRST event + const setStatus = await setTablePermissions( + fx.ownerToken, + fx.connectionId, + fx.groupId, + testTableName, + { + visibility: true, + readonly: true, + add: false, + delete: false, + edit: false, + triggerCustomAction: false, + }, + [{ eventId: fx.eventId, tableName: testTableName, accessLevel: { trigger: true } }], + ); + t.true(setStatus < 300, `set permissions failed: ${setStatus}`); + + const scope = nock(fx.fakeUrl).post('/').reply(201, { status: 201 }); + const firstRes = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(firstRes.status, 201); + scope.done(); + + const secondRes = await request(app.getHttpServer()) + .post(`/event/actions/activate/${secondEvent.id}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(secondRes.status, 403); + t.is(JSON.parse(secondRes.text).message, Messages.DONT_HAVE_PERMISSIONS); +}); + +test.serial(`${currentTest} blanket trigger works without table:read`, async (t) => { + const fx = await bootstrapTriggerPermissionFixture(); + const setStatus = await setTablePermissions(fx.ownerToken, fx.connectionId, fx.groupId, testTableName, { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + triggerCustomAction: true, + }); + t.true(setStatus < 300, `set permissions failed: ${setStatus}`); + + const scope = nock(fx.fakeUrl).post('/').reply(201, { status: 201 }); + const res = await request(app.getHttpServer()) + .post(`/event/actions/activate/${fx.eventId}/${fx.connectionId}`) + .set('Cookie', fx.inviteeToken) + .send([{ id: 1 }]) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(res.status, 201); + scope.done(); +}); diff --git a/backend/test/ava-tests/saas-tests/api-key-e2e.test.ts b/backend/test/ava-tests/saas-tests/api-key-e2e.test.ts index 407c900a1..aeb835280 100644 --- a/backend/test/ava-tests/saas-tests/api-key-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/api-key-e2e.test.ts @@ -106,7 +106,7 @@ test.serial(`${currentTest} should return created api key for this user`, async t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/connection-properties-e2e.test.ts b/backend/test/ava-tests/saas-tests/connection-properties-e2e.test.ts index 370c994cf..e13f6d6aa 100644 --- a/backend/test/ava-tests/saas-tests/connection-properties-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/connection-properties-e2e.test.ts @@ -337,7 +337,7 @@ test.serial( t.is(Object.hasOwn(findTablesRO.tables[testTableIndex], 'table'), true); t.is(Object.hasOwn(findTablesRO.tables[testTableIndex], 'permissions'), true); t.is(typeof findTablesRO.tables[testTableIndex].permissions, 'object'); - t.is(Object.keys(findTablesRO.tables[testTableIndex].permissions).length, 6); + t.is(Object.keys(findTablesRO.tables[testTableIndex].permissions).length, 7); t.is(findTablesRO.tables[testTableIndex].table, testTableName); t.is(findTablesRO.tables[testTableIndex].permissions.visibility, true); t.is(findTablesRO.tables[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-cassandra-agent.e2e.test.ts b/backend/test/ava-tests/saas-tests/table-cassandra-agent.e2e.test.ts index 1c8d3fe3e..4203c2802 100644 --- a/backend/test/ava-tests/saas-tests/table-cassandra-agent.e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-cassandra-agent.e2e.test.ts @@ -119,7 +119,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-cassandra.e2e.test.ts b/backend/test/ava-tests/saas-tests/table-cassandra.e2e.test.ts index 4b8474b2e..890d22d53 100644 --- a/backend/test/ava-tests/saas-tests/table-cassandra.e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-cassandra.e2e.test.ts @@ -103,7 +103,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-clickhouse-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-clickhouse-agent-e2e.test.ts index 5d4b15c21..01005f2c2 100644 --- a/backend/test/ava-tests/saas-tests/table-clickhouse-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-clickhouse-agent-e2e.test.ts @@ -117,7 +117,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-clickhouse-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-clickhouse-e2e.test.ts index b744cbc6c..76a40804f 100644 --- a/backend/test/ava-tests/saas-tests/table-clickhouse-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-clickhouse-e2e.test.ts @@ -104,7 +104,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts index c06d33c43..e3e27442a 100644 --- a/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts @@ -97,7 +97,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts index d090bf5ce..16489e825 100644 --- a/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts @@ -102,7 +102,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-ibmdb2-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-ibmdb2-agent-e2e.test.ts index 09967a4d9..cccb205d1 100644 --- a/backend/test/ava-tests/saas-tests/table-ibmdb2-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-ibmdb2-agent-e2e.test.ts @@ -124,7 +124,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-ibmdb2-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-ibmdb2-e2e.test.ts index 7836817cb..c984340cf 100644 --- a/backend/test/ava-tests/saas-tests/table-ibmdb2-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-ibmdb2-e2e.test.ts @@ -103,7 +103,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts index 97e46cd4d..512163b8c 100644 --- a/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts @@ -116,7 +116,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts index 17340c60e..94d878da4 100644 --- a/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts @@ -102,7 +102,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mssql-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mssql-agent-e2e.test.ts index 59687877d..5e864ab46 100644 --- a/backend/test/ava-tests/saas-tests/table-mssql-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mssql-agent-e2e.test.ts @@ -159,7 +159,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mssql-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mssql-e2e.test.ts index 8ddf1c60f..735c9aea9 100644 --- a/backend/test/ava-tests/saas-tests/table-mssql-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mssql-e2e.test.ts @@ -106,7 +106,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mssql-schema-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mssql-schema-e2e.test.ts index c08a100ab..99c58aadf 100644 --- a/backend/test/ava-tests/saas-tests/table-mssql-schema-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mssql-schema-e2e.test.ts @@ -98,7 +98,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mysql-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mysql-agent-e2e.test.ts index 6646b999e..2979b992f 100644 --- a/backend/test/ava-tests/saas-tests/table-mysql-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mysql-agent-e2e.test.ts @@ -162,7 +162,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-mysql-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mysql-e2e.test.ts index 07172da42..5cfc91932 100644 --- a/backend/test/ava-tests/saas-tests/table-mysql-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mysql-e2e.test.ts @@ -106,7 +106,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-oracle-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-oracle-agent-e2e.test.ts index 809cbacee..4c9192c6b 100644 --- a/backend/test/ava-tests/saas-tests/table-oracle-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-oracle-agent-e2e.test.ts @@ -162,7 +162,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts index f4f1dd383..543d97580 100644 --- a/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts @@ -107,7 +107,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-oracledb-schema-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-oracledb-schema-e2e.test.ts index dbcde1a33..d136f466a 100644 --- a/backend/test/ava-tests/saas-tests/table-oracledb-schema-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-oracledb-schema-e2e.test.ts @@ -101,7 +101,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-postgres-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-postgres-agent-e2e.test.ts index 9eae9e0f8..7b7a0b4f7 100644 --- a/backend/test/ava-tests/saas-tests/table-postgres-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-postgres-agent-e2e.test.ts @@ -164,7 +164,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts index 066c14e92..8f891e566 100644 --- a/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts @@ -105,7 +105,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-postgres-encrypted-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-postgres-encrypted-e2e.test.ts index 2c0ecc4e9..534f90e91 100644 --- a/backend/test/ava-tests/saas-tests/table-postgres-encrypted-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-postgres-encrypted-e2e.test.ts @@ -101,7 +101,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-postgres-schema-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-postgres-schema-e2e.test.ts index 156574673..8c177c3f3 100644 --- a/backend/test/ava-tests/saas-tests/table-postgres-schema-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-postgres-schema-e2e.test.ts @@ -99,7 +99,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts index 050a50533..bf6c06462 100644 --- a/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts @@ -118,7 +118,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false); diff --git a/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts index a72d77dde..27e93ee9f 100644 --- a/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts @@ -102,7 +102,7 @@ test.serial(`${currentTest} should return list of tables in connection`, async ( t.is(Object.hasOwn(getTablesRO[testTableIndex], 'table'), true); t.is(Object.hasOwn(getTablesRO[testTableIndex], 'permissions'), true); t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); - t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 6); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 7); t.is(getTablesRO[testTableIndex].table, testTableName); t.is(getTablesRO[testTableIndex].permissions.visibility, true); t.is(getTablesRO[testTableIndex].permissions.readonly, false);