From 613efb319d369738694bfa2efbaa0b80f9d5adcb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:01:31 +0200 Subject: [PATCH] Restrict support for filtering to public access --- apps/api/src/app/access/access.controller.ts | 10 ++- apps/api/src/app/access/access.service.ts | 13 +++- .../src/app/endpoints/mcp/mcp.controller.ts | 61 ++----------------- .../impersonation/impersonation.service.ts | 4 +- .../lib/types/impersonation-context.type.ts | 3 +- 5 files changed, 24 insertions(+), 67 deletions(-) diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index eebe28a53..20cbbc314 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -116,7 +116,10 @@ export class AccessController { type, scopes: data.scopes }), - settings: this.accessService.buildSettings(data.filters), + settings: this.accessService.buildSettings({ + type, + filters: data.filters + }), user: { connect: { id: this.request.user.id } } }); } catch { @@ -204,7 +207,10 @@ export class AccessController { scopes: data.scopes ?? originalAccess.scopes, type: originalAccess.type }), - settings: this.accessService.buildSettings(data.filters) + settings: this.accessService.buildSettings({ + filters: data.filters, + type: originalAccess.type + }) }, where: { id } }); diff --git a/apps/api/src/app/access/access.service.ts b/apps/api/src/app/access/access.service.ts index 40dd467c9..a711fa9bc 100644 --- a/apps/api/src/app/access/access.service.ts +++ b/apps/api/src/app/access/access.service.ts @@ -3,7 +3,7 @@ import { AccessSettings, Filter } from '@ghostfolio/common/interfaces'; import { AccessWithGranteeUser } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { Access, Prisma } from '@prisma/client'; +import { Access, AccessType, Prisma } from '@prisma/client'; import { isBefore, isToday, isValid } from 'date-fns'; @Injectable() @@ -41,8 +41,15 @@ export class AccessService { }); } - public buildSettings(filters?: Filter[]) { - const settings: AccessSettings = filters?.length ? { filters } : {}; + public buildSettings({ + filters, + type + }: { + filters?: Filter[]; + type: AccessType; + }) { + const settings: AccessSettings = + type === 'PUBLIC' && filters?.length ? { filters } : {}; return settings as Prisma.InputJsonValue; } diff --git a/apps/api/src/app/endpoints/mcp/mcp.controller.ts b/apps/api/src/app/endpoints/mcp/mcp.controller.ts index 9e6f29e52..114a9c58f 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.controller.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.controller.ts @@ -88,12 +88,7 @@ export class GhostfolioMcpController { }) public async getActivities( @Impersonation() - { - filters, - scopes: scopesOfAccess, - userId, - userSettings - }: ImpersonationContext, + { scopes: scopesOfAccess, userId, userSettings }: ImpersonationContext, @Payload() { activityTypes, @@ -113,65 +108,18 @@ export class GhostfolioMcpController { })); } - const filtersOfAccess = filters ?? []; - - const filtersOfTool = this.apiService.buildFiltersFromQueryParams({ + const filters = this.apiService.buildFiltersFromQueryParams({ filterByAssetClasses: assetClasses?.join(','), filterByDataSource: holding?.dataSource, filterBySymbol: holding?.symbol }); - // A tool must never widen the access, hence a filter of the tool which - // the access does not permit gives no activity - const filtersOfToolOutsideAccess = filtersOfTool.filter(({ id, type }) => { - const filtersOfAccessOfType = filtersOfAccess.filter((filter) => { - return filter.type === type; - }); - - return ( - filtersOfAccessOfType.length > 0 && - !filtersOfAccessOfType.some((filter) => { - return filter.id === id; - }) - ); - }); - - if (filtersOfToolOutsideAccess.length > 0) { - const valuesOutsideAccess = filtersOfToolOutsideAccess - .map(({ id }) => { - return id; - }) - .join(', '); - - return { - content: [ - { - text: `No activities found. The access does not permit these values of the parameters: ${valuesOutsideAccess}.`, - type: 'text' as const - } - ] - }; - } - - const typesOfFiltersOfTool = new Set( - filtersOfTool.map(({ type }) => { - return type; - }) - ); - - // The filters of a type are combined with a logical or, hence a filter of - // the tool replaces the filters of the access of the same type instead of - // joining them - const filtersOfAccessOutsideTool = filtersOfAccess.filter(({ type }) => { - return !typesOfFiltersOfTool.has(type); - }); - const table = await this.aiService.getActivitiesTable({ endDate, + filters, skip, startDate, userId, - filters: [...filtersOfAccessOutsideTool, ...filtersOfTool], take: take ?? MCP_MAX_ACTIVITIES, types: activityTypes, userCurrency: userSettings.baseCurrency, @@ -194,10 +142,9 @@ export class GhostfolioMcpController { name: 'get-portfolio' }) public async getPortfolio( - @Impersonation() { filters, userId, userSettings }: ImpersonationContext + @Impersonation() { userId, userSettings }: ImpersonationContext ) { const prompt = await this.aiService.getPrompt({ - filters, userId, languageCode: DEFAULT_LANGUAGE_CODE, mode: 'portfolio', diff --git a/apps/api/src/services/impersonation/impersonation.service.ts b/apps/api/src/services/impersonation/impersonation.service.ts index 2a7915aa5..1cce830b5 100644 --- a/apps/api/src/services/impersonation/impersonation.service.ts +++ b/apps/api/src/services/impersonation/impersonation.service.ts @@ -3,7 +3,7 @@ import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscripti import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; -import { AccessSettings, UserSettings } from '@ghostfolio/common/interfaces'; +import { UserSettings } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { getScopesOfAccess, @@ -65,11 +65,9 @@ export class ImpersonationService { where: { id: impersonatedUserId } }); - const { filters } = (access?.settings ?? {}) as AccessSettings; const settings = impersonatedUser?.settings?.settings as UserSettings; return { - filters, accessId: impersonationId, authenticatedUserSubscription: user?.subscription, isActive: true, diff --git a/libs/common/src/lib/types/impersonation-context.type.ts b/libs/common/src/lib/types/impersonation-context.type.ts index b125191db..29b7970b1 100644 --- a/libs/common/src/lib/types/impersonation-context.type.ts +++ b/libs/common/src/lib/types/impersonation-context.type.ts @@ -1,4 +1,4 @@ -import { Filter, UserSettings } from '@ghostfolio/common/interfaces'; +import { UserSettings } from '@ghostfolio/common/interfaces'; import { UserWithSettings } from '@ghostfolio/common/types'; /** @@ -10,7 +10,6 @@ import { UserWithSettings } from '@ghostfolio/common/types'; export interface ImpersonationContext { accessId?: string; authenticatedUserSubscription?: UserWithSettings['subscription']; - filters?: Filter[]; isActive: boolean; scopes: string[]; userId: string;