From 20f69cd4c59c9dfc51c02a2f66a9cebafb1dd4d2 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 19 Mar 2026 10:03:08 +0000 Subject: [PATCH 1/3] ceadr migration --- .../global-database-context.interface.ts | 2 - .../application/global-database-context.ts | 10 - .../cedar-authorization.module.ts | 4 +- .../cedar-authorization.service.interface.ts | 1 - .../cedar-authorization.service.ts | 6 - .../cedar-permissions.service.ts | 355 ++++++++++++++++++ .../find-all-connections.use.case.ts | 4 +- .../use-cases/find-one-connection.use.case.ts | 6 +- .../get-user-groups-in-connection.use.case.ts | 4 +- ...ssions-for-group-in-connection.use.case.ts | 8 +- .../find-all-user-groups.use.case.ts | 4 +- .../activate-actions-in-rule.use.case.ts | 4 +- ...d-table-categories-with-tables.use.case.ts | 79 +--- .../use-cases/export-logs-as-csv.use.case.ts | 4 +- .../use-cases/find-logs.use.case.ts | 4 +- .../use-cases/add-row-in-table.use.case.ts | 6 +- .../find-tables-in-connection-v2.use.case.ts | 82 +--- .../find-tables-in-connection.use.case.ts | 83 +--- .../get-row-by-primary-key.use.case.ts | 8 +- .../use-cases/get-table-rows.use.case.ts | 6 +- .../use-cases/get-table-structure.use.case.ts | 4 +- .../use-cases/update-row-in-table.use.case.ts | 6 +- backend/src/guards/connection-edit.guard.ts | 53 +-- backend/src/guards/connection-read.guard.ts | 53 +-- backend/src/guards/dashboard-create.guard.ts | 52 +-- backend/src/guards/dashboard-edit.guard.ts | 55 +-- backend/src/guards/dashboard-read.guard.ts | 55 +-- backend/src/guards/group-edit.guard.ts | 51 +-- backend/src/guards/group-read.guard.ts | 50 +-- backend/src/guards/table-add.guard.ts | 57 +-- backend/src/guards/table-delete.guard.ts | 56 +-- backend/src/guards/table-edit.guard.ts | 57 +-- backend/src/guards/table-read.guard.ts | 57 +-- 33 files changed, 533 insertions(+), 753 deletions(-) create mode 100644 backend/src/entities/cedar-authorization/cedar-permissions.service.ts diff --git a/backend/src/common/application/global-database-context.interface.ts b/backend/src/common/application/global-database-context.interface.ts index db6bab219..9d12fde0c 100644 --- a/backend/src/common/application/global-database-context.interface.ts +++ b/backend/src/common/application/global-database-context.interface.ts @@ -51,7 +51,6 @@ import { IUserInvitationRepository } from '../../entities/user/user-invitation/r import { IPasswordResetRepository } from '../../entities/user/user-password/repository/password-reset-repository.interface.js'; import { IUserSessionSettings } from '../../entities/user/user-session-settings/reposiotory/user-session-settings-repository.interface.js'; import { UserSessionSettingsEntity } from '../../entities/user/user-session-settings/user-session-settings.entity.js'; -import { IUserAccessRepository } from '../../entities/user-access/repository/user-access.repository.interface.js'; import { IUserActionRepository } from '../../entities/user-actions/repository/user-action.repository.interface.js'; import { IUserSecretRepository } from '../../entities/user-secret/repository/user-secret-repository.interface.js'; import { UserSecretEntity } from '../../entities/user-secret/user-secret.entity.js'; @@ -73,7 +72,6 @@ export interface IGlobalDatabaseContext extends IDatabaseContext { groupRepository: IGroupRepository; permissionRepository: IPermissionRepository; tableSettingsRepository: Repository & ITableSettingsRepository; - userAccessRepository: IUserAccessRepository; agentRepository: IAgentRepository; emailVerificationRepository: IEmailVerificationRepository; passwordResetRepository: IPasswordResetRepository; diff --git a/backend/src/common/application/global-database-context.ts b/backend/src/common/application/global-database-context.ts index fdbce430b..5899e73b3 100644 --- a/backend/src/common/application/global-database-context.ts +++ b/backend/src/common/application/global-database-context.ts @@ -92,8 +92,6 @@ import { userPasswordResetCustomRepositoryExtension } from '../../entities/user/ import { userSessionSettingsRepositoryExtension } from '../../entities/user/user-session-settings/reposiotory/user-session-settings-custom-repository.extension.js'; import { IUserSessionSettings } from '../../entities/user/user-session-settings/reposiotory/user-session-settings-repository.interface.js'; import { UserSessionSettingsEntity } from '../../entities/user/user-session-settings/user-session-settings.entity.js'; -import { IUserAccessRepository } from '../../entities/user-access/repository/user-access.repository.interface.js'; -import { userAccessCustomReposiotoryExtension } from '../../entities/user-access/repository/user-access-custom-repository-extension.js'; import { IUserActionRepository } from '../../entities/user-actions/repository/user-action.repository.interface.js'; import { userActionCustomRepositoryExtension } from '../../entities/user-actions/repository/user-action-custom-repository-extension.js'; import { UserActionEntity } from '../../entities/user-actions/user-action.entity.js'; @@ -127,7 +125,6 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { private _groupRepository: IGroupRepository; private _permissionRepository: IPermissionRepository; private _tableSettingsRepository: Repository & ITableSettingsRepository; - private _userAccessRepository: IUserAccessRepository; private _agentRepository: IAgentRepository; private _emailVerificationRepository: IEmailVerificationRepository; private _passwordResetRepository: IPasswordResetRepository; @@ -184,9 +181,6 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { this._tableSettingsRepository = this.appDataSource .getRepository(TableSettingsEntity) .extend(tableSettingsCustomRepositoryExtension); - this._userAccessRepository = this.appDataSource - .getRepository(PermissionEntity) - .extend(userAccessCustomReposiotoryExtension); this._agentRepository = this.appDataSource.getRepository(AgentEntity).extend(customAgentRepositoryExtension); this._emailVerificationRepository = this.appDataSource .getRepository(EmailVerificationEntity) @@ -299,10 +293,6 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { return this._tableSettingsRepository; } - public get userAccessRepository(): IUserAccessRepository { - return this._userAccessRepository; - } - public get agentRepository(): IAgentRepository { return this._agentRepository; } diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.module.ts b/backend/src/entities/cedar-authorization/cedar-authorization.module.ts index 4bc29c781..220b44c48 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.module.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.module.ts @@ -7,6 +7,7 @@ import { LogOutEntity } from '../log-out/log-out.entity.js'; import { UserEntity } from '../user/user.entity.js'; import { CedarAuthorizationController } from './cedar-authorization.controller.js'; import { CedarAuthorizationService } from './cedar-authorization.service.js'; +import { CedarPermissionsService } from './cedar-permissions.service.js'; @Global() @Module({ @@ -17,9 +18,10 @@ import { CedarAuthorizationService } from './cedar-authorization.service.js'; useClass: GlobalDatabaseContext, }, CedarAuthorizationService, + CedarPermissionsService, ], controllers: [CedarAuthorizationController], - exports: [CedarAuthorizationService], + exports: [CedarAuthorizationService, CedarPermissionsService], }) export class CedarAuthorizationModule implements NestModule { public configure(consumer: MiddlewareConsumer): void { diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts index d98ecaea5..fc2117dd3 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.interface.ts @@ -2,7 +2,6 @@ import { IComplexPermission } from '../permission/permission.interface.js'; import { CedarValidationRequest } from './cedar-action-map.js'; export interface ICedarAuthorizationService { - isFeatureEnabled(): boolean; validate(request: CedarValidationRequest): Promise; invalidatePolicyCacheForConnection(connectionId: string): void; getSchema(): Record; diff --git a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts index d357574c3..a8f6303a4 100644 --- a/backend/src/entities/cedar-authorization/cedar-authorization.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-authorization.service.ts @@ -31,16 +31,10 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On ) {} async onModuleInit(): Promise { - if (!this.isFeatureEnabled()) return; this.schema = CEDAR_SCHEMA as Record; this.logger.log('Cedar authorization service initialized'); } - isFeatureEnabled(): boolean { - // return process.env.CEDAR_AUTHORIZATION_ENABLED === 'true'; - return true; - } - async validate(request: CedarValidationRequest): Promise { const { userId, action, groupId, tableName, dashboardId } = request; let { connectionId } = request; diff --git a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts new file mode 100644 index 000000000..885ff1cac --- /dev/null +++ b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts @@ -0,0 +1,355 @@ +import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { AccessLevelEnum } from '../../enums/index.js'; +import { Messages } from '../../exceptions/text/messages.js'; +import { Cacher } from '../../helpers/cache/cacher.js'; +import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js'; +import { BaseType } from '../../common/data-injection.tokens.js'; +import { GroupEntity } from '../group/group.entity.js'; +import { ITablePermissionData } from '../permission/permission.interface.js'; +import { + CedarAction, + CedarResourceType, + CEDAR_ACTION_TYPE, + CEDAR_USER_TYPE, +} from './cedar-action-map.js'; +import { buildCedarEntities } from './cedar-entity-builder.js'; +import { CEDAR_SCHEMA } from './cedar-schema.js'; +import * as cedarWasm from '@cedar-policy/cedar-wasm/nodejs'; +import { IUserAccessRepository } from '../user-access/repository/user-access.repository.interface.js'; + +@Injectable() +export class CedarPermissionsService implements IUserAccessRepository { + private readonly schema: Record = CEDAR_SCHEMA as Record; + + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + private readonly globalDbContext: IGlobalDatabaseContext, + ) {} + + async getUserConnectionAccessLevel(cognitoUserName: string, connectionId: string): Promise { + await this.assertUserNotSuspended(cognitoUserName); + const editAllowed = await this.evaluateAction(cognitoUserName, connectionId, CedarAction.ConnectionEdit); + if (editAllowed) return AccessLevelEnum.edit; + const readAllowed = await this.evaluateAction(cognitoUserName, connectionId, CedarAction.ConnectionRead); + if (readAllowed) return AccessLevelEnum.readonly; + return AccessLevelEnum.none; + } + + async checkUserConnectionRead(cognitoUserName: string, connectionId: string): Promise { + const level = await this.getUserConnectionAccessLevel(cognitoUserName, connectionId); + return level === AccessLevelEnum.edit || level === AccessLevelEnum.readonly; + } + + async checkUserConnectionEdit(cognitoUserName: string, connectionId: string): Promise { + const level = await this.getUserConnectionAccessLevel(cognitoUserName, connectionId); + return level === AccessLevelEnum.edit; + } + + async getGroupAccessLevel(cognitoUserName: string, groupId: string): Promise { + await this.assertUserNotSuspended(cognitoUserName); + const connectionId = await this.getConnectionId(groupId); + const editAllowed = await this.evaluateAction(cognitoUserName, connectionId, CedarAction.GroupEdit, undefined, groupId); + if (editAllowed) return AccessLevelEnum.edit; + const readAllowed = await this.evaluateAction(cognitoUserName, connectionId, CedarAction.GroupRead, undefined, groupId); + if (readAllowed) return AccessLevelEnum.readonly; + return AccessLevelEnum.none; + } + + async checkUserGroupRead(cognitoUserName: string, groupId: string): Promise { + const level = await this.getGroupAccessLevel(cognitoUserName, groupId); + return level === AccessLevelEnum.edit || level === AccessLevelEnum.readonly; + } + + async checkUserGroupEdit(cognitoUserName: string, groupId: string): Promise { + const level = await this.getGroupAccessLevel(cognitoUserName, groupId); + return level === AccessLevelEnum.edit; + } + + async getUserTablePermissions( + cognitoUserName: string, + connectionId: string, + tableName: string, + _masterPwd: string, + ): Promise { + await this.assertUserNotSuspended(cognitoUserName); + const results = await this.evaluateBatch(cognitoUserName, connectionId, [ + CedarAction.TableRead, + CedarAction.TableAdd, + CedarAction.TableEdit, + CedarAction.TableDelete, + ], tableName); + + const canRead = results.get(CedarAction.TableRead); + const canAdd = results.get(CedarAction.TableAdd); + const canEdit = results.get(CedarAction.TableEdit); + const canDelete = results.get(CedarAction.TableDelete); + + return { + tableName, + accessLevel: { + visibility: canRead || canAdd || canEdit || canDelete, + readonly: canRead && !canAdd && !canEdit && !canDelete, + add: canAdd, + delete: canDelete, + edit: canEdit, + }, + }; + } + + async getUserPermissionsForAvailableTables( + cognitoUserName: string, + connectionId: string, + tableNames: Array, + ): Promise> { + await this.assertUserNotSuspended(cognitoUserName); + + const userGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnection(connectionId, cognitoUserName); + if (userGroups.length === 0) { + return []; + } + const groupPolicies = await this.loadPoliciesPerGroup(connectionId, userGroups); + if (groupPolicies.length === 0) { + return []; + } + + const actions = [CedarAction.TableRead, CedarAction.TableAdd, CedarAction.TableEdit, CedarAction.TableDelete]; + const result: Array = []; + + for (const tableName of tableNames) { + const entities = buildCedarEntities(cognitoUserName, userGroups, connectionId, tableName); + const actionResults = new Map(); + + for (const action of actions) { + const resourceId = `${connectionId}/${tableName}`; + const allowed = this.evaluatePolicies( + cognitoUserName, action, CedarResourceType.Table, resourceId, groupPolicies, entities, + ); + actionResults.set(action, allowed); + } + + const canRead = actionResults.get(CedarAction.TableRead); + const canAdd = actionResults.get(CedarAction.TableAdd); + const canEdit = actionResults.get(CedarAction.TableEdit); + const canDelete = actionResults.get(CedarAction.TableDelete); + const visibility = canRead || canAdd || canEdit || canDelete; + + if (visibility) { + result.push({ + tableName, + accessLevel: { + visibility: true, + readonly: canRead && !canAdd && !canEdit && !canDelete, + add: canAdd, + delete: canDelete, + edit: canEdit, + }, + }); + } + } + + return result; + } + + async checkTableRead( + cognitoUserName: string, + connectionId: string, + tableName: string, + _masterPwd: string, + ): Promise { + await this.assertUserNotSuspended(cognitoUserName); + return this.evaluateAction(cognitoUserName, connectionId, CedarAction.TableRead, tableName); + } + + async checkTableAdd( + cognitoUserName: string, + connectionId: string, + tableName: string, + _masterPwd: string, + ): Promise { + await this.assertUserNotSuspended(cognitoUserName); + return this.evaluateAction(cognitoUserName, connectionId, CedarAction.TableAdd, tableName); + } + + async checkTableDelete( + cognitoUserName: string, + connectionId: string, + tableName: string, + _masterPwd: string, + ): Promise { + await this.assertUserNotSuspended(cognitoUserName); + return this.evaluateAction(cognitoUserName, connectionId, CedarAction.TableDelete, tableName); + } + + async checkTableEdit( + cognitoUserName: string, + connectionId: string, + tableName: string, + _masterPwd: string, + ): Promise { + await this.assertUserNotSuspended(cognitoUserName); + return this.evaluateAction(cognitoUserName, connectionId, CedarAction.TableEdit, tableName); + } + + async getConnectionId(groupId: string): Promise { + const group = await this.globalDbContext.groupRepository.findGroupByIdWithConnectionAndUsers(groupId); + if (!group?.connection?.id) { + throw new HttpException({ message: Messages.CONNECTION_NOT_FOUND }, HttpStatus.BAD_REQUEST); + } + return group.connection.id; + } + + async improvedCheckTableRead(userId: string, connectionId: string, tableName: string, _masterPwd?: string): Promise { + const cachedReadPermission: boolean | null = Cacher.getUserTableReadPermissionCache( + userId, + connectionId, + tableName, + ); + if (cachedReadPermission !== null) { + return cachedReadPermission; + } + + const canRead = await this.evaluateAction(userId, connectionId, CedarAction.TableRead, tableName); + Cacher.setUserTableReadPermissionCache(userId, connectionId, tableName, canRead); + return canRead; + } + + private async evaluateBatch( + userId: string, + connectionId: string, + actions: CedarAction[], + tableName?: string, + groupId?: string, + ): Promise> { + const userGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnection(connectionId, userId); + if (userGroups.length === 0) { + return new Map(actions.map(a => [a, false])); + } + + const groupPolicies = await this.loadPoliciesPerGroup(connectionId, userGroups); + if (groupPolicies.length === 0) { + return new Map(actions.map(a => [a, false])); + } + + const dashboardId = undefined; + const entities = buildCedarEntities(userId, userGroups, connectionId, tableName, dashboardId); + + const results = new Map(); + for (const action of actions) { + 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; + resourceId = groupId; + break; + case 'table': + resourceType = CedarResourceType.Table; + resourceId = `${connectionId}/${tableName}`; + break; + default: + results.set(action, false); + continue; + } + + results.set(action, this.evaluatePolicies(userId, action, resourceType, resourceId, groupPolicies, entities)); + } + + return results; + } + + private evaluatePolicies( + userId: string, + action: CedarAction, + resourceType: CedarResourceType, + resourceId: string, + policies: string[], + entities: ReturnType, + ): boolean { + for (const policy of policies) { + 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: policy }, + entities: entities, + schema: this.schema, + }; + + const result = cedarWasm.isAuthorized(call as Parameters[0]); + if (result.type === 'success' && result.response.decision === 'allow') { + return true; + } + } + return false; + } + + private async evaluateAction( + userId: string, + connectionId: string, + action: CedarAction, + tableName?: string, + groupId?: string, + ): Promise { + const userGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnection(connectionId, userId); + if (userGroups.length === 0) return false; + + const groupPolicies = await this.loadPoliciesPerGroup(connectionId, userGroups); + if (groupPolicies.length === 0) return false; + + const entities = buildCedarEntities(userId, userGroups, connectionId, tableName); + + 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; + resourceId = groupId; + break; + case 'table': + resourceType = CedarResourceType.Table; + resourceId = `${connectionId}/${tableName}`; + break; + default: + return false; + } + + return this.evaluatePolicies(userId, action, resourceType, resourceId, groupPolicies, entities); + } + + private async loadPoliciesPerGroup(connectionId: string, userGroups: Array): Promise { + const groups = await this.globalDbContext.groupRepository.findAllGroupsInConnection(connectionId); + const userGroupIdSet = new Set(userGroups.map((g) => g.id)); + return groups + .filter((g) => userGroupIdSet.has(g.id)) + .map((g) => g.cedarPolicy) + .filter(Boolean); + } + + private async assertUserNotSuspended(userId: string): Promise { + const user = await this.globalDbContext.userRepository.findOne({ + where: { id: userId }, + select: ['id', 'suspended'], + }); + if (user?.suspended) { + throw new HttpException( + { + message: Messages.ACCOUNT_SUSPENDED, + }, + HttpStatus.FORBIDDEN, + ); + } + } +} diff --git a/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts b/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts index e05edce7c..b1e5ad45e 100644 --- a/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts +++ b/backend/src/entities/connection/use-cases/find-all-connections.use.case.ts @@ -7,6 +7,7 @@ import { Messages } from '../../../exceptions/text/messages.js'; import { isSaaS } from '../../../helpers/app/is-saas.js'; import { Constants } from '../../../helpers/constants/constants.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { GroupEntity } from '../../group/group.entity.js'; import { PermissionEntity } from '../../permission/permission.entity.js'; import { CreateUserDs } from '../../user/application/data-structures/create-user.ds.js'; @@ -33,6 +34,7 @@ export class FindAllConnectionsUseCase @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, private amplitudeService: AmplitudeService, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -111,7 +113,7 @@ export class FindAllConnectionsUseCase const connectionsWithPermissions = await Promise.all( allFoundUserConnections.map(async (connection) => { - const accessLevel = await this._dbContext.userAccessRepository.getUserConnectionAccessLevel( + const accessLevel = await this.cedarPermissions.getUserConnectionAccessLevel( user.id, connection.id, ); diff --git a/backend/src/entities/connection/use-cases/find-one-connection.use.case.ts b/backend/src/entities/connection/use-cases/find-one-connection.use.case.ts index cee24c95d..3ce8dcced 100644 --- a/backend/src/entities/connection/use-cases/find-one-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/find-one-connection.use.case.ts @@ -7,6 +7,7 @@ import { AccessLevelEnum } from '../../../enums/index.js'; import { Messages } from '../../../exceptions/text/messages.js'; import { Constants } from '../../../helpers/constants/constants.js'; import { Encryptor } from '../../../helpers/encryption/encryptor.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { FoundConnectionPropertiesDs } from '../../connection-properties/application/data-structures/found-connection-properties.ds.js'; import { buildFoundConnectionPropertiesDs } from '../../connection-properties/utils/build-found-connection-properties-ds.js'; import { FindOneConnectionDs } from '../application/data-structures/find-one-connection.ds.js'; @@ -24,6 +25,7 @@ export class FindOneConnectionUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -33,7 +35,7 @@ export class FindOneConnectionUseCase if (!connection) { throw new BadRequestException(Messages.CONNECTION_NOT_FOUND); } - const accessLevel: AccessLevelEnum = await this._dbContext.userAccessRepository.getUserConnectionAccessLevel( + const accessLevel: AccessLevelEnum = await this.cedarPermissions.getUserConnectionAccessLevel( inputData.cognitoUserName, inputData.connectionId, ); @@ -120,7 +122,7 @@ export class FindOneConnectionUseCase } const groupsInConnection = await this._dbContext.groupRepository.findAllGroupsInConnection(connectionId); for (const group of groupsInConnection) { - const groupRead = await this._dbContext.userAccessRepository.checkUserGroupRead(cognitoUserName, group.id); + const groupRead = await this.cedarPermissions.checkUserGroupRead(cognitoUserName, group.id); if (groupRead) { return true; } diff --git a/backend/src/entities/connection/use-cases/get-user-groups-in-connection.use.case.ts b/backend/src/entities/connection/use-cases/get-user-groups-in-connection.use.case.ts index be076cd20..eb9f8ccfc 100644 --- a/backend/src/entities/connection/use-cases/get-user-groups-in-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/get-user-groups-in-connection.use.case.ts @@ -2,6 +2,7 @@ import { 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 { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { GroupEntity } from '../../group/group.entity.js'; import { GetGroupsInConnectionDs } from '../application/data-structures/get-groups-in-connection.ds.js'; import { FoundUserGroupsInConnectionDTO } from '../application/dto/found-user-groups-in-connection.dto.js'; @@ -16,6 +17,7 @@ export class GetUserGroupsInConnectionUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -27,7 +29,7 @@ export class GetUserGroupsInConnectionUseCase ); return await Promise.all( userGroups.map(async (group: GroupEntity) => { - const userAccessLevel = await this._dbContext.userAccessRepository.getGroupAccessLevel( + const userAccessLevel = await this.cedarPermissions.getGroupAccessLevel( inputData.cognitoUserName, group.id, ); diff --git a/backend/src/entities/connection/use-cases/get-user-permissions-for-group-in-connection.use.case.ts b/backend/src/entities/connection/use-cases/get-user-permissions-for-group-in-connection.use.case.ts index 286c5ddd5..41e8a2647 100644 --- a/backend/src/entities/connection/use-cases/get-user-permissions-for-group-in-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/get-user-permissions-for-group-in-connection.use.case.ts @@ -3,6 +3,7 @@ import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-acce 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 { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { TablePermissionDs } from '../../permission/application/data-structures/create-permissions.ds.js'; import { FoundPermissionsInConnectionDs } from '../application/data-structures/found-permissions-in-connection.ds.js'; import { GetPermissionsInConnectionDs } from '../application/data-structures/get-permissions-in-connection.ds.js'; @@ -16,17 +17,18 @@ export class GetUserPermissionsForGroupInConnectionUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } protected async implementation(inputData: GetPermissionsInConnectionDs): Promise { const { connectionId, groupId, cognitoUserName, masterPwd } = inputData; - const userConnectionAccessLevel = await this._dbContext.userAccessRepository.getUserConnectionAccessLevel( + const userConnectionAccessLevel = await this.cedarPermissions.getUserConnectionAccessLevel( cognitoUserName, connectionId, ); - const userGroupAccessLevel = await this._dbContext.userAccessRepository.getGroupAccessLevel( + const userGroupAccessLevel = await this.cedarPermissions.getGroupAccessLevel( cognitoUserName, groupId, ); @@ -36,7 +38,7 @@ export class GetUserPermissionsForGroupInConnectionUseCase const tables: Array = (await dao.getTablesFromDB()).map((table) => table.tableName); const tablesWithAccessLevels: Array = await Promise.all( tables.map(async (table) => { - return await this._dbContext.userAccessRepository.getUserTablePermissions( + return await this.cedarPermissions.getUserTablePermissions( cognitoUserName, connectionId, table, diff --git a/backend/src/entities/group/use-cases/find-all-user-groups.use.case.ts b/backend/src/entities/group/use-cases/find-all-user-groups.use.case.ts index e781ce86d..17c227220 100644 --- a/backend/src/entities/group/use-cases/find-all-user-groups.use.case.ts +++ b/backend/src/entities/group/use-cases/find-all-user-groups.use.case.ts @@ -3,6 +3,7 @@ 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 { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { FoundUserGroupsDs } from '../application/data-sctructures/found-user-groups.ds.js'; import { GroupEntity } from '../group.entity.js'; import { IFindUserGroups } from './use-cases.interfaces.js'; @@ -12,6 +13,7 @@ export class FindAllUserGroupsUseCase extends AbstractUseCase = await Promise.all( foundUserGroups.map(async (group: GroupEntity) => { - const accessLevel = await this._dbContext.userAccessRepository.getGroupAccessLevel(userId, group.id); + const accessLevel = await this.cedarPermissions.getGroupAccessLevel(userId, group.id); return { group: group, accessLevel: accessLevel, 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 6aa5b8ac0..04f6d690c 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 @@ -9,6 +9,7 @@ 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'; import { ActivatedTableActionsDTO } from '../application/dto/activated-table-actions.dto.js'; +import { CedarPermissionsService } from '../../../cedar-authorization/cedar-permissions.service.js'; import { IActivateTableActionsInRule } from './action-rules-use-cases.interface.js'; @Injectable() @@ -21,6 +22,7 @@ export class ActivateActionsInEventUseCase protected _dbContext: IGlobalDatabaseContext, private tableLogsService: TableLogsService, private tableActionActivationService: TableActionActivationService, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -44,7 +46,7 @@ export class ActivateActionsInEventUseCase ); } const tableName = foundActionsWithCustomEvents[0].action_rule.table_name; - const canUserReadTable = await this._dbContext.userAccessRepository.checkTableRead( + const canUserReadTable = await this.cedarPermissions.checkTableRead( userId, connectionId, tableName, diff --git a/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts b/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts index a40ed469c..798618aef 100644 --- a/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts +++ b/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts @@ -5,12 +5,11 @@ 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 { AccessLevelEnum } from '../../../enums/access-level.enum.js'; import { ExceptionOperations } from '../../../exceptions/custom-exceptions/exception-operation.js'; import { UnknownSQLException } from '../../../exceptions/custom-exceptions/unknown-sql-exception.js'; import { Messages } from '../../../exceptions/text/messages.js'; import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; -import { isObjectPropertyExists } from '../../../helpers/validators/is-object-property-exists-validator.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { ITableAndViewPermissionData } from '../../permission/permission.interface.js'; import { FindTablesDs } from '../../table/application/data-structures/find-tables.ds.js'; @@ -27,6 +26,7 @@ export class FindTableCategoriesWithTablesUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -78,7 +78,12 @@ export class FindTableCategoriesWithTablesUseCase throw new UnknownSQLException(e.message, ExceptionOperations.FAILED_TO_GET_TABLES); } - const tablesWithPermissions = await this.getUserPermissionsForAvailableTables(userId, connectionId, tables); + const tableNames = tables.map((t) => t.tableName); + const permissionsArr = await this.cedarPermissions.getUserPermissionsForAvailableTables(userId, connectionId, tableNames); + const tablesWithPermissions: Array = permissionsArr.map((perm) => ({ + ...perm, + isView: tables.find((t) => t.tableName === perm.tableName)?.isView || false, + })); const excludedTables = await this._dbContext.connectionPropertiesRepository.findConnectionProperties(connectionId); let tablesRO = await this.addDisplayNamesForTables(connectionId, tablesWithPermissions); if (excludedTables?.hidden_tables?.length) { @@ -155,72 +160,4 @@ export class FindTableCategoriesWithTablesUseCase }); } - private async getUserPermissionsForAvailableTables( - userId: string, - connectionId: string, - tables: Array, - ): Promise> { - const connectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit(userId, connectionId); - if (connectionEdit) { - return tables.map((table) => { - return { - tableName: table.tableName, - isView: table.isView, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }; - }); - } - - const allTablePermissions = - await this._dbContext.permissionRepository.getAllUserPermissionsForAllTablesInConnection(userId, connectionId); - const tablesAndAccessLevels = {}; - tables.map((table) => { - if (table.tableName !== '__proto__') { - tablesAndAccessLevels[table.tableName] = []; - } - }); - tables.map((table) => { - allTablePermissions.map((permission) => { - if ( - permission.tableName === table.tableName && - isObjectPropertyExists(tablesAndAccessLevels, table.tableName) - ) { - tablesAndAccessLevels[table.tableName].push(permission.accessLevel); - } - }); - }); - const tablesWithPermissions: Array = []; - for (const key in tablesAndAccessLevels) { - // eslint-disable-next-line security/detect-object-injection - const addPermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.add); - // eslint-disable-next-line security/detect-object-injection - const deletePermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.delete); - // eslint-disable-next-line security/detect-object-injection - const editPermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.edit); - - const readOnly = !(addPermission || deletePermission || editPermission); - tablesWithPermissions.push({ - tableName: key, - isView: tables.find((table) => table.tableName === key).isView, - accessLevel: { - // eslint-disable-next-line security/detect-object-injection - visibility: tablesAndAccessLevels[key].includes(AccessLevelEnum.visibility), - // eslint-disable-next-line security/detect-object-injection - readonly: tablesAndAccessLevels[key].includes(AccessLevelEnum.readonly) && !readOnly, - add: addPermission, - delete: deletePermission, - edit: editPermission, - }, - }); - } - return tablesWithPermissions.filter((tableWithPermission: ITableAndViewPermissionData) => { - return !!tableWithPermission.accessLevel.visibility; - }); - } } diff --git a/backend/src/entities/table-logs/use-cases/export-logs-as-csv.use.case.ts b/backend/src/entities/table-logs/use-cases/export-logs-as-csv.use.case.ts index d9b129ca8..5fc2263c5 100644 --- a/backend/src/entities/table-logs/use-cases/export-logs-as-csv.use.case.ts +++ b/backend/src/entities/table-logs/use-cases/export-logs-as-csv.use.case.ts @@ -11,6 +11,7 @@ import { Constants } from '../../../helpers/constants/constants.js'; import { validateStringWithEnum } from '../../../helpers/validators/validate-string-with-enum.js'; import { FindLogsDs } from '../application/data-structures/find-logs.ds.js'; import { IFindLogsOptions } from '../repository/table-logs-repository.interface.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IExportLogsAsCsv } from './use-cases.interface.js'; @Injectable() @@ -18,13 +19,14 @@ export class ExportLogsAsCsvUseCase extends AbstractUseCase { const { connectionId, query, userId, operationTypes } = inputData; - const userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit(userId, connectionId); + const userConnectionEdit = await this.cedarPermissions.checkUserConnectionEdit(userId, connectionId); const tableName = query.tableName; let order = query.order; let limit: string | number = query.limit; diff --git a/backend/src/entities/table-logs/use-cases/find-logs.use.case.ts b/backend/src/entities/table-logs/use-cases/find-logs.use.case.ts index 9046f4583..1f6137170 100644 --- a/backend/src/entities/table-logs/use-cases/find-logs.use.case.ts +++ b/backend/src/entities/table-logs/use-cases/find-logs.use.case.ts @@ -6,6 +6,7 @@ import { LogOperationTypeEnum, QueryOrderingEnum } from '../../../enums/index.js import { Messages } from '../../../exceptions/text/messages.js'; import { Constants } from '../../../helpers/constants/constants.js'; import { validateStringWithEnum } from '../../../helpers/validators/validate-string-with-enum.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { FindLogsDs } from '../application/data-structures/find-logs.ds.js'; import { FoundLogsDs, FoundLogsEntities } from '../application/data-structures/found-logs.ds.js'; import { IFindLogsOptions } from '../repository/table-logs-repository.interface.js'; @@ -17,13 +18,14 @@ export class FindLogsUseCase extends AbstractUseCase im constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } protected async implementation(inputData: FindLogsDs): Promise { const { connectionId, query, userId, operationTypes } = inputData; - const userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit(userId, connectionId); + const userConnectionEdit = await this.cedarPermissions.checkUserConnectionEdit(userId, connectionId); const tableName = query.tableName; let order = query.order; let limit: string | number = query.limit; diff --git a/backend/src/entities/table/use-cases/add-row-in-table.use.case.ts b/backend/src/entities/table/use-cases/add-row-in-table.use.case.ts index c1584a1a2..b799a0587 100644 --- a/backend/src/entities/table/use-cases/add-row-in-table.use.case.ts +++ b/backend/src/entities/table/use-cases/add-row-in-table.use.case.ts @@ -31,6 +31,7 @@ import { hashPasswordsInRowUtil } from '../utils/hash-passwords-in-row.util.js'; import { processUuidsInRowUtil } from '../utils/process-uuids-in-row-util.js'; import { removePasswordsFromRowsUtil } from '../utils/remove-password-from-row.util.js'; import { validateTableRowUtil } from '../utils/validate-table-row.util.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IAddRowInTable } from './table-use-cases.interface.js'; @Injectable() @@ -41,6 +42,7 @@ export class AddRowInTableUseCase extends AbstractUseCase { - const canUserReadTable = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const canUserReadTable = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, referencedByTable.table_name, @@ -164,7 +166,7 @@ export class AddRowInTableUseCase extends AbstractUseCase { - const canRead = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const canRead = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, foreignKey.referenced_table_name, diff --git a/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts b/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts index 655fa1321..d58d8061f 100644 --- a/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts +++ b/backend/src/entities/table/use-cases/find-tables-in-connection-v2.use.case.ts @@ -7,13 +7,11 @@ import PQueue from 'p-queue'; 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/access-level.enum.js'; import { AmplitudeEventTypeEnum } from '../../../enums/amplitude-event-type.enum.js'; import { ExceptionOperations } from '../../../exceptions/custom-exceptions/exception-operation.js'; import { UnknownSQLException } from '../../../exceptions/custom-exceptions/unknown-sql-exception.js'; import { Messages } from '../../../exceptions/text/messages.js'; import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; -import { isObjectPropertyExists } from '../../../helpers/validators/is-object-property-exists-validator.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { isTestConnectionUtil } from '../../connection/utils/is-test-connection-util.js'; @@ -24,6 +22,7 @@ import { TableSettingsEntity } from '../../table-settings/common-table-settings/ import { FindTablesDs } from '../application/data-structures/find-tables.ds.js'; import { FoundTableDs, FoundTablesWithCategoriesDS } from '../application/data-structures/found-table.ds.js'; import { buildTableFieldInfoEntity, buildTableInfoEntity } from '../utils/save-tables-info-in-database.util.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IFindTablesInConnectionV2 } from './table-use-cases.interface.js'; @Injectable({ scope: Scope.REQUEST }) @@ -36,6 +35,7 @@ export class FindTablesInConnectionV2UseCase protected _dbContext: IGlobalDatabaseContext, private amplitudeService: AmplitudeService, private readonly logger: WinstonLogger, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -110,7 +110,12 @@ export class FindTablesInConnectionV2UseCase this.saveTableInfoInDatabase(connection.id, userId, tables, masterPwd); } } - const tablesWithPermissions = await this.getUserPermissionsForAvailableTables(userId, connectionId, tables); + const tableNames = tables.map((t) => t.tableName); + const permissionsArr = await this.cedarPermissions.getUserPermissionsForAvailableTables(userId, connectionId, tableNames); + const tablesWithPermissions: Array = permissionsArr.map((perm) => ({ + ...perm, + isView: tables.find((t) => t.tableName === perm.tableName)?.isView || false, + })); const foundConnectionProperties = await this._dbContext.connectionPropertiesRepository.findConnectionPropertiesWithTablesCategories(connectionId); let tablesRO = await this.addDisplayNamesForTables(connectionId, tablesWithPermissions); @@ -120,7 +125,7 @@ export class FindTablesInConnectionV2UseCase return !foundConnectionProperties.hidden_tables.includes(tableRO.table); }); } else { - const userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit( + const userConnectionEdit = await this.cedarPermissions.checkUserConnectionEdit( userId, connectionId, ); @@ -179,75 +184,6 @@ export class FindTablesInConnectionV2UseCase }); } - private async getUserPermissionsForAvailableTables( - userId: string, - connectionId: string, - tables: Array, - ): Promise> { - const connectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit(userId, connectionId); - if (connectionEdit) { - return tables.map((table) => { - return { - tableName: table.tableName, - isView: table.isView, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }; - }); - } - - const allTablePermissions = - await this._dbContext.permissionRepository.getAllUserPermissionsForAllTablesInConnection(userId, connectionId); - const tablesAndAccessLevels = {}; - tables.map((table) => { - if (table.tableName !== '__proto__') { - tablesAndAccessLevels[table.tableName] = []; - } - }); - tables.map((table) => { - allTablePermissions.map((permission) => { - if ( - permission.tableName === table.tableName && - isObjectPropertyExists(tablesAndAccessLevels, table.tableName) - ) { - tablesAndAccessLevels[table.tableName].push(permission.accessLevel); - } - }); - }); - const tablesWithPermissions: Array = []; - for (const key in tablesAndAccessLevels) { - // eslint-disable-next-line security/detect-object-injection - const addPermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.add); - // eslint-disable-next-line security/detect-object-injection - const deletePermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.delete); - // eslint-disable-next-line security/detect-object-injection - const editPermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.edit); - - const readOnly = !(addPermission || deletePermission || editPermission); - tablesWithPermissions.push({ - tableName: key, - isView: tables.find((table) => table.tableName === key).isView, - accessLevel: { - // eslint-disable-next-line security/detect-object-injection - visibility: tablesAndAccessLevels[key].includes(AccessLevelEnum.visibility), - // eslint-disable-next-line security/detect-object-injection - readonly: tablesAndAccessLevels[key].includes(AccessLevelEnum.readonly) && !readOnly, - add: addPermission, - delete: deletePermission, - edit: editPermission, - }, - }); - } - return tablesWithPermissions.filter((tableWithPermission: ITableAndViewPermissionData) => { - return !!tableWithPermission.accessLevel.visibility; - }); - } - private async saveTableInfoInDatabase( connectionId: string, _userId: string, diff --git a/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts b/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts index e99d653b0..429318e65 100644 --- a/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts +++ b/backend/src/entities/table/use-cases/find-tables-in-connection.use.case.ts @@ -8,12 +8,11 @@ import PQueue from 'p-queue'; 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, AmplitudeEventTypeEnum } from '../../../enums/index.js'; +import { AmplitudeEventTypeEnum } from '../../../enums/index.js'; import { ExceptionOperations } from '../../../exceptions/custom-exceptions/exception-operation.js'; import { UnknownSQLException } from '../../../exceptions/custom-exceptions/unknown-sql-exception.js'; import { Messages } from '../../../exceptions/text/messages.js'; import { isConnectionTypeAgent } from '../../../helpers/index.js'; -import { isObjectPropertyExists } from '../../../helpers/validators/is-object-property-exists-validator.js'; import { AmplitudeService } from '../../amplitude/amplitude.service.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { isTestConnectionUtil } from '../../connection/utils/is-test-connection-util.js'; @@ -24,6 +23,7 @@ import { TableSettingsEntity } from '../../table-settings/common-table-settings/ import { FindTablesDs } from '../application/data-structures/find-tables.ds.js'; import { FoundTableDs } from '../application/data-structures/found-table.ds.js'; import { buildTableFieldInfoEntity, buildTableInfoEntity } from '../utils/save-tables-info-in-database.util.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IFindTablesInConnection } from './table-use-cases.interface.js'; @Injectable() @@ -36,6 +36,7 @@ export class FindTablesInConnectionUseCase protected _dbContext: IGlobalDatabaseContext, private amplitudeService: AmplitudeService, private readonly logger: WinstonLogger, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -113,7 +114,12 @@ export class FindTablesInConnectionUseCase this.saveTableInfoInDatabase(connection.id, userId, tables, masterPwd); } } - const tablesWithPermissions = await this.getUserPermissionsForAvailableTables(userId, connectionId, tables); + const tableNames = tables.map((t) => t.tableName); + const permissionsArr = await this.cedarPermissions.getUserPermissionsForAvailableTables(userId, connectionId, tableNames); + const tablesWithPermissions: Array = permissionsArr.map((perm) => ({ + ...perm, + isView: tables.find((t) => t.tableName === perm.tableName)?.isView || false, + })); const excludedTables = await this._dbContext.connectionPropertiesRepository.findConnectionProperties(connectionId); let tablesRO = await this.addDisplayNamesForTables(connectionId, tablesWithPermissions); if (excludedTables?.hidden_tables?.length) { @@ -122,7 +128,7 @@ export class FindTablesInConnectionUseCase return !excludedTables.hidden_tables.includes(tableRO.table); }); } else { - const userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit( + const userConnectionEdit = await this.cedarPermissions.checkUserConnectionEdit( userId, connectionId, ); @@ -179,75 +185,6 @@ export class FindTablesInConnectionUseCase }); } - private async getUserPermissionsForAvailableTables( - userId: string, - connectionId: string, - tables: Array, - ): Promise> { - const connectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit(userId, connectionId); - if (connectionEdit) { - return tables.map((table) => { - return { - tableName: table.tableName, - isView: table.isView, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - }; - }); - } - - const allTablePermissions = - await this._dbContext.permissionRepository.getAllUserPermissionsForAllTablesInConnection(userId, connectionId); - const tablesAndAccessLevels = {}; - tables.map((table) => { - if (table.tableName !== '__proto__') { - tablesAndAccessLevels[table.tableName] = []; - } - }); - tables.map((table) => { - allTablePermissions.map((permission) => { - if ( - permission.tableName === table.tableName && - isObjectPropertyExists(tablesAndAccessLevels, table.tableName) - ) { - tablesAndAccessLevels[table.tableName].push(permission.accessLevel); - } - }); - }); - const tablesWithPermissions: Array = []; - for (const key in tablesAndAccessLevels) { - // eslint-disable-next-line security/detect-object-injection - const addPermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.add); - // eslint-disable-next-line security/detect-object-injection - const deletePermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.delete); - // eslint-disable-next-line security/detect-object-injection - const editPermission = tablesAndAccessLevels[key].includes(AccessLevelEnum.edit); - - const readOnly = !(addPermission || deletePermission || editPermission); - tablesWithPermissions.push({ - tableName: key, - isView: tables.find((table) => table.tableName === key).isView, - accessLevel: { - // eslint-disable-next-line security/detect-object-injection - visibility: tablesAndAccessLevels[key].includes(AccessLevelEnum.visibility), - // eslint-disable-next-line security/detect-object-injection - readonly: tablesAndAccessLevels[key].includes(AccessLevelEnum.readonly) && !readOnly, - add: addPermission, - delete: deletePermission, - edit: editPermission, - }, - }); - } - return tablesWithPermissions.filter((tableWithPermission: ITableAndViewPermissionData) => { - return !!tableWithPermission.accessLevel.visibility; - }); - } - private async saveTableInfoInDatabase( connectionId: string, _userId: string, diff --git a/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts b/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts index 7b210f041..5063ccabb 100644 --- a/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts +++ b/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts @@ -26,6 +26,7 @@ import { convertHexDataInPrimaryKeyUtil } from '../utils/convert-hex-data-in-pri import { findAvailableFields } from '../utils/find-available-fields.utils.js'; import { formFullTableStructure } from '../utils/form-full-table-structure.js'; import { removePasswordsFromRowsUtil } from '../utils/remove-password-from-row.util.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IGetRowByPrimaryKey } from './table-use-cases.interface.js'; @Injectable() @@ -36,6 +37,7 @@ export class GetRowByPrimaryKeyUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -93,7 +95,7 @@ export class GetRowByPrimaryKeyUseCase dao.getTablePrimaryColumns(tableName, userEmail), this._dbContext.actionEventsRepository.findCustomEventsForTable(connectionId, tableName), dao.getReferencedTableNamesAndColumns(tableName, userEmail), - this._dbContext.userAccessRepository.getUserTablePermissions(userId, connectionId, tableName, masterPwd), + this.cedarPermissions.getUserTablePermissions(userId, connectionId, tableName, masterPwd), ]); primaryKey = convertHexDataInPrimaryKeyUtil(primaryKey, tableStructure); const availablePrimaryColumns: Array = tablePrimaryKeys.map((column) => column.column_name); @@ -131,7 +133,7 @@ export class GetRowByPrimaryKeyUseCase canRead: boolean; }> = await Promise.all( tableForeignKeys.map(async (foreignKey) => { - const cenTableRead = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const cenTableRead = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, foreignKey.referenced_table_name, @@ -184,7 +186,7 @@ export class GetRowByPrimaryKeyUseCase for (const referencedTable of referencedTableNamesAndColumns) { referencedTable.referenced_by = await Promise.all( referencedTable.referenced_by.map(async (referencedByTable) => { - const canUserReadTable = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const canUserReadTable = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, referencedByTable.table_name, diff --git a/backend/src/entities/table/use-cases/get-table-rows.use.case.ts b/backend/src/entities/table/use-cases/get-table-rows.use.case.ts index 5fba95296..65b7db1d5 100644 --- a/backend/src/entities/table/use-cases/get-table-rows.use.case.ts +++ b/backend/src/entities/table/use-cases/get-table-rows.use.case.ts @@ -43,6 +43,7 @@ import { findOrderingFieldUtil } from '../utils/find-ordering-field.util.js'; import { formFullTableStructure } from '../utils/form-full-table-structure.js'; import { isHexString } from '../utils/is-hex-string.js'; import { processRowsUtil } from '../utils/process-found-rows-util.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IGetTableRows } from './table-use-cases.interface.js'; @Injectable() @@ -52,6 +53,7 @@ export class GetTableRowsUseCase extends AbstractUseCase - this._dbContext.userAccessRepository + this.cedarPermissions .improvedCheckTableRead(userId, connectionId, foreignKey.referenced_table_name, masterPwd) .then((canRead) => ({ tableName: foreignKey.referenced_table_name, diff --git a/backend/src/entities/table/use-cases/get-table-structure.use.case.ts b/backend/src/entities/table/use-cases/get-table-structure.use.case.ts index 5e5ab07ac..411c4da1d 100644 --- a/backend/src/entities/table/use-cases/get-table-structure.use.case.ts +++ b/backend/src/entities/table/use-cases/get-table-structure.use.case.ts @@ -19,6 +19,7 @@ import { buildFoundTableWidgetDs } from '../../widget/utils/build-found-table-wi import { GetTableStructureDs } from '../application/data-structures/get-table-structure-ds.js'; import { ForeignKeyDSInfo, TableStructureDs } from '../table-datastructures.js'; import { formFullTableStructure } from '../utils/form-full-table-structure.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IGetTableStructure } from './table-use-cases.interface.js'; @Injectable() @@ -29,6 +30,7 @@ export class GetTableStructureUseCase constructor( @Inject(BaseType.GLOBAL_DB_CONTEXT) protected _dbContext: IGlobalDatabaseContext, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -101,7 +103,7 @@ export class GetTableStructureUseCase canRead: boolean; }> = await Promise.all( tableForeignKeys.map(async (foreignKey) => { - const cenTableRead = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const cenTableRead = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, foreignKey.referenced_table_name, diff --git a/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts b/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts index dc80aeaf2..683210f84 100644 --- a/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts +++ b/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts @@ -39,6 +39,7 @@ import { formFullTableStructure } from '../utils/form-full-table-structure.js'; import { hashPasswordsInRowUtil } from '../utils/hash-passwords-in-row.util.js'; import { processUuidsInRowUtil } from '../utils/process-uuids-in-row-util.js'; import { removePasswordsFromRowsUtil } from '../utils/remove-password-from-row.util.js'; +import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js'; import { IUpdateRowInTable } from './table-use-cases.interface.js'; @Injectable() @@ -52,6 +53,7 @@ export class UpdateRowInTableUseCase private amplitudeService: AmplitudeService, private tableLogsService: TableLogsService, private tableActionActivationService: TableActionActivationService, + private readonly cedarPermissions: CedarPermissionsService, ) { super(); } @@ -118,7 +120,7 @@ export class UpdateRowInTableUseCase for (const referencedTable of referencedTableNamesAndColumns) { referencedTable.referenced_by = await Promise.all( referencedTable.referenced_by.map(async (referencedByTable) => { - const canUserReadTable = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const canUserReadTable = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, referencedByTable.table_name, @@ -192,7 +194,7 @@ export class UpdateRowInTableUseCase canRead: boolean; }> = await Promise.all( tableForeignKeys.map(async (foreignKey) => { - const cenTableRead = await this._dbContext.userAccessRepository.improvedCheckTableRead( + const cenTableRead = await this.cedarPermissions.improvedCheckTableRead( userId, connectionId, foreignKey.referenced_table_name, diff --git a/backend/src/guards/connection-edit.guard.ts b/backend/src/guards/connection-edit.guard.ts index e3e1b567b..e9a55fdd6 100644 --- a/backend/src/guards/connection-edit.guard.ts +++ b/backend/src/guards/connection-edit.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -19,11 +15,7 @@ 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, ) {} @@ -40,46 +32,19 @@ export class ConnectionEditGuard implements CanActivate { 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( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.ConnectionEdit, connectionId, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userConnectionEdit) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/connection-read.guard.ts b/backend/src/guards/connection-read.guard.ts index 2b76b71cb..f19cfa2a7 100644 --- a/backend/src/guards/connection-read.guard.ts +++ b/backend/src/guards/connection-read.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -19,11 +15,7 @@ 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, ) {} @@ -40,46 +32,19 @@ export class ConnectionReadGuard implements CanActivate { 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( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.ConnectionRead, connectionId, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userConnectionRead) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/dashboard-create.guard.ts b/backend/src/guards/dashboard-create.guard.ts index b5f9a8cf8..99164484c 100644 --- a/backend/src/guards/dashboard-create.guard.ts +++ b/backend/src/guards/dashboard-create.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -19,11 +15,7 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class DashboardCreateGuard implements CanActivate { - private readonly logger = new Logger(DashboardCreateGuard.name); - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, private readonly cedarAuthService: CedarAuthorizationService, ) {} @@ -40,45 +32,19 @@ export class DashboardCreateGuard implements CanActivate { return; } - if (this.cedarAuthService.isFeatureEnabled()) { - try { - const allowed = await this.cedarAuthService.validate({ - userId: cognitoUserName, - action: CedarAction.DashboardCreate, - 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 - dashboards use connection-level edit access - let userConnectionEdit = false; try { - userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.DashboardCreate, connectionId, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userConnectionEdit) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/dashboard-edit.guard.ts b/backend/src/guards/dashboard-edit.guard.ts index 71f22bcf9..19de8df02 100644 --- a/backend/src/guards/dashboard-edit.guard.ts +++ b/backend/src/guards/dashboard-edit.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -19,11 +15,7 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class DashboardEditGuard implements CanActivate { - private readonly logger = new Logger(DashboardEditGuard.name); - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, private readonly cedarAuthService: CedarAuthorizationService, ) {} @@ -43,47 +35,20 @@ export class DashboardEditGuard implements CanActivate { const dashboardId: string = request.params?.dashboardId; const action = request.method === 'DELETE' ? CedarAction.DashboardDelete : CedarAction.DashboardEdit; - // Cedar-first authorization - if (this.cedarAuthService.isFeatureEnabled()) { - try { - const allowed = await this.cedarAuthService.validate({ - userId: cognitoUserName, - action, - connectionId, - dashboardId, - }); - 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 - dashboards use connection-level edit access - let userConnectionEdit = false; try { - userConnectionEdit = await this._dbContext.userAccessRepository.checkUserConnectionEdit( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action, connectionId, - ); + dashboardId, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userConnectionEdit) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/dashboard-read.guard.ts b/backend/src/guards/dashboard-read.guard.ts index 8636a2009..04ab1a5bf 100644 --- a/backend/src/guards/dashboard-read.guard.ts +++ b/backend/src/guards/dashboard-read.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -19,11 +15,7 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js'; @Injectable() export class DashboardReadGuard implements CanActivate { - private readonly logger = new Logger(DashboardReadGuard.name); - constructor( - @Inject(BaseType.GLOBAL_DB_CONTEXT) - protected _dbContext: IGlobalDatabaseContext, private readonly cedarAuthService: CedarAuthorizationService, ) {} @@ -42,47 +34,20 @@ export class DashboardReadGuard implements CanActivate { const dashboardId: string = request.params?.dashboardId; - // Cedar-first authorization - if (this.cedarAuthService.isFeatureEnabled()) { - try { - const allowed = await this.cedarAuthService.validate({ - userId: cognitoUserName, - action: CedarAction.DashboardRead, - connectionId, - dashboardId, - }); - 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 - dashboards use connection-level read access - let userConnectionRead = false; try { - userConnectionRead = await this._dbContext.userAccessRepository.checkUserConnectionRead( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.DashboardRead, connectionId, - ); + dashboardId, + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userConnectionRead) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/group-edit.guard.ts b/backend/src/guards/group-edit.guard.ts index 920399a4e..b66e549d5 100644 --- a/backend/src/guards/group-edit.guard.ts +++ b/backend/src/guards/group-edit.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -18,11 +14,7 @@ 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, ) {} @@ -39,44 +31,19 @@ export class GroupEditGuard implements CanActivate { 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)); + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.GroupEdit, + groupId, + }); + if (allowed) { + resolve(true); 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); + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - - if (userGroupEdit) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/group-read.guard.ts b/backend/src/guards/group-read.guard.ts index f6718199e..a7dc2c6aa 100644 --- a/backend/src/guards/group-read.guard.ts +++ b/backend/src/guards/group-read.guard.ts @@ -3,14 +3,10 @@ import { CanActivate, ExecutionContext, 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'; @@ -18,11 +14,7 @@ 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, ) {} @@ -39,43 +31,19 @@ export class GroupReadGuard implements CanActivate { 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)); + try { + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.GroupRead, + groupId, + }); + if (allowed) { + resolve(true); 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); + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userGroupRead) { - resolve(true); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/table-add.guard.ts b/backend/src/guards/table-add.guard.ts index 5dd02ca26..900c4262b 100644 --- a/backend/src/guards/table-add.guard.ts +++ b/backend/src/guards/table-add.guard.ts @@ -3,28 +3,19 @@ import { CanActivate, ExecutionContext, 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'; 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, ) {} @@ -34,7 +25,6 @@ export class TableAddGuard implements CanActivate { const cognitoUserName = request.decoded.sub; const connectionId: string = request.params?.slug || request.params?.connectionId; const tableName: string = request.query?.tableName; - const masterPwd = getMasterPwd(request); if (!tableName) { reject(new BadRequestException(Messages.TABLE_NAME_MISSING)); return; @@ -44,49 +34,20 @@ export class TableAddGuard implements CanActivate { 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( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableAdd, connectionId, tableName, - masterPwd, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userTableAdd) { - resolve(userTableAdd); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/table-delete.guard.ts b/backend/src/guards/table-delete.guard.ts index cff05d2f6..30e47738e 100644 --- a/backend/src/guards/table-delete.guard.ts +++ b/backend/src/guards/table-delete.guard.ts @@ -3,28 +3,19 @@ import { CanActivate, ExecutionContext, 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'; 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, ) {} @@ -34,7 +25,6 @@ export class TableDeleteGuard implements CanActivate { const cognitoUserName = request.decoded.sub; const connectionId: string = request.params?.slug || request.params?.connectionId; const tableName: string = request.query?.tableName; - const masterPwd = getMasterPwd(request); if (!tableName) { reject(new BadRequestException(Messages.TABLE_NAME_MISSING)); return; @@ -44,48 +34,20 @@ export class TableDeleteGuard implements CanActivate { 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( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableDelete, connectionId, tableName, - masterPwd, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userTableDelete) { - resolve(userTableDelete); - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/table-edit.guard.ts b/backend/src/guards/table-edit.guard.ts index 1f4342c8d..17ec08875 100644 --- a/backend/src/guards/table-edit.guard.ts +++ b/backend/src/guards/table-edit.guard.ts @@ -3,28 +3,19 @@ import { CanActivate, ExecutionContext, 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'; 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, ) {} @@ -34,7 +25,6 @@ export class TableEditGuard implements CanActivate { const cognitoUserName = request.decoded.sub; const connectionId: string = request.params?.slug || request.params?.connectionId; const tableName: string = request.query?.tableName; - const masterPwd = getMasterPwd(request); if (!tableName) { reject(new BadRequestException(Messages.TABLE_NAME_MISSING)); return; @@ -44,49 +34,20 @@ export class TableEditGuard implements CanActivate { 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( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableEdit, connectionId, tableName, - masterPwd, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userTableEdit) { - resolve(userTableEdit); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } diff --git a/backend/src/guards/table-read.guard.ts b/backend/src/guards/table-read.guard.ts index 101e3dd66..1a72e0e31 100644 --- a/backend/src/guards/table-read.guard.ts +++ b/backend/src/guards/table-read.guard.ts @@ -3,28 +3,19 @@ import { CanActivate, ExecutionContext, 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'; 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, ) {} @@ -34,7 +25,6 @@ export class TableReadGuard implements CanActivate { const cognitoUserName = request.decoded.sub; const connectionId: string = request.params?.slug || request.params?.connectionId; const tableName: string = request.query?.tableName; - const masterPwd = getMasterPwd(request); if (!tableName) { reject(new BadRequestException(Messages.TABLE_NAME_MISSING)); return; @@ -44,49 +34,20 @@ export class TableReadGuard implements CanActivate { 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( - cognitoUserName, + const allowed = await this.cedarAuthService.validate({ + userId: cognitoUserName, + action: CedarAction.TableRead, connectionId, tableName, - masterPwd, - ); + }); + if (allowed) { + resolve(true); + return; + } + reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); } catch (e) { reject(e); - return; - } - if (userTableRead) { - resolve(userTableRead); - return; - } else { - reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS)); - return; } }); } From 6a63a63bff1b566db4acaa33cf96ab4832f1141a Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 19 Mar 2026 11:23:07 +0000 Subject: [PATCH 2/3] feat: add connection edit permissions and enhance user access control --- .../cedar-permissions.service.ts | 18 + .../saas-cedar-permissions-e2e.test.ts | 850 +++++++++++++++++- 2 files changed, 867 insertions(+), 1 deletion(-) diff --git a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts index 885ff1cac..d25badff9 100644 --- a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts @@ -112,6 +112,24 @@ export class CedarPermissionsService implements IUserAccessRepository { return []; } + // If user has connection:edit, they get full access to all tables + const connEditEntities = buildCedarEntities(cognitoUserName, userGroups, connectionId); + const hasConnectionEdit = this.evaluatePolicies( + cognitoUserName, CedarAction.ConnectionEdit, CedarResourceType.Connection, connectionId, groupPolicies, connEditEntities, + ); + if (hasConnectionEdit) { + return tableNames.map((tableName) => ({ + tableName, + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + })); + } + const actions = [CedarAction.TableRead, CedarAction.TableAdd, CedarAction.TableEdit, CedarAction.TableDelete]; const result: Array = []; 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 index b9fb5a7cf..c4d6a97d7 100644 --- 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 @@ -19,7 +19,13 @@ 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'; +import { + createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions, + createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection, + createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions, + createConnectionAndInviteUserWithConnectionEditOnly, + createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions, +} from '../../utils/user-with-different-permissions-utils.js'; let app: INestApplication; let _testUtils: TestUtils; @@ -1102,3 +1108,845 @@ test.serial( } }, ); + +//****************************** DEEP PERMISSIONS: Cross-connection isolation ****************************** + +currentTest = 'CROSS-CONNECTION ISOLATION'; + +test.serial( + `${currentTest} should deny access when user tries to read tables from a connection they are NOT in via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + secondTableInfo, + users: { simpleUserToken }, + } = testData; + + // User is only in first connection's group, NOT in second connection + // TablesReceiveGuard returns 400 CONNECTION_NOT_FOUND when user is not from connection + const getTablesInSecondConnection = await request(app.getHttpServer()) + .get(`/connection/tables/${connections.secondId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesInSecondConnection.status, 400); + t.is(JSON.parse(getTablesInSecondConnection.text).message, Messages.CONNECTION_NOT_FOUND); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} should deny row access on second connection even with valid table name via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + // Try to read rows from first connection's table name but using second connection's ID + const getTableRows = await request(app.getHttpServer()) + .get(`/table/rows/${connections.secondId}?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 deny adding rows to second connection that user has no access to via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${connections.secondId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(addRowInTable.status, 403); + t.is(JSON.parse(addRowInTable.text).message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** DEEP PERMISSIONS: Admin group (isMain=true) hierarchy ****************************** + +currentTest = 'ADMIN GROUP HIERARCHY'; + +test.serial( + `${currentTest} admin group user should have full connection edit access via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const findAll = await request(app.getHttpServer()) + .get('/connections') + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .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.truthy(targetConnection); + t.is(targetConnection.accessLevel, AccessLevelEnum.edit); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} admin group user should be able to create groups via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const newGroup = mockFactory.generateCreateGroupDto1(); + const createGroupResponse = await request(app.getHttpServer()) + .post(`/connection/group/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .send(newGroup) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createGroupResponse.status, 201); + t.truthy(JSON.parse(createGroupResponse.text).id); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} admin group user should have full table CRUD access via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + // Read rows + 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); + + // Add row + const addRowInTable = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(addRowInTable.status, 201); + + // Edit row + const updateRow = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(updateRow.status, 200); + + // Delete row + const deleteRow = 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(deleteRow.status, 200); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} admin group user should NOT have access to second connection they are NOT in via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInAdminGroupOfFirstConnection(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + // Admin in first connection should NOT see second connection's tables + // TablesReceiveGuard returns 400 CONNECTION_NOT_FOUND when user is not from connection + const getTablesInSecondConnection = await request(app.getHttpServer()) + .get(`/connection/tables/${connections.secondId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesInSecondConnection.status, 400); + t.is(JSON.parse(getTablesInSecondConnection.text).message, Messages.CONNECTION_NOT_FOUND); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** DEEP PERMISSIONS: Table-only permissions (no connection/group access) ****************************** + +currentTest = 'TABLE-ONLY PERMISSIONS'; + +test.serial( + `${currentTest} user with only table permissions should be able to read table rows via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(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 result = JSON.parse(getTableRows.text); + t.truthy(result.rows); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} user with only table permissions should get none access level for connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const findOneResponse = await request(app.getHttpServer()) + .get(`/connection/one/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(findOneResponse.status, 200); + const findOneRO = JSON.parse(findOneResponse.text); + t.is(findOneRO.accessLevel, AccessLevelEnum.none); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} user with only table permissions should NOT be able to update connection via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithOnlyTablePermissions(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; + } + }, +); + +//****************************** DEEP PERMISSIONS: connection:edit with no table permissions ****************************** + +currentTest = 'CONNECTION EDIT NO TABLES'; + +test.serial( + `${currentTest} user with connection:edit but no table perms should get edit access level via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const findAll = await request(app.getHttpServer()) + .get('/connections') + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .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.truthy(targetConnection); + t.is(targetConnection.accessLevel, AccessLevelEnum.edit); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} user with connection:edit should be able to create groups via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); + const { + connections, + users: { simpleUserToken }, + } = testData; + + const newGroup = mockFactory.generateCreateGroupDto1(); + const createGroupResponse = await request(app.getHttpServer()) + .post(`/connection/group/${connections.firstId}`) + .set('Cookie', simpleUserToken) + .send(newGroup) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createGroupResponse.status, 201); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} user with connection:edit should see all tables with full permissions via Cedar`, + async (t) => { + try { + const testData = await createConnectionAndInviteUserWithConnectionEditOnly(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 tables = JSON.parse(getTablesInConnection.text); + t.true(tables.length > 0); + const targetTable = tables.find((table: any) => table.table === firstTableInfo.testTableName); + t.truthy(targetTable); + t.is(targetTable.permissions.visibility, true); + t.is(targetTable.permissions.add, true); + t.is(targetTable.permissions.edit, true); + t.is(targetTable.permissions.delete, true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** DEEP PERMISSIONS: Group permissions hierarchy ****************************** + +currentTest = 'GROUP PERMISSION HIERARCHY'; + +test.serial( + `${currentTest} user with group:edit should be able to see groups with edit access level via Cedar`, + async (t) => { + try { + const testData = await createConnectionsAndInviteNewUserInNewGroupWithGroupPermissions(app); + const { + connections, + groups, + 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.true(result.length > 0); + + const createdGroup = result.find((el: any) => el.group.id === groups.createdGroupId); + t.truthy(createdGroup); + t.is(createdGroup.accessLevel, AccessLevelEnum.edit); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} readonly group user should see groups with readonly access level via Cedar`, + async (t) => { + try { + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + ); + const { + connections, + groups, + 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); + const createdGroup = result.find((el: any) => el.group.id === groups.createdGroupId); + t.truthy(createdGroup); + t.is(createdGroup.accessLevel, AccessLevelEnum.readonly); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** DEEP PERMISSIONS: Table permissions returned from API match Cedar policy ****************************** + +currentTest = 'TABLE PERMISSIONS ACCURACY'; + +test.serial( + `${currentTest} getUserTablePermissions should return exact table permissions matching Cedar policy via Cedar`, + async (t) => { + try { + const permissionMixed = { + visibility: true, + readonly: false, + add: true, + delete: false, + edit: true, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionMixed, + ); + const { + connections, + firstTableInfo, + groups, + users: { simpleUserToken }, + } = testData; + + // Check permissions returned from connection/permissions endpoint + const response = await request(app.getHttpServer()) + .get(`/connection/permissions?connectionId=${connections.firstId}&groupId=${groups.createdGroupId}`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(response.status, 200); + + const result = JSON.parse(response.text); + const tablePermission = result.tables.find((t: any) => t.tableName === firstTableInfo.testTableName); + t.truthy(tablePermission); + t.is(tablePermission.accessLevel.visibility, true); + t.is(tablePermission.accessLevel.add, true); + t.is(tablePermission.accessLevel.delete, false); + t.is(tablePermission.accessLevel.edit, true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} table list should only show tables with visibility=true 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 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 tables = JSON.parse(getTablesInConnection.text); + // The table with visibility=false should NOT appear in the list + const hiddenTable = tables.find((table: any) => table.table === firstTableInfo.testTableName); + t.is(hiddenTable, undefined); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** DEEP PERMISSIONS: Per-action granularity ****************************** + +currentTest = 'PER-ACTION GRANULARITY'; + +test.serial( + `${currentTest} user with only add permission should be able to add but not edit or delete via Cedar`, + async (t) => { + try { + const permissionAddOnly = { + visibility: true, + readonly: false, + add: true, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionAddOnly, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + // Can add + const addRow = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(addRow.status, 201); + + // Cannot edit + const editRow = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(editRow.status, 403); + + // Cannot delete + const deleteRow = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=18`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRow.status, 403); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} user with only edit permission should be able to edit but not add or delete via Cedar`, + async (t) => { + try { + const permissionEditOnly = { + visibility: true, + readonly: false, + add: false, + delete: false, + edit: true, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionEditOnly, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + // Cannot add + const addRow = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(addRow.status, 403); + + // Can edit + const editRow = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(editRow.status, 200); + + // Cannot delete + const deleteRow = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=18`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRow.status, 403); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} user with only delete permission should be able to delete but not add or edit via Cedar`, + async (t) => { + try { + const permissionDeleteOnly = { + visibility: true, + readonly: false, + add: false, + delete: true, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionDeleteOnly, + ); + const { + connections, + firstTableInfo, + users: { simpleUserToken }, + } = testData; + + // Cannot add + const addRow = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(addRow.status, 403); + + // Cannot edit + const editRow = await request(app.getHttpServer()) + .put(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=2`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(editRow.status, 403); + + // Can delete + const deleteRow = await request(app.getHttpServer()) + .delete(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}&id=17`) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteRow.status, 200); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +//****************************** DEEP PERMISSIONS: Admin vs non-admin user asymmetry ****************************** + +currentTest = 'ADMIN VS NON-ADMIN ASYMMETRY'; + +test.serial( + `${currentTest} admin should have full access while non-admin has restricted access on same connection via Cedar`, + async (t) => { + try { + const permissionReadOnly = { + visibility: true, + readonly: true, + add: false, + delete: false, + edit: false, + }; + const testData = + await createConnectionsAndInviteNewUserInNewGroupWithTableDifferentConnectionGroupReadOnlyPermissions( + app, + permissionReadOnly, + ); + const { + connections, + firstTableInfo, + users: { adminUserToken, simpleUserToken }, + } = testData; + + // Both can read rows + const adminRead = await request(app.getHttpServer()) + .get(`/table/rows/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .set('Cookie', adminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(adminRead.status, 200); + + const simpleRead = 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(simpleRead.status, 200); + + // Admin can add rows + const adminAdd = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.testTableSecondColumnName]: faker.internet.email(), + created_at: new Date(), + updated_at: new Date(), + }) + .set('Cookie', adminUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(adminAdd.status, 201); + + // Non-admin cannot add rows + const simpleAdd = await request(app.getHttpServer()) + .post(`/table/row/${connections.firstId}?tableName=${firstTableInfo.testTableName}`) + .send({ + [firstTableInfo.testTableColumnName]: faker.person.firstName(), + [firstTableInfo.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(simpleAdd.status, 403); + + // Non-admin cannot update connection + const simpleUpdate = await request(app.getHttpServer()) + .put(`/connection/${connections.firstId}`) + .send(updateConnection) + .set('Cookie', simpleUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(simpleUpdate.status, 403); + } catch (e) { + console.error(e); + throw e; + } + }, +); From 0c42bc9d0c8e9b62c5cb1b53fd480c11113696b4 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 19 Mar 2026 12:02:07 +0000 Subject: [PATCH 3/3] refactor: update connection:edit permissions to restrict table access --- .../cedar-permissions.service.ts | 18 ------------------ .../saas-cedar-permissions-e2e.test.ts | 10 +++------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts index d25badff9..885ff1cac 100644 --- a/backend/src/entities/cedar-authorization/cedar-permissions.service.ts +++ b/backend/src/entities/cedar-authorization/cedar-permissions.service.ts @@ -112,24 +112,6 @@ export class CedarPermissionsService implements IUserAccessRepository { return []; } - // If user has connection:edit, they get full access to all tables - const connEditEntities = buildCedarEntities(cognitoUserName, userGroups, connectionId); - const hasConnectionEdit = this.evaluatePolicies( - cognitoUserName, CedarAction.ConnectionEdit, CedarResourceType.Connection, connectionId, groupPolicies, connEditEntities, - ); - if (hasConnectionEdit) { - return tableNames.map((tableName) => ({ - tableName, - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true, - }, - })); - } - const actions = [CedarAction.TableRead, CedarAction.TableAdd, CedarAction.TableEdit, CedarAction.TableDelete]; const result: Array = []; 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 index c4d6a97d7..f94cda3b3 100644 --- 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 @@ -1487,7 +1487,7 @@ test.serial( ); test.serial( - `${currentTest} user with connection:edit should see all tables with full permissions via Cedar`, + `${currentTest} user with connection:edit but no table perms should see empty table list via Cedar`, async (t) => { try { const testData = await createConnectionAndInviteUserWithConnectionEditOnly(app); @@ -1504,13 +1504,9 @@ test.serial( .set('Accept', 'application/json'); t.is(getTablesInConnection.status, 200); const tables = JSON.parse(getTablesInConnection.text); - t.true(tables.length > 0); + // connection:edit does NOT grant table access — tables should be empty const targetTable = tables.find((table: any) => table.table === firstTableInfo.testTableName); - t.truthy(targetTable); - t.is(targetTable.permissions.visibility, true); - t.is(targetTable.permissions.add, true); - t.is(targetTable.permissions.edit, true); - t.is(targetTable.permissions.delete, true); + t.is(targetTable, undefined); } catch (e) { console.error(e); throw e;