diff --git a/src/lib/features/metrics/instance/instance-service.test.ts b/src/lib/features/metrics/instance/instance-service.test.ts index 09d13da536..5c79419807 100644 --- a/src/lib/features/metrics/instance/instance-service.test.ts +++ b/src/lib/features/metrics/instance/instance-service.test.ts @@ -3,10 +3,11 @@ import type { IClientApp } from '../../../types/model'; import FakeEventStore from '../../../../test/fixtures/fake-event-store'; import { createTestConfig } from '../../../../test/config/test-config'; import { FakePrivateProjectChecker } from '../../private-project/fakePrivateProjectChecker'; -import type { IUnleashConfig } from '../../../types'; +import type { IClientApplicationsStore, IUnleashConfig } from '../../../types'; import FakeClientMetricsStoreV2 from '../client-metrics/fake-client-metrics-store-v2'; import FakeStrategiesStore from '../../../../test/fixtures/fake-strategies-store'; import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store'; +import type { IApplicationOverview } from './models'; let config: IUnleashConfig; beforeAll(() => { @@ -189,3 +190,68 @@ test('No registrations during a time period will not call stores', async () => { expect(appStoreSpy).toHaveBeenCalledTimes(0); expect(bulkSpy).toHaveBeenCalledTimes(0); }); + +test('filter out private projects from overview', async () => { + const clientApplicationsStore = { + async getApplicationOverview( + appName: string, + ): Promise { + return { + environments: [ + { + name: 'development', + instanceCount: 1, + sdks: ['unleash-client-node:3.5.1'], + lastSeen: new Date(), + issues: { + missingFeatures: [], + outdatedSdks: [], + }, + }, + ], + projects: ['privateProject', 'publicProject'], + issues: { + missingStrategies: [], + }, + featureCount: 0, + }; + }, + } as IClientApplicationsStore; + const privateProjectsChecker = { + async filterUserAccessibleProjects( + userId: number, + projects: string[], + ): Promise { + return projects.filter((project) => !project.includes('private')); + }, + } as FakePrivateProjectChecker; + const clientInstanceService = new ClientInstanceService( + { clientApplicationsStore } as any, + config, + privateProjectsChecker, + ); + + const overview = await clientInstanceService.getApplicationOverview( + 'appName', + 123, + ); + + expect(overview).toMatchObject({ + environments: [ + { + name: 'development', + instanceCount: 1, + sdks: ['unleash-client-node:3.5.1'], + issues: { + missingFeatures: [], + outdatedSdks: ['unleash-client-node:3.5.1'], + }, + }, + ], + projects: ['publicProject'], + issues: { + missingStrategies: [], + }, + featureCount: 0, + }); +}); diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts index 2db09ffce2..2855ac5d27 100644 --- a/src/lib/features/metrics/instance/instance-service.ts +++ b/src/lib/features/metrics/instance/instance-service.ts @@ -220,9 +220,16 @@ export default class ClientInstanceService { async getApplicationOverview( appName: string, + userId: number, ): Promise { const result = await this.clientApplicationsStore.getApplicationOverview(appName); + const accessibleProjects = + await this.privateProjectChecker.filterUserAccessibleProjects( + userId, + result.projects, + ); + result.projects = accessibleProjects; result.environments.forEach((environment) => { environment.issues.outdatedSdks = findOutdatedSDKs( environment.sdks, diff --git a/src/lib/features/private-project/fakePrivateProjectChecker.ts b/src/lib/features/private-project/fakePrivateProjectChecker.ts index 27a061ce2d..fff0308d50 100644 --- a/src/lib/features/private-project/fakePrivateProjectChecker.ts +++ b/src/lib/features/private-project/fakePrivateProjectChecker.ts @@ -2,6 +2,12 @@ import type { IPrivateProjectChecker } from './privateProjectCheckerType'; import { ALL_PROJECT_ACCESS, type ProjectAccess } from './privateProjectStore'; export class FakePrivateProjectChecker implements IPrivateProjectChecker { + async filterUserAccessibleProjects( + userId: number, + projects: string[], + ): Promise { + return projects; + } // eslint-disable-next-line @typescript-eslint/no-unused-vars async getUserAccessibleProjects(userId: number): Promise { return ALL_PROJECT_ACCESS; diff --git a/src/lib/features/private-project/privateProjectChecker.test.ts b/src/lib/features/private-project/privateProjectChecker.test.ts new file mode 100644 index 0000000000..698f7e56fa --- /dev/null +++ b/src/lib/features/private-project/privateProjectChecker.test.ts @@ -0,0 +1,68 @@ +import { PrivateProjectChecker } from './privateProjectChecker'; +import type { IPrivateProjectStore } from './privateProjectStoreType'; + +test('filter user accessible projects', async () => { + const checker = new PrivateProjectChecker( + { + privateProjectStore: { + async getUserAccessibleProjects() { + return { + mode: 'limited', + projects: ['projectA', 'projectB'], + }; + }, + } as IPrivateProjectStore, + }, + { isEnterprise: true }, + ); + + const projects = await checker.filterUserAccessibleProjects(123, [ + 'projectA', + 'projectC', + ]); + + expect(projects).toEqual(['projectA']); +}); + +test('do not filter for non enterprise', async () => { + const checker = new PrivateProjectChecker( + { + privateProjectStore: { + async getUserAccessibleProjects() { + return { + mode: 'limited', + projects: ['projectA', 'projectB'], + }; + }, + } as IPrivateProjectStore, + }, + { isEnterprise: false }, + ); + + const projects = await checker.filterUserAccessibleProjects(123, [ + 'projectA', + 'projectC', + ]); + + expect(projects).toEqual(['projectA', 'projectC']); +}); + +test('do not filter for all mode', async () => { + const checker = new PrivateProjectChecker( + { + privateProjectStore: { + async getUserAccessibleProjects() { + return { mode: 'all' }; + }, + } as IPrivateProjectStore, + }, + { isEnterprise: false }, + ); + + const projects = await checker.filterUserAccessibleProjects(123, [ + 'projectA', + 'projectC', + ]); + + expect(projects).toEqual(['projectA', 'projectC']); +}); diff --git a/src/lib/features/private-project/privateProjectChecker.ts b/src/lib/features/private-project/privateProjectChecker.ts index 89db3cd7b5..007ff6497b 100644 --- a/src/lib/features/private-project/privateProjectChecker.ts +++ b/src/lib/features/private-project/privateProjectChecker.ts @@ -22,6 +22,21 @@ export class PrivateProjectChecker implements IPrivateProjectChecker { : Promise.resolve(ALL_PROJECT_ACCESS); } + async filterUserAccessibleProjects( + userId: number, + projects: string[], + ): Promise { + if (!this.isEnterprise) { + return projects; + } + const accessibleProjects = + await this.privateProjectStore.getUserAccessibleProjects(userId); + if (accessibleProjects.mode === 'all') return projects; + return projects.filter((project) => + accessibleProjects.projects.includes(project), + ); + } + async hasAccessToProject( userId: number, projectId: string, diff --git a/src/lib/features/private-project/privateProjectCheckerType.ts b/src/lib/features/private-project/privateProjectCheckerType.ts index 39552dc1d9..ddf3cfe57d 100644 --- a/src/lib/features/private-project/privateProjectCheckerType.ts +++ b/src/lib/features/private-project/privateProjectCheckerType.ts @@ -2,5 +2,9 @@ import type { ProjectAccess } from './privateProjectStore'; export interface IPrivateProjectChecker { getUserAccessibleProjects(userId: number): Promise; + filterUserAccessibleProjects( + userId: number, + projects: string[], + ): Promise; hasAccessToProject(userId: number, projectId: string): Promise; } diff --git a/src/lib/routes/admin-api/metrics.ts b/src/lib/routes/admin-api/metrics.ts index 9cbc1857e6..481d71e19d 100644 --- a/src/lib/routes/admin-api/metrics.ts +++ b/src/lib/routes/admin-api/metrics.ts @@ -281,12 +281,16 @@ class MetricsController extends Controller { } async getApplicationOverview( - req: Request<{ appName: string }>, + req: IAuthRequest<{ appName: string }>, res: Response, ): Promise { const { appName } = req.params; + const { user } = req; const overview = - await this.clientInstanceService.getApplicationOverview(appName); + await this.clientInstanceService.getApplicationOverview( + appName, + extractUserIdFromUser(user), + ); this.openApiService.respondWithValidation( 200,