From ac018447f9c254819db3dbe065d75a2baabe62f1 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Fri, 22 Sep 2023 11:54:33 +0300 Subject: [PATCH] feat: optimize private projects for enterprise (#4812) --- .../__snapshots__/create-config.test.ts.snap | 1 + src/lib/create-config.test.ts | 16 +++++++++ src/lib/create-config.ts | 6 ++++ .../features/playground/playground-service.ts | 18 +++++----- .../createPrivateProjectChecker.ts | 9 +++-- .../fakePrivateProjectChecker.ts | 3 +- .../private-project/privateProjectChecker.ts | 25 ++++++++----- .../privateProjectCheckerType.ts | 4 ++- .../private-project/privateProjectStore.ts | 36 ++++++++++++++----- .../privateProjectStoreType.ts | 4 ++- .../client-metrics/instance-service.ts | 26 ++++++++------ src/lib/services/feature-toggle-service.ts | 18 +++++++--- src/lib/services/index.ts | 1 + src/lib/services/project-service.ts | 13 ++++--- src/lib/types/no-auth-user.ts | 3 +- src/lib/types/option.ts | 1 + 16 files changed, 134 insertions(+), 50 deletions(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 0e40bd6a30..771f220e82 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -162,6 +162,7 @@ exports[`should create default config 1`] = ` "keepExisting": false, }, "inlineSegmentConstraints": true, + "isEnterprise": false, "listen": { "host": undefined, "port": 4242, diff --git a/src/lib/create-config.test.ts b/src/lib/create-config.test.ts index d7057df9b0..0d8b137595 100644 --- a/src/lib/create-config.test.ts +++ b/src/lib/create-config.test.ts @@ -471,3 +471,19 @@ test.each(['demo', '/demo', '/demo/'])( expect(config.server.baseUriPath).toBe('/demo'); }, ); + +test('Config with enterpriseVersion set and pro environment should set isEnterprise to false', async () => { + let config = createConfig({ + enterpriseVersion: '5.3.0', + ui: { environment: 'pro' }, + }); + expect(config.isEnterprise).toBe(false); +}); + +test('Config with enterpriseVersion set and not pro environment should set isEnterprise to true', async () => { + let config = createConfig({ + enterpriseVersion: '5.3.0', + ui: { environment: 'Enterprise' }, + }); + expect(config.isEnterprise).toBe(true); +}); diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index a18015f686..80ee6cfcde 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -481,6 +481,11 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { const clientFeatureCaching = loadClientCachingOptions(options); const prometheusApi = options.prometheusApi || process.env.PROMETHEUS_API; + + const isEnterprise = + Boolean(options.enterpriseVersion) && + ui.environment?.toLowerCase() !== 'pro'; + return { db, session, @@ -513,6 +518,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { prometheusApi, publicFolder: options.publicFolder, disableScheduler: options.disableScheduler, + isEnterprise: isEnterprise, }; } diff --git a/src/lib/features/playground/playground-service.ts b/src/lib/features/playground/playground-service.ts index 4ffcf8d813..4dbb64f42c 100644 --- a/src/lib/features/playground/playground-service.ts +++ b/src/lib/features/playground/playground-service.ts @@ -102,17 +102,19 @@ export class PlaygroundService { let filteredProjects: typeof projects; if (this.flagResolver.isEnabled('privateProjects')) { - const accessibleProjects = + const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects( userId, ); - filteredProjects = - projects === ALL - ? accessibleProjects - : projects.filter((project) => - accessibleProjects.includes(project), - ); - console.log(accessibleProjects); + if (projectAccess.mode === 'all') { + filteredProjects = projects; + } else if (projects === ALL) { + filteredProjects = projectAccess.projects; + } else { + filteredProjects = projects.filter((project) => + projectAccess.projects.includes(project), + ); + } } const environmentFeatures = await Promise.all( diff --git a/src/lib/features/private-project/createPrivateProjectChecker.ts b/src/lib/features/private-project/createPrivateProjectChecker.ts index 4844025ca9..7000815dcb 100644 --- a/src/lib/features/private-project/createPrivateProjectChecker.ts +++ b/src/lib/features/private-project/createPrivateProjectChecker.ts @@ -10,9 +10,12 @@ export const createPrivateProjectChecker = ( const { getLogger } = config; const privateProjectStore = new PrivateProjectStore(db, getLogger); - return new PrivateProjectChecker({ - privateProjectStore: privateProjectStore, - }); + return new PrivateProjectChecker( + { + privateProjectStore: privateProjectStore, + }, + config, + ); }; export const createFakePrivateProjectChecker = diff --git a/src/lib/features/private-project/fakePrivateProjectChecker.ts b/src/lib/features/private-project/fakePrivateProjectChecker.ts index 2dba73db65..f538908473 100644 --- a/src/lib/features/private-project/fakePrivateProjectChecker.ts +++ b/src/lib/features/private-project/fakePrivateProjectChecker.ts @@ -1,9 +1,10 @@ import { IPrivateProjectChecker } from './privateProjectCheckerType'; import { Promise } from 'ts-toolbelt/out/Any/Promise'; +import { ProjectAccess } from './privateProjectStore'; export class FakePrivateProjectChecker implements IPrivateProjectChecker { // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getUserAccessibleProjects(userId: number): Promise { + async getUserAccessibleProjects(userId: number): Promise { throw new Error('Method not implemented.'); } diff --git a/src/lib/features/private-project/privateProjectChecker.ts b/src/lib/features/private-project/privateProjectChecker.ts index daa30a1a51..2af389a8f7 100644 --- a/src/lib/features/private-project/privateProjectChecker.ts +++ b/src/lib/features/private-project/privateProjectChecker.ts @@ -1,26 +1,35 @@ -import { IUnleashStores } from '../../types'; +import { IUnleashConfig, IUnleashStores } from '../../types'; import { IPrivateProjectStore } from './privateProjectStoreType'; import { IPrivateProjectChecker } from './privateProjectCheckerType'; +import { ALL_PROJECT_ACCESS, ProjectAccess } from './privateProjectStore'; export class PrivateProjectChecker implements IPrivateProjectChecker { private privateProjectStore: IPrivateProjectStore; - constructor({ - privateProjectStore, - }: Pick) { + private isEnterprise: boolean; + + constructor( + { privateProjectStore }: Pick, + { isEnterprise }: Pick, + ) { this.privateProjectStore = privateProjectStore; + this.isEnterprise = isEnterprise; } - async getUserAccessibleProjects(userId: number): Promise { - return this.privateProjectStore.getUserAccessibleProjects(userId); + async getUserAccessibleProjects(userId: number): Promise { + return this.isEnterprise + ? this.privateProjectStore.getUserAccessibleProjects(userId) + : Promise.resolve(ALL_PROJECT_ACCESS); } async hasAccessToProject( userId: number, projectId: string, ): Promise { - return (await this.getUserAccessibleProjects(userId)).includes( - projectId, + const projectAccess = await this.getUserAccessibleProjects(userId); + return ( + projectAccess.mode === 'all' || + projectAccess.projects.includes(projectId) ); } } diff --git a/src/lib/features/private-project/privateProjectCheckerType.ts b/src/lib/features/private-project/privateProjectCheckerType.ts index c8b537a906..8ddb3b9ce4 100644 --- a/src/lib/features/private-project/privateProjectCheckerType.ts +++ b/src/lib/features/private-project/privateProjectCheckerType.ts @@ -1,4 +1,6 @@ +import { ProjectAccess } from './privateProjectStore'; + export interface IPrivateProjectChecker { - getUserAccessibleProjects(userId: number): Promise; + getUserAccessibleProjects(userId: number): Promise; hasAccessToProject(userId: number, projectId: string): Promise; } diff --git a/src/lib/features/private-project/privateProjectStore.ts b/src/lib/features/private-project/privateProjectStore.ts index 0a3be0b48a..9666cfe9e3 100644 --- a/src/lib/features/private-project/privateProjectStore.ts +++ b/src/lib/features/private-project/privateProjectStore.ts @@ -1,8 +1,20 @@ import { Db } from '../../db/db'; import { Logger, LogProvider } from '../../logger'; import { IPrivateProjectStore } from './privateProjectStoreType'; +import { ADMIN_TOKEN_ID } from '../../types'; -const ADMIN_TOKEN_ID = -1; +export type ProjectAccess = + | { + mode: 'all'; + } + | { + mode: 'limited'; + projects: string[]; + }; + +export const ALL_PROJECT_ACCESS: ProjectAccess = { + mode: 'all', +}; class PrivateProjectStore implements IPrivateProjectStore { private db: Db; @@ -16,10 +28,9 @@ class PrivateProjectStore implements IPrivateProjectStore { destroy(): void {} - async getUserAccessibleProjects(userId: number): Promise { + async getUserAccessibleProjects(userId: number): Promise { if (userId === ADMIN_TOKEN_ID) { - const allProjects = await this.db('projects').pluck('id'); - return allProjects; + return ALL_PROJECT_ACCESS; } const isViewer = await this.db('role_user') .join('roles', 'role_user.role_id', 'roles.id') @@ -32,11 +43,10 @@ class PrivateProjectStore implements IPrivateProjectStore { .first(); if (!isViewer || isViewer.count == 0) { - const allProjects = await this.db('projects').pluck('id'); - return allProjects; + return ALL_PROJECT_ACCESS; } - const accessibleProjects = await this.db + const accessibleProjects: string[] = await this.db .from((db) => { db.distinct() .select('projects.id as project_id') @@ -46,7 +56,15 @@ class PrivateProjectStore implements IPrivateProjectStore { 'projects.id', 'project_settings.project', ) - .where('project_settings.project_mode', '!=', 'private') + .where((builder) => { + builder + .whereNull('project_settings.project') + .orWhere( + 'project_settings.project_mode', + '!=', + 'private', + ); + }) .unionAll((queryBuilder) => { queryBuilder .select('projects.id as project_id') @@ -89,7 +107,7 @@ class PrivateProjectStore implements IPrivateProjectStore { .select('*') .pluck('project_id'); - return accessibleProjects; + return { mode: 'limited', projects: accessibleProjects }; } } diff --git a/src/lib/features/private-project/privateProjectStoreType.ts b/src/lib/features/private-project/privateProjectStoreType.ts index a554aaab5a..98b213775f 100644 --- a/src/lib/features/private-project/privateProjectStoreType.ts +++ b/src/lib/features/private-project/privateProjectStoreType.ts @@ -1,3 +1,5 @@ +import { ProjectAccess } from './privateProjectStore'; + export interface IPrivateProjectStore { - getUserAccessibleProjects(userId: number): Promise; + getUserAccessibleProjects(userId: number): Promise; } diff --git a/src/lib/services/client-metrics/instance-service.ts b/src/lib/services/client-metrics/instance-service.ts index 19cca5a26b..f819dc486e 100644 --- a/src/lib/services/client-metrics/instance-service.ts +++ b/src/lib/services/client-metrics/instance-service.ts @@ -184,16 +184,22 @@ export default class ClientInstanceService { await this.privateProjectChecker.getUserAccessibleProjects( userId, ); - return applications.map((application) => { - return { - ...application, - usage: application.usage?.filter( - (usageItem) => - usageItem.project === ALL_PROJECTS || - accessibleProjects.includes(usageItem.project), - ), - }; - }); + if (accessibleProjects.mode === 'all') { + return applications; + } else { + return applications.map((application) => { + return { + ...application, + usage: application.usage?.filter( + (usageItem) => + usageItem.project === ALL_PROJECTS || + accessibleProjects.projects.includes( + usageItem.project, + ), + ), + }; + }); + } } return applications; } diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 17b35c2596..6bb6bd3a62 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -1031,11 +1031,15 @@ class FeatureToggleService { }); if (this.flagResolver.isEnabled('privateProjects') && userId) { - const projects = + const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects( userId, ); - return features.filter((f) => projects.includes(f.project)); + return projectAccess.mode === 'all' + ? features + : features.filter((f) => + projectAccess.projects.includes(f.project), + ); } return features; } @@ -1860,11 +1864,17 @@ class FeatureToggleService { ): Promise { const features = await this.featureToggleStore.getAll({ archived }); if (this.flagResolver.isEnabled('privateProjects')) { - const projects = + const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects( userId, ); - return features.filter((f) => projects.includes(f.project)); + if (projectAccess.mode === 'all') { + return features; + } else { + return features.filter((f) => + projectAccess.projects.includes(f.project), + ); + } } return features; } diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index a3db29e6cf..03265589bd 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -201,6 +201,7 @@ export const createServices = ( changeRequestAccessReadModel, config, ); + const privateProjectChecker = db ? createPrivateProjectChecker(db, config) : createFakePrivateProjectChecker(); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 41cf1334a3..ba2c76677a 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -164,13 +164,18 @@ export default class ProjectService { ): Promise { const projects = await this.store.getProjectsWithCounts(query, userId); if (this.flagResolver.isEnabled('privateProjects') && userId) { - const accessibleProjects = + const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects( userId, ); - return projects.filter((project) => - accessibleProjects.includes(project.id), - ); + + if (projectAccess.mode === 'all') { + return projects; + } else { + return projects.filter((project) => + projectAccess.projects.includes(project.id), + ); + } } return projects; } diff --git a/src/lib/types/no-auth-user.ts b/src/lib/types/no-auth-user.ts index fbb65e8cde..7ceb1fc424 100644 --- a/src/lib/types/no-auth-user.ts +++ b/src/lib/types/no-auth-user.ts @@ -1,5 +1,6 @@ import { ADMIN } from './permissions'; +export const ADMIN_TOKEN_ID = -1; export default class NoAuthUser { isAPI: boolean; @@ -11,7 +12,7 @@ export default class NoAuthUser { constructor( username: string = 'unknown', - id: number = -1, + id: number = ADMIN_TOKEN_ID, permissions: string[] = [ADMIN], ) { this.isAPI = true; diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index d6d85358ad..a948c7e800 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -215,4 +215,5 @@ export interface IUnleashConfig { prometheusApi?: string; publicFolder?: string; disableScheduler?: boolean; + isEnterprise: boolean; }