Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
import AbstractUseCase from '../../../../common/abstract-use.case.js';
import { IGlobalDatabaseContext } from '../../../../common/application/global-database-context.interface.js';
import { BaseType } from '../../../../common/data-injection.tokens.js';
import { CedarAction } from '../../../cedar-authorization/cedar-action-map.js';
import { CedarAuthorizationService } from '../../../cedar-authorization/cedar-authorization.service.js';
import { Messages } from '../../../../exceptions/text/messages.js';
import { FindAllDashboardsDs } from '../data-structures/find-all-dashboards.ds.js';
import { FoundDashboardDto } from '../dto/found-dashboard.dto.js';
Expand All @@ -16,12 +18,13 @@ export class FindAllDashboardsUseCase
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private readonly cedarAuthService: CedarAuthorizationService,
) {
super();
}

public async implementation(inputData: FindAllDashboardsDs): Promise<FoundDashboardDto[]> {
const { connectionId, masterPassword } = inputData;
const { connectionId, masterPassword, userId } = inputData;

const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection(
connectionId,
Expand All @@ -35,6 +38,17 @@ export class FindAllDashboardsUseCase
const dashboards =
await this._dbContext.dashboardRepository.findAllDashboardsWithWidgetsByConnectionId(connectionId);

Comment on lines 38 to 40

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findAllDashboardsWithWidgetsByConnectionId loads widgets for every dashboard before authorization filtering. With per-dashboard Cedar checks added below, this can fetch potentially large widget payloads for dashboards the caller is not allowed to see, increasing DB load and response time. Consider fetching only dashboard metadata/IDs first, filter by permissions, then load widgets only for the allowed dashboards.

Copilot uses AI. Check for mistakes.
return dashboards.map(buildFoundDashboardDto);
const accessChecks = await Promise.all(
dashboards.map((dashboard) =>
this.cedarAuthService.validate({
userId,
action: CedarAction.DashboardRead,
connectionId,
dashboardId: dashboard.id,
}),
),
);
Comment on lines +41 to +50

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does one cedarAuthService.validate call per dashboard and runs them all concurrently via Promise.all. validate() performs DB lookups (user suspension + group membership) and Cedar evaluation, so this becomes an N-per-dashboard query/CPU pattern and can overwhelm the DB/pool for connections with many dashboards. Consider adding a batch/streamed validation path that reuses the loaded userGroups/policies once per request (or at least limiting concurrency).

Copilot uses AI. Check for mistakes.

return dashboards.filter((_, index) => accessChecks[index]).map(buildFoundDashboardDto);
}
}
18 changes: 18 additions & 0 deletions backend/src/guards/dashboard-read.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
CanActivate,
ExecutionContext,
ForbiddenException,
Inject,
Injectable,
} 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';
Expand All @@ -17,6 +20,8 @@ import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js';
export class DashboardReadGuard implements CanActivate {
constructor(
private readonly cedarAuthService: CedarAuthorizationService,
@Inject(BaseType.GLOBAL_DB_CONTEXT)
private readonly _dbContext: IGlobalDatabaseContext,
) {}

canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
Expand All @@ -35,6 +40,19 @@ export class DashboardReadGuard implements CanActivate {
const dashboardId: string = request.params?.dashboardId;

try {
if (!dashboardId) {
const isUserInConnection = await this._dbContext.connectionRepository.isUserFromConnection(
cognitoUserName,
connectionId,
);
if (isUserInConnection) {
resolve(true);
return;
}
reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS));
return;
}

const allowed = await this.cedarAuthService.validate({
userId: cognitoUserName,
action: CedarAction.DashboardRead,
Expand Down
Loading
Loading